diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 5769e409b..c28165ca3 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -57,6 +57,7 @@ jobs: - name: Update apt-get (needed for act docker image) run: | + sudo rm /etc/apt/sources.list.d/microsoft-prod.list sudo apt-get -qq update - name: Install tree diff --git a/.github/workflows/build-includes.yml b/.github/workflows/build-includes.yml index 9ad48f7ff..7babdf325 100644 --- a/.github/workflows/build-includes.yml +++ b/.github/workflows/build-includes.yml @@ -110,7 +110,7 @@ jobs: - name: Run LuaSrcDiet run: | luasrcdiet --basic --opt-emptylines ./build/result/Moose_Include_Static/Moose.lua -o ./build/result/Moose_Include_Static/Moose_.lua - + ######################################################################### # Push to MOOSE_INCLUDE ######################################################################### diff --git a/.gitignore b/.gitignore index 941c6a3a9..63ed14ae4 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,13 @@ local.properties .buildpath +##################### +## Visual Studio Code +##################### +*.code-workspace +.vscode/ + + ################# ## Visual Studio ################# diff --git a/Moose Development/Moose/.vscode/settings.json b/Moose Development/Moose/.vscode/settings.json index 13211d027..4656f0257 100644 --- a/Moose Development/Moose/.vscode/settings.json +++ b/Moose Development/Moose/.vscode/settings.json @@ -1,7 +1,17 @@ { - "Lua.workspace.preloadFileSize": 1000, + "Lua.workspace.preloadFileSize": 10000, "Lua.diagnostics.disable": [ - "undefined-doc-name" + "undefined-doc-name", + "duplicate-set-field", + "trailing-space", + "need-check-nil", + "ambiguity-1", + "undefined-doc-param", + "redundant-parameter", + "param-type-mismatch", + "deprecated", + "undefined-global", + "lowercase-global" ], "Lua.diagnostics.globals": [ "BASE", diff --git a/Moose Development/Moose/AI/AI_A2A_Cap.lua b/Moose Development/Moose/AI/AI_A2A_Cap.lua index 693895d6d..08c4a7273 100644 --- a/Moose Development/Moose/AI/AI_A2A_Cap.lua +++ b/Moose Development/Moose/AI/AI_A2A_Cap.lua @@ -11,7 +11,7 @@ -- @module AI.AI_A2A_Cap -- @image AI_Combat_Air_Patrol.JPG ---- @type AI_A2A_CAP +-- @type AI_A2A_CAP -- @extends AI.AI_Air_Patrol#AI_AIR_PATROL -- @extends AI.AI_Air_Engage#AI_AIR_ENGAGE diff --git a/Moose Development/Moose/AI/AI_A2A_Gci.lua b/Moose Development/Moose/AI/AI_A2A_Gci.lua index 807fa9b78..3d56e0227 100644 --- a/Moose Development/Moose/AI/AI_A2A_Gci.lua +++ b/Moose Development/Moose/AI/AI_A2A_Gci.lua @@ -13,8 +13,8 @@ ---- @type AI_A2A_GCI --- @extends AI.AI_Air_Engage#AI_AIR_ENGAGE +-- @type AI_A2A_GCI +-- @extends AI.AI_A2A#AI_A2A --- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. @@ -41,6 +41,8 @@ -- -- ## 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. diff --git a/Moose Development/Moose/AI/AI_A2A_Patrol.lua b/Moose Development/Moose/AI/AI_A2A_Patrol.lua index d858fb9e2..6c7ce68cc 100644 --- a/Moose Development/Moose/AI/AI_A2A_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2A_Patrol.lua @@ -10,8 +10,8 @@ -- @image AI_Air_Patrolling.JPG ---- @type AI_A2A_PATROL --- @extends AI.AI_Air_Patrol#AI_AIR_PATROL +-- @type AI_A2A_PATROL +-- @extends AI.AI_A2A#AI_A2A --- Implements the core functions to patrol a @{Core.Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group}. -- diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua index 729dbf5c2..8a39ad0d2 100644 --- a/Moose Development/Moose/AI/AI_A2G_BAI.lua +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -11,9 +11,8 @@ -- @module AI.AI_A2G_BAI -- @image AI_Air_To_Ground_Engage.JPG ---- @type AI_A2G_BAI --- @extends AI.AI_Air_Patrol#AI_AIR_PATROL --- @extends AI.AI_Air_Engage#AI_AIR_ENGAGE +-- @type AI_A2G_BAI +-- @extends AI.AI_A2A_Engage#AI_A2A_Engage -- TODO: Documentation. This class does not exist, unable to determine what it extends. --- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. -- @@ -49,7 +48,7 @@ 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 ) + 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 ) diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua index 830eb8b66..d4c0fe417 100644 --- a/Moose Development/Moose/AI/AI_A2G_CAS.lua +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -11,9 +11,8 @@ -- @module AI.AI_A2G_CAS -- @image AI_Air_To_Ground_Engage.JPG ---- @type AI_A2G_CAS --- @extends AI.AI_Air_Patrol#AI_AIR_PATROL --- @extends AI.AI_Air_Engage#AI_AIR_ENGAGE +-- @type AI_A2G_CAS +-- @extends AI.AI_A2G_Patrol#AI_AIR_PATROL TODO: Documentation. This class does not exist, unable to determine what it extends. --- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. -- @@ -49,7 +48,7 @@ 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 ) + 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 ) diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua index bf876db35..4ee600fcf 100644 --- a/Moose Development/Moose/AI/AI_A2G_SEAD.lua +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -13,9 +13,8 @@ ---- @type AI_A2G_SEAD --- @extends AI.AI_Air_Patrol#AI_AIR_PATROL --- @extends AI.AI_Air_Engage#AI_AIR_ENGAGE +-- @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. diff --git a/Moose Development/Moose/AI/AI_Air_Patrol.lua b/Moose Development/Moose/AI/AI_Air_Patrol.lua index 3e57476c5..b389d10ff 100644 --- a/Moose Development/Moose/AI/AI_Air_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Air_Patrol.lua @@ -9,7 +9,7 @@ -- @module AI.AI_Air_Patrol -- @image AI_Air_To_Ground_Patrol.JPG ---- @type AI_AIR_PATROL +-- @type AI_AIR_PATROL -- @extends AI.AI_Air#AI_AIR --- The AI_AIR_PATROL class implements the core functions to patrol a @{Core.Zone} by an AI @{Wrapper.Group} diff --git a/Moose Development/Moose/AI/AI_BAI.lua b/Moose Development/Moose/AI/AI_BAI.lua index 3037feb18..46c5c086a 100644 --- a/Moose Development/Moose/AI/AI_BAI.lua +++ b/Moose Development/Moose/AI/AI_BAI.lua @@ -409,7 +409,7 @@ function AI_BAI_ZONE:onafterStart( Controllable, From, Event, To ) self:SetDetectionDeactivated() -- When not engaging, set the detection off. end ---- @param Wrapper.Controllable#CONTROLLABLE AIControllable +-- @param Wrapper.Controllable#CONTROLLABLE AIControllable function _NewEngageRoute( AIControllable ) AIControllable:T( "NewEngageRoute" ) @@ -418,7 +418,7 @@ function _NewEngageRoute( AIControllable ) end ---- @param #AI_BAI_ZONE self +-- @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. @@ -430,7 +430,7 @@ function AI_BAI_ZONE:onbeforeEngage( Controllable, From, Event, To ) end end ---- @param #AI_BAI_ZONE self +-- @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. @@ -479,7 +479,7 @@ function AI_BAI_ZONE:onafterTarget( Controllable, From, Event, To ) end ---- @param #AI_BAI_ZONE self +-- @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. @@ -489,7 +489,7 @@ function AI_BAI_ZONE:onafterAbort( Controllable, From, Event, To ) self:__Route( 1 ) end ---- @param #AI_BAI_ZONE self +-- @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. @@ -613,7 +613,7 @@ function AI_BAI_ZONE:onafterEngage( Controllable, From, Event, To, end ---- @param #AI_BAI_ZONE self +-- @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. @@ -624,7 +624,7 @@ function AI_BAI_ZONE:onafterAccomplish( Controllable, From, Event, To ) end ---- @param #AI_BAI_ZONE self +-- @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. @@ -638,7 +638,7 @@ function AI_BAI_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) end ---- @param #AI_BAI_ZONE self +-- @param #AI_BAI_ZONE self -- @param Core.Event#EVENTDATA EventData function AI_BAI_ZONE:OnEventDead( EventData ) self:F( { "EventDead", EventData } ) diff --git a/Moose Development/Moose/AI/AI_Balancer.lua b/Moose Development/Moose/AI/AI_Balancer.lua index b3c7fe1d1..f11a12c18 100644 --- a/Moose Development/Moose/AI/AI_Balancer.lua +++ b/Moose Development/Moose/AI/AI_Balancer.lua @@ -27,7 +27,7 @@ -- @module AI.AI_Balancer -- @image AI_Balancing.JPG ---- @type AI_BALANCER +-- @type AI_BALANCER -- @field Core.Set#SET_CLIENT SetClient -- @field Core.Spawn#SPAWN SpawnAI -- @field Wrapper.Group#GROUP Test diff --git a/Moose Development/Moose/AI/AI_CAP.lua b/Moose Development/Moose/AI/AI_CAP.lua index 70f6fb438..8f1f1501b 100644 --- a/Moose Development/Moose/AI/AI_CAP.lua +++ b/Moose Development/Moose/AI/AI_CAP.lua @@ -31,7 +31,7 @@ -- @module AI.AI_CAP -- @image AI_Combat_Air_Patrol.JPG ---- @type AI_CAP_ZONE +-- @type AI_CAP_ZONE -- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling. -- @field Core.Zone#ZONE_BASE TargetZone The @{Core.Zone} where the patrol needs to be executed. -- @extends AI.AI_Patrol#AI_PATROL_ZONE @@ -346,7 +346,7 @@ function AI_CAP_ZONE:onafterStart( Controllable, From, Event, To ) end ---- @param AI.AI_CAP#AI_CAP_ZONE +-- @param AI.AI_CAP#AI_CAP_ZONE -- @param Wrapper.Group#GROUP EngageGroup function AI_CAP_ZONE.EngageRoute( EngageGroup, Fsm ) @@ -357,7 +357,7 @@ function AI_CAP_ZONE.EngageRoute( EngageGroup, Fsm ) end end ---- @param #AI_CAP_ZONE self +-- @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. @@ -369,7 +369,7 @@ function AI_CAP_ZONE:onbeforeEngage( Controllable, From, Event, To ) end end ---- @param #AI_CAP_ZONE self +-- @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. @@ -397,7 +397,7 @@ function AI_CAP_ZONE:onafterDetected( Controllable, From, Event, To ) end end ---- @param #AI_CAP_ZONE self +-- @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. @@ -407,7 +407,7 @@ function AI_CAP_ZONE:onafterAbort( Controllable, From, Event, To ) self:__Route( 1 ) end ---- @param #AI_CAP_ZONE self +-- @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. @@ -507,7 +507,7 @@ function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) end end ---- @param #AI_CAP_ZONE self +-- @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. @@ -517,7 +517,7 @@ function AI_CAP_ZONE:onafterAccomplish( Controllable, From, Event, To ) self:SetDetectionOff() end ---- @param #AI_CAP_ZONE self +-- @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. @@ -530,7 +530,7 @@ function AI_CAP_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) end end ---- @param #AI_CAP_ZONE self +-- @param #AI_CAP_ZONE self -- @param Core.Event#EVENTDATA EventData function AI_CAP_ZONE:OnEventDead( EventData ) self:F( { "EventDead", EventData } ) diff --git a/Moose Development/Moose/AI/AI_CAS.lua b/Moose Development/Moose/AI/AI_CAS.lua index 1c99401a8..8fec1ca3f 100644 --- a/Moose Development/Moose/AI/AI_CAS.lua +++ b/Moose Development/Moose/AI/AI_CAS.lua @@ -165,7 +165,6 @@ function AI_CAS_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude -- @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. @@ -366,7 +365,7 @@ function AI_CAS_ZONE:onafterStart( Controllable, From, Event, To ) self:SetDetectionDeactivated() -- When not engaging, set the detection off. end ---- @param AI.AI_CAS#AI_CAS_ZONE +-- @param AI.AI_CAS#AI_CAS_ZONE -- @param Wrapper.Group#GROUP EngageGroup function AI_CAS_ZONE.EngageRoute( EngageGroup, Fsm ) @@ -378,7 +377,7 @@ function AI_CAS_ZONE.EngageRoute( EngageGroup, Fsm ) end ---- @param #AI_CAS_ZONE self +-- @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. @@ -390,7 +389,7 @@ function AI_CAS_ZONE:onbeforeEngage( Controllable, From, Event, To ) end end ---- @param #AI_CAS_ZONE self +-- @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. @@ -423,7 +422,7 @@ function AI_CAS_ZONE:onafterTarget( Controllable, From, Event, To ) end ---- @param #AI_CAS_ZONE self +-- @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. @@ -433,7 +432,7 @@ function AI_CAS_ZONE:onafterAbort( Controllable, From, Event, To ) self:__Route( 1 ) end ---- @param #AI_CAS_ZONE self +-- @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. @@ -533,7 +532,7 @@ function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To, end ---- @param #AI_CAS_ZONE self +-- @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. @@ -544,7 +543,7 @@ function AI_CAS_ZONE:onafterAccomplish( Controllable, From, Event, To ) end ---- @param #AI_CAS_ZONE self +-- @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. @@ -558,7 +557,7 @@ function AI_CAS_ZONE:onafterDestroy( Controllable, From, Event, To, EventData ) end ---- @param #AI_CAS_ZONE self +-- @param #AI_CAS_ZONE self -- @param Core.Event#EVENTDATA EventData function AI_CAS_ZONE:OnEventDead( EventData ) self:F( { "EventDead", EventData } ) diff --git a/Moose Development/Moose/AI/AI_Cargo_APC.lua b/Moose Development/Moose/AI/AI_Cargo_APC.lua index ee026c599..baf202d0d 100644 --- a/Moose Development/Moose/AI/AI_Cargo_APC.lua +++ b/Moose Development/Moose/AI/AI_Cargo_APC.lua @@ -9,7 +9,7 @@ -- @module AI.AI_Cargo_APC -- @image AI_Cargo_Dispatching_For_APC.JPG ---- @type AI_CARGO_APC +-- @type AI_CARGO_APC -- @extends AI.AI_Cargo#AI_CARGO diff --git a/Moose Development/Moose/AI/AI_Cargo_Airplane.lua b/Moose Development/Moose/AI/AI_Cargo_Airplane.lua index 4db63ee86..ff814d6be 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Airplane.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Airplane.lua @@ -9,7 +9,7 @@ -- @module AI.AI_Cargo_Airplane -- @image AI_Cargo_Dispatching_For_Airplanes.JPG ---- @type AI_CARGO_AIRPLANE +-- @type AI_CARGO_AIRPLANE -- @extends Core.Fsm#FSM_CONTROLLABLE diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_APC.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_APC.lua index 73d27da45..ca0ec07d8 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_APC.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_APC.lua @@ -30,7 +30,7 @@ -- @module AI.AI_Cargo_Dispatcher_APC -- @image AI_Cargo_Dispatching_For_APC.JPG ---- @type AI_CARGO_DISPATCHER_APC +-- @type AI_CARGO_DISPATCHER_APC -- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Airplane.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Airplane.lua index cc4835576..b70a43f4c 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Airplane.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Airplane.lua @@ -24,7 +24,7 @@ -- @image AI_Cargo_Dispatching_For_Airplanes.JPG ---- @type AI_CARGO_DISPATCHER_AIRPLANE +-- @type AI_CARGO_DISPATCHER_AIRPLANE -- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua index 182987d70..1c228e872 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua @@ -25,7 +25,7 @@ -- @module AI.AI_Cargo_Dispatcher_Helicopter -- @image AI_Cargo_Dispatching_For_Helicopters.JPG ---- @type AI_CARGO_DISPATCHER_HELICOPTER +-- @type AI_CARGO_DISPATCHER_HELICOPTER -- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Ship.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Ship.lua index 906d0801d..f0afedeb3 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Ship.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Ship.lua @@ -23,7 +23,7 @@ -- @module AI.AI_Cargo_Dispatcher_Ship -- @image AI_Cargo_Dispatcher.JPG ---- @type AI_CARGO_DISPATCHER_SHIP +-- @type AI_CARGO_DISPATCHER_SHIP -- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER @@ -162,7 +162,7 @@ AI_CARGO_DISPATCHER_SHIP = { -- 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() +-- local ShippingLane = SET_GROUP:New():FilterPrefixes( "ShippingLane" ):FilterOnce():GetSetObjects() -- -- AICargoDispatcherShip = AI_CARGO_DISPATCHER_SHIP:New( SetShip, SetCargoInfantry, SetPickupZones, SetDeployZones, ShippingLane ) -- AICargoDispatcherShip:Start() diff --git a/Moose Development/Moose/AI/AI_Cargo_Helicopter.lua b/Moose Development/Moose/AI/AI_Cargo_Helicopter.lua index c0c0c1403..61294ba6d 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Helicopter.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Helicopter.lua @@ -9,7 +9,7 @@ -- @module AI.AI_Cargo_Helicopter -- @image AI_Cargo_Dispatching_For_Helicopters.JPG ---- @type AI_CARGO_HELICOPTER +-- @type AI_CARGO_HELICOPTER -- @extends Core.Fsm#FSM_CONTROLLABLE @@ -289,7 +289,7 @@ function AI_CARGO_HELICOPTER:SetLandingSpeedAndHeight(speed, height) return self end ---- @param #AI_CARGO_HELICOPTER self +-- @param #AI_CARGO_HELICOPTER self -- @param Wrapper.Group#GROUP Helicopter -- @param From -- @param Event @@ -328,7 +328,7 @@ function AI_CARGO_HELICOPTER:onafterLanded( Helicopter, From, Event, To ) end ---- @param #AI_CARGO_HELICOPTER self +-- @param #AI_CARGO_HELICOPTER self -- @param Wrapper.Group#GROUP Helicopter -- @param From -- @param Event @@ -411,7 +411,7 @@ function AI_CARGO_HELICOPTER:onafterQueue( Helicopter, From, Event, To, Coordina end ---- @param #AI_CARGO_HELICOPTER self +-- @param #AI_CARGO_HELICOPTER self -- @param Wrapper.Group#GROUP Helicopter -- @param From -- @param Event diff --git a/Moose Development/Moose/AI/AI_Cargo_Ship.lua b/Moose Development/Moose/AI/AI_Cargo_Ship.lua index 2073c7a27..259c2009f 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Ship.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Ship.lua @@ -9,7 +9,7 @@ -- @module AI.AI_Cargo_Ship -- @image AI_Cargo_Dispatcher.JPG ---- @type AI_CARGO_SHIP +-- @type AI_CARGO_SHIP -- @extends AI.AI_Cargo#AI_CARGO --- Brings a dynamic cargo handling capability for an AI naval group. diff --git a/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua b/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua index 14e8661a6..1b0370b54 100644 --- a/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua @@ -15,7 +15,7 @@ -- @image MOOSE.JPG ---- @type AI_ESCORT_DISPATCHER_REQUEST +-- @type AI_ESCORT_DISPATCHER_REQUEST -- @extends Core.Fsm#FSM @@ -35,7 +35,7 @@ AI_ESCORT_DISPATCHER_REQUEST = { ClassName = "AI_ESCORT_DISPATCHER_REQUEST", } ---- @field #list +-- @field #list AI_ESCORT_DISPATCHER_REQUEST.AI_Escorts = {} @@ -82,7 +82,7 @@ function AI_ESCORT_DISPATCHER_REQUEST:onafterStart( From, Event, To ) end ---- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param #AI_ESCORT_DISPATCHER_REQUEST self -- @param Core.Event#EVENTDATA EventData function AI_ESCORT_DISPATCHER_REQUEST:OnEventExit( EventData ) @@ -99,7 +99,7 @@ function AI_ESCORT_DISPATCHER_REQUEST:OnEventExit( EventData ) end ---- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param #AI_ESCORT_DISPATCHER_REQUEST self -- @param Core.Event#EVENTDATA EventData function AI_ESCORT_DISPATCHER_REQUEST:OnEventBirth( EventData ) diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua index 511e7ec2c..843f75160 100644 --- a/Moose Development/Moose/AI/AI_Escort_Request.lua +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -138,12 +138,12 @@ -- -- 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. --- --- # Developer Note --- --- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE --- Therefore, this class is considered to be deprecated --- + -- + -- # Developer Note + -- + -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE + -- Therefore, this class is considered to be deprecated + -- -- === -- -- ### Authors: **FlightControl** @@ -155,7 +155,7 @@ ---- @type AI_ESCORT_REQUEST +-- @type AI_ESCORT_REQUEST -- @extends AI.AI_Escort#AI_ESCORT --- AI_ESCORT_REQUEST class @@ -230,7 +230,7 @@ function AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, EscortAirbase, EscortNa return self end ---- @param #AI_ESCORT_REQUEST self +-- @param #AI_ESCORT_REQUEST self function AI_ESCORT_REQUEST:SpawnEscort() local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase, SPAWN.Takeoff.Hot ) @@ -255,7 +255,7 @@ function AI_ESCORT_REQUEST:SpawnEscort() self:_InitEscortMenus( EscortGroup ) self:_InitEscortRoute( EscortGroup ) - --- @param #AI_ESCORT self + -- @param #AI_ESCORT self -- @param Core.Event#EVENTDATA EventData function EscortGroup:OnEventDeadOrCrash( EventData ) self:F( { "EventDead", EventData } ) @@ -270,7 +270,7 @@ function AI_ESCORT_REQUEST:SpawnEscort() end ---- @param #AI_ESCORT_REQUEST self +-- @param #AI_ESCORT_REQUEST self -- @param Core.Set#SET_GROUP EscortGroupSet function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet ) @@ -292,14 +292,14 @@ function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet ) end ---- @param #AI_ESCORT_REQUEST self +-- @param #AI_ESCORT_REQUEST self -- @param Core.Set#SET_GROUP EscortGroupSet function AI_ESCORT_REQUEST:onafterStop( EscortGroupSet ) self:F() EscortGroupSet:ForEachGroup( - --- @param Wrapper.Group#GROUP EscortGroup + -- @param Core.Group#GROUP EscortGroup function( EscortGroup ) EscortGroup:WayPointInitialize() diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 4496ab65b..425585e90 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -34,8 +34,8 @@ -- @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 FollowGroup. --- @field DCS#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the FollowGroup. +-- @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. @@ -94,12 +94,12 @@ -- local LargeFormation = AI_FORMATION:New( LeaderUnit, FollowGroupSet, "Center Wing Formation", "Briefing" ) -- LargeFormation:FormationCenterWing( 500, 50, 0, 250, 250 ) -- LargeFormation:__Start( 1 ) --- --- # Developer Note --- --- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE --- Therefore, this class is considered to be deprecated --- + -- + -- # Developer Note + -- + -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE + -- Therefore, this class is considered to be deprecated + -- -- @field #AI_FORMATION AI_FORMATION = { ClassName = "AI_FORMATION", @@ -119,7 +119,7 @@ AI_FORMATION = { AI_FORMATION.__Enum = {} ---- @type AI_FORMATION.__Enum.Formation +-- @type AI_FORMATION.__Enum.Formation -- @field #number None -- @field #number Line -- @field #number Trail @@ -144,7 +144,7 @@ AI_FORMATION.__Enum.Formation = { Box = 10, } ---- @type AI_FORMATION.__Enum.Mode +-- @type AI_FORMATION.__Enum.Mode -- @field #number Mission -- @field #number Formation AI_FORMATION.__Enum.Mode = { @@ -154,12 +154,13 @@ AI_FORMATION.__Enum.Mode = { Reconnaissance = "R", } ---- @type AI_FORMATION.__Enum.ReportType +-- @type AI_FORMATION.__Enum.ReportType -- @field #number All -- @field #number Airborne -- @field #number GroundRadar -- @field #number Ground AI_FORMATION.__Enum.ReportType = { + All = "*", Airborne = "A", GroundRadar = "R", Ground = "G", @@ -997,7 +998,7 @@ function AI_FORMATION:SetFlightModeMission( FollowGroup ) FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission ) else self.FollowGroupSet:ForSomeGroupAlive( - --- @param Wrapper.Group#GROUP EscortGroup + -- @param Core.Group#GROUP EscortGroup function( FollowGroup ) FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission ) @@ -1021,7 +1022,7 @@ function AI_FORMATION:SetFlightModeAttack( FollowGroup ) FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Attack ) else self.FollowGroupSet:ForSomeGroupAlive( - --- @param Wrapper.Group#GROUP EscortGroup + -- @param Core.Group#GROUP EscortGroup function( FollowGroup ) FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Attack ) @@ -1045,7 +1046,7 @@ function AI_FORMATION:SetFlightModeFormation( FollowGroup ) FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) else self.FollowGroupSet:ForSomeGroupAlive( - --- @param Wrapper.Group#GROUP EscortGroup + -- @param Core.Group#GROUP EscortGroup function( FollowGroup ) FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) @@ -1223,6 +1224,7 @@ function AI_FORMATION:FollowMe(FollowGroup, ClientUnit, CT1, CV1, CT2, CV2) 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), } diff --git a/Moose Development/Moose/Actions/Act_Account.lua b/Moose Development/Moose/Actions/Act_Account.lua index 9a26f437c..83fd6bf5c 100644 --- a/Moose Development/Moose/Actions/Act_Account.lua +++ b/Moose Development/Moose/Actions/Act_Account.lua @@ -276,7 +276,7 @@ do -- ACT_ACCOUNT_DEADS --- DCS Events - --- @param #ACT_ACCOUNT_DEADS self + -- @param #ACT_ACCOUNT_DEADS self -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:OnEventHit( EventData ) self:T( { "EventDead", EventData } ) @@ -287,7 +287,7 @@ do -- ACT_ACCOUNT_DEADS end end - --- @param #ACT_ACCOUNT_DEADS self + -- @param #ACT_ACCOUNT_DEADS self -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:onfuncEventDead( EventData ) self:T( { "EventDead", EventData } ) @@ -299,7 +299,7 @@ do -- ACT_ACCOUNT_DEADS --- DCS Events - --- @param #ACT_ACCOUNT_DEADS self + -- @param #ACT_ACCOUNT_DEADS self -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:onfuncEventCrash( EventData ) self:T( { "EventDead", EventData } ) diff --git a/Moose Development/Moose/Actions/Act_Assist.lua b/Moose Development/Moose/Actions/Act_Assist.lua index badc69965..2017c6ab9 100644 --- a/Moose Development/Moose/Actions/Act_Assist.lua +++ b/Moose Development/Moose/Actions/Act_Assist.lua @@ -201,7 +201,7 @@ do -- ACT_ASSIST_SMOKE_TARGETS_ZONE function ACT_ASSIST_SMOKE_TARGETS_ZONE:onenterSmoking( ProcessUnit, From, Event, To ) self.TargetSetUnit:ForEachUnit( - --- @param Wrapper.Unit#UNIT SmokeUnit + -- @param Wrapper.Unit#UNIT SmokeUnit function( SmokeUnit ) if math.random( 1, ( 100 * self.TargetSetUnit:Count() ) / 4 ) <= 100 then SCHEDULER:New( self, diff --git a/Moose Development/Moose/Cargo/CargoCrate.lua b/Moose Development/Moose/Cargo/CargoCrate.lua index 7048e503f..b9dcb2761 100644 --- a/Moose Development/Moose/Cargo/CargoCrate.lua +++ b/Moose Development/Moose/Cargo/CargoCrate.lua @@ -81,7 +81,7 @@ do -- CARGO_CRATE return self end - --- @param #CARGO_CRATE self + -- @param #CARGO_CRATE self -- @param Core.Event#EVENTDATA EventData function CARGO_CRATE:OnEventCargoDead( EventData ) diff --git a/Moose Development/Moose/Cargo/CargoGroup.lua b/Moose Development/Moose/Cargo/CargoGroup.lua index 4532a21fa..b756f456c 100644 --- a/Moose Development/Moose/Cargo/CargoGroup.lua +++ b/Moose Development/Moose/Cargo/CargoGroup.lua @@ -601,7 +601,7 @@ do -- CARGO_GROUP end - --- Get the amount of cargo units in the group. + --- Get the underlying GROUP object from the CARGO_GROUP. -- @param #CARGO_GROUP self -- @return #CARGO_GROUP function CARGO_GROUP:GetGroup( Cargo ) diff --git a/Moose Development/Moose/Cargo/CargoSlingload.lua b/Moose Development/Moose/Cargo/CargoSlingload.lua index a8cb5022a..90209e8c0 100644 --- a/Moose Development/Moose/Cargo/CargoSlingload.lua +++ b/Moose Development/Moose/Cargo/CargoSlingload.lua @@ -74,7 +74,7 @@ do -- CARGO_SLINGLOAD end - --- @param #CARGO_SLINGLOAD self + -- @param #CARGO_SLINGLOAD self -- @param Core.Event#EVENTDATA EventData function CARGO_SLINGLOAD:OnEventCargoDead( EventData ) diff --git a/Moose Development/Moose/Core/ClientMenu.lua b/Moose Development/Moose/Core/ClientMenu.lua new file mode 100644 index 000000000..fea72914d --- /dev/null +++ b/Moose Development/Moose/Core/ClientMenu.lua @@ -0,0 +1,934 @@ +--- **Core** - Client Menu Management. +-- +-- **Main Features:** +-- +-- * For complex, non-static menu structures +-- * Lightweigt implementation as alternative to MENU +-- * Separation of menu tree creation from menu on the clients's side +-- * Works with a SET_CLIENT set of clients +-- * Allow manipulation of the shadow tree in various ways +-- * Push to all or only one client +-- * Change entries' menu text +-- * Option to make an entry usable once only across all clients +-- * Auto appends GROUP and CLIENT objects to menu calls +-- +-- === +-- +-- ### Author: **applevangelist** +-- +-- === +-- +-- @module Core.ClientMenu +-- @image Core_Menu.JPG +-- last change: Sept 2025 + +-- TODO +---------------------------------------------------------------------------------------------------------------- +-- +-- CLIENTMENU +-- +---------------------------------------------------------------------------------------------------------------- + +--- +-- @type CLIENTMENU +-- @field #string ClassName Class Name +-- @field #string lid Lid for log entries +-- @field #string version Version string +-- @field #string name Name +-- @field #string groupname Group name +-- @field #table path +-- @field #table parentpath +-- @field #CLIENTMENU Parent +-- @field Wrapper.Client#CLIENT client +-- @field #number GroupID Group ID +-- @field #number ID Entry ID +-- @field Wrapper.Group#GROUP group +-- @field #string UUID Unique ID based on path+name +-- @field #string Function +-- @field #table Functionargs +-- @field #table Children +-- @field #boolean Once +-- @field #boolean Generic +-- @field #boolean debug +-- @field #CLIENTMENUMANAGER Controller +-- @field #active boolean +-- @extends Core.Base#BASE + +--- +-- @field #CLIENTMENU +CLIENTMENU = { + ClassName = "CLIENTMENU", + lid = "", + version = "0.1.3", + name = nil, + path = nil, + group = nil, + client = nil, + GroupID = nil, + Children = {}, + Once = false, + Generic = false, + debug = false, + Controller = nil, + groupname = nil, + active = false, +} + +--- +-- @field #CLIENTMENU_ID +CLIENTMENU_ID = 0 + +--- Create an new CLIENTMENU object. +-- @param #CLIENTMENU self +-- @param Wrapper.Client#CLIENT Client The client for whom this entry is. Leave as nil for a generic entry. +-- @param #string Text Text of the F10 menu entry. +-- @param #CLIENTMENU Parent The parent menu entry. +-- @param #string Function (optional) Function to call when the entry is used. +-- @param ... (optional) Arguments for the Function, comma separated +-- @return #CLIENTMENU self +function CLIENTMENU:NewEntry(Client,Text,Parent,Function,...) + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) -- #CLIENTMENU + CLIENTMENU_ID = CLIENTMENU_ID + 1 + self.ID = CLIENTMENU_ID + if Client then + self.group = Client:GetGroup() + self.client = Client + self.GroupID = self.group:GetID() + self.groupname = self.group:GetName() or "Unknown Groupname" + else + self.Generic = true + end + self.name = Text or "unknown entry" + if Parent then + if Parent:IsInstanceOf("MENU_BASE") then + self.parentpath = Parent.MenuPath + else + self.parentpath = Parent:GetPath() + Parent:AddChild(self) + end + end + self.Parent = Parent + self.Function = Function + self.Functionargs = arg or {} + table.insert(self.Functionargs,self.group) + table.insert(self.Functionargs,self.client) + if self.Functionargs and self.debug then + self:T({"Functionargs",self.Functionargs}) + end + if not self.Generic and self.active == false then + if Function ~= nil then + local ErrorHandler = function( errmsg ) + env.info( "MOOSE Error in CLIENTMENU COMMAND function: " .. errmsg ) + if BASE.Debug ~= nil then + env.info( BASE.Debug.traceback() ) + end + return errmsg + end + self.CallHandler = function() + local function MenuFunction() + return self.Function( unpack( self.Functionargs ) ) + end + local Status, Result = xpcall( MenuFunction, ErrorHandler) + if self.Once == true then + self:Clear() + end + end + self.path = missionCommands.addCommandForGroup(self.GroupID,Text,self.parentpath, self.CallHandler) + self.active = true + else + self.path = missionCommands.addSubMenuForGroup(self.GroupID,Text,self.parentpath) + self.active = true + end + else + if self.parentpath then + self.path = UTILS.DeepCopy(self.parentpath) + else + self.path = {} + end + self.path[#self.path+1] = Text + end + self.UUID = table.concat(self.path,";") + self:T({self.UUID}) + self.Once = false + -- Log id. + self.lid=string.format("CLIENTMENU %s | %s | ", self.ID, self.name) + self:T(self.lid.."Created") + return self +end + +--- Create a UUID +-- @param #CLIENTMENU self +-- @param #CLIENTMENU Parent The parent object if any +-- @param #string Text The menu entry text +-- @return #string UUID +function CLIENTMENU:CreateUUID(Parent,Text) + local path = {} + if Parent and Parent.path then + path = Parent.path + end + path[#path+1] = Text + local UUID = table.concat(path,";") + return UUID +end + +--- Set the CLIENTMENUMANAGER for this entry. +-- @param #CLIENTMENU self +-- @param #CLIENTMENUMANAGER Controller The controlling object. +-- @return #CLIENTMENU self +function CLIENTMENU:SetController(Controller) + self.Controller = Controller + return self +end + +--- The entry will be deleted after being used used - for menu entries with functions only. +-- @param #CLIENTMENU self +-- @return #CLIENTMENU self +function CLIENTMENU:SetOnce() + self:T(self.lid.."SetOnce") + self.Once = true + return self +end + +--- Remove the entry from the F10 menu. +-- @param #CLIENTMENU self +-- @return #CLIENTMENU self +function CLIENTMENU:RemoveF10() + self:T(self.lid.."RemoveF10") + if self.GroupID then + --self:I(self.lid.."Removing "..table.concat(self.path,";")) + local function RemoveFunction() + return missionCommands.removeItemForGroup(self.GroupID , self.path ) + end + local status, err = pcall(RemoveFunction) + if not status then + self:I(string.format("**** Error Removing Menu Entry %s for %s!",tostring(self.name),self.groupname)) + end + self.active = false + end + return self +end + +--- Get the menu path table. +-- @param #CLIENTMENU self +-- @return #table Path +function CLIENTMENU:GetPath() + self:T(self.lid.."GetPath") + return self.path +end + +--- Get the UUID. +-- @param #CLIENTMENU self +-- @return #string UUID +function CLIENTMENU:GetUUID() + self:T(self.lid.."GetUUID") + return self.UUID +end + +--- Link a child entry. +-- @param #CLIENTMENU self +-- @param #CLIENTMENU Child The entry to link as a child. +-- @return #CLIENTMENU self +function CLIENTMENU:AddChild(Child) + self:T(self.lid.."AddChild "..Child.ID) + table.insert(self.Children,Child.ID,Child) + return self +end + +--- Remove a child entry. +-- @param #CLIENTMENU self +-- @param #CLIENTMENU Child The entry to remove from the children. +-- @return #CLIENTMENU self +function CLIENTMENU:RemoveChild(Child) + self:T(self.lid.."RemoveChild "..Child.ID) + table.remove(self.Children,Child.ID) + return self +end + +--- Remove all subentries (children) from this entry. +-- @param #CLIENTMENU self +-- @return #CLIENTMENU self +function CLIENTMENU:RemoveSubEntries() + self:T(self.lid.."RemoveSubEntries") + self:T({self.Children}) + for _id,_entry in pairs(self.Children) do + self:T("Removing ".._id) + if _entry then + _entry:RemoveSubEntries() + _entry:RemoveF10() + if _entry.Parent then + _entry.Parent:RemoveChild(self) + end + --if self.Controller then + --self.Controller:_RemoveByID(_entry.ID) + --end + --_entry = nil + end + end + return self +end + +--- Remove this entry and all subentries (children) from this entry. +-- @param #CLIENTMENU self +-- @return #CLIENTMENU self +function CLIENTMENU:Clear() + self:T(self.lid.."Clear") + for _id,_entry in pairs(self.Children) do + if _entry then + _entry:RemoveSubEntries() + _entry = nil + end + end + self:RemoveF10() + if self.Parent then + self.Parent:RemoveChild(self) + end + --if self.Controller then + --self.Controller:_RemoveByID(self.ID) + --end + return self +end + +-- TODO +---------------------------------------------------------------------------------------------------------------- +-- +-- CLIENTMENUMANAGER +-- +---------------------------------------------------------------------------------------------------------------- + + +--- Class CLIENTMENUMANAGER +-- @type CLIENTMENUMANAGER +-- @field #string ClassName Class Name +-- @field #string lid Lid for log entries +-- @field #string version Version string +-- @field #string name Name +-- @field Core.Set#SET_CLIENT clientset The set of clients this menu manager is for +-- @field #table flattree +-- @field #table rootentries +-- @field #table menutree +-- @field #number entrycount +-- @field #boolean debug +-- @field #table PlayerMenu +-- @field #number Coalition +-- @extends Core.Base#BASE + +--- *As a child my family's menu consisted of two choices: take it, or leave it.* +-- +-- === +-- +-- ## CLIENTMENU and CLIENTMENUMANAGER +-- +-- Manage menu structures for a SET_CLIENT of clients. +-- +-- ## Concept +-- +-- Separate creation of a menu tree structure from pushing it to each client. Create a shadow "reference" menu structure tree for your client pilot's in a mission. +-- This can then be propagated to all clients. Manipulate the entries in the structure with removing, clearing or changing single entries, create replacement sub-structures +-- for entries etc, push to one or all clients. +-- +-- Many functions can either change the tree for one client or for all clients. +-- +-- ## Conceptual remarks +-- +-- There's a couple of things to fully understand: +-- +-- 1) **CLIENTMENUMANAGER** manages a set of entries from **CLIENTMENU**, it's main purpose is to administer the *shadow menu tree*, ie. a menu structure which is not +-- (yet) visible to any client +-- 2) The entries are **CLIENTMENU** objects, which are linked in a tree form. There's two ways to create them: +-- A) in the manager with ":NewEntry()" which initially +-- adds it to the shadow menu **only** +-- B) stand-alone directly as `CLIENTMENU:NewEntry()` - here it depends on whether or not you gave a CLIENT object if the entry is created as generic entry or pushed +-- a **specific** client. **Be aware** though that the entries are not managed by the CLIENTMANAGER before the next step! +-- A generic entry can be added to the manager (and the shadow tree) with `:AddEntry()` - this will also push it to all clients(!) if no client is given, or a specific client only. +-- 3) Pushing only works for alive clients. +-- 4) Live and shadow tree entries are managed via the CLIENTMENUMANAGER object. +-- 5) `Propagate()`refreshes the menu tree for all, or a single client. +-- +-- ## Create a base reference tree and send to all clients +-- +-- local clientset = SET_CLIENT:New():FilterStart() +-- +-- local menumgr = CLIENTMENUMANAGER:New(clientset,"Dayshift") +-- local mymenu = menumgr:NewEntry("Top") +-- local mymenu_lv1a = menumgr:NewEntry("Level 1 a",mymenu) +-- local mymenu_lv1b = menumgr:NewEntry("Level 1 b",mymenu) +-- -- next one is a command menu entry, which can only be used once +-- local mymenu_lv1c = menumgr:NewEntry("Action Level 1 c",mymenu, testfunction, "testtext"):SetOnce() +-- +-- local mymenu_lv2a = menumgr:NewEntry("Go here",mymenu_lv1a) +-- local mymenu_lv2b = menumgr:NewEntry("Level 2 ab",mymenu_lv1a) +-- local mymenu_lv2c = menumgr:NewEntry("Level 2 ac",mymenu_lv1a) +-- +-- local mymenu_lv2ba = menumgr:NewEntry("Level 2 ba",mymenu_lv1b) +-- local mymenu_lv2bb = menumgr:NewEntry("Level 2 bb",mymenu_lv1b) +-- local mymenu_lv2bc = menumgr:NewEntry("Level 2 bc",mymenu_lv1b) +-- +-- local mymenu_lv3a = menumgr:NewEntry("Level 3 aaa",mymenu_lv2a) +-- local mymenu_lv3b = menumgr:NewEntry("Level 3 aab",mymenu_lv2a) +-- local mymenu_lv3c = menumgr:NewEntry("Level 3 aac",mymenu_lv2a) +-- +-- menumgr:Propagate() -- propagate **once** to all clients in the SET_CLIENT +-- +-- ## Remove a single entry's subtree +-- +-- menumgr:RemoveSubEntries(mymenu_lv3a) +-- + -- ## Remove a single entry and also it's subtree +-- +-- menumgr:DeleteEntry(mymenu_lv3a) +-- +-- ## Add a single entry +-- +-- local baimenu = menumgr:NewEntry("BAI",mymenu_lv1b) +-- +-- menumgr:AddEntry(baimenu) +-- +-- ## Add an entry with a function +-- +-- local baimenu = menumgr:NewEntry("Task Action", mymenu_lv1b, TestFunction, Argument1, Argument1) +-- +-- Now, the class will **automatically append the call with GROUP and CLIENT objects**, as this is can only be done when pushing the entry to the clients. So, the actual function implementation needs to look like this: +-- +-- function TestFunction( Argument1, Argument2, Group, Client) +-- +-- **Caveat is**, that you need to ensure your arguments are not **nil** or **false**, as LUA will optimize those away. You would end up having Group and Client in wrong places in the function call. Hence, +-- if you need/ want to send **nil** or **false**, send a place holder instead and ensure your function can handle this, e.g. +-- +-- local baimenu = menumgr:NewEntry("Task Action", mymenu_lv1b, TestFunction, "nil", Argument1) +-- +-- ## Change the text of a leaf entry in the menu tree +-- +-- menumgr:ChangeEntryTextForAll(mymenu_lv1b,"Attack") +-- +-- ## Reset a single clients menu tree +-- +-- menumgr:ResetMenu(client) +-- +-- ## Reset all and clear the reference tree +-- +-- menumgr:ResetMenuComplete() +-- +-- ## Set to auto-propagate for CLIENTs joining the SET_CLIENT **after** the script is loaded - handy if you have a single menu tree. +-- +-- menumgr:InitAutoPropagation() +-- +-- @field #CLIENTMENUMANAGER +CLIENTMENUMANAGER = { + ClassName = "CLIENTMENUMANAGER", + lid = "", + version = "0.1.7", + name = nil, + clientset = nil, + menutree = {}, + flattree = {}, + playertree = {}, + entrycount = 0, + rootentries = {}, + debug = true, + PlayerMenu = {}, + Coalition = nil, +} + +--- Create a new ClientManager instance. +-- @param #CLIENTMENUMANAGER self +-- @param Core.Set#SET_CLIENT ClientSet The set of clients to manage. +-- @param #string Alias The name of this manager. +-- @param #number Coalition (Optional) Coalition of this Manager, defaults to coalition.side.BLUE +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:New(ClientSet, Alias, Coalition) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, BASE:New()) -- #CLIENTMENUMANAGER + self.clientset = ClientSet + self.PlayerMenu = {} + self.name = Alias or "Nightshift" + self.Coalition = Coalition or coalition.side.BLUE + -- Log id. + self.lid=string.format("CLIENTMENUMANAGER %s | %s | ", self.version, self.name) + if self.debug then + self:I(self.lid.."Created") + end + return self +end + +--- [Internal] Event handling +-- @param #CLIENTMENUMANAGER self +-- @param Core.Event#EVENTDATA EventData +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:_EventHandler(EventData,Retry) + self:T(self.lid.."_EventHandler: "..EventData.id) + --self:I(self.lid.."_EventHandler: "..tostring(EventData.IniPlayerName)) + if EventData.id == EVENTS.PlayerLeaveUnit or EventData.id == EVENTS.Ejection or EventData.id == EVENTS.Crash or EventData.id == EVENTS.PilotDead then + self:T(self.lid.."Leave event for player: "..tostring(EventData.IniPlayerName)) + local Client = _DATABASE:FindClient( EventData.IniUnitName ) + if Client then + self:ResetMenu(Client) + end + elseif (EventData.id == EVENTS.PlayerEnterAircraft) and EventData.IniCoalition == self.Coalition then + if EventData.IniPlayerName and EventData.IniGroup then + if (not self.clientset:IsIncludeObject(_DATABASE:FindClient( EventData.IniUnitName ))) then + self:T(self.lid.."Client not in SET: "..EventData.IniPlayerName) + if not Retry then + -- try again in 2 secs + self:ScheduleOnce(2,CLIENTMENUMANAGER._EventHandler,self,EventData,true) + end + return self + end + --self:I(self.lid.."Join event for player: "..EventData.IniPlayerName) + local player = _DATABASE:FindClient( EventData.IniUnitName ) + self:Propagate(player) + end + elseif EventData.id == EVENTS.PlayerEnterUnit then + -- special for CA slots + local grp = GROUP:FindByName(EventData.IniGroupName) + if grp:IsGround() then + self:T(string.format("Player %s entered GROUND unit %s!",EventData.IniPlayerName,EventData.IniUnitName)) + local IsPlayer = EventData.IniDCSUnit:getPlayerName() + if IsPlayer then + + local client=_DATABASE.CLIENTS[EventData.IniDCSUnitName] --Wrapper.Client#CLIENT + + -- Add client in case it does not exist already. + if not client then + + -- Debug info. + self:I(string.format("Player '%s' joined ground unit '%s' of group '%s'", tostring(EventData.IniPlayerName), tostring(EventData.IniDCSUnitName), tostring(EventData.IniDCSGroupName))) + + client=_DATABASE:AddClient(EventData.IniDCSUnitName) + + -- Add player. + client:AddPlayer(EventData.IniPlayerName) + + -- Add player. + if not _DATABASE.PLAYERS[EventData.IniPlayerName] then + _DATABASE:AddPlayer( EventData.IniUnitName, EventData.IniPlayerName ) + end + + -- Player settings. + local Settings = SETTINGS:Set( EventData.IniPlayerName ) + Settings:SetPlayerMenu(EventData.IniUnit) + end + --local player = _DATABASE:FindClient( EventData.IniPlayerName ) + self:Propagate(client) + end + end + end + + return self +end + +--- Set this Client Manager to auto-propagate menus **once** to newly joined players. Useful if you have **one** menu structure only. Does not automatically push follow-up changes to the client(s). +-- @param #CLIENTMENUMANAGER self +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:InitAutoPropagation() + -- Player Events + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler) + self:HandleEvent(EVENTS.Ejection, self._EventHandler) + self:HandleEvent(EVENTS.Crash, self._EventHandler) + self:HandleEvent(EVENTS.PilotDead, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) + self:SetEventPriority(6) + return self +end + +--- Create a new entry in the **generic** structure. +-- @param #CLIENTMENUMANAGER self +-- @param #string Text Text of the F10 menu entry. +-- @param #CLIENTMENU Parent The parent menu entry. +-- @param #string Function (optional) Function to call when the entry is used. +-- @param ... (optional) Arguments for the Function, comma separated. +-- @return #CLIENTMENU Entry +function CLIENTMENUMANAGER:NewEntry(Text,Parent,Function,...) + self:T(self.lid.."NewEntry "..Text or "None") + self.entrycount = self.entrycount + 1 + local entry = CLIENTMENU:NewEntry(nil,Text,Parent,Function,unpack(arg)) + if not Parent then + self.rootentries[self.entrycount] = entry + end + local depth = #entry.path + if not self.menutree[depth] then self.menutree[depth] = {} end + table.insert(self.menutree[depth],entry.UUID) + self.flattree[entry.UUID] = entry + return entry +end + +--- Check matching entry in the generic structure by UUID. +-- @param #CLIENTMENUMANAGER self +-- @param #string UUID UUID of the menu entry. +-- @return #boolean Exists +function CLIENTMENUMANAGER:EntryUUIDExists(UUID) + local exists = self.flattree[UUID] and true or false + return exists +end + +--- Find matching entry in the generic structure by UUID. +-- @param #CLIENTMENUMANAGER self +-- @param #string UUID UUID of the menu entry. +-- @return #CLIENTMENU Entry The #CLIENTMENU object found or nil. +function CLIENTMENUMANAGER:FindEntryByUUID(UUID) + self:T(self.lid.."FindEntryByUUID "..UUID or "None") + local entry = nil + for _gid,_entry in pairs(self.flattree) do + local Entry = _entry -- #CLIENTMENU + if Entry and Entry.UUID == UUID then + entry = Entry + end + end + return entry +end + +--- Find matching entries by text in the generic structure by UUID. +-- @param #CLIENTMENUMANAGER self +-- @param #string Text Text or partial text of the menu entry to find. +-- @param #CLIENTMENU Parent (Optional) Only find entries under this parent entry. +-- @return #table Table of matching UUIDs of #CLIENTMENU objects +-- @return #table Table of matching #CLIENTMENU objects +-- @return #number Number of matches +function CLIENTMENUMANAGER:FindUUIDsByText(Text,Parent) + self:T(self.lid.."FindUUIDsByText "..Text or "None") + local matches = {} + local entries = {} + local n = 0 + for _uuid,_entry in pairs(self.flattree) do + local Entry = _entry -- #CLIENTMENU + if Parent then + if Entry and string.find(Entry.name,Text,1,true) and string.find(Entry.UUID,Parent.UUID,1,true) then + table.insert(matches,_uuid) + table.insert(entries,Entry ) + n=n+1 + end + else + if Entry and string.find(Entry.name,Text,1,true) then + table.insert(matches,_uuid) + table.insert(entries,Entry ) + n=n+1 + end + end + end + return matches, entries, n +end + +--- Find matching entries in the generic structure by the menu text. +-- @param #CLIENTMENUMANAGER self +-- @param #string Text Text or partial text of the F10 menu entry. +-- @param #CLIENTMENU Parent (Optional) Only find entries under this parent entry. +-- @return #table Table of matching #CLIENTMENU objects. +-- @return #number Number of matches +function CLIENTMENUMANAGER:FindEntriesByText(Text,Parent) + self:T(self.lid.."FindEntriesByText "..Text or "None") + local matches, objects, number = self:FindUUIDsByText(Text, Parent) + return objects, number +end + +--- Find matching entries under a parent in the generic structure by UUID. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Parent Find entries under this parent entry. +-- @return #table Table of matching UUIDs of #CLIENTMENU objects +-- @return #table Table of matching #CLIENTMENU objects +-- @return #number Number of matches +function CLIENTMENUMANAGER:FindUUIDsByParent(Parent) + self:T(self.lid.."FindUUIDsByParent") + local matches = {} + local entries = {} + local n = 0 + for _uuid,_entry in pairs(self.flattree) do + local Entry = _entry -- #CLIENTMENU + if Parent then + if Entry and string.find(Entry.UUID,Parent.UUID,1,true) then + table.insert(matches,_uuid) + table.insert(entries,Entry ) + n=n+1 + end + end + end + return matches, entries, n +end + +--- Find matching entries in the generic structure under a parent. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Parent Find entries under this parent entry. +-- @return #table Table of matching #CLIENTMENU objects. +-- @return #number Number of matches +function CLIENTMENUMANAGER:FindEntriesByParent(Parent) + self:T(self.lid.."FindEntriesByParent") + local matches, objects, number = self:FindUUIDsByParent(Parent) + return objects, number +end + +--- Alter the text of a leaf entry in the generic structure and push to one specific client's F10 menu. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The menu entry. +-- @param #string Text New Text of the F10 menu entry. +-- @param Wrapper.Client#CLIENT Client (optional) The client for whom to alter the entry, if nil done for all clients. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:ChangeEntryText(Entry, Text, Client) + self:T(self.lid.."ChangeEntryText "..Text or "None") + local newentry = CLIENTMENU:NewEntry(nil,Text,Entry.Parent,Entry.Function,unpack(Entry.Functionargs)) + self:DeleteF10Entry(Entry,Client) + self:DeleteGenericEntry(Entry) + if not Entry.Parent then + self.rootentries[self.entrycount] = newentry + end + local depth = #newentry.path + if not self.menutree[depth] then self.menutree[depth] = {} end + table.insert(self.menutree[depth],newentry.UUID) + self.flattree[newentry.UUID] = newentry + self:AddEntry(newentry,Client) + return self +end + +--- Push the complete menu structure to each of the clients in the set - refresh the menu tree of the clients. +-- @param #CLIENTMENUMANAGER self +-- @param Wrapper.Client#CLIENT Client (optional) If given, propagate only for this client. +-- @return #CLIENTMENU Entry +function CLIENTMENUMANAGER:Propagate(Client) + self:T(self.lid.."Propagate") + --self:I(UTILS.PrintTableToLog(Client,1)) + local knownunits = {} -- track so we can ID multi seated + local Set = self.clientset.Set + if Client then + Set = {Client} + end + self:ResetMenu(Client) + for _,_client in pairs(Set) do + local client = _client -- Wrapper.Client#CLIENT + if client and client:IsAlive() then + local playerunit = client:GetName() + --local playergroup = client:GetGroup() + local playername = client:GetPlayerName() or "none" + if not knownunits[playerunit] then + knownunits[playerunit] = true + else + self:I("Player in multi seat unit: "..playername) + break -- multi seat already build + end + if not self.playertree[playername] then + self.playertree[playername] = {} + end + for level,branch in pairs (self.menutree) do + self:T("Building branch:" .. level) + for _,leaf in pairs(branch) do + self:T("Building leaf:" .. leaf) + local entry = self:FindEntryByUUID(leaf) + if entry then + self:T("Found generic entry:" .. entry.UUID) + local parent = nil + if entry.Parent and entry.Parent.UUID then + parent = self.playertree[playername][entry.Parent.UUID] or self:FindEntryByUUID(entry.Parent.UUID) + end + self.playertree[playername][entry.UUID] = CLIENTMENU:NewEntry(client,entry.name,parent,entry.Function,unpack(entry.Functionargs)) + self.playertree[playername][entry.UUID].Once = entry.Once + else + self:T("NO generic entry for:" .. leaf) + end + end + end + end + end + return self +end + +--- Push a single previously created entry into the F10 menu structure of all clients. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The entry to add. +-- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:AddEntry(Entry,Client) + self:T(self.lid.."AddEntry") + local Set = self.clientset.Set + local knownunits = {} + if Client then + Set = {Client} + end + for _,_client in pairs(Set) do + local client = _client -- Wrapper.Client#CLIENT + if client and client:IsAlive() then + local playername = client:GetPlayerName() or "None" + local unitname = client:GetName() + if not knownunits[unitname] then + knownunits[unitname] = true + else + self:I("Player in multi seat unit: "..playername) + break + end + if Entry then + self:T("Adding generic entry:" .. Entry.UUID) + local parent = nil + if not self.playertree[playername] then + self.playertree[playername] = {} + end + if Entry.Parent and Entry.Parent.UUID then + parent = self.playertree[playername][Entry.Parent.UUID] or self:FindEntryByUUID(Entry.Parent.UUID) + end + self.playertree[playername][Entry.UUID] = CLIENTMENU:NewEntry(client,Entry.name,parent,Entry.Function,unpack(Entry.Functionargs)) + self.playertree[playername][Entry.UUID].Once = Entry.Once + else + self:T("NO generic entry given") + end + end + end + return self +end + +--- Blank out the menu - remove **all root entries** and all entries below from the client's F10 menus, leaving the generic structure untouched. +-- @param #CLIENTMENUMANAGER self +-- @param Wrapper.Client#CLIENT Client (optional) If given, remove only for this client. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:ResetMenu(Client) + self:T(self.lid.."ResetMenu") + for _,_entry in pairs(self.rootentries) do + --local RootEntry = self.structure.generic[_entry] + if _entry then + self:DeleteF10Entry(_entry,Client) + end + end + return self +end + +--- Blank out the menu - remove **all root entries** and all entries below from all clients' F10 menus, and **delete** the generic structure. +-- @param #CLIENTMENUMANAGER self +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:ResetMenuComplete() + self:T(self.lid.."ResetMenuComplete") + for _,_entry in pairs(self.rootentries) do + --local RootEntry = self.structure.generic[_entry] + if _entry then + self:DeleteF10Entry(_entry) + end + end + self.playertree = nil + self.playertree = {} + self.rootentries = nil + self.rootentries = {} + self.menutree = nil + self.menutree = {} + return self +end + +--- Remove the entry and all entries below the given entry from the client's F10 menus. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The entry to remove +-- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:DeleteEntry(Entry,Client) + self:T(self.lid.."DeleteEntry") + return self:DeleteF10Entry(Entry,Client) +end + +--- Remove the entry and all entries below the given entry from the client's F10 menus. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The entry to remove +-- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:DeleteF10Entry(Entry,Client) + self:T(self.lid.."DeleteF10Entry") + local Set = self.clientset.Set + if Client then + Set = {Client} + end + for _,_client in pairs(Set) do + if _client and _client:IsAlive() then + local playername = _client:GetPlayerName() + if self.playertree[playername] then + local centry = self.playertree[playername][Entry.UUID] -- #CLIENTMENU + if centry then + --self:I("Match for "..Entry.UUID) + centry:Clear() + end + end + end + end + return self +end + +--- Remove the entry and all entries below the given entry from the generic tree. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The entry to remove +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:DeleteGenericEntry(Entry) + self:T(self.lid.."DeleteGenericEntry") + + if Entry.Children and #Entry.Children > 0 then + self:RemoveGenericSubEntries(Entry) + end + + local depth = #Entry.path + local uuid = Entry.UUID + + local tbl = UTILS.DeepCopy(self.menutree) + + if tbl[depth] then + for i=depth,#tbl do + --self:I("Level = "..i) + for _id,_uuid in pairs(tbl[i]) do + self:T(_uuid) + if string.find(_uuid,uuid,1,true) or _uuid == uuid then + --self:I("Match for ".._uuid) + self.menutree[i][_id] = nil + self.flattree[_uuid] = nil + end + end + end + end + + return self +end + +--- Remove all entries below the given entry from the generic tree. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The entry where to start. This entry stays. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:RemoveGenericSubEntries(Entry) + self:T(self.lid.."RemoveGenericSubEntries") + + local depth = #Entry.path + 1 + local uuid = Entry.UUID + + local tbl = UTILS.DeepCopy(self.menutree) + + if tbl[depth] then + for i=depth,#tbl do + self:T("Level = "..i) + for _id,_uuid in pairs(tbl[i]) do + self:T(_uuid) + if string.find(_uuid,uuid,1,true) then + self:T("Match for ".._uuid) + self.menutree[i][_id] = nil + self.flattree[_uuid] = nil + end + end + end + end + return self +end + + +--- Remove all entries below the given entry from the client's F10 menus. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The entry where to start. This entry stays. +-- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. In this case the generic structure will not be touched. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:RemoveF10SubEntries(Entry,Client) + self:T(self.lid.."RemoveSubEntries") + local Set = self.clientset.Set + if Client then + Set = {Client} + end + for _,_client in pairs(Set) do + if _client and _client:IsAlive() then + local playername = _client:GetPlayerName() + if self.playertree[playername] then + local centry = self.playertree[playername][Entry.UUID] -- #CLIENTMENU + centry:RemoveSubEntries() + end + end + end + return self +end + +---------------------------------------------------------------------------------------------------------------- +-- +-- End ClientMenu +-- +---------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Core/Fsm.lua b/Moose Development/Moose/Core/Fsm.lua index b72ca63d9..e86b88267 100644 --- a/Moose Development/Moose/Core/Fsm.lua +++ b/Moose Development/Moose/Core/Fsm.lua @@ -79,7 +79,8 @@ do -- FSM - --- @type FSM + --- FSM class + -- @type FSM -- @field #string ClassName Name of the class. -- @field Core.Scheduler#SCHEDULER CallScheduler Call scheduler. -- @field #table options Options. @@ -948,8 +949,9 @@ do -- FSM end do -- FSM_CONTROLLABLE - - --- @type FSM_CONTROLLABLE + + --- + -- @type FSM_CONTROLLABLE -- @field Wrapper.Controllable#CONTROLLABLE Controllable -- @extends Core.Fsm#FSM @@ -1081,8 +1083,9 @@ do -- FSM_CONTROLLABLE end do -- FSM_PROCESS - - --- @type FSM_PROCESS + + --- + -- @type FSM_PROCESS -- @field Tasking.Task#TASK Task -- @extends Core.Fsm#FSM_CONTROLLABLE diff --git a/Moose Development/Moose/Core/Goal.lua b/Moose Development/Moose/Core/Goal.lua index bc33246f6..cff272dbd 100644 --- a/Moose Development/Moose/Core/Goal.lua +++ b/Moose Development/Moose/Core/Goal.lua @@ -24,7 +24,7 @@ do -- Goal - --- @type 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. @@ -71,10 +71,10 @@ do -- Goal ClassName = "GOAL", } - --- @field #table GOAL.Players + -- @field #table GOAL.Players GOAL.Players = {} - --- @field #number GOAL.TotalContributions + -- @field #number GOAL.TotalContributions GOAL.TotalContributions = 0 --- GOAL Constructor. @@ -145,7 +145,7 @@ do -- Goal self.TotalContributions = self.TotalContributions + 1 end - --- @param #GOAL self + -- @param #GOAL self -- @param #number Player contribution. function GOAL:GetPlayerContribution( PlayerName ) return self.Players[PlayerName] or 0 diff --git a/Moose Development/Moose/Core/Pathline.lua b/Moose Development/Moose/Core/Pathline.lua index 8fdda2bc6..bc6e9305e 100644 --- a/Moose Development/Moose/Core/Pathline.lua +++ b/Moose Development/Moose/Core/Pathline.lua @@ -49,7 +49,7 @@ -- -- # Mark on F10 map -- --- The ponints of the PATHLINE can be marked on the F10 map with the @{#PATHLINE.MarkPoints}(`true`) function. The mark points contain information of the surface type, land height and +-- The points of the PATHLINE can be marked on the F10 map with the @{#PATHLINE.MarkPoints}(`true`) function. The mark points contain information of the surface type, land height and -- water depth. -- -- To remove the marks, use @{#PATHLINE.MarkPoints}(`false`). @@ -69,11 +69,12 @@ PATHLINE = { -- @field #number landHeight Land height in meters. -- @field #number depth Water depth in meters. -- @field #number markerID Marker ID. +-- @field #number lineID Line marker ID. --- PATHLINE class version. -- @field #string version -PATHLINE.version="0.1.1" +PATHLINE.version="0.2.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -301,18 +302,42 @@ function PATHLINE:GetPoint2DFromIndex(n) return nil end +--- Calculate the length of the line. +-- @param #PATHLINE self +-- @param #boolean Project2D Calculate 2D distance between points. +-- @return #number Length in meters. +function PATHLINE:GetLength(Project2D) + local l=0 + + local np=#self.points + + for i=1,np-1 do + local p1=self.points[i] --#PATHLINE.Point + local p2=self.points[i+1] --#PATHLINE.Point + + if Project2D then + l=l+UTILS.VecDist2D(p1.vec2, p2.vec2) + else + l=l+UTILS.VecDist3D(p1.vec3, p2.vec3) + end + + end + + return l +end + --- Mark points on F10 map. -- @param #PATHLINE self -- @param #boolean Switch If `true` or nil, set marks. If `false`, remove marks. --- @return List of DCS#Vec3 points. +-- @return #PATHLINE self function PATHLINE:MarkPoints(Switch) for i,_point in pairs(self.points) do local point=_point --#PATHLINE.Point if Switch==false then if point.markerID then - UTILS.RemoveMark(point.markerID, Delay) + UTILS.RemoveMark(point.markerID) end else @@ -329,6 +354,56 @@ function PATHLINE:MarkPoints(Switch) end end + return self +end + +--- Draw line on F10 map. +-- @param #PATHLINE self +-- @param #number Recipient Recipent of the line: -1=All. +-- @param #table Color Color as RGB table plus alpha value. Default {1, 0, 0, 1.0}. +-- @param #number LineType Line type: 1=Solid (default). +-- @return #PATHLINE self +function PATHLINE:DrawLine(Recipient, Color, LineType) + + -- Input + Recipient= Recipient or -1 + Color= Color or {1,0,0, 1.0} + LineType=LineType or 1 + local ReadOnly=false + + + local np=#self.points + + for i=1,np-1 do + local p1=self.points[i] --#PATHLINE.Point + local p2=self.points[i+1] --#PATHLINE.Point + + p1.lineID = UTILS.GetMarkID() + + trigger.action.lineToAll(Recipient, p1.lineID, p1.vec3, p2.vec3, Color, LineType, ReadOnly, "") + + end + + return self +end + +--- Remove line on F10 map. +-- @param #PATHLINE self +-- @param #number Delay Delay in seconds before line is removed. +-- @return #PATHLINE self +function PATHLINE:UnDrawLine(Delay) + + + local np=#self.points + + for _,_point in pairs(self.points) do + local p=_point --#PATHLINE.Point + if p.lineID then + UTILS.RemoveMark(p.lineID, Delay) + end + end + + return self end diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index fcf67ca02..066c358ec 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -3,8 +3,6 @@ -- ## 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. -- -- === -- @@ -2060,6 +2058,40 @@ do -- COORDINATE return Path, Way, GotPath end + + --- Returns a table of coordinates to a destination using only roads or railroads. + -- The first point is the closest point on road of the given coordinate. + -- By default, the last point is the closest point on road of the ToCoord. Hence, the coordinate itself and the final ToCoord are not necessarily included in the path. + -- @param #COORDINATE self + -- @param #COORDINATE ToCoord Coordinate of destination. + -- @param #boolean IncludeEndpoints (Optional) Include the coordinate itself and the ToCoordinate in the path. + -- @param #boolean Railroad (Optional) If true, path on railroad is returned. Default false. + -- @return Core.Pathline#PATHLINE Pathline containing the points on road. If no path on road can be found, nil is returned or just the endpoints. + function COORDINATE:GetPathlineOnRoad(ToCoord, IncludeEndpoints, Railroad) + + -- Set road type. + local RoadType="roads" + if Railroad==true then + RoadType="railroads" + end + + -- DCS API function returning a table of vec2. + local path = land.findPathOnRoads(RoadType, self.x, self.z, ToCoord.x, ToCoord.z) + + if IncludeEndpoints then + path=path or {} + table.insert(path, 1, self:GetVec2()) + table.insert(path, ToCoord:GetVec2()) + end + + local pathline=nil + if path then + pathline=PATHLINE:NewFromVec2Array(RoadType, path) + end + + return pathline + end + --- Gets the surface type at the coordinate. -- @param #COORDINATE self @@ -3841,171 +3873,3 @@ do -- COORDINATE end end - -do - - --- 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 COORDINATE 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. - -- - -- **DEPRECATED - PLEASE USE COORDINATE!** - -- - -- **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 - -end - -do - - --- @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. - -- - -- **DEPRECATED - PLEASE USE COORDINATE!** - -- - -- ## 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 - -end diff --git a/Moose Development/Moose/Core/Report.lua b/Moose Development/Moose/Core/Report.lua index d8225adcc..8d55f05d3 100644 --- a/Moose Development/Moose/Core/Report.lua +++ b/Moose Development/Moose/Core/Report.lua @@ -15,7 +15,8 @@ -- @module Core.Report -- @image Core_Report.JPG ---- @type REPORT +--- +-- @type REPORT -- @extends Core.Base#BASE --- Provides a handy means to create messages and reports. diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index 8e062d82b..7f0102943 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -1,4 +1,4 @@ ---- **Core** - SCHEDULEDISPATCHER dispatches the different schedules. +---- **Core** - SCHEDULEDISPATCHER dispatches the different schedules. -- -- === -- diff --git a/Moose Development/Moose/Core/UserFlag.lua b/Moose Development/Moose/Core/UserFlag.lua index 1c47e787f..94bff742a 100644 --- a/Moose Development/Moose/Core/UserFlag.lua +++ b/Moose Development/Moose/Core/UserFlag.lua @@ -18,7 +18,7 @@ do -- UserFlag - --- @type USERFLAG + -- @type USERFLAG -- @field #string ClassName Name of the class -- @field #string UserFlagName Name of the flag. -- @extends Core.Base#BASE diff --git a/Moose Development/Moose/Core/Vector.lua b/Moose Development/Moose/Core/Vector.lua new file mode 100644 index 000000000..ce8ab3631 --- /dev/null +++ b/Moose Development/Moose/Core/Vector.lua @@ -0,0 +1,1381 @@ +--- **CORE** - Vector algebra. +-- +-- **Main Features:** +-- +-- * Easy vector algebra function +-- * Redefinition of `+`, `-`, `*`, `/`, `%` operators to be compatible with vectors +-- * Interface to DCS API functions +-- * Reduced confusion of DCS coordinate system +-- * Better performance than related classes (COORDINATE, POINT_VEC) +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Core%20-%20Vector). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Core.Vector +-- @image CORE_Vector.png + + +--- VECTOR class. +-- @type VECTOR +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field #number x Component pointing North if > 0 and South if < 0. +-- @field #number y Component pointing up if >0 and down if < 0. This describes the altitude above main sea level. +-- @field #number z Component pointing East if > 0 and West if < 0. + +--- *Mathematics knows no races or geographic boundaries; for mathematics, the cultural world is one country.* --David Hilbert +-- +-- === +-- +-- # The VECTOR Concept +-- +-- The VECTOR class has a great concept! +-- +-- https://github.com/automattf/vector.lua/blob/master/vector.lua +-- +-- # The DCS Coordinate System +-- +-- DCS has a rather unconventional way to define the coordinate system. The definition even depends whether you work with a 2D vector or a 3D vector. +-- The good think is, that you usually do not need to worry about this unless you directly call the DCS API functions. Plus, this class tries to +-- hide the differences between 2D and 3D conventions as good as possible with internal if-cases. +-- +-- Still, it is important to unterstand the differences. So let us explain: +-- +-- Usually, we draw a coordinate system in 2D and label the horizonal axis (pointing right) as the x-axis. The vertical axis (poining up on a piece of paper) +-- +-- ## 3D +-- +-- The x-axis points North. The z-axis points East. The y-axis points upwards and defines the altitue with respect to the mean sea level. +-- +-- ## 2D +-- +-- The x-axis points North (just like in the 3D) case. The y-axis points East. +-- +-- # Constructors +-- +-- There are different ways to create a new instance of a VECTOR. All methods start with `New`. +-- +-- ## From Components +-- +-- ## From 2D or 3D Vectors +-- +-- ## From Polar Coordinates +-- +-- ## From Spherical Coordinates +-- +-- +-- # Operators +-- +-- ## Addition [MATH] `+` +-- +-- ## Subtraction [MATH] `-` +-- +-- ## Multiplication [MATH] `*` +-- +-- ## Devision [MATH] `/` +-- +-- ## Modulo [MATH] `%` +-- +-- +-- # DCS API Interface +-- +-- This class offers easy and convenient ways to access all vector related DCS API functions. +-- +-- ## Land and Surface +-- +-- ## Atmosphere +-- +-- ## Effects and Actions +-- +-- ## Map Markings +-- +-- ## Coordinates +-- +-- # Inferface to other MOOSE Classes +-- +-- Of course, this class is interfaced with other MOOSE classes in the sense that you can obtain and work with VECTOR instances from MOOSE objects, from which a position or direction +-- vector can be derived. +-- +-- ## GROUP +-- +-- ## UNIT +-- +-- ## STATIC +-- +-- ## SCENERY +-- +-- ## ZONE +-- +-- # Examples +-- +-- A new `VECTOR` object can be created with the @{#VECTOR.New} function. +-- Here we create two vectors, a and b, and add them to create a new vector c. +-- +-- local a=VECTOR:New(1, 0) +-- local b=VECTOR:New(0, 1) +-- local c=a+b +-- +-- This is how it works. +-- +-- @field #VECTOR +VECTOR = { + ClassName = "VECTOR", + verbose = 0, +} + +--- VECTOR class version. +-- @field #string version +VECTOR.version="0.1.0" + +--- VECTOR unique ID +_VECTORID=0 + +--- VECTOR private index. +-- @field #VECTOR __index +VECTOR.__index = VECTOR + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: 3D rotation +-- DONE: Markers +-- TODO: Documentation + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new VECTOR class instance from given cartesian coordinates `x`, `y` and `z`, where `z` is optional. +-- **Note** that whether the `z` component is passed or nil has great impact on the interpretation of the `y` component. +-- If `z` is not given (`nil`), then `y` is used as `z` (the coordinate pointing East) because we always constuct a 3D vector. +-- If `z` is given (not `nil`), then `y` is used as coordinate pointing up (out of plane) as for all 3D DCS vectors. +-- @param #VECTOR self +-- @param #number x Component of vector along x-axis (pointing North in both 2D and 3D). +-- @param #number y Component of vector along y-axis (pointing East in 2D and Up in 3D). +-- @param #number z (Optional) Component of the z-axis (pointing East in 3D). +-- @return #VECTOR self +function VECTOR:New(x, y, z) + + if z==nil then + -- z-component is not given ==> 2D ==> we take y as z and set y=0. + self=setmetatable({x=x or 0, y=0, z=y or 0}, VECTOR) + else + self=setmetatable({x=x or 0, y=y or 0, z=z or 0}, VECTOR) + end + + _VECTORID=_VECTORID+1 + + self.uid=_VECTORID + + return self +end + +--- Create a new VECTOR class instance from given 2D or 3D vector object. +-- @param #VECTOR self +-- @param DCS#Vec3 Vec Vector object with `x`, `y` and optionally `z` components. Can be a DCS#Vec2, DCS#Vec3, `COORDINATE`, `VECTOR` object. +-- @return #VECTOR self +function VECTOR:NewFromVec(Vec) + + -- The :New function takes care whether this is a 2D or 3D vector. + local vector=VECTOR:New(Vec.x, Vec.y, Vec.z) + + return vector +end + +--- Create a new VECTOR class instance from polar coordinates (r, phi). +-- @param #VECTOR self +-- @param #number r Distance. +-- @param #number phi Angle in Degrees. Note that 0° corresponds to North, 90° East etc. +-- @return #VECTOR self +function VECTOR:NewFromPolar(r, phi) + + -- Convert deg to rad. + local Phi=math.rad(phi) + + -- Polar coordinates. What we want in DCS is: + -- North: Phi=0 ==> x= 1, y= 0 + -- East : Phi=90 ==> x= 0, y= 1 + -- South: Phi=180 ==> x=-1, y= 0 + -- West : Phi=270 ==> x= 0, y=-1 + + -- sin(0)=0 sin(90)=1, sin(180)=0, sin(270)=-1 ==> y + -- cos(0)=1 cos(90)=0, cos(180)=-1, cos(270)=0 ==> x + + local x=r*math.cos(phi) + local y=r*math.sin(phi) + + -- Create new vector. As z is nil, the 2D character is taken care of. + local v=VECTOR:New(x, y) + + return self +end + +--- Create a new VECTOR class instance from spherical coordinates (r, theta, phi). +-- @param #VECTOR self +-- @param #number r Distance in meters with r>=0. +-- @param #number theta Polar angle in Degrees measured from a fixed polar axis or zenith direction. This angle is in [0°, 180°]. +-- @param #number phi Azimuthal angle in Degrees. This angle is in [0°, 360°). +-- @return #VECTOR self +function VECTOR:NewFromSpherical(r, theta, phi) + + local sinPhi=math.sin(math.rad(phi)) + local cosPhi=math.cos(math.rad(phi)) + local sinTheta=math.sin(math.rad(theta)) + local cosTheta=math.cos(math.rad(theta)) + + --TODO: Check x,y,z convention for DCS. + local x=r*sinTheta*cosPhi + local y=r*sinTheta*sinPhi + local z=r*cosTheta + + local v=VECTOR:New(x, y, z) + + return self +end + +--- Get the directional vector that points from a given vector `a` to another given vector `b`. +-- The vector is `c=-a+b=b-a`. +-- @param #VECTOR self +-- @param #VECTOR a Vector a. This can also be given as any table with x, y and z components (z optional). +-- @param #VECTOR b Vector b. This can also be given as any table with x, y and z components (z optional). +-- @return #VECTOR Directional vector from a to b. +function VECTOR:NewDirectionalVector(a, b) + + local x=b.x-a.x + local y + local z + + if a.z and b.z then + -- Both given vectors are 3D + y=b.y-a.y + z=b.z-a.z + elseif b.z then + -- a is 2D and b is 3D + y=b.y-0 + z=b.z-a.y + elseif a.z then + -- a is 3D and b is 2D + y=0-a.y + z=b.y-a.z + else + -- a is 2D and b is 2D + y=b.y-a.y + z=nil --We leave z=nil, so the New function takes care of the 2D character. + end + + local c=VECTOR:New(x, y, z) + + return c +end + + +--- Creates a new VECTOR instance from given the latitude and longitude in decimal degrees (DD). +-- @param #VECTOR 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 2D position. +-- @return #VECTOR self +function VECTOR: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. + self=VECTOR:NewFromVec(vec3) + + -- Adjust height + if Altitude then + self.y=Altitude + end + + return self +end + +--- Creates a new VECTOR instance from given latitude and longitude in degrees, minutes and seconds (DMS). +-- **Note** that latitude and longitude are passed as strings and the characters `°`, `'` and `"` are important. +-- @param #VECTOR self +-- @param #string Latitude Latitude in DMS as string, e.g. "`42° 24' 14.3"`". +-- @param #string Longitude Longitude in DMS as string, e.g. "`42° 24' 14.3"`". +-- @param #number Altitude (Optional) Altitude in meters. Default is the land height at the coordinate. +-- @return #VECTOR +function VECTOR:NewFromLLDMS(Latitude, Longitude, Altitude) + + local lat=UTILS.LLDMSstringToDD(Latitude) + local lon=UTILS.LLDMSstringToDD(Longitude) + + self=VECTOR:NewFromLLDD(lat, lon, Altitude) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get 2D vector as simple table. +-- @param #VECTOR self +-- @return DCS#Vec2 2D array {x, y}. +function VECTOR:GetVec2() + + local vec={x=self.x, y=self.z} + + return vec +end + +--- Get 3D vector as simple table. +-- @param #VECTOR self +-- @param #boolean OnSurface If `true`, the `y` component is set the land height [m] of the 2D position. +-- @return DCS#Vec3 3D array {x=x, y=y, z=z}. +function VECTOR:GetVec3(OnSurface) + + local x=self.x + local y=OnSurface and land.getHeight({x=self.x, y=self.z}) or self.y + local z=self.z + + local vec={x=x, y=y, z=z} --DCS#Vec3 + + return vec +end + +--- Get a COORDINATE object. +-- @param #VECTOR self +-- @param #boolean OnSurface If `true`, the `y` component is set the land height [m] of the 2D position. +-- @return Core.Point#COORDINATE The COORDINATE object. +function VECTOR:GetCoordinate(OnSurface) + + local vec3=self:GetVec3(OnSurface) + + local coordinate=COORDINATE:NewFromVec3(vec3) + + return coordinate +end + +--- Get the distance from this vector to another vector. +-- @param #VECTOR self +-- @param #VECTOR Vector Vector to which the distance is requested. +-- @param #boolean Only2D If `true`, calculate only the projected 2D distance. +-- @return #number Distance in meters. +function VECTOR:GetDistance(Vector, Only2D) + + local dx=self.x-Vector.x + local dy=0 + local dz=0 + + if Vector.z then + if not Only2D then + dy=self.y-Vector.y + end + dz=self.z-Vector.z + else + -- Given vector is 2D. + dy=0 + dz=self.z-Vector.y + end + + -- Calculate the distance. + local dist=math.sqrt( dx*dx + dy*dy + dz*dz ) + + return dist +end + +--- Get the directional vector that points from this VECTOR to another VECTOR `a`. +-- @param #VECTOR self +-- @param #VECTOR a Vector `a`. This can also be given as any table with `x`, `y` and `z` components, where `z` is optional. +-- @return #VECTOR Directional vector from this vector to `a`. +function VECTOR:GetDirectionalVectorTo(a) + + local x=a.x-self.x + local y=0 + local z=nil + + if a.z then + -- a is 3D + y=a.y-self.y + z=a.z-self.z + else + -- a is 2D + y=a.y-self.z + z=nil --We leave z=nil, so the New function takes care of the 2D character. + end + + local c=VECTOR:New(x, y, z) + + return c +end + +--- Get the directional vector that points from another VECTOR `a` to this VECTOR. +-- @param #VECTOR self +-- @param #VECTOR a Vector a. This can also be given as any table with x,y and z components (z optional). +-- @return #VECTOR Directional vector from self to a. +function VECTOR:GetDirectionalVectorFrom(a) + + local x=self.x-a.x + local y + local z + + if a.z then + -- a is 3D + y=self.y-a.y + z=self.z-a.z + else + -- a is 2D ==> we work in 2D and take z component of self + y=self.z-a.y + z=nil --We leave z=nil, so the New function takes care of the 2D character. + end + + local c=VECTOR:New(x, y, z) + + return c +end + +--- Get length/norm/magnitude of this vector. +-- @param #VECTOR self +-- @return #number Length of vector. +function VECTOR:GetLength() + + local l=math.sqrt(self.x*self.x+self.y*self.y+self.z*self.z) + + return l +end + +--- Get heading of vector. +-- Note that a heading of +-- +-- * 000° = North +-- * 090° = East +-- * 180° = South +-- * 270° = West. +-- +-- @param #VECTOR self +-- @param #boolean To360 If `true` or `nil`, adjust heading to [0,360) range. If `false`, headings not in this range can occur. +-- @return #number Heading in degrees. +function VECTOR:GetHeading(To360) + + -- Get heading. + local heading=math.atan2(self.z, self.x) + + -- Convert to degrees. + heading=math.deg(heading) + + + if To360==nil or To360==true then + -- Adjust heading so it is in [0,360). + heading=UTILS.AdjustHeading360(heading) + else + -- Just make sure 360 is returned a 0. + if heading==360.0 then + heading=0.0 + end + end + + return heading +end + +--- Get the heading from this vector to another given vector. +-- @param #VECTOR self +-- @param #VECTOR Vector Vector to which the heading is requested. +-- @return #number Heading from this vector to the other vector in degrees. +function VECTOR:GetHeadingTo(Vector) + + -- Get directional vector from given vector to this vector. + local a=self:GetDirectionalVectorTo(Vector) + + -- Get heading of directional vector. + local heading=math.deg(math.atan2(a.z, a.x)) + + -- Adjust heading so it is in [0,360). + heading=UTILS.AdjustHeading360(heading) + + return heading +end + +--- Get the heading from a given vector to this vector. +-- @param #VECTOR self +-- @param #VECTOR Vector Vector from which the heading is requested. +-- @return #number Heading from the other vector to this vector in degrees. +function VECTOR:GetHeadingFrom(Vector) + + -- Get directional vector from given vector to this vector. + local a=self:GetDirectionalVectorFrom(Vector) + + -- Get heading of directional vector. + local heading=math.deg(math.atan2(a.z, a.x)) + + -- Adjust heading so it is in [0,360). + heading=UTILS.AdjustHeading360(heading) + + return heading +end + + +--- Get latitude and longitude of this vector. +-- @param #VECTOR self +-- @return #number Latitude in decimal degrees (DD). +-- @return #number Longitude in decimal degrees (DD). +function VECTOR:GetLatitudeLongitude() + + local vec3=self:GetVec3() + + local latitude, longitude, altitude=coord.LOtoLL(vec3) + + return latitude, longitude +end + + +--- Get MGRS information of this vector. +-- +-- `MGRS = {UTMZone = string, MGRSDigraph = string, Easting = number, Northing = number}` +-- +-- @param #VECTOR self +-- @return #table MGRS table with `UTMZone`, `MGRSDiGraph`, `Easting` and `Northing` keys. +function VECTOR:GetMGRS() + + local lat, long=self:GetLatitudeLongitude() + + local mgrs=coord.LLtoMGRS(lat, long) + + -- Example table returned by coord.LLtoMGRS + --[[ + MGRS = { + UTMZone = string, + MGRSDigraph = string, + Easting = number, + Northing = number + } + ]] + + return mgrs +end + +--- Get the difference of the heading of this vector w. +-- +-- **Example 1:** +-- This vector has a heading of 90° (pointing East) and the other vector has a heading of 225° (pointing South-West), +-- we would optain a delta of 225°-90°=135°. +-- +-- **Example 2:** +-- This vector has a heading of 180 (pointing South) and the other vector has a heading of 90° (pointing East), +-- we would optain a delta of 90°-180°=-90°. +-- +-- @param #VECTOR self +-- @param #VECTOR Vector Vector to which the heading is requested. +-- @return #number Heading from this vector to the other vector in degrees. +function VECTOR:GetHeadingDelta(Vector) + + local h1=self:GetHeading(false) + + local h2=Vector:GetHeading(false) + + local delta=h2-h1 + + return delta +end + +--- Return an intermediate VECTOR between this and another given vector. +-- @param #VECTOR self +-- @param #VECTOR Vector The destination vector. +-- @param #number Fraction The fraction (0,1) where the new vector is created. Default 0.5, *i.e.* in the middle. +-- @return #VECTOR Vector between this and the other vector. +function VECTOR:GetIntermediateVector(Vector, Fraction) + + local f=Fraction or 0.5 + + -- Get the directional vector to the given Vector. + local vec=self:GetDirectionalVectorTo(Vector) + + -- Get the length of the vector. + local length=vec:GetLength() + + -- Set/scale the length. + vec:SetLength(f*length) + + -- Get to the desired position. + vec=self+vec + + return vec +end + + + +--- Set length/norm/magnitude of this vector. +-- @param #VECTOR self +-- @param #number Length Desired length of vector. +function VECTOR:SetLength(Length) + + -- Normalize this vector to a length of 1. + self:Normalize() + + -- Scale the vector to the desired length. + local v=self*Length + + -- Replace with scaled version. + self:Replace(v) + + return self +end + +--- Set x-component of vector. The x-axis points to the North. +-- @param #VECTOR self +-- @param #number x Value of x. Default 0. +-- @return #VECTOR self +function VECTOR:SetX(x) + self.x=x or 0 + return self +end + +--- Set y-component of vector. The y-axis points to the upwards and describes the altitude above mean sea level. +-- @param #VECTOR self +-- @param #number y Value of y. Default land/surface height at this point. +-- @return #VECTOR self +function VECTOR:SetY(y) + + if y==nil then + y=self:GetSurfaceHeight() + end + self.y=y + return self +end + +--- Set z-component of vector. The z-axis points to the East. +-- @param #VECTOR self +-- @param #number z Value of z. Default 0. +-- @return #VECTOR self +function VECTOR:SetZ(z) + self.z=z or 0 + return self +end + + +--- Add a vector to this. This function works for DCS#Vec2, DCS#Vec3, VECTOR, COORDINATE objects. +-- Note that if you want to add a VECTOR, you can also simply use the `+` operator. +-- @param #VECTOR self +-- @param DCS#Vec3 Vec Vector to add. Can also be a DCS#Vec2, DCS#Vec3, COORDINATE or VECTOR object. +-- @return #VECTOR self +function VECTOR:AddVec(Vec) + + self.x=self.x+Vec.x + + if Vec.z then + self.y=self.y+Vec.y + self.z=self.z+Vec.z + else + -- Vec is 2D ==> we take its y-component. + self.z=self.z+Vec.y + end + + return self +end + +--- Subtract a vector from this one. This function works for DCS#Vec2, DCS#Vec3, VECTOR, COORDINATE objects. +-- +-- **Note** that if you want to add a VECTOR, you can also simply use the `-` operator. +-- @param #VECTOR self +-- @param DCS#Vec3 Vec Vector to substract. Can also be a DCS#Vec2, DCS#Vec3, COORDINATE or VECTOR object. +-- @return #VECTOR self +function VECTOR:SubVec(Vec) + + self.x=self.x-Vec.x + + if Vec.z then + self.y=self.y-Vec.y + self.z=self.z-Vec.z + else + -- Vec is 2D ==> we take its y-component. + self.z=self.z-Vec.y + end + + return self +end + +--- Calculate the dot product of this VECTOR with another vector. This function works for DCS#Vec2, DCS#Vec3, VECTOR, COORDINATE objects. +-- @param #VECTOR self +-- @param DCS#Vec3 Vec The other vector. Can also be a DCS#Vec2, DCS#Vec3, COORDINATE or VECTOR object. +-- @return #number Dot product Sum_i(a[i]*b[i]). Note that this is a **scalar** and not a vector any more! +function VECTOR:Dot(Vec) + + local dot=self.x*Vec.x + if Vec.z then + dot=dot+self.y*Vec.y+self.z*Vec.z + else + -- Vec is 2D ==> we take its y-component for z. + dot=dot+self.z*Vec.y + end + + return dot +end + +--- Calculate the rotation or cross product of this VECTOR with another vector. This function works for DCS#Vec2, DCS#Vec3, VECTOR, COORDINATE objects. +-- @param #VECTOR self +-- @param DCS#Vec3 Vec The other vector. Can also be a DCS#Vec2, DCS#Vec3, COORDINATE or VECTOR object. +-- @return #VECTOR The cross product vector. +function VECTOR:Rot(Vec) + + -- TODO: + local dot=self.x*Vec.x + if Vec.z then + dot=dot+self.y*Vec.y+self.z*Vec.z + else + -- Vec is 2D ==> we take its y-component for z. + dot=dot+self.z*Vec.y + end + + return dot +end + + +--- Get a clone (deep copy) of this vector. +-- @param #VECTOR self +-- @return #VECTOR Copy of the vector. +function VECTOR:Copy() + + local c=VECTOR:New(self.x, self.y, self.z) + + return c +end + +--- Replace this vector with another one. +-- If the given vector is 2D, we +-- @param #VECTOR self +-- @param #VECTOR Vector The vector that is used to replace this vector. +-- @param #boolean Project2D If `true` and the given vector is 2D, we project the updated vector to 2D (`y=0`). Otherwise, we leave `y` untouched. +-- @return #VECTOR self updated +function VECTOR:Replace(Vector, Project2D) + + self.x=Vector.x + + if Vector.z then + -- Given vector is 3D + self.y=Vector.y + self.z=Vector.z + else + -- Given vector is 2D + if Project2D then + self.y=0 + end + self.z=Vector.y + end + + return self +end + +--- Normalize this vector, so that has a length of 1. +-- @param #VECTOR self +-- @return #VECTOR self +function VECTOR:Normalize() + + local l=self:GetLength() + + if l~=0 then + self:Replace(self/l) + end + + return self +end + +--- Translate the vector by a given distance and angle. +-- @param #VECTOR self +-- @param #number Distance Distance in meters. Default 1000 meters. +-- @param #number Heading Heading angle in degrees. Default 0° = North. +-- @param #boolean Copy Create a copy of the VECTOR so the original stays unchanged. +-- @return #VECTOR The translated vector or a copy of it. +function VECTOR:Translate(Distance, Heading, Copy) + + -- Set default distance if not passed. + Distance=Distance or 1000 + + -- Angle in rad. + local alpha = math.rad(Heading or 0) + + -- Create a copy if requested. + local vector=Copy and self:Copy() or self + + -- Set new coordinates. + vector.x = Distance * math.cos(alpha) + vector.x -- New x + vector.z = Distance * math.sin(alpha) + vector.z -- New z + + return vector +end + +--- Rotate the VECTOR clockwise in the 2D (x,z) plane. +-- @param #VECTOR self +-- @param #number Angle Rotation angle in degrees). Default 0. +-- @param #boolean Copy Create a copy of the VECTOR so the original stays unchanged. +-- @return #VECTOR The translated vector or a copy of it. +function VECTOR:Rotate2D(Angle, Copy) + + -- Angle in rad. + local phi = -math.rad(Angle or 0) + + -- Sin/Cos of angle. + local sinPhi = math.sin(phi) + local cosPhi = math.cos(phi) + + -- Get more convenient notation. + local X=self.z + local Y=self.x + + -- Apply rotation matrix. + local z = X*cosPhi - Y*sinPhi + local x = X*sinPhi + Y*cosPhi + + -- Create new vector. + if Copy then + local vector=VECTOR:New(x, self.y, z) + return vector + else + self:SetX(x) + self:SetZ(z) + return self + end +end + + + +--- Provides an MGRS string. +-- @param #VECTOR 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 VECTOR:ToStringMGRS(Settings) + + -- Get Accuracy. + 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 ) + + local text="MGRS " .. UTILS.tostringMGRS( MGRS, MGRS_Accuracy ) + + return text +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS API Wrapper Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get the surface type at the vector. +-- +-- * LAND = 1 +-- * SHALLOW_WATER = 2 +-- * WATER = 3 +-- * ROAD = 4 +-- * RUNWAY = 5 +-- +-- @param #VECTOR self +-- @return #number Surface type +function VECTOR:GetSurfaceType() + + local vec2=self:GetVec2() + + local s=land.getSurfaceType(vec2) + + return s +end + + +--- Get the name of surface type at the vector. +-- +-- * LAND = 1 +-- * SHALLOW_WATER = 2 +-- * WATER = 3 +-- * ROAD = 4 +-- * RUNWAY = 5 +-- +-- @param #VECTOR self +-- @return #string Surface type name +function VECTOR:GetSurfaceTypeName() + + local vec2=self:GetVec2() + + local s=land.getSurfaceType(vec2) + + for name,id in land.SurfaceType() do + if id==s then + return name + end + end + + return "unknown" +end + +--- Check if a given vector has line of sight with this vector. +-- @param #VECTOR self +-- @param #VECTOR Vec The other vector. +-- @return #number Surface Type +function VECTOR:IsVisible(Vec) + + local vec1=self:GetVec3() + + local vec2={x=Vec.x, Vec.y, Vec.z} + + local los=land.isVisible(vec1, vec2) + + return los +end + +--- Get a vector on the closest road. +-- @param #VECTOR self +-- @return #VECTOR Closest vector to a road. +function VECTOR:GetClosestRoad() + + local vec2=self:GetVec2() + + local x,y=land.getClosestPointOnRoads('roads', vec2.x, vec2.y) + + local road=nil + if x and y then + road=VECTOR:New(x, y) + end + + return road +end + +--- Get a vector on the closest railroad. +-- @param #VECTOR self +-- @return #VECTOR Closest vector to a railroad. +function VECTOR:GetClosestRailroad() + + local vec2=self:GetVec2() + + local x,y=land.getClosestPointOnRoads('railroads', vec2.x, vec2.y) + + local road=nil + if x and y then + road=VECTOR:New(x, y) + end + + return road +end + + +--- Get the path on road from this vector to a given other vector. +-- @param #VECTOR self +-- @param #VECTOR Vec The destination vector. +-- @return Core.Pathline#PATHLINE Pathline with points on road. +function VECTOR:GetPathOnRoad(Vec) + + local vec1=self:GetVec2() + local vec2=Vec:GetVec2() + + local vec2points=land.findPathOnRoads("roads", vec1.x , vec1.y, vec2.x, vec2.y) + + local path=nil + if vec2points then + path=PATHLINE:NewFromVec2Array("Road", vec2points) + end + + return path +end + + +--- Get profile of the land between the two passed points. +-- @param #VECTOR self +-- @param #VECTOR Vec3 The 3D destination vector. If a 2D vector is passed, `y` is set to the land height. +-- @return Core.Pathline#PATHLINE Pathline with points of the profile. +function VECTOR:GetProfile(Vec3) + + local vec3=self:GetVec3() + + -- Get profile + local vec3s=land.profile(vec3, Vec3) + + local profile=nil + if vec3s then + + profile=PATHLINE:NewFromVec3Array("Profile", vec3s) + + end + + return profile +end + +--- Returns an intercept point at which a ray drawn from the this vector in the passed normalized direction for a specified distance. +-- @param #VECTOR self +-- @param DCS#Vec3 DirectionVector Directional vector. +-- @param #number Distance Distance in meters. Default 1000 m. +-- @return #VECTOR Intercept vector. Can be `nil` if no intercept point is found. +function VECTOR:GetInterceptPoint(DirectionVector, Distance) + + local vec3=self:GetVec3() + + local ip3=land.getIP(vec3, DirectionVector, Distance or 1000) --DCS#Vec3 + + local ipvector=nil + + if ip3 then + ipvector=VECTOR:New(ip3.x, ip3.y , ip3.z) + end + + return ipvector +end + +--- Returns the distance from sea level at this vector. +-- @param #VECTOR self +-- @return #number Distance above sea leavel in meters. +function VECTOR:GetSurfaceHeight() + + local vec2=self:GetVec2() + + local h=land.getHeight(vec2) + + return h +end + + +--- Returns the distance from sea level at this vector. +-- @param #VECTOR self +-- @return #number Heigh above sea leavel in meters. +-- @return #number Depth (positive) at this point in meters. +function VECTOR:GetSurfaceHeightAndDepth() + + local vec2=self:GetVec2() + + local h,d=land.getSurfaceHeightWithSeabed(vec2) + + return h,d +end + +--- Returns a velocity vector of the wind at this vector. Turbolences can be optionally be included. +-- @param #VECTOR self +-- @param #boolean WithTurbulence If `true`, return wind including turbulence. +-- @return #VECTOR Velocity 3D vector [m/s] the wind is blowing to. +function VECTOR:GetWindVector(WithTurbulence) + + local vec3=self:GetVec3() + + local wind=nil + if WithTurbulence then + wind=atmosphere.getWindWithTurbulence(vec3) + + else + wind=atmosphere.getWind(vec3) + end + + local vector=VECTOR:New(wind) + + return vector +end + +--- Returns a temperature and pressure at this vector. +-- @param #VECTOR self +-- @return #number Temperatur in Kelvin. +-- @return #number Pressure in Pascals. +function VECTOR:GetTemperaturAndPressure() + + local vec3=self:GetVec3() + + local t,p=atmosphere.getTemperatureAndPressure(vec3) + + return t,p +end + +--- Creates a smoke at this vector. +-- @param #VECTOR self +-- @param #number Color Color of the smoke: 0=Green, 1=Red, 2=White, 3=Orange, 4=Blue. Default 0. +-- @param #number Duration (Optional) Duration of the smoke in seconds. Default nil. +-- @return #string Name of the smoke object. Can be used to stop it. +function VECTOR:Smoke(Color, Duration) + + local vec3=self:GetVec3() + + Color=Color or 0 + + -- Create a name for the smoke object + local name=string.format("Vector-Smoke-%d", self.uid) + + -- Create smoke at this position + trigger.action.smoke(vec3, Color, name) + + if Duration and Duration>0 then + self:StopSmoke(name, Duration) + end + + return name +end + + +--- Creates a large smoke and fire effect of a specified type and density at this vector. +-- +-- * 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 #VECTOR self +-- @param #number Preset Preset of smoke. Default `BIGSMOKEPRESET.LargeSmokeAndFire`. +-- @param #number Density Density between [0,1]. Default 0.5. +-- @param #number Duration (Optional) Duration of the smoke and fire in seconds. +-- @return #string Name of the smoke. Can be used to stop it. +function VECTOR:SmokeAndFire(Preset, Density, Duration) + + Preset=Preset or BIGSMOKEPRESET.LargeSmokeAndFire + Density=Density or 0.5 + + local vec3=self:GetVec3() + + -- Get a name of this smoke & fire object + local name=string.format("Vector-Fire-%d", self.uid) + + trigger.action.effectSmokeBig(vec3, Preset, Density, name) + + if Duration and Duration>0 then + self:StopSmoke(name, Duration) + end + + return name +end + +--- Stop smoke or fire effect. +-- @param #VECTOR self +-- @param #string Name Name of the smoke object. +-- @param #number Delay Delay in seconds before the smoke is stopped. +-- @return #VECTOR self +function VECTOR:StopSmoke(Name, Delay) + + if Delay and Delay>0 then + TIMER:New(VECTOR.StopSmoke, self, Name):Start(Delay) + else + if Name then + trigger.action.effectSmokeStop(Name) + else + env.error(string.format("No name provided in VECTOR.StopSmoke function!")) + end + end + + return self +end + + +--- Creates an illumination bomb at the specified point. +-- @param #VECTOR self +-- @param #number Power The power in Candela (cd). Should be between 1 and 1000000. Default 1000 cd. +-- @param #number Altitude (Optional) Altitude [m] at which the illumination bomb is created. +-- @return #VECTOR self +function VECTOR:IlluminationBomb(Power, Altitude) + + local vec3=self:GetVec3() + + if Altitude then + vec3.y=Altitude + end + + -- Create an illumination bomb. + trigger.action.illuminationBomb(vec3, Power or 1000) + + return self +end + +--- Creates an explosion at a given point at the specified power. +-- @param #VECTOR self +-- @param #number Power The power in kg TNT. Default 100 kg. +-- @return #VECTOR self +function VECTOR:Explosion(Power) + + local vec3=self:GetVec3() + + trigger.action.explosion(vec3, Power or 100) + + return self +end + +--- Creates a signal flare at the given point in the specified color. The flare will be launched in the direction of the azimuth angle. +-- @param #VECTOR self +-- @param #number Color Color of flare. Default Green. +-- @param #number Azimuth Azimuth angle in degrees. Default 0. +-- @return #VECTOR self +function VECTOR:Flare(Color, Azimuth) + + local vec3=self:GetVec3() + + trigger.action.signalFlare(vec3, Color or 0, math.rad(Azimuth or 0)) + + return self +end + + +--- Creates a arrow from this VECTOR to another vector on the F10 map. +-- @param #VECTOR self +-- @param #VECTOR Vector The vector defining the endpoint. +-- @param #number Coalition Coalition Id: -1=All, 0=Neutral, 1=Red, 2=Blue. Default -1. +-- @param #table Color RGB color with alpha {r, g, b, alpha}. Default {1, 0, 0, 0.7}. +-- @param #table FillColor RGB color with alpha {r, g, b, alpha}. Default {1, 0, 0, 0.5}. +-- @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. +-- @return #number Marker ID. Can be used to remove the drawing. +function VECTOR:ArrowTo(Vector, Coalition, Color, FillColor, LineType) + + local vec3End=self:GetVec3() + local vec3Start=Vector:GetVec3() + + local id=UTILS.GetMarkID() + + Coalition=Coalition or -1 + Color=Color or {1,0,0,0.7} + FillColor=FillColor or {1,0,0,0.5} + LineType=LineType or 1 + + local readOnly=false + + trigger.action.arrowToAll(Coalition , id, vec3Start, vec3End, Color, FillColor , LineType, readOnly, "") + + return id +end + + +--- Create mark on F10 map. +-- @param #VECTOR self +-- @param #string MarkText Free format text that shows the marking clarification. +-- @param #number Recipient Recipient of the mark: -1=All (default), 0=Neutral, 1=Red, 2=Blue. Can also be a `GROUP` object. +-- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. +-- @return #number Mark ID. +function VECTOR:Mark(MarkText, Recipient, ReadOnly) + + Recipient=Recipient or -1 + + if type(Recipient)=="number" then + + local MarkID = UTILS.GetMarkID() + + if Recipient==-1 then + trigger.action.markToAll(MarkID, MarkText, self:GetVec3(), ReadOnly, "") + elseif Recipient==0 then + trigger.action.markToCoalition(MarkID, MarkText, self:GetVec3(), coalition.side.NEUTRAL, ReadOnly, "") + elseif Recipient==1 then + trigger.action.markToCoalition(MarkID, MarkText, self:GetVec3(), coalition.side.RED, ReadOnly, "") + elseif Recipient==2 then + trigger.action.markToCoalition(MarkID, MarkText, self:GetVec3(), coalition.side.BLUE, ReadOnly, "") + end + + return MarkID + + elseif type(Recipient)=="table" then + + local MarkID = UTILS.GetMarkID() + + local group=Recipient --Wrapper.Group#GROUP + + trigger.action.markToGroup( MarkID, MarkText, self:GetVec3(), group:GetID(), ReadOnly, "") + + return MarkID + + end + + return nil +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Private Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if passed object is a Vector. +-- @param #table t The object to be tested. +-- @return #boolean Returns `true` if `t` is a #VECTOR and `false` otherwise. +function VECTOR._IsVector(t) + return getmetatable(t) == VECTOR +end + +--- Meta function to add vectors together. +-- @param #VECTOR a Vector a. +-- @param #VECTOR b Vector b. +-- @return #VECTOR Returns a new VECTOR c with c[i]=a[i]+b[i] for i=x,y,z. +function VECTOR.__add(a,b) + + assert(VECTOR._IsVector(a) and VECTOR._IsVector(b), "ERROR in VECTOR.__add: wrong argument types! (expected and )") + + local c=VECTOR:New(a.x+b.x, a.y+b.y, a.z+b.z) + + return c +end + +--- Meta function to subtract vectors. +-- @param #VECTOR a Vector a. +-- @param #VECTOR b Vector b. +-- @return #VECTOR Returns a new VECTOR c with c[i]=a[i]-b[i] for i=x,y,z. +function VECTOR.__sub(a,b) + + assert(VECTOR._IsVector(a) and VECTOR._IsVector(b), "ERROR in VECTOR.__sub: wrong argument types: (expected and )") + + local c=VECTOR:New(a.x-b.x, a.y-b.y, a.z-b.z) + + return c +end + + +--- Meta function to multiplicate vector by another vector or a scalar. +-- @param #VECTOR a Vector a. Can also be a #number. +-- @param #VECTOR b Vector b. Can also be a #number. +-- @return #VECTOR Returns a new VECTOR c with c[i]=a[i]*b[i] for i=x,y,z. +function VECTOR.__mul(a, b) + + local c=nil --#VECTOR + + if type(a)=='number' then + c=VECTOR:New(a*b.x, a*b.y, a*b.z) + elseif type(b)=='number' then + c=VECTOR:New(b*a.x, b*a.y, b*a.z) + else + c=VECTOR:New(a.x*b.x, a.y*b.y, a.z*b.z) + end + + return c +end + +--- Meta function for dividing a vector by a scalar or by another vector. +-- @param #VECTOR a Vector a. +-- @param #VECTOR b Vector b. Can also be a #number. +-- @return #VECTOR Returns a new VECTOR c with c[i]=a[i]/b or c[i]=a[i]/b[i] for i=x,y,z. +function VECTOR.__div(a, b) + + assert(VECTOR._IsVector(a) and (type(b) == "number" or VECTOR._IsVector(b)), "div: wrong argument types (expected and ( or ))") + + local c=nil --#VECTOR + + if type(b) == "number" then + c=VECTOR:New(a.x/b, a.y/b, a.z/b) + else + c=VECTOR:New(a.x/b.x, a.y/b.y, a.z/b.z) + end + + return c +end + +--- Meta function to make vectors negative. +-- @param #VECTOR v Vector v. +function VECTOR.__unm(v) + local c=VECTOR:New(-v.x, -v.y, -v.z) + return c +end + +--- Meta function to check if two vectors are equal. +-- @param #VECTOR a Vector a. +-- @param #VECTOR b Vector b. +-- @return #boolean If `true`, both vectors are equal +function VECTOR.__eq(a, b) + return a.x==b.x and a.y==b.y and a.z==b.z +end + + +--- Meta function to change how vectors appear as string. +-- @param #VECTOR self +-- @return #string String representation of vector. +function VECTOR:__tostring() + local text=string.format("VECTOR: x=%.1f, y=%.1f, z=%.1f |v|=%.1f Phi=%4.1f°", self.x, self.y, self.z, self:GetLength(), self:GetHeading(false)) + return text +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Core/Velocity.lua b/Moose Development/Moose/Core/Velocity.lua index e71282208..1181271c7 100644 --- a/Moose Development/Moose/Core/Velocity.lua +++ b/Moose Development/Moose/Core/Velocity.lua @@ -20,7 +20,7 @@ do -- Velocity - --- @type VELOCITY + -- @type VELOCITY -- @extends Core.Base#BASE @@ -127,7 +127,7 @@ end do -- VELOCITY_POSITIONABLE - --- @type VELOCITY_POSITIONABLE + -- @type VELOCITY_POSITIONABLE -- @extends Core.Base#BASE diff --git a/Moose Development/Moose/Functional/AICSAR.lua b/Moose Development/Moose/Functional/AICSAR.lua new file mode 100644 index 000000000..7ec2f73dc --- /dev/null +++ b/Moose Development/Moose/Functional/AICSAR.lua @@ -0,0 +1,1377 @@ +--- **Functional** - AI CSAR system. +-- +-- === +-- +-- ## Features: +-- +-- * Send out helicopters to downed pilots +-- * Rescues players and AI alike +-- * Coalition specific +-- * Starting from a FARP or Airbase +-- * Dedicated MASH zone +-- * Some FSM functions to include in your mission scripts +-- * Limit number of available helos +-- * SRS voice output via TTS or soundfiles +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Functional/AICSAR). +-- +-- === +-- +-- ### Author: **Applevangelist** +-- Last Update July 2025 +-- +-- === +-- @module Functional.AICSAR +-- @image MOOSE.JPG + + + +--- AI CSAR class. +-- @type AICSAR +-- @field #string ClassName Name of this class. +-- @field #string version Versioning. +-- @field #string lid LID for log entries. +-- @field #number coalition Colition side. +-- @field #string template Template for pilot. +-- @field #string helotemplate Template for CSAR helo. +-- @field #string alias Alias Name. +-- @field Wrapper.Airbase#AIRBASE farp FARP object from where to start. +-- @field Core.Zone#ZONE farpzone MASH zone to drop rescued pilots. +-- @field #number maxdistance Max distance to go for a rescue. +-- @field #table pilotqueue Queue of pilots to rescue. +-- @field #number pilotindex Table index to bind pilot to helo. +-- @field #table helos Table of Ops.FlightGroup#FLIGHTGROUP objects +-- @field #boolean verbose Switch more output. +-- @field #number rescuezoneradius Radius around downed pilot for the helo to land in. +-- @field #table rescued Track number of rescued pilot. +-- @field #boolean autoonoff Only send a helo when no human heli pilots are available. +-- @field Core.Set#SET_CLIENT playerset Track if alive heli pilots are available. +-- @field #boolean limithelos limit available number of helos going on mission (defaults to true) +-- @field #number helonumber number of helos available (default: 3) +-- @field Utilities.FiFo#FIFO PilotStore +-- @field #number Altitude Default altitude setting for the helicopter FLIGHTGROUP 1500ft. +-- @field #number Speed Default speed setting for the helicopter FLIGHTGROUP is 100kn. +-- @field #boolean UseEventEject In case Event LandingAfterEjection isn't working, use set this to true. +-- @field #number Delay In case of UseEventEject wait this long until we spawn a landed pilot. +-- @field #boolean UseRescueZone If true, use a rescue zone and not the max distance to FARP/MASH +-- @field Core.Zone#ZONE_RADIUS RescueZone Use this zone as operational area for the AICSAR instance. +-- @extends Core.Fsm#FSM + + +--- *I once donated a pint of my finest red corpuscles to the great American Red Cross and the doctor opined my blood was very helpful; contained so much alcohol they could use it to sterilize their instruments.* +-- W.C.Fields +-- +-- === +-- +-- # AICSAR Concept +-- +-- For an AI or human pilot landing with a parachute, a rescue mission will be spawned. The helicopter will fly to the pilot, pick him or her up, +-- and fly back to a designated MASH (medical) zone, drop the pilot and then return to base. +-- Operational maxdistance can be set as well as the landing radius around the downed pilot. +-- Keep in mind that AI helicopters cannot hover-load at the time of writing, so rescue operations over water or in the mountains might not +-- work. +-- Optionally, if you have a CSAR operation with human pilots in your mission, you can set AICSAR to ignore missions when human helicopter +-- pilots are around. +-- +-- ## Setup +-- +-- Setup is a one-liner: +-- +-- -- @param #string Alias Name of this instance. +-- -- @param #number Coalition Coalition as in coalition.side.BLUE, can also be passed as "blue", "red" or "neutral" +-- -- @param #string Pilottemplate Pilot template name. +-- -- @param #string Helotemplate Helicopter template name. Set the template to "cold start". Hueys work best. +-- -- @param Wrapper.Airbase#AIRBASE FARP FARP object or Airbase from where to start. +-- -- @param Core.Zone#ZONE MASHZone Zone where to drop pilots after rescue. +-- local my_aicsar=AICSAR:New("Luftrettung",coalition.side.BLUE,"Downed Pilot","Rescue Helo",AIRBASE:FindByName("Test FARP"),ZONE:New("MASH")) +-- +-- ## Options are +-- +-- my_aicsar.maxdistance -- maximum operational distance in meters. Defaults to 50NM or 92.6km +-- my_aicsar.rescuezoneradius -- landing zone around downed pilot. Defaults to 200m +-- my_aicsar.autoonoff -- stop operations when human helicopter pilots are around. Defaults to true. +-- my_aicsar.verbose -- text messages to own coalition about ongoing operations. Defaults to true. +-- my_aicsar.limithelos -- limit available number of helos going on mission (defaults to true) +-- my_aicsar.helonumber -- number of helos available (default: 3) +-- my_aicsar.verbose -- boolean, set to `true`for message output on-screen +-- +-- ## Radio output options +-- +-- Radio messages, soundfile names and (for SRS) lengths are defined in three enumerators, so you can customize, localize messages and soundfiles to your liking: +-- +-- Defaults are: +-- +-- AICSAR.Messages = { +-- EN = { +-- INITIALOK = "Roger, Pilot, we hear you. Stay where you are, a helo is on the way!", +-- INITIALNOTOK = "Sorry, Pilot. You're behind maximum operational distance! Good Luck!", +-- PILOTDOWN = "Mayday, mayday, mayday! Pilot down at ", -- note that this will be appended with the position in MGRS +-- PILOTKIA = "Pilot KIA!", +-- HELODOWN = "CSAR Helo Down!", +-- PILOTRESCUED = "Pilot rescued!", +-- PILOTINHELO = "Pilot picked up!", +-- }, +-- } +-- +-- Correspondingly, sound file names are defined as these defaults: +-- +-- AICSAR.RadioMessages = { +-- EN = { +-- INITIALOK = "initialok.ogg", +-- INITIALNOTOK = "initialnotok.ogg", +-- PILOTDOWN = "pilotdown.ogg", +-- PILOTKIA = "pilotkia.ogg", +-- HELODOWN = "helodown.ogg", +-- PILOTRESCUED = "pilotrescued.ogg", +-- PILOTINHELO = "pilotinhelo.ogg", +-- }, +-- } +-- +-- and these default transmission lengths in seconds: +-- +-- AICSAR.RadioLength = { +-- EN = { +-- INITIALOK = 4.1, +-- INITIALNOTOK = 4.6, +-- PILOTDOWN = 2.6, +-- PILOTKIA = 1.1, +-- HELODOWN = 2.1, +-- PILOTRESCUED = 3.5, +-- PILOTINHELO = 2.6, +-- }, +-- } +-- +-- ## Radio output via SRS and Text-To-Speech (TTS) +-- +-- Radio output can be done via SRS and Text-To-Speech. No extra sound files required! +-- [Initially, Have a look at the guide on setting up SRS TTS for Moose](https://github.com/FlightControl-Master/MOOSE_GUIDES/blob/master/documents/Moose%20TTS%20Setup%20Guide.pdf). +-- The text from the `AICSAR.Messages` table above is converted on the fly to an .ogg-file, which is then played back via SRS on the selected frequency and mdulation. +-- Hint - the small black window popping up shortly is visible in Single-Player only. +-- +-- To set up AICSAR for SRS TTS output, add e.g. the following to your script: +-- +-- -- setup for google TTS, radio 243 AM, SRS server port 5002 with a google standard-quality voice (google cloud account required) +-- my_aicsar:SetSRSTTSRadio(true,"C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio",243,radio.modulation.AM,5002,MSRS.Voices.Google.Standard.en_US_Standard_D,"en-US","female","C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio\\google.json") +-- +-- -- alternatively for MS Desktop TTS (voices need to be installed locally first!) +-- my_aicsar:SetSRSTTSRadio(true,"C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio",243,radio.modulation.AM,5002,MSRS.Voices.Microsoft.Hazel,"en-GB","female") +-- +-- -- define a different voice for the downed pilot(s) +-- my_aicsar:SetPilotTTSVoice(MSRS.Voices.Google.Standard.en_AU_Standard_D,"en-AU","male") +-- +-- -- define another voice for the operator +-- my_aicsar:SetOperatorTTSVoice(MSRS.Voices.Google.Standard.en_GB_Standard_A,"en-GB","female") +-- +-- ## Radio output via preproduced soundfiles +-- +-- The easiest way to add a soundfile to your mission is to use the "Sound to..." trigger in the mission editor. This will effectively +-- save your sound file inside of the .miz mission file. [Example soundfiles are located on github](https://github.com/FlightControl-Master/MOOSE_SOUND/tree/master/AICSAR) +-- +-- To customize or localize your texts and sounds, you can take e.g. the following approach to add a German language version: +-- +-- -- parameters are: locale, ID, text, soundfilename, duration +-- my_aicsar.gettext:AddEntry("de","INITIALOK","Copy, Pilot, wir hören Sie. Bleiben Sie, wo Sie sind, ein Hubschrauber sammelt Sie auf!","okneu.ogg",5.0) +-- my_aicsar.locale = "de" -- plays and shows the defined German language texts and sound. Fallback is "en", if something is undefined. +-- +-- Switch on radio transmissions via **either** SRS **or** "normal" DCS radio e.g. like so: +-- +-- my_aicsar:SetSRSRadio(true,"C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio",270,radio.modulation.AM,nil,5002) +-- +-- or +-- +-- my_aicsar:SetDCSRadio(true,300,radio.modulation.AM,GROUP:FindByName("FARP-Radio")) +-- +-- See the function documentation for parameter details. +-- +-- === +--- +-- +-- @field #AICSAR +AICSAR = { + ClassName = "AICSAR", + version = "0.1.18", + lid = "", + coalition = coalition.side.BLUE, + template = "", + helotemplate = "", + alias = "", + farp = nil, + farpzone = nil, + maxdistance = UTILS.NMToMeters(50), + pilotqueue = {}, + pilotindex = 0, + helos = {}, + verbose = false, + rescuezoneradius = 200, + rescued = {}, + autoonoff = true, + playerset = nil, + Messages = {}, + SRS = nil, + SRSRadio = false, + SRSFrequency = 243, + SRSPath = "\\", + SRSModulation = radio.modulation.AM, + SRSSoundPath = nil, -- defaults to "l10n/DEFAULT/", i.e. add messages via "Sount to..." in the ME + SRSPort = 5002, + DCSRadio = false, + DCSFrequency = 243, + DCSModulation = radio.modulation.AM, + DCSRadioGroup = nil, + limithelos = true, + helonumber = 3, + gettext = nil, + locale ="en", -- default text language + SRSTTSRadio = false, + SRSGoogle = false, + SRSQ = nil, + SRSPilot = nil, + SRSPilotVoice = false, + SRSOperator = nil, + SRSOperatorVoice = false, + PilotStore = nil, + Speed = 100, + Altitude = 1500, + UseEventEject = false, + Delay = 100, + UseRescueZone = false, + RescueZone = nil, +} + +-- TODO Messages +--- Messages enum +-- @field Messages +AICSAR.Messages = { + EN = { + INITIALOK = "Roger, Pilot, we hear you. Stay where you are, a helo is on the way!", + INITIALNOTOK = "Sorry, Pilot. You're behind maximum operational distance! Good Luck!", + PILOTDOWN = "Mayday, mayday, mayday! Pilot down at ", + PILOTKIA = "Pilot KIA!", + HELODOWN = "CSAR Helo Down!", + PILOTRESCUED = "Pilot rescued!", + PILOTINHELO = "Pilot picked up!", + }, + DE = { + INITIALOK = "Copy, Pilot, wir hören Sie. Bleiben Sie, wo Sie sind!\nEin Hubschrauber sammelt Sie auf!", + INITIALNOTOK = "Verstehe, Pilot. Sie sind zu weit weg von uns.\nViel Glück!", + PILOTDOWN = "Mayday, mayday, mayday! Pilot abgestürzt: ", + PILOTKIA = "Pilot gefallen!", + HELODOWN = "CSAR Hubschrauber verloren!", + PILOTRESCUED = "Pilot gerettet!", + PILOTINHELO = "Pilot an Bord geholt!", + }, +} + +-- TODO Radio Messages +--- Radio Messages enum for ogg files +-- @field RadioMessages +AICSAR.RadioMessages = { + EN = { + INITIALOK = "initialok.ogg", -- 4.1 secs + INITIALNOTOK = "initialnotok.ogg", -- 4.6 secs + PILOTDOWN = "pilotdown.ogg", -- 2.6 secs + PILOTKIA = "pilotkia.ogg", -- 1.1 sec + HELODOWN = "helodown.ogg", -- 2.1 secs + PILOTRESCUED = "pilotrescued.ogg", -- 3.5 secs + PILOTINHELO = "pilotinhelo.ogg", -- 2.6 secs + }, +} + +-- TODO Radio Messages +--- Radio Messages enum for ogg files length in secs +-- @field RadioLength +AICSAR.RadioLength = { + EN = { + INITIALOK = 4.1, + INITIALNOTOK = 4.6, + PILOTDOWN = 2.6, + PILOTKIA = 1.1, + HELODOWN = 2.1, + PILOTRESCUED = 3.5, + PILOTINHELO = 2.6, + }, +} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function to create a new AICSAR object +-- @param #AICSAR self +-- @param #string Alias Name of this instance. +-- @param #number Coalition Coalition as in coalition.side.BLUE, can also be passed as "blue", "red" or "neutral" +-- @param #string Pilottemplate Pilot template name. +-- @param #string Helotemplate Helicopter template name. +-- @param Wrapper.Airbase#AIRBASE FARP FARP object or Airbase from where to start. +-- @param Core.Zone#ZONE MASHZone Zone where to drop pilots after rescue. +-- @param #number Helonumber Max number of alive Ai Helos at the same time. Defaults to three. +-- @return #AICSAR self +function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone,Helonumber) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) + + --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 AICSAR!") + 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 + + self.template = Pilottemplate + self.helotemplate = Helotemplate + self.farp = FARP + self.farpzone = MASHZone + self.playerset = SET_CLIENT:New():FilterActive(true):FilterCategories("helicopter"):FilterStart() + self.UseEventEject = false + self.Delay = 300 + + -- Radio + self.SRS = nil + self.SRSRadio = false + self.SRSTTSRadio = false + self.SRSGoogle = false + self.SRSQ = nil + self.SRSFrequency = 243 + self.SRSPath = "\\" + self.SRSModulation = radio.modulation.AM + self.SRSSoundPath = nil -- defaults to "l10n/DEFAULT/", i.e. add messages via "Sound to..." in the ME + self.SRSPort = 5002 + + -- DCS Radio - add messages via "Sound to..." in the ME + self.DCSRadio = false + self.DCSFrequency = 243 + self.DCSModulation = radio.modulation.AM + self.DCSRadioGroup = nil + self.DCSRadioQueue = nil + + self.MGRS_Accuracy = 2 + + -- limit number of available helos at the same time + self.limithelos = true + self.helonumber = Helonumber or 3 + + -- localization + self:InitLocalization() + + -- 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") + + --Pilot Store + self.PilotStore = FIFO:New() + + -- 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", "*") -- Pilot down + self:AddTransition("*", "PilotPickedUp", "*") -- Pilot in helo + self:AddTransition("*", "PilotUnloaded", "*") -- Pilot Unloaded from helo + self:AddTransition("*", "PilotRescued", "*") -- Pilot Rescued + self:AddTransition("*", "PilotKIA", "*") -- Pilot dead + self:AddTransition("*", "HeloDown", "*") -- Helo dead + self:AddTransition("*", "HeloOnDuty", "*") -- Helo spawnd + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self:HandleEvent(EVENTS.LandingAfterEjection,self._EventHandler) + self:HandleEvent(EVENTS.Ejection,self._EjectEventHandler) + + self:__Start(math.random(2,5)) + + local text = string.format("%sAICSAR Version %s Starting",self.lid,self.version) + + self:I(text) + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Status". + -- @function [parent=#AICSAR] Status + -- @param #AICSAR self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#AICSAR] __Status + -- @param #AICSAR self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". + -- @function [parent=#AICSAR] Stop + -- @param #AICSAR self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#AICSAR] __Stop + -- @param #AICSAR self + -- @param #number delay Delay in seconds. + + --- On after "PilotDown" event. + -- @function [parent=#AICSAR] OnAfterPilotDown + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Coordinate Location of the pilot. + -- @param #boolean InReach True if in maxdistance else false. + + --- On after "PilotPickedUp" event. + -- @function [parent=#AICSAR] OnAfterPilotPickedUp + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.FlightGroup#FLIGHTGROUP Helo + -- @param #table CargoTable of Ops.OpsGroup#OPSGROUP Cargo objects + -- @param #number Index + + --- On after "PilotRescued" event. + -- @function [parent=#AICSAR] OnAfterPilotRescued + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string PilotName + + --- On after "PilotUnloaded" event. + -- @function [parent=#AICSAR] OnAfterPilotUnloaded + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.FlightGroup#FLIGHTGROUP Helo + -- @param Ops.OpsGroup#OPSGROUP OpsGroup + + --- On after "PilotKIA" event. + -- @function [parent=#AICSAR] OnAfterPilotKIA + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On after "HeloOnDuty" event. + -- @function [parent=#AICSAR] OnAfterHeloOnDuty + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#GROUP Helo Helo group object + + --- On after "HeloDown" event. + -- @function [parent=#AICSAR] OnAfterHeloDown + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.FlightGroup#FLIGHTGROUP Helo + -- @param #number Index + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- [Internal] Create the Moose TextAndSoundEntries +-- @param #AICSAR self +-- @return #AICSAR self +function AICSAR:InitLocalization() + self:T(self.lid .. "InitLocalization") + -- English standard localization + self.gettext=TEXTANDSOUND:New(self.ClassName, "en") + self.gettext:AddEntry("en","INITIALOK",AICSAR.Messages.EN.INITIALOK,AICSAR.RadioMessages.EN.INITIALOK,AICSAR.RadioLength.INITIALOK) + self.gettext:AddEntry("en","INITIALNOTOK",AICSAR.Messages.EN.INITIALNOTOK,AICSAR.RadioMessages.EN.INITIALNOTOK,AICSAR.RadioLength.EN.INITIALNOTOK) + self.gettext:AddEntry("en","HELODOWN",AICSAR.Messages.EN.HELODOWN,AICSAR.RadioMessages.EN.HELODOWN,AICSAR.RadioLength.EN.HELODOWN) + self.gettext:AddEntry("en","PILOTDOWN",AICSAR.Messages.EN.PILOTDOWN,AICSAR.RadioMessages.EN.PILOTDOWN,AICSAR.RadioLength.EN.PILOTDOWN) + self.gettext:AddEntry("en","PILOTINHELO",AICSAR.Messages.EN.PILOTINHELO,AICSAR.RadioMessages.EN.PILOTINHELO,AICSAR.RadioLength.EN.PILOTINHELO) + self.gettext:AddEntry("en","PILOTKIA",AICSAR.Messages.EN.PILOTKIA,AICSAR.RadioMessages.EN.PILOTKIA,AICSAR.RadioLength.EN.PILOTKIA) + self.gettext:AddEntry("en","PILOTRESCUED",AICSAR.Messages.EN.PILOTRESCUED,AICSAR.RadioMessages.EN.PILOTRESCUED,AICSAR.RadioLength.EN.PILOTRESCUED) + -- German localization - we keep the sound files English + self.gettext:AddEntry("de","INITIALOK",AICSAR.Messages.DE.INITIALOK,AICSAR.RadioMessages.EN.INITIALOK,AICSAR.RadioLength.INITIALOK) + self.gettext:AddEntry("de","INITIALNOTOK",AICSAR.Messages.DE.INITIALNOTOK,AICSAR.RadioMessages.EN.INITIALNOTOK,AICSAR.RadioLength.EN.INITIALNOTOK) + self.gettext:AddEntry("de","HELODOWN",AICSAR.Messages.DE.HELODOWN,AICSAR.RadioMessages.EN.HELODOWN,AICSAR.RadioLength.EN.HELODOWN) + self.gettext:AddEntry("de","PILOTDOWN",AICSAR.Messages.DE.PILOTDOWN,AICSAR.RadioMessages.EN.PILOTDOWN,AICSAR.RadioLength.EN.PILOTDOWN) + self.gettext:AddEntry("de","PILOTINHELO",AICSAR.Messages.DE.PILOTINHELO,AICSAR.RadioMessages.EN.PILOTINHELO,AICSAR.RadioLength.EN.PILOTINHELO) + self.gettext:AddEntry("de","PILOTKIA",AICSAR.Messages.DE.PILOTKIA,AICSAR.RadioMessages.EN.PILOTKIA,AICSAR.RadioLength.EN.PILOTKIA) + self.gettext:AddEntry("de","PILOTRESCUED",AICSAR.Messages.DE.PILOTRESCUED,AICSAR.RadioMessages.EN.PILOTRESCUED,AICSAR.RadioLength.EN.PILOTRESCUED) + self.locale = "en" + return self +end + +--- [User] Use a defined zone as area of operation and not the distance to FARP. +-- @param #AICSAR self +-- @param Core.Zone#ZONE Zone The operational zone to use. Downed pilots in this area will be rescued. Can be any known #ZONE type. +-- @return #AICSAR self +function AICSAR:SetUsingRescueZone(Zone) + self.UseRescueZone = true + self.RescueZone = Zone + return self +end + +--- [User] Switch sound output on and use SRS output for sound files. +-- @param #AICSAR self +-- @param #boolean OnOff Switch on (true) or off (false). +-- @param #string Path Path to your SRS Server External Audio Component, e.g. "C:\\\\Program Files\\\\DCS-SimpleRadio-Standalone\\\\ExternalAudio" +-- @param #number Frequency Defaults to 243 (guard) +-- @param #number Modulation Radio modulation. Defaults to radio.modulation.AM +-- @param #string SoundPath Where to find the audio files. Defaults to nil, i.e. add messages via "Sound to..." in the Mission Editor. +-- @param #number Port Port of the SRS, defaults to 5002. +-- @return #AICSAR self +function AICSAR:SetSRSRadio(OnOff,Path,Frequency,Modulation,SoundPath,Port) + self:T(self.lid .. "SetSRSRadio") + self.SRSRadio = OnOff and true + self.SRSTTSRadio = false + self.SRSFrequency = Frequency or 243 + self.SRSPath = Path or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" + self.SRS:SetLabel("ACSR") + self.SRS:SetCoalition(self.coalition) + self.SRSModulation = Modulation or radio.modulation.AM + local soundpath = os.getenv('TMP') .. "\\DCS\\Mission\\l10n\\DEFAULT" -- defaults to "l10n/DEFAULT/", i.e. add messages by "Sound to..." in the ME + self.SRSSoundPath = SoundPath or soundpath + self.SRSPort = Port or MSRS.port or 5002 + if OnOff then + self.SRS = MSRS:New(Path,Frequency,Modulation) + self.SRS:SetPort(self.SRSPort) + end + return self +end + +--- [User] Switch sound output on and use SRS-TTS output. The voice will be used across all outputs, unless you define an extra voice for downed pilots and/or the operator. +-- See `AICSAR:SetPilotTTSVoice()` and `AICSAR:SetOperatorTTSVoice()` +-- @param #AICSAR self +-- @param #boolean OnOff Switch on (true) or off (false). +-- @param #string Path Path to your SRS Server Component, e.g. "E:\\\\Program Files\\\\DCS-SimpleRadio-Standalone\\ExternalAudio" +-- @param #number Frequency (Optional) Defaults to 243 (guard) +-- @param #number Modulation (Optional) Radio modulation. Defaults to radio.modulation.AM +-- @param #number Port (Optional) Port of the SRS, defaults to 5002. +-- @param #string Voice (Optional) The voice to be used. +-- @param #string Culture (Optional) The culture to be used, defaults to "en-GB" +-- @param #string Gender (Optional) The gender to be used, defaults to "male" +-- @param #string GoogleCredentials (Optional) Path to google credentials +-- @return #AICSAR self +function AICSAR:SetSRSTTSRadio(OnOff,Path,Frequency,Modulation,Port,Voice,Culture,Gender,GoogleCredentials) + self:T(self.lid .. "SetSRSTTSRadio") + self.SRSTTSRadio = OnOff and true + self.SRSRadio = false + self.SRSFrequency = Frequency or 243 + self.SRSPath = Path or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" + self.SRSModulation = Modulation or radio.modulation.AM + self.SRSPort = Port or MSRS.port or 5002 + if OnOff then + self.SRS = MSRS:New(self.SRSPath,Frequency,Modulation) + self.SRS:SetPort(self.SRSPort) + self.SRS:SetCoalition(self.coalition) + self.SRS:SetLabel("ACSR") + self.SRS:SetVoice(Voice) + self.SRS:SetCulture(Culture) + self.SRS:SetGender(Gender) + if GoogleCredentials then + self.SRS:SetProviderOptionsGoogle(GoogleCredentials,GoogleCredentials) + self.SRS:SetProvider(MSRS.Provider.GOOGLE) + self.SRSGoogle = true + end + self.SRSQ = MSRSQUEUE:New(self.alias) + end + return self +end + +--- [User] Set SRS TTS Voice of downed pilot. `AICSAR:SetSRSTTSRadio()` needs to be set first! +-- @param #AICSAR self +-- @param #string Voice The voice to be used, e.g. `MSRS.Voices.Google.Standard.en_US_Standard_J` for Google or `MSRS.Voices.Microsoft.David` for Microsoft. +-- Specific voices override culture and gender! +-- @param #string Culture (Optional) The culture to be used, defaults to "en-US" +-- @param #string Gender (Optional) The gender to be used, defaults to "male" +-- @return #AICSAR self +function AICSAR:SetPilotTTSVoice(Voice,Culture,Gender) + self:T(self.lid .. "SetPilotTTSVoice") + self.SRSPilotVoice = true + self.SRSPilot = MSRS:New(self.SRSPath,self.SRSFrequency,self.SRSModulation) + self.SRSPilot:SetCoalition(self.coalition) + self.SRSPilot:SetVoice(Voice) + self.SRSPilot:SetCulture(Culture or "en-US") + self.SRSPilot:SetGender(Gender or "male") + self.SRSPilot:SetLabel("PILOT") + if self.SRSGoogle then + local poptions = self.SRS:GetProviderOptions(MSRS.Provider.GOOGLE) -- Sound.SRS#MSRS.ProviderOptions + self.SRSPilot:SetProviderOptionsGoogle(poptions.credentials,poptions.key) + self.SRSPilot:SetProvider(MSRS.Provider.GOOGLE) + end + return self +end + +--- [User] Set SRS TTS Voice of the rescue operator. `AICSAR:SetSRSTTSRadio()` needs to be set first! +-- @param #AICSAR self +-- @param #string Voice The voice to be used, e.g. `MSRS.Voices.Google.Standard.en_US_Standard_J` for Google or `MSRS.Voices.Microsoft.David` for Microsoft. +-- Specific voices override culture and gender! +-- @param #string Culture (Optional) The culture to be used, defaults to "en-GB" +-- @param #string Gender (Optional) The gender to be used, defaults to "female" +-- @return #AICSAR self +function AICSAR:SetOperatorTTSVoice(Voice,Culture,Gender) + self:T(self.lid .. "SetOperatorTTSVoice") + self.SRSOperatorVoice = true + self.SRSOperator = MSRS:New(self.SRSPath,self.SRSFrequency,self.SRSModulation) + self.SRSOperator:SetCoalition(self.coalition) + self.SRSOperator:SetVoice(Voice) + self.SRSOperator:SetCulture(Culture or "en-GB") + self.SRSOperator:SetGender(Gender or "female") + self.SRSOperator:SetLabel("RESCUE") + if self.SRSGoogle then + local poptions = self.SRS:GetProviderOptions(MSRS.Provider.GOOGLE) -- Sound.SRS#MSRS.ProviderOptions + self.SRSOperator:SetProviderOptionsGoogle(poptions.credentials,poptions.key) + self.SRSOperator:SetProvider(MSRS.Provider.GOOGLE) + end + return self +end + +--- [User] Switch sound output on and use normale (DCS) radio +-- @param #AICSAR self +-- @param #boolean OnOff Switch on (true) or off (false). +-- @param #number Frequency Defaults to 243 (guard). +-- @param #number Modulation Radio modulation. Defaults to radio.modulation.AM. +-- @param Wrapper.Group#GROUP Group The group to use as sending station. +-- @return #AICSAR self +function AICSAR:SetDCSRadio(OnOff,Frequency,Modulation,Group) + self:T(self.lid .. "SetDCSRadio") + self:T(self.lid .. "SetDCSRadio to "..tostring(OnOff)) + self.DCSRadio = OnOff and true + self.DCSFrequency = Frequency or 243 + self.DCSModulation = Modulation or radio.modulation.AM + self.DCSRadioGroup = Group + if self.DCSRadio then + self.DCSRadioQueue = RADIOQUEUE:New(Frequency,Modulation,"AI-CSAR") + self.DCSRadioQueue:Start(5,5) + self.DCSRadioQueue:SetRadioPower(1000) + self.DCSRadioQueue:SetSenderCoordinate(Group:GetCoordinate()) + else + if self.DCSRadioQueue then + self.DCSRadioQueue:Stop() + end + end + return self +end + +--- [Internal] Sound output via non-SRS Radio. Add message files (.ogg) via "Sound to..." in the ME. +-- @param #AICSAR self +-- @param #string Soundfile Name of the soundfile +-- @param #number Duration Duration of the sound +-- @param #string Subtitle Text to display +-- @return #AICSAR self +function AICSAR:DCSRadioBroadcast(Soundfile,Duration,Subtitle) + self:T(self.lid .. "DCSRadioBroadcast") + local radioqueue = self.DCSRadioQueue -- Sound.RadioQueue#RADIOQUEUE + radioqueue:NewTransmission(Soundfile,Duration,nil,2,nil,Subtitle,10) + return self +end + +--- [Internal] Catch the ejection and save the pilot name +-- @param #AICSAR self +-- @param Core.Event#EVENTDATA EventData +-- @return #AICSAR self +function AICSAR:_EjectEventHandler(EventData) + local _event = EventData -- Core.Event#EVENTDATA + if _event.IniPlayerName then + self.PilotStore:Push(_event.IniPlayerName) + self:T(self.lid.."Pilot Ejected: ".._event.IniPlayerName) + if self.UseEventEject then + -- get position and spawn in a template pilot + local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p) + local _country = _event.initiator:getCountry() + local _coalition = coalition.getCountryCoalition( _country ) + --local data = UTILS.DeepCopy(EventData) + Unit.destroy(_event.initiator) -- shagrat remove static Pilot model + self:ScheduleOnce(self.Delay,self._DelayedSpawnPilot,self,_LandingPos,_coalition) + end + end + return self +end + +--- [Internal] Spawn a pilot +-- @param #AICSAR self +-- @param Core.Point#COORDINATE _LandingPos Landing Postion +-- @param #number _coalition Coalition side +-- @return #AICSAR self +function AICSAR:_DelayedSpawnPilot(_LandingPos,_coalition) + + local distancetofarp = _LandingPos:Get2DDistance(self.farp:GetCoordinate()) + if self.UseRescueZone == true and self.RescueZone ~= nil then + if self.RescueZone:IsCoordinateInZone(_LandingPos) then + distancetofarp = self.maxdistance - 10 + else + distancetofarp = self.maxdistance + 10 + end + end + -- Mayday Message + local Text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTDOWN",self.locale) + local text = "" + local setting = {} + setting.MGRS_Accuracy = self.MGRS_Accuracy + local location = _LandingPos:ToStringMGRS(setting) + local msgtxt = Text..location.."!" + location = string.gsub(location,"MGRS ","") + location = string.gsub(location,"%s+","") + location = string.gsub(location,"([%a%d])","%1;") -- "0 5 1 " + location = string.gsub(location,"0","zero") + location = string.gsub(location,"9","niner") + location = "MGRS;"..location + if self.SRSGoogle then + location = string.format("%s",location) + end + text = Text .. location .. "!" + local ttstext = Text .. location .. "! Repeat! "..location + if _coalition == self.coalition then + if self.verbose then + MESSAGE:New(msgtxt,15,"AICSAR"):ToCoalition(self.coalition) + -- MESSAGE:New(msgtxt,15,"AICSAR"):ToLog() + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + elseif self.SRSTTSRadio then + if self.SRSPilotVoice then + self.SRSQ:NewTransmission(ttstext,nil,self.SRSPilot,nil,1) + else + self.SRSQ:NewTransmission(ttstext,nil,self.SRS,nil,1) + end + end + end + + -- further processing + if _coalition == self.coalition and distancetofarp <= self.maxdistance then + -- in reach + self:T(self.lid .. "Spawning new Pilot") + self.pilotindex = self.pilotindex + 1 + local newpilot = SPAWN:NewWithAlias(self.template,string.format("%s-AICSAR-%d",self.template, self.pilotindex)) + newpilot:InitDelayOff() + newpilot:OnSpawnGroup( + function (grp) + self.pilotqueue[self.pilotindex] = grp + end + ) + newpilot:SpawnFromCoordinate(_LandingPos) + + self:__PilotDown(2,_LandingPos,true) + elseif _coalition == self.coalition and distancetofarp > self.maxdistance then + -- out of reach, apologies, too far off + self:T(self.lid .. "Pilot out of reach") + self:__PilotDown(2,_LandingPos,false) + end + return self +end + +--- [Internal] Catch the landing after ejection and spawn a pilot in situ. +-- @param #AICSAR self +-- @param Core.Event#EVENTDATA EventData +-- @param #boolean FromEject +-- @return #AICSAR self +function AICSAR:_EventHandler(EventData, FromEject) + self:T(self.lid .. "OnEventLandingAfterEjection ID=" .. EventData.id) + + -- autorescue on off? + if self.autoonoff then + if self.playerset:CountAlive() > 0 then + return self + end + end + + if self.UseEventEject and (not FromEject) then return self end + + local _event = EventData -- Core.Event#EVENTDATA + -- get position and spawn in a template pilot + local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p) + local _country = _event.initiator:getCountry() + local _coalition = coalition.getCountryCoalition( _country ) + + -- DONE: add distance check + local distancetofarp = _LandingPos:Get2DDistance(self.farp:GetCoordinate()) + if self.UseRescueZone == true and self.RescueZone ~= nil then + if self.RescueZone:IsCoordinateInZone(_LandingPos) then + distancetofarp = self.maxdistance - 10 + else + distancetofarp = self.maxdistance + 10 + end + end + -- Mayday Message + local Text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTDOWN",self.locale) + local text = "" + local setting = {} + setting.MGRS_Accuracy = self.MGRS_Accuracy + local location = _LandingPos:ToStringMGRS(setting) + local msgtxt = Text..location.."!" + location = string.gsub(location,"MGRS ","") + location = string.gsub(location,"%s+","") + location = string.gsub(location,"([%a%d])","%1;") -- "0 5 1 " + location = string.gsub(location,"0","zero") + location = string.gsub(location,"9","niner") + location = "MGRS;"..location + if self.SRSGoogle then + location = string.format("%s",location) + end + text = Text .. location .. "!" + local ttstext = Text .. location .. "! Repeat! "..location + if _coalition == self.coalition then + if self.verbose then + MESSAGE:New(msgtxt,15,"AICSAR"):ToCoalition(self.coalition) + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + elseif self.SRSTTSRadio then + if self.SRSPilotVoice then + self.SRSQ:NewTransmission(ttstext,nil,self.SRSPilot,nil,1) + else + self.SRSQ:NewTransmission(ttstext,nil,self.SRS,nil,1) + end + end + end + + -- further processing + if _coalition == self.coalition and distancetofarp <= self.maxdistance then + -- in reach + self:T(self.lid .. "Spawning new Pilot") + self.pilotindex = self.pilotindex + 1 + local newpilot = SPAWN:NewWithAlias(self.template,string.format("%s-AICSAR-%d",self.template, self.pilotindex)) + newpilot:InitDelayOff() + newpilot:OnSpawnGroup( + function (grp) + self.pilotqueue[self.pilotindex] = grp + end + ) + newpilot:SpawnFromCoordinate(_LandingPos) + + Unit.destroy(_event.initiator) -- shagrat remove static Pilot model + self:__PilotDown(2,_LandingPos,true) + elseif _coalition == self.coalition and distancetofarp > self.maxdistance then + -- out of reach, apologies, too far off + self:T(self.lid .. "Pilot out of reach") + self:__PilotDown(2,_LandingPos,false) + end + return self +end + +--- [Internal] Get FlightGroup +-- @param #AICSAR self +-- @return Ops.FlightGroup#FLIGHTGROUP The FlightGroup +function AICSAR:_GetFlight() + self:T(self.lid .. "_GetFlight") + -- Helo Carrier. + local newhelo = SPAWN:NewWithAlias(self.helotemplate,self.helotemplate..math.random(1,10000)) + :InitDelayOff() + :InitUnControlled(true) + :OnSpawnGroup( + function(Group) + Group:OptionPreferVerticalLanding() + self:__HeloOnDuty(1,Group) + end + ) + :Spawn() + + local nhelo=FLIGHTGROUP:New(newhelo) + nhelo:SetHomebase(self.farp) + nhelo:Activate() + return nhelo +end + +--- [Internal] Create a new rescue mission +-- @param #AICSAR self +-- @param Wrapper.Group#GROUP Pilot The pilot to be rescued. +-- @param #number Index Index number of this pilot +-- @return #AICSAR self +function AICSAR:_InitMission(Pilot,Index) + self:T(self.lid .. "_InitMission") + + local pickupzone = ZONE_GROUP:New(Pilot:GetName(),Pilot,self.rescuezoneradius) + --local pilotset = SET_GROUP:New() + --pilotset:AddGroup(Pilot) + + -- Cargo transport assignment. + local opstransport=OPSTRANSPORT:New(Pilot, pickupzone, self.farpzone) + --opstransport:SetVerbosity(3) + + local helo = self:_GetFlight() + -- inject reservation + helo.AICSARReserved = true + + helo:SetDefaultAltitude(self.Altitude or 1500) + + helo:SetDefaultSpeed(self.Speed or 100) + + -- Cargo transport assignment to first Huey group. + helo:AddOpsTransport(opstransport) + + -- callback functions + local function AICPickedUp(Helo,Cargo,Index) + self:__PilotPickedUp(2,Helo,Cargo,Index) + end + + local function AICHeloDead(Helo,Index) + self:__HeloDown(2,Helo,Index) + end + + local function AICHeloUnloaded(Helo,OpsGroup) + self:__PilotUnloaded(2,Helo,OpsGroup) + end + + function helo:OnAfterLoading(From,Event,To) + AICPickedUp(helo,helo:GetCargoGroups(),Index) + helo:__LoadingDone(5) + end + + function helo:OnAfterDead(From,Event,To) + AICHeloDead(helo,Index) + end + + function helo:OnAfterUnloaded(From,Event,To,OpsGroupCargo) + AICHeloUnloaded(helo,OpsGroupCargo) + helo:__UnloadingDone(5) + end + + function helo:OnAfterLandAtAirbase(From,Event,To,airbase) + helo:Despawn(2) + end + + self.helos[Index] = helo + + return self +end + +--- [Internal] Check if pilot arrived in rescue zone (MASH) +-- @param #AICSAR self +-- @param Wrapper.Group#GROUP Pilot The pilot to be rescued. +-- @return #boolean outcome +function AICSAR:_CheckInMashZone(Pilot) + self:T(self.lid .. "_CheckInMashZone") + if Pilot:IsInZone(self.farpzone) then + return true + else + return false + end +end + +--- [User] Set default helo speed. Note - AI might have other ideas. Defaults to 100kn. +-- @param #AICSAR self +-- @param #number Knots Speed in knots. +-- @return #AICSAR self +function AICSAR:SetDefaultSpeed(Knots) + self:T(self.lid .. "SetDefaultSpeed") + self.Speed = Knots or 100 + return self +end + +--- [User] Set default helo altitudeAGL. Note - AI might have other ideas. Defaults to 1500ft. +-- @param #AICSAR self +-- @param #number Feet AGL set in feet. +-- @return #AICSAR self +function AICSAR:SetDefaultAltitude(Feet) + self:T(self.lid .. "SetDefaultAltitude") + self.Altitude = Feet or 1500 + return self +end + +--- [Internal] Check helo queue +-- @param #AICSAR self +-- @return #AICSAR self +function AICSAR:_CheckHelos() + self:T(self.lid .. "_CheckHelos") + for _index,_helo in pairs(self.helos) do + local helo = _helo -- Ops.FlightGroup#FLIGHTGROUP + if helo and helo.ClassName == "FLIGHTGROUP" then + local state = helo:GetState() + local name = helo:GetName() + self:T("Helo group "..name.." in state "..state) + if state == "Arrived" then + --helo:__Stop(5) + helo.OnAfterDead = nil + helo:Despawn(35) + self.helos[_index] = nil + end + else + self.helos[_index] = nil + end + end + return self +end + +--- [Internal] Count helos queue +-- @param #AICSAR self +-- @return #number Number of helos on mission +function AICSAR:_CountHelos() + self:T(self.lid .. "_CountHelos") + local count = 0 + for _index,_helo in pairs(self.helos) do + count = count + 1 + end + return count +end + +--- [Internal] Check pilot queue for next mission +-- @param #AICSAR self +-- @param Ops.OpsGroup#OPSGROUP OpsGroup +-- @return #AICSAR self +function AICSAR:_CheckQueue(OpsGroup) + self:T(self.lid .. "_CheckQueue") + for _index, _pilot in pairs(self.pilotqueue) do + local classname = _pilot.ClassName and _pilot.ClassName or "NONE" + local name = _pilot.GroupName and _pilot.GroupName or "NONE" + local playername = "John Doe" + local helocount = self:_CountHelos() + --self:T("Looking at " .. classname .. " " .. name) + -- find one w/o mission + if _pilot and _pilot.ClassName and _pilot.ClassName == "GROUP" then + local flightgroup = self.helos[_index] -- Ops.FlightGroup#FLIGHTGROUP + -- rescued? + if self:_CheckInMashZone(_pilot) then + self:T("Pilot" .. _pilot.GroupName .. " rescued!") + if OpsGroup then + --OpsGroup:Despawn(10) + else + _pilot:Destroy(true,10) + end + self.pilotqueue[_index] = nil + self.rescued[_index] = true + if self.PilotStore:Count() > 0 then + playername = self.PilotStore:Pull() + end + self:__PilotRescued(2,playername) + if flightgroup then + flightgroup.AICSARReserved = false + end + end -- end rescued + -- has no mission assigned? + if not _pilot.AICSAR then + -- helo available? + if self.limithelos and helocount >= self.helonumber then + -- none free + break + end -- end limit + _pilot.AICSAR = {} + _pilot.AICSAR.Status = "Initiated" + _pilot.AICSAR.Boarded = false + self:_InitMission(_pilot,_index) + break + else + -- update status from OPSGROUP + if flightgroup then + local state = flightgroup:GetState() + _pilot.AICSAR.Status = state + end + --self:T("Flight for " .. _pilot.GroupName .. " in state " .. state) + end -- end has mission + end -- end if pilot + end -- end loop + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- [Internal] onafterStart +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AICSAR self +function AICSAR:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:__Status(3) + return self +end + +--- [Internal] onafterStatus +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AICSAR self +function AICSAR:onafterStatus(From, Event, To) + self:T({From, Event, To}) + --self:_CheckQueue() + self:_CheckHelos() + self:__Status(30) + return self +end + +--- [Internal] onafterStop +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AICSAR self +function AICSAR:onafterStop(From, Event, To) + self:T({From, Event, To}) + self:UnHandleEvent(EVENTS.LandingAfterEjection) + if self.DCSRadioQueue then + self.DCSRadioQueue:Stop() + end + return self +end + +--- [Internal] onafterPilotDown +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Core.Point#COORDINATE Coordinate Location of the pilot. +-- @param #boolean InReach True if in maxdistance else false. +-- @return #AICSAR self +function AICSAR:onafterPilotDown(From, Event, To, Coordinate, InReach) + self:T({From, Event, To}) + local CoordinateText = Coordinate:ToStringMGRS() + local inreach = tostring(InReach) + --local text = string.format("Pilot down at %s. In reach = %s",CoordinateText,inreach) + if InReach then + local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("INITIALOK",self.locale) + --local text = AICSAR.Messages.EN.INITIALOK + self:T(text) + if self.verbose then + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + elseif self.SRSTTSRadio then + if self.SRSOperatorVoice then + self.SRSQ:NewTransmission(text,nil,self.SRSOperator,nil,1) + else + self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) + end + end + else + local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("INITIALNOTOK",self.locale) + --local text = AICSAR.Messages.EN.INITIALNOTOK + self:T(text) + if self.verbose then + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + elseif self.SRSTTSRadio then + if self.SRSOperatorVoice then + self.SRSQ:NewTransmission(text,nil,self.SRSOperator,nil,1) + else + self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) + end + end + end + self:_CheckQueue() + return self +end + +--- [Internal] onafterPilotKIA +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AICSAR self +function AICSAR:onafterPilotKIA(From, Event, To) + self:T({From, Event, To}) + local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTKIA",self.locale) + if self.verbose then + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + elseif self.SRSTTSRadio then + self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) + end + return self +end + +--- [Internal] onafterHeloDown +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.FlightGroup#FLIGHTGROUP Helo +-- @param #number Index +-- @return #AICSAR self +function AICSAR:onafterHeloDown(From, Event, To, Helo, Index) + self:T({From, Event, To}) + local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("HELODOWN",self.locale) + if self.verbose then + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + elseif self.SRSTTSRadio then + if self.SRSOperatorVoice then + self.SRSQ:NewTransmission(text,nil,self.SRSOperator,nil,1) + else + self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) + end + end + local findex = 0 + local fhname = Helo:GetName() + -- find index of Helo + if Index and Index > 0 then + findex=Index + else + for _index, _helo in pairs(self.helos) do + local helo = _helo -- Ops.FlightGroup#FLIGHTGROUP + local hname = helo:GetName() + if fhname == hname then + findex = _index + break + end + end + end + -- find pilot + if findex > 0 and not self.rescued[findex] then + local pilot = self.pilotqueue[findex] + self.helos[findex] = nil + if pilot.AICSAR.Boarded then + self:T("Helo Down: Found DEAD Pilot ID " .. findex .. " with name " .. pilot:GetName()) + -- pilot also dead + self:__PilotKIA(2) + self.pilotqueue[findex] = nil + else + -- initiate new mission + self:T("Helo Down: Found ALIVE Pilot ID " .. findex .. " with name " .. pilot:GetName()) + self:_InitMission(pilot,findex) + end + end + return self +end + +--- [Internal] onafterPilotRescued +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #string PilotName +-- @return #AICSAR self +function AICSAR:onafterPilotRescued(From, Event, To, PilotName) + self:T({From, Event, To}) + local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTRESCUED",self.locale) + if self.verbose then + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + elseif self.SRSTTSRadio then + self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) + end + return self +end + +--- [Internal] onafterPilotUnloaded +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.FlightGroup#FLIGHTGROUP Helo +-- @param Ops.OpsGroup#OPSGROUP OpsGroup +-- @return #AICSAR self +function AICSAR:onafterPilotUnloaded(From, Event, To, Helo, OpsGroup) + self:T({From, Event, To}) + self:_CheckQueue(OpsGroup) + return self +end + +--- [Internal] onafterPilotPickedUp +-- @param #AICSAR self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.FlightGroup#FLIGHTGROUP Helo +-- @param #table CargoTable of Ops.OpsGroup#OPSGROUP Cargo objects +-- @param #number Index +-- @return #AICSAR self +function AICSAR:onafterPilotPickedUp(From, Event, To, Helo, CargoTable, Index) + self:T({From, Event, To}) + local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTINHELO",self.locale) + if self.verbose then + MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition) + end + if self.SRSRadio then + local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength) + sound:SetPlayWithSRS(true) + self.SRS:PlaySoundFile(sound,2) + elseif self.DCSRadio then + self:DCSRadioBroadcast(Soundfile,Soundlength,text) + elseif self.SRSTTSRadio then + self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1) + end + local findex = 0 + local fhname = Helo:GetName() + if Index and Index > 0 then + findex = Index + else + -- find index of Helo + for _index, _helo in pairs(self.helos) do + local helo = _helo -- Ops.FlightGroup#FLIGHTGROUP + local hname = helo:GetName() + if fhname == hname then + findex = _index + break + end + end + end + -- find pilot + if findex > 0 then + local pilot = self.pilotqueue[findex] + self:T("Boarded: Found Pilot ID " .. findex .. " with name " .. pilot:GetName()) + pilot.AICSAR.Boarded = true -- mark as boarded + end + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- END AICSAR +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/AmmoTruck.lua b/Moose Development/Moose/Functional/AmmoTruck.lua new file mode 100644 index 000000000..d4181f1c8 --- /dev/null +++ b/Moose Development/Moose/Functional/AmmoTruck.lua @@ -0,0 +1,806 @@ +--- **Functional** -- Send a truck to supply artillery groups. +-- +-- === +-- +-- **AMMOTRUCK** - Send a truck to supply artillery groups. +-- +-- === +-- +-- ## Missions: +-- +-- Demo missions can be found on [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Functional/AmmoTruck) +-- +-- === +-- +-- ### Author : **applevangelist** +-- +-- @module Functional.AmmoTruck +-- @image Artillery.JPG +-- +-- Last update: July 2023 + +------------------------------------------------------------------------- +--- **AMMOTRUCK** class, extends Core.Fsm#FSM +-- @type AMMOTRUCK +-- @field #string ClassName Class Name +-- @field #string lid Lid for log entries +-- @field #string version Version string +-- @field #string alias Alias name +-- @field #boolean debug Debug flag +-- @field #table trucklist List of (alive) #AMMOTRUCK.data trucks +-- @field #table targetlist List of (alive) #AMMOTRUCK.data artillery +-- @field #number coalition Coalition this is for +-- @field Core.Set#SET_GROUP truckset SET of trucks +-- @field Core.Set#SET_GROUP targetset SET of artillery +-- @field #table remunitionqueue List of (alive) #AMMOTRUCK.data artillery to be reloaded +-- @field #table waitingtargets List of (alive) #AMMOTRUCK.data artillery waiting +-- @field #number ammothreshold Threshold (min) ammo before sending a truck +-- @field #number remunidist Max distance trucks will go +-- @field #number monitor Monitor interval in seconds +-- @field #number unloadtime Unload time in seconds +-- @field #number waitingtime Max waiting time in seconds +-- @field #boolean routeonroad Route truck on road if true (default) +-- @field #number reloads Number of reloads a single truck can do before he must return home +-- @extends Core.Fsm#FSM + +--- *Amateurs talk about tactics, but professionals study logistics.* - General Robert H Barrow, USMC +-- +-- Simple Class to re-arm your artillery with trucks. +-- +-- #AMMOTRUCK +-- +-- * Controls a SET\_GROUP of trucks which will re-arm a SET\_GROUP of artillery groups when they run out of ammunition. +-- +-- ## 1 The AMMOTRUCK concept +-- +-- A SET\_GROUP of trucks which will re-arm a SET\_GROUP of artillery groups when they run out of ammunition. They will be based on a +-- homebase and drive from there to the artillery groups and then back home. +-- Trucks are the **only known in-game mechanic** to re-arm artillery and other units in DCS. Working units are e.g.: M-939 (blue), Ural-375 and ZIL-135 (both red). +-- +-- ## 2 Set-up +-- +-- Define a set of trucks and a set of artillery: +-- +-- local truckset = SET_GROUP:New():FilterCoalitions("blue"):FilterActive(true):FilterCategoryGround():FilterPrefixes("Ammo Truck"):FilterStart() +-- local ariset = SET_GROUP:New():FilterCoalitions("blue"):FilterActive(true):FilterCategoryGround():FilterPrefixes("Artillery"):FilterStart() +-- +-- Create an AMMOTRUCK object to take care of the artillery using the trucks, with a homezone: +-- +-- local ammotruck = AMMOTRUCK:New(truckset,ariset,coalition.side.BLUE,"Logistics",ZONE:FindByName("HomeZone") +-- +-- ## 2 Options and their default values +-- +-- ammotruck.ammothreshold = 5 -- send a truck when down to this many rounds +-- ammotruck.remunidist = 20000 -- 20km - send trucks max this far from home +-- ammotruck.unloadtime = 600 -- 10 minutes - min time to unload ammunition +-- ammotruck.waitingtime = 1800 -- 30 mintes - wait max this long until remunition is done +-- ammotruck.monitor = -60 -- 1 minute - AMMOTRUCK checks run every one minute +-- ammotruck.routeonroad = true -- Trucks will **try** to drive on roads +-- ammotruck.usearmygroup = false -- If true, will make use of ARMYGROUP in the background (if used in DEV branch) +-- ammotruck.reloads = 5 -- Maxn re-arms a truck can do before he needs to go home and restock. Set to -1 for unlimited +-- +-- ## 3 FSM Events to shape mission +-- +-- Truck has been sent off: +-- +-- function ammotruck:OnAfterRouteTruck(From, Event, To, Truckdata, Aridata) +-- ... +-- end +-- +-- Truck has arrived: +-- +-- function ammotruck:OnAfterTruckArrived(From, Event, To, Truckdata) +-- ... +-- end +-- +-- Truck is unloading: +-- +-- function ammotruck:OnAfterTruckUnloading(From, Event, To, Truckdata) +-- ... +-- end +-- +-- Truck is returning home: +-- +-- function ammotruck:OnAfterTruckReturning(From, Event, To, Truckdata) +-- ... +-- end +-- +-- Truck is arrived at home: +-- +-- function ammotruck:OnAfterTruckHome(From, Event, To, Truckdata) +-- ... +-- end +-- +-- @field #AMMOTRUCK +AMMOTRUCK = { + ClassName = "AMMOTRUCK", + lid = "", + version = "0.0.12", + alias = "", + debug = false, + trucklist = {}, + targetlist = {}, + coalition = nil, + truckset = nil, + targetset = nil, + remunitionqueue = {}, + waitingtargets = {}, + ammothreshold = 5, + remunidist = 20000, + monitor = -60, + unloadtime = 600, + waitingtime = 1800, + routeonroad = true, + reloads = 5, +} + +--- +-- @type AMMOTRUCK.State +AMMOTRUCK.State = { + IDLE = "idle", + DRIVING = "driving", + ARRIVED = "arrived", + UNLOADING = "unloading", + RETURNING = "returning", + WAITING = "waiting", + RELOADING = "reloading", + OUTOFAMMO = "outofammo", + REQUESTED = "requested", +} + +--- +--@type AMMOTRUCK.data +--@field Wrapper.Group#GROUP group +--@field #string name +--@field #AMMOTRUCK.State statusquo +--@field #number timestamp +--@field #number ammo +--@field Core.Point#COORDINATE coordinate +--@field #string targetname +--@field Wrapper.Group#GROUP targetgroup +--@field Core.Point#COORDINATE targetcoordinate +--@field #number reloads + +--- +-- @param #AMMOTRUCK self +-- @param Core.Set#SET_GROUP Truckset Set of truck groups +-- @param Core.Set#SET_GROUP Targetset Set of artillery groups +-- @param #number Coalition Coalition +-- @param #string Alias Alias Name +-- @param Core.Zone#ZONE Homezone Home, return zone for trucks +-- @return #AMMOTRUCK self +-- @usage +-- Define a set of trucks and a set of artillery: +-- local truckset = SET_GROUP:New():FilterCoalitions("blue"):FilterActive(true):FilterCategoryGround():FilterPrefixes("Ammo Truck"):FilterStart() +-- local ariset = SET_GROUP:New():FilterCoalitions("blue"):FilterActive(true):FilterCategoryGround():FilterPrefixes("Artillery"):FilterStart() +-- +-- Create an AMMOTRUCK object to take care of the artillery using the trucks, with a homezone: +-- local ammotruck = AMMOTRUCK:New(truckset,ariset,coalition.side.BLUE,"Logistics",ZONE:FindByName("HomeZone") +function AMMOTRUCK:New(Truckset,Targetset,Coalition,Alias,Homezone) + + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, FSM:New()) -- #AMMOTRUCK + + self.truckset = Truckset -- Core.Set#SET_GROUP + self.targetset = Targetset -- Core.Set#SET_GROUP + self.coalition = Coalition -- #number + self.alias = Alias -- #string + self.debug = false + self.remunitionqueue = {} + self.trucklist = {} + self.targetlist = {} + self.ammothreshold = 5 + self.remunidist = 20000 + self.homezone = Homezone -- Core.Zone#ZONE + self.waitingtime = 1800 + self.usearmygroup = false + self.hasarmygroup = false + + -- Log id. + self.lid=string.format("AMMOTRUCK %s | %s | ", self.version, self.alias) + + self:SetStartState("Stopped") + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("*", "Monitor", "*") + self:AddTransition("*", "RouteTruck", "*") + self:AddTransition("*", "TruckArrived", "*") + self:AddTransition("*", "TruckUnloading", "*") + self:AddTransition("*", "TruckReturning", "*") + self:AddTransition("*", "TruckHome", "*") + self:AddTransition("*", "Stop", "Stopped") + + self:__Start(math.random(5,10)) + + self:I(self.lid .. "Started") + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Stop". Stops the AMMOTRUCK and all its event handlers. + -- @function [parent=#AMMOTRUCK] Stop + -- @param #AMMOTRUCK self + + --- Triggers the FSM event "Stop" after a delay. Stops the AMMOTRUCK and all its event handlers. + -- @function [parent=#AMMOTRUCK] __Stop + -- @param #AMMOTRUCK self + -- @param #number delay Delay in seconds. + + --- On after "RouteTruck" event. + -- @function [parent=#AMMOTRUCK] OnAfterRouteTruck + -- @param #AMMOTRUCK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #AMMOTRUCK.data Truck + -- @param #AMMOTRUCK.data Artillery + + --- On after "TruckUnloading" event. + -- @function [parent=#AMMOTRUCK] OnAfterTruckUnloading + -- @param #AMMOTRUCK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #AMMOTRUCK.data Truck + + --- On after "TruckReturning" event. + -- @function [parent=#AMMOTRUCK] OnAfterTruckReturning + -- @param #AMMOTRUCK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #AMMOTRUCK.data Truck + + --- On after "RouteTruck" event. + -- @function [parent=#AMMOTRUCK] OnAfterRouteTruck + -- @param #AMMOTRUCK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #AMMOTRUCK.data Truck + + --- On after "TruckHome" event. + -- @function [parent=#AMMOTRUCK] OnAfterTruckHome + -- @param #AMMOTRUCK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #AMMOTRUCK.data Truck + + return self +end + +--- +-- @param #AMMOTRUCK self +-- @param #table dataset table of #AMMOTRUCK.data entries +-- @return #AMMOTRUCK self +function AMMOTRUCK:CheckDrivingTrucks(dataset) + self:T(self.lid .. " CheckDrivingTrucks") + local data = dataset + for _,_data in pairs (data) do + local truck = _data -- #AMMOTRUCK.data + -- see if we arrived at destination + local coord = truck.group:GetCoordinate() + local tgtcoord = truck.targetcoordinate + local dist = coord:Get2DDistance(tgtcoord) + if dist <= 150 then + -- arrived + truck.statusquo = AMMOTRUCK.State.ARRIVED + truck.timestamp = timer.getAbsTime() + truck.coordinate = coord + self:__TruckArrived(1,truck) + end + -- still driving? + local Tnow = timer.getAbsTime() + if Tnow - truck.timestamp > 30 then + local group = truck.group + if self.usearmygroup then + group = truck.group:GetGroup() + end + local currspeed = group:GetVelocityKMH() + if truck.lastspeed then + if truck.lastspeed == 0 and currspeed == 0 then + self:T(truck.group:GetName().." Is not moving!") + -- try and move it + truck.timestamp = timer.getAbsTime() + if self.routeonroad then + group:RouteGroundOnRoad(truck.targetcoordinate,30,2,"Vee") + else + group:RouteGroundTo(truck.targetcoordinate,30,"Vee",2) + end + end + truck.lastspeed = currspeed + else + truck.lastspeed = currspeed + truck.timestamp = timer.getAbsTime() + end + self:I({truck=truck.group:GetName(),currspeed=currspeed,lastspeed=truck.lastspeed}) + end + end + return self +end + +--- +-- @param #AMMOTRUCK self +-- @param Wrapper.Group#GROUP Group +-- @return #AMMOTRUCK self +function AMMOTRUCK:GetAmmoStatus(Group) + local ammotot, shells, rockets, bombs, missiles, narti = Group:GetAmmunition() + return rockets+missiles+narti +end + +--- +-- @param #AMMOTRUCK self +-- @param #table dataset table of #AMMOTRUCK.data entries +-- @return #AMMOTRUCK self +function AMMOTRUCK:CheckWaitingTargets(dataset) + self:T(self.lid .. " CheckWaitingTargets") + local data = dataset + for _,_data in pairs (data) do + local truck = _data -- #AMMOTRUCK.data + -- see how long we're waiting - maybe ammo truck is dead? + local Tnow = timer.getAbsTime() + local Tdiff = Tnow - truck.timestamp + if Tdiff > self.waitingtime then + local hasammo = self:GetAmmoStatus(truck.group) + if hasammo <= self.ammothreshold then + truck.statusquo = AMMOTRUCK.State.OUTOFAMMO + else + truck.statusquo = AMMOTRUCK.State.IDLE + end + end + end + return self +end + +--- +-- @param #AMMOTRUCK self +-- @param #table dataset table of #AMMOTRUCK.data entries +-- @return #AMMOTRUCK self +function AMMOTRUCK:CheckReturningTrucks(dataset) + self:T(self.lid .. " CheckReturningTrucks") + local data = dataset + local tgtcoord = self.homezone:GetCoordinate() + local radius = self.homezone:GetRadius() + for _,_data in pairs (data) do + local truck = _data -- #AMMOTRUCK.data + -- see if we arrived at destination + local coord = truck.group:GetCoordinate() + local dist = coord:Get2DDistance(tgtcoord) + self:T({name=truck.name,radius=radius,distance=dist}) + if dist <= radius then + -- arrived + truck.statusquo = AMMOTRUCK.State.IDLE + truck.timestamp = timer.getAbsTime() + truck.coordinate = coord + truck.reloads = self.reloads or 5 + self:__TruckHome(1,truck) + end + end + return self +end + +--- +-- @param #AMMOTRUCK self +-- @param #string name Artillery group name to find +-- @return #AMMOTRUCK.data Data +function AMMOTRUCK:FindTarget(name) + self:T(self.lid .. " FindTarget") + local data = nil + local dataset = self.targetlist + for _,_entry in pairs(dataset) do + local entry = _entry -- #AMMOTRUCK.data + if entry.name == name then + data = entry + break + end + end + return data +end + +--- +-- @param #AMMOTRUCK self +-- @param #string name Truck group name to find +-- @return #AMMOTRUCK.data Data +function AMMOTRUCK:FindTruck(name) + self:T(self.lid .. " FindTruck") + local data = nil + local dataset = self.trucklist + for _,_entry in pairs(dataset) do + local entry = _entry -- #AMMOTRUCK.data + if entry.name == name then + data = entry + break + end + end + return data +end + +--- +-- @param #AMMOTRUCK self +-- @param #table dataset table of #AMMOTRUCK.data entries +-- @return #AMMOTRUCK self +function AMMOTRUCK:CheckArrivedTrucks(dataset) + self:T(self.lid .. " CheckArrivedTrucks") + local data = dataset + for _,_data in pairs (data) do + -- set to unloading + local truck = _data -- #AMMOTRUCK.data + truck.statusquo = AMMOTRUCK.State.UNLOADING + truck.timestamp = timer.getAbsTime() + self:__TruckUnloading(2,truck) + -- set target to reloading + local aridata = self:FindTarget(truck.targetname) -- #AMMOTRUCK.data + if aridata then + aridata.statusquo = AMMOTRUCK.State.RELOADING + aridata.timestamp = timer.getAbsTime() + end + end + return self +end + +--- +-- @param #AMMOTRUCK self +-- @param #table dataset table of #AMMOTRUCK.data entries +-- @return #AMMOTRUCK self +function AMMOTRUCK:CheckUnloadingTrucks(dataset) + self:T(self.lid .. " CheckUnloadingTrucks") + local data = dataset + for _,_data in pairs (data) do + -- check timestamp + local truck = _data -- #AMMOTRUCK.data + local Tnow = timer.getAbsTime() + local Tpassed = Tnow - truck.timestamp + local hasammo = self:GetAmmoStatus(truck.targetgroup) + if Tpassed > self.unloadtime and hasammo > self.ammothreshold then + truck.statusquo = AMMOTRUCK.State.RETURNING + truck.timestamp = timer.getAbsTime() + self:__TruckReturning(2,truck) + -- set target to reloaded + local aridata = self:FindTarget(truck.targetname) -- #AMMOTRUCK.data + if aridata then + aridata.statusquo = AMMOTRUCK.State.IDLE + aridata.timestamp = timer.getAbsTime() + end + end + end + return self +end + +--- +-- @param #AMMOTRUCK self +-- @return #AMMOTRUCK self +function AMMOTRUCK:CheckTargetsAlive() + self:T(self.lid .. " CheckTargetsAlive") + local arilist = self.targetlist + for _,_ari in pairs(arilist) do + local ari = _ari -- #AMMOTRUCK.data + if ari.group and ari.group:IsAlive() then + -- everything fine + else + -- ari dead + self.targetlist[ari.name] = nil + end + end + -- new arrivals? + local aritable = self.targetset:GetSetObjects() --#table + for _,_ari in pairs(aritable) do + local ari = _ari -- Wrapper.Group#GROUP + if ari and ari:IsAlive() and not self.targetlist[ari:GetName()] then + local name = ari:GetName() + local newari = {} -- #AMMOTRUCK.data + newari.name = name + newari.group = ari + newari.statusquo = AMMOTRUCK.State.IDLE + newari.timestamp = timer.getAbsTime() + newari.coordinate = ari:GetCoordinate() + local hasammo = self:GetAmmoStatus(ari) + --newari.ammo = ari:GetAmmunition() + newari.ammo = hasammo + self.targetlist[name] = newari + end + end + return self +end + +--- +-- @param #AMMOTRUCK self +-- @return #AMMOTRUCK self +function AMMOTRUCK:CheckTrucksAlive() + self:T(self.lid .. " CheckTrucksAlive") + local trucklist = self.trucklist + for _,_truck in pairs(trucklist) do + local truck = _truck -- #AMMOTRUCK.data + if truck.group and truck.group:IsAlive() then + -- everything fine + else + -- truck dead + local tgtname = truck.targetname + local targetdata = self:FindTarget(tgtname) -- #AMMOTRUCK.data + if targetdata then + if targetdata.statusquo ~= AMMOTRUCK.State.IDLE then + targetdata.statusquo = AMMOTRUCK.State.IDLE + end + end + self.trucklist[truck.name] = nil + end + end + -- new arrivals? + local trucktable = self.truckset:GetSetObjects() --#table + for _,_truck in pairs(trucktable) do + local truck = _truck -- Wrapper.Group#GROUP + if truck and truck:IsAlive() and not self.trucklist[truck:GetName()] then + local name = truck:GetName() + local newtruck = {} -- #AMMOTRUCK.data + newtruck.name = name + newtruck.group = truck + if self.hasarmygroup then + -- is (not) already ARMYGROUP? + if truck.ClassName and truck.ClassName == "GROUP" then + local trucker = ARMYGROUP:New(truck) + trucker:Activate() + newtruck.group = trucker + end + end + newtruck.statusquo = AMMOTRUCK.State.IDLE + newtruck.timestamp = timer.getAbsTime() + newtruck.coordinate = truck:GetCoordinate() + newtruck.reloads = self.reloads or 5 + self.trucklist[name] = newtruck + end + end + return self +end + +--- +-- @param #AMMOTRUCK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AMMOTRUCK self +function AMMOTRUCK:onafterStart(From, Event, To) + self:T({From, Event, To}) + if ARMYGROUP and self.usearmygroup then + self.hasarmygroup = true + else + self.hasarmygroup = false + end + if self.debug then + BASE:TraceOn() + BASE:TraceClass("AMMOTRUCK") + end + self:CheckTargetsAlive() + self:CheckTrucksAlive() + self:__Monitor(-30) + return self +end + +--- +-- @param #AMMOTRUCK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AMMOTRUCK self +function AMMOTRUCK:onafterMonitor(From, Event, To) + self:T({From, Event, To}) + self:CheckTargetsAlive() + self:CheckTrucksAlive() + -- update ammo state + local remunition = false + local remunitionqueue = {} + local waitingtargets = {} + for _,_ari in pairs(self.targetlist) do + local data = _ari -- #AMMOTRUCK.data + if data.group and data.group:IsAlive() then + data.ammo = self:GetAmmoStatus(data.group) + data.timestamp = timer.getAbsTime() + local text = string.format("Ari %s | Ammo %d | State %s",data.name,data.ammo,data.statusquo) + self:T(text) + if data.ammo <= self.ammothreshold and (data.statusquo == AMMOTRUCK.State.IDLE or data.statusquo == AMMOTRUCK.State.OUTOFAMMO) then + -- add to remu queue + data.statusquo = AMMOTRUCK.State.OUTOFAMMO + remunitionqueue[#remunitionqueue+1] = data + remunition = true + elseif data.statusquo == AMMOTRUCK.State.WAITING then + waitingtargets[#waitingtargets+1] = data + end + else + self.targetlist[data.name] = nil + end + end + -- sort trucks in buckets + local idletrucks = {} + local drivingtrucks = {} + local unloadingtrucks = {} + local arrivedtrucks = {} + local returningtrucks = {} + local found = false + for _,_truckdata in pairs(self.trucklist) do + local data = _truckdata -- #AMMOTRUCK.data + if data.group and data.group:IsAlive() then + -- check state + local text = string.format("Truck %s | State %s",data.name,data.statusquo) + self:T(text) + if data.statusquo == AMMOTRUCK.State.IDLE then + idletrucks[#idletrucks+1] = data + found = true + elseif data.statusquo == AMMOTRUCK.State.DRIVING then + drivingtrucks[#drivingtrucks+1] = data + elseif data.statusquo == AMMOTRUCK.State.ARRIVED then + arrivedtrucks[#arrivedtrucks+1] = data + elseif data.statusquo == AMMOTRUCK.State.UNLOADING then + unloadingtrucks[#unloadingtrucks+1] = data + elseif data.statusquo == AMMOTRUCK.State.RETURNING then + returningtrucks[#returningtrucks+1] = data + if data.reloads > 0 or data.reloads == -1 then + idletrucks[#idletrucks+1] = data + found = true + end + end + else + self.truckset[data.name] = nil + end + end + -- see if we can/need route one + local n=0 + if found and remunition then + -- match + --local match = false + for _,_truckdata in pairs(idletrucks) do + local truckdata = _truckdata -- #AMMOTRUCK.data + local truckcoord = truckdata.group:GetCoordinate() -- Core.Point#COORDINATE + for _,_aridata in pairs(remunitionqueue) do + local aridata = _aridata -- #AMMOTRUCK.data + local aricoord = aridata.coordinate + local distance = truckcoord:Get2DDistance(aricoord) + if distance <= self.remunidist and aridata.statusquo == AMMOTRUCK.State.OUTOFAMMO and n <= #idletrucks then + n = n + 1 + aridata.statusquo = AMMOTRUCK.State.REQUESTED + self:__RouteTruck(n*5,truckdata,aridata) + break + end + end + end + end + + -- check driving trucks + if #drivingtrucks > 0 then + self:CheckDrivingTrucks(drivingtrucks) + end + + -- check arrived trucks + if #arrivedtrucks > 0 then + self:CheckArrivedTrucks(arrivedtrucks) + end + + -- check unloading trucks + if #unloadingtrucks > 0 then + self:CheckUnloadingTrucks(unloadingtrucks) + end + + -- check returningtrucks trucks + if #returningtrucks > 0 then + self:CheckReturningTrucks(returningtrucks) + end + + -- check waiting targets + if #waitingtargets > 0 then + self:CheckWaitingTargets(waitingtargets) + end + + self:__Monitor(self.monitor) + return self +end + +--- +-- @param #AMMOTRUCK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #AMMOTRUCK.data Truckdata +-- @param #AMMOTRUCK.data Aridata +-- @return #AMMOTRUCK self +function AMMOTRUCK:onafterRouteTruck(From, Event, To, Truckdata, Aridata) + self:T({From, Event, To, Truckdata.name, Aridata.name}) + local truckdata = Truckdata -- #AMMOTRUCK.data + local aridata = Aridata -- #AMMOTRUCK.data + local tgtgrp = aridata.group + local tgtzone = ZONE_GROUP:New(aridata.name,tgtgrp,30) + local tgtcoord = tgtzone:GetRandomCoordinate(15) + if self.hasarmygroup then + local mission = AUFTRAG:NewONGUARD(tgtcoord) + local oldmission = truckdata.group:GetMissionCurrent() + if oldmission then oldmission:Cancel() end + mission:SetTime(5) + mission:SetTeleport(false) + truckdata.group:AddMission(mission) + elseif self.routeonroad then + truckdata.group:RouteGroundOnRoad(tgtcoord,30) + else + truckdata.group:RouteGroundTo(tgtcoord,30) + end + truckdata.statusquo = AMMOTRUCK.State.DRIVING + truckdata.targetgroup = tgtgrp + truckdata.targetname = aridata.name + truckdata.targetcoordinate = tgtcoord + aridata.statusquo = AMMOTRUCK.State.WAITING + aridata.timestamp = timer.getAbsTime() + return self +end + +--- +-- @param #AMMOTRUCK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #AMMOTRUCK.data Truckdata +-- @return #AMMOTRUCK self + function AMMOTRUCK:onafterTruckUnloading(From, Event, To, Truckdata) + local m = MESSAGE:New("Truck "..Truckdata.name.." unloading!",15,"AmmoTruck"):ToCoalitionIf(self.coalition,self.debug) + local truck = Truckdata -- Functional.AmmoTruck#AMMOTRUCK.data + local coord = truck.group:GetCoordinate() + local heading = truck.group:GetHeading() + heading = heading < 180 and (360-heading) or (heading - 180) + local cid = self.coalition == coalition.side.BLUE and country.id.USA or country.id.RUSSIA + cid = self.coalition == coalition.side.NEUTRAL and country.id.UN_PEACEKEEPERS or cid + + local ammo = {} + for i=1,5 do + ammo[i] = SPAWNSTATIC:NewFromType("ammo_cargo","Cargos",cid) + :InitCoordinate(coord:Translate((15+((i-1)*4)),heading)) + :Spawn(0,"AmmoCrate-"..math.random(1,10000)) + end + + local function destroyammo(ammo) + for _,_crate in pairs(ammo) do + _crate:Destroy(false) + end + end + + local scheduler = SCHEDULER:New(nil,destroyammo,{ammo},self.waitingtime) + + -- one reload less + if truck.reloads ~= -1 then + truck.reloads = truck.reloads - 1 + end + return self + end + +--- +-- @param #AMMOTRUCK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #AMMOTRUCK.data Truck +-- @return #AMMOTRUCK self +function AMMOTRUCK:onafterTruckReturning(From, Event, To, Truck) + self:T({From, Event, To, Truck.name}) + -- route home + local truckdata = Truck -- #AMMOTRUCK.data + local tgtzone = self.homezone + local tgtcoord = tgtzone:GetRandomCoordinate() + if self.hasarmygroup then + local mission = AUFTRAG:NewONGUARD(tgtcoord) + local oldmission = truckdata.group:GetMissionCurrent() + if oldmission then oldmission:Cancel() end + mission:SetTime(5) + mission:SetTeleport(false) + truckdata.group:AddMission(mission) + elseif self.routeonroad then + truckdata.group:RouteGroundOnRoad(tgtcoord,30,1,"Cone") + else + truckdata.group:RouteGroundTo(tgtcoord,30,"Cone",1) + end + return self +end + +--- +-- @param #AMMOTRUCK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AMMOTRUCK self +function AMMOTRUCK:onafterStop(From, Event, To) + self:T({From, Event, To}) + return self +end diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index eec160374..669fde4e0 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -2386,12 +2386,12 @@ function ARTY:OnEventShot(EventData) self.Nukes=self.Nukes-1 end - -- Decrease available illuminatin shells because we just fired one. + -- Decrease available illumination 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. + -- Decrease available smoke shells because we just fired one. if self.currentTarget.weapontype==ARTY.WeaponType.SmokeShells then self.Nsmoke=self.Nsmoke-1 end @@ -3804,51 +3804,6 @@ function ARTY:_NuclearBlast(_coord) 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. diff --git a/Moose Development/Moose/Functional/Autolase.lua b/Moose Development/Moose/Functional/Autolase.lua new file mode 100644 index 000000000..1772e24a3 --- /dev/null +++ b/Moose Development/Moose/Functional/Autolase.lua @@ -0,0 +1,1333 @@ +--- **Functional** - Autolase targets in the field. +-- +-- === +-- +-- **AUOTLASE** - Autolase targets in the field. +-- +-- === +-- +-- ## Missions: +-- +-- None yet. +-- +-- === +-- +-- **Main Features:** +-- +-- * Detect and lase contacts automatically +-- * Targets are lased by threat priority order +-- * Use FSM events to link functionality into your scripts +-- * Easy setup +-- +-- === +-- +--- Spot on! +-- +-- === +-- +-- # 1 Autolase concept +-- +-- * Detect and lase contacts automatically +-- * Targets are lased by threat priority order +-- * Use FSM events to link functionality into your scripts +-- * Set laser codes and smoke colors per Recce unit +-- * Easy set-up +-- +-- # 2 Basic usage +-- +-- ## 2.2 Set up a group of Recce Units: +-- +-- local FoxSet = SET_GROUP:New():FilterPrefixes("Recce"):FilterCoalitions("blue"):FilterStart() +-- +-- ## 2.3 (Optional) Set up a group of pilots, this will drive who sees the F10 menu entry: +-- +-- local Pilotset = SET_CLIENT:New():FilterCoalitions("blue"):FilterActive(true):FilterStart() +-- +-- ## 2.4 Set up and start Autolase: +-- +-- local autolaser = AUTOLASE:New(FoxSet,coalition.side.BLUE,"Wolfpack",Pilotset) +-- +-- ## 2.5 Example - Using a fixed laser code and color for a specific Recce unit: +-- +-- local recce = SPAWN:New("Reaper") +-- :InitDelayOff() +-- :OnSpawnGroup( +-- function (group) +-- local unit = group:GetUnit(1) +-- local name = unit:GetName() +-- autolaser:SetRecceLaserCode(name,1688) +-- autolaser:SetRecceSmokeColor(name,SMOKECOLOR.Red) +-- end +-- ) +-- :InitCleanUp(60) +-- :InitLimit(1,0) +-- :SpawnScheduled(30,0.5) +-- +-- ## 2.6 Example - Inform pilots about events: +-- +-- autolaser:SetNotifyPilots(true) -- defaults to true, also shown if debug == true +-- -- Note - message are shown to pilots in the #SET_CLIENT only if using the pilotset option, else to the coalition. +-- +-- +-- ### Author: **applevangelist** +-- @module Functional.Autolase +-- @image Designation.JPG +-- +-- Date: 24 Oct 2021 +-- Last Update: Mar 2025 +-- +--- Class AUTOLASE +-- @type AUTOLASE +-- @field #string ClassName +-- @field #string lid +-- @field #number verbose +-- @field #string alias +-- @field #boolean debug +-- @field #string version +-- @field Core.Set#SET_GROUP RecceSet +-- @field #table LaserCodes +-- @field #table playermenus +-- @field #boolean smokemenu +-- @field #boolean threatmenu +-- @field #number RoundingPrecision +-- @field #table smokeoffset +-- @field #boolean increasegroundawareness +-- @field #number MonitorFrequency +-- @extends Ops.Intel#INTEL + +--- +-- @field #AUTOLASE +AUTOLASE = { + ClassName = "AUTOLASE", + lid = "", + verbose = 0, + alias = "", + debug = false, + smokemenu = true, + RoundingPrecision = 0, + increasegroundawareness = false, + MonitorFrequency = 30, +} + +--- Laser spot info +-- @type AUTOLASE.LaserSpot +-- @field Core.Spot#SPOT laserspot +-- @field Wrapper.Unit#UNIT lasedunit +-- @field Wrapper.Unit#UNIT lasingunit +-- @field #number lasercode +-- @field #string location +-- @field #number timestamp +-- @field #string unitname +-- @field #string reccename +-- @field #string unittype +-- @field Core.Point#COORDINATE coordinate + + +--- AUTOLASE class version. +-- @field #string version +AUTOLASE.version = "0.1.31" + +------------------------------------------------------------------- +-- 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.smokeoffset = nil + self.notifypilots = true + self.targetsperrecce = {} + self.RecceUnits = {} + self.forcecooldown = true + self.cooldowntime = 60 + self.useSRS = false + self.SRSPath = "" + self.SRSFreq = 251 + self.SRSMod = radio.modulation.AM + self.NoMenus = false + self.minthreatlevel = 0 + self.blacklistattributes = {} + self:SetLaserCodes( { 1688, 1130, 4785, 6547, 1465, 4578 } ) -- set self.LaserCodes + self.playermenus = {} + self.smokemenu = true + self.threatmenu = true + self.RoundingPrecision = 0 + self.increasegroundawareness = false + self.MonitorFrequency = 30 + + self:EnableSmokeMenu({Angle=math.random(0,359),Distance=math.random(10,20)}) + + -- Set some string id for output to DCS.log file. + self.lid=string.format("AUTOLASE %s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "Monitor", "*") -- Start FSM + self:AddTransition("*", "Lasing", "*") -- Lasing target + self:AddTransition("*", "TargetLost", "*") -- Lost target + self:AddTransition("*", "TargetDestroyed", "*") -- Target destroyed + self:AddTransition("*", "RecceKIA", "*") -- Recce KIA + self:AddTransition("*", "LaserTimeout", "*") -- Laser timed out + self:AddTransition("*", "Cancel", "*") -- Stop Autolase + + -- Menu Entry + if PilotSet then + self.usepilotset = true + self.pilotset = PilotSet + self:HandleEvent(EVENTS.PlayerEnterAircraft,self._EventHandler) + --self:SetPilotMenu() + end + --self.SetPilotMenu() + + + self:SetClusterAnalysis(false, false) + + self:__Start(2) + self:__Monitor(math.random(5,10)) + + return self + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Monitor". + -- @function [parent=#AUTOLASE] Status + -- @param #AUTOLASE self + + --- Triggers the FSM event "Monitor" after a delay. + -- @function [parent=#AUTOLASE] __Status + -- @param #AUTOLASE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Cancel". + -- @function [parent=#AUTOLASE] Cancel + -- @param #AUTOLASE self + + --- Triggers the FSM event "Cancel" after a delay. + -- @function [parent=#AUTOLASE] __Cancel + -- @param #AUTOLASE self + -- @param #number delay Delay in seconds. + + --- On After "RecceKIA" event. + -- @function [parent=#AUTOLASE] OnAfterRecceKIA + -- @param #AUTOLASE self + -- @param #string From The from state + -- @param #string Event The event + -- @param #string To The to state + -- @param #string RecceName The lost Recce + + --- On After "TargetDestroyed" event. + -- @function [parent=#AUTOLASE] OnAfterTargetDestroyed + -- @param #AUTOLASE self + -- @param #string From The from state + -- @param #string Event The event + -- @param #string To The to state + -- @param #string UnitName The destroyed unit\'s name + -- @param #string RecceName The Recce name lasing + + --- On After "TargetLost" event. + -- @function [parent=#AUTOLASE] OnAfterTargetLost + -- @param #AUTOLASE self + -- @param #string From The from state + -- @param #string Event The event + -- @param #string To The to state + -- @param #string UnitName The lost unit\'s name + -- @param #string RecceName The Recce name lasing + + --- On After "LaserTimeout" event. + -- @function [parent=#AUTOLASE] OnAfterLaserTimeout + -- @param #AUTOLASE self + -- @param #string From The from state + -- @param #string Event The event + -- @param #string To The to state + -- @param #string UnitName The lost unit\'s name + -- @param #string RecceName The Recce name lasing + + --- On After "Lasing" event. + -- @function [parent=#AUTOLASE] OnAfterLasing + -- @param #AUTOLASE self + -- @param #string From The from state + -- @param #string Event The event + -- @param #string To The to state + -- @param Functional.Autolase#AUTOLASE.LaserSpot LaserSpot The LaserSpot data table + +end + +------------------------------------------------------------------- +-- Helper Functions +------------------------------------------------------------------- + +--- [User] When using Monitor, set the frequency here in which the report will appear +-- @param #AUTOLASE self +-- @param #number Seconds Run the report loop every number of seconds defined here. +-- @return #AUTOLASE self +function AUTOLASE:SetMonitorFrequency(Seconds) + self.MonitorFrequency = Seconds or 30 + return self +end + +--- [User] Set a table of possible laser codes. +-- Each new RECCE can select a code from this table, default is { 1688, 1130, 4785, 6547, 1465, 4578 }. +-- @param #AUTOLASE self +-- @param #list<#number> LaserCodes +-- @return #AUTOLASE self +function AUTOLASE:SetLaserCodes( LaserCodes ) + self.LaserCodes = ( type( LaserCodes ) == "table" ) and LaserCodes or { LaserCodes } + return self +end + +--- [User] Improve ground unit detection by using a zone scan and LOS check. +-- @param #AUTOLASE self +-- @return #AUTOLASE self +function AUTOLASE:EnableImproveGroundUnitsDetection() + self.increasegroundawareness = true + return self +end + +--- [User] Do not improve ground unit detection by using a zone scan and LOS check. +-- @param #AUTOLASE self +-- @return #AUTOLASE self +function AUTOLASE:DisableImproveGroundUnitsDetection() + self.increasegroundawareness = false + return self +end + +--- (Internal) Function to set pilot menu. +-- @param #AUTOLASE self +-- @return #AUTOLASE self +function AUTOLASE:SetPilotMenu() + if self.usepilotset then + local pilottable = self.pilotset:GetSetObjects() or {} + local grouptable = {} + for _,_unit in pairs (pilottable) do + local Unit = _unit -- Wrapper.Unit#UNIT + if Unit and Unit:IsAlive() then + local Group = Unit:GetGroup() + local GroupName = Group:GetName() or "none" + local unitname = Unit:GetName() + if not grouptable[GroupName] == true then + if self.playermenus[unitname] then self.playermenus[unitname]:Remove() end -- menus + local lasetopm = MENU_GROUP:New(Group,"Autolase",nil) + self.playermenus[unitname] = lasetopm + local lasemenu = MENU_GROUP_COMMAND:New(Group,"Status",lasetopm,self.ShowStatus,self,Group,Unit) + if self.smokemenu then + local smoke = (self.smoketargets == true) and "off" or "on" + local smoketext = string.format("Switch smoke targets to %s",smoke) + local smokemenu = MENU_GROUP_COMMAND:New(Group,smoketext,lasetopm,self.SetSmokeTargets,self,(not self.smoketargets)) + end -- smokement + if self.threatmenu then + local threatmenutop = MENU_GROUP:New(Group,"Set min lasing threat",lasetopm) + for i=0,10,2 do + local text = "Threatlevel "..tostring(i) + local threatmenu = MENU_GROUP_COMMAND:New(Group,text,threatmenutop,self.SetMinThreatLevel,self,i) + end -- threatlevel + end -- threatmenu + for _,_grp in pairs(self.RecceSet.Set) do + local grp = _grp -- Wrapper.Group#GROUP + local unit = grp:GetUnit(1) + --local name = grp:GetName() + if unit and unit:IsAlive() then + local name = unit:GetName() + local mname = string.gsub(name,".%d+.%d+$","") + local code = self:GetLaserCode(name) + local unittop = MENU_GROUP:New(Group,"Change laser code for "..mname,lasetopm) + for _,_code in pairs(self.LaserCodes) do + local text = tostring(_code) + if _code == code then text = text.."(*)" end + local changemenu = MENU_GROUP_COMMAND:New(Group,text,unittop,self.SetRecceLaserCode,self,name,_code,true) + end -- Codes + end -- unit alive + end -- Recceset + grouptable[GroupName] = true + end -- grouptable[GroupName] + --lasemenu:Refresh() + end -- unit alive + end -- pilot loop + else + if not self.NoMenus then + self.Menu = MENU_COALITION_COMMAND:New(self.coalition,"Autolase",nil,self.ShowStatus,self) + end + end + return self +end + +--- (Internal) Event function for new pilots. +-- @param #AUTOLASE self +-- @param Core.Event#EVENTDATA EventData +-- @return #AUTOLASE self +function AUTOLASE:_EventHandler(EventData) + self:SetPilotMenu() + return self +end + +--- (User) Set minimum threat level for target selection, can be 0 (lowest) to 10 (highest). +-- @param #AUTOLASE self +-- @param #number Level Level used for filtering, defaults to 0. SAM systems and manpads have level 7 to 10, AAA level 6, MTBs and armoured vehicles level 3 to 5, APC, Artillery, Infantry and EWR level 1 to 2. +-- @return #AUTOLASE self +-- @usage Filter for level 3 and above: +-- `myautolase:SetMinThreatLevel(3)` +function AUTOLASE:SetMinThreatLevel(Level) + local level = Level or 0 + if level < 0 or level > 10 then level = 0 end + self.minthreatlevel = level + return self +end + +--- (User) Set list of #UNIT level attributes that won't be lased. For list of attributes see [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_enum_attributes) and [GitHub](https://github.com/mrSkortch/DCS-miscScripts/tree/master/ObjectDB) +-- @param #AUTOLASE self +-- @param #table Attributes Table of #string attributes to blacklist. Can be handed over as a single #string. +-- @return #AUTOLASE self +-- @usage To exclude e.g. manpads from being lased: +-- +-- `myautolase:AddBlackListAttributes("MANPADS")` +-- +-- To exclude trucks and artillery: +-- +-- `myautolase:AddBlackListAttributes({"Trucks","Artillery"})` +-- +function AUTOLASE:AddBlackListAttributes(Attributes) + local attributes = Attributes + if type(attributes) ~= "table" then + attributes = {attributes} + end + for _,_attr in pairs(attributes) do + table.insert(self.blacklistattributes,_attr) + end + return self +end + +--- (Internal) Function to get a laser code by recce name +-- @param #AUTOLASE self +-- @param #string RecceName Unit(!) name of the Recce +-- @return #AUTOLASE self +function AUTOLASE:GetLaserCode(RecceName) + local code = 1688 + if self.RecceLaserCode[RecceName] == nil then + code = self.LaserCodes[math.random(#self.LaserCodes)] + self.RecceLaserCode[RecceName] = code + else + code = self.RecceLaserCode[RecceName] + end + return code +end + +--- (Internal) Function to get a smoke color by recce name +-- @param #AUTOLASE self +-- @param #string RecceName Unit(!) name of the Recce +-- @return #AUTOLASE self +function AUTOLASE:GetSmokeColor(RecceName) + local color = self.smokecolor + if self.RecceSmokeColor[RecceName] == nil then + self.RecceSmokeColor[RecceName] = color + else + color = self.RecceSmokeColor[RecceName] + end + return color +end + +--- (User) Function enable sending messages via SRS. +-- @param #AUTOLASE self +-- @param #boolean OnOff Switch usage on and off +-- @param #string Path Path to SRS TTS directory, e.g. C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio +-- @param #number Frequency Frequency to send, e.g. 243 +-- @param #number Modulation Modulation i.e. radio.modulation.AM or radio.modulation.FM +-- @param #string Label (Optional) Short label to be used on the SRS Client Overlay +-- @param #string Gender (Optional) Defaults to "male" +-- @param #string Culture (Optional) Defaults to "en-US" +-- @param #number Port (Optional) Defaults to 5002 +-- @param #string Voice (Optional) Use a specifc voice with the @{Sound.SRS#SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. +-- Note that this must be installed on your windows system. Can also be Google voice types, if you are using Google TTS. +-- @param #number Volume (Optional) Volume - between 0.0 (silent) and 1.0 (loudest) +-- @param #string PathToGoogleKey (Optional) Path to your google key if you want to use google TTS +-- @return #AUTOLASE self +function AUTOLASE:SetUsingSRS(OnOff,Path,Frequency,Modulation,Label,Gender,Culture,Port,Voice,Volume,PathToGoogleKey) + if OnOff then + self.useSRS = true + self.SRSPath = Path or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" + self.SRSFreq = Frequency or 271 + self.SRSMod = Modulation or radio.modulation.AM + self.Gender = Gender or MSRS.gender or "male" + self.Culture = Culture or MSRS.culture or "en-US" + self.Port = Port or MSRS.port or 5002 + self.Voice = Voice + self.PathToGoogleKey = PathToGoogleKey + self.Volume = Volume or 1.0 + self.Label = Label + -- set up SRS + self.SRS = MSRS:New(self.SRSPath,self.SRSFreq,self.SRSMod) + self.SRS:SetCoalition(self.coalition) + self.SRS:SetLabel(self.MenuName or self.Name) + self.SRS:SetGender(self.Gender) + self.SRS:SetCulture(self.Culture) + self.SRS:SetPort(self.Port) + self.SRS:SetVoice(self.Voice) + self.SRS:SetCoalition(self.coalition) + self.SRS:SetVolume(self.Volume) + if self.PathToGoogleKey then + self.SRS:SetProviderOptionsGoogle(PathToGoogleKey,PathToGoogleKey) + self.SRS:SetProvider(MSRS.Provider.GOOGLE) + end + self.SRSQueue = MSRSQUEUE:New(self.alias) + else + self.useSRS = false + self.SRS= nil + self.SRSQueue = nil + end + return self +end + +--- (User) Function set max lasing targets +-- @param #AUTOLASE self +-- @param #number Number Max number of targets to lase at once +-- @return #AUTOLASE self +function AUTOLASE:SetMaxLasingTargets(Number) + self.maxlasing = Number or 4 + return self +end + +--- (Internal) Function set notify pilots on events +-- @param #AUTOLASE self +-- @param #boolean OnOff Switch messaging on (true) or off (false) +-- @return #AUTOLASE self +function AUTOLASE:SetNotifyPilots(OnOff) + self.notifypilots = OnOff and true + return self +end + +--- (User) Function to set a specific code to a Recce. +-- @param #AUTOLASE self +-- @param #string RecceName (Unit!) Name of the Recce +-- @param #number Code The lase code +-- @param #boolean Refresh If true, refresh menu entries +-- @return #AUTOLASE self +function AUTOLASE:SetRecceLaserCode(RecceName, Code, Refresh) + local code = Code or 1688 + self.RecceLaserCode[RecceName] = code + if Refresh then + self:SetPilotMenu() + if self.notifypilots then + if string.find(RecceName,"#") then + RecceName = string.match(RecceName,"^(.*)#") + end + self:NotifyPilots(string.format("Code for %s set to: %d",RecceName,Code),15) + end + end + return self +end + +--- (User) Function to set a specific smoke color for a Recce. +-- @param #AUTOLASE self +-- @param #string RecceName (Unit!) Name of the Recce +-- @param #number Color The color, e.g. SMOKECOLOR.Red, SMOKECOLOR.Green etc +-- @return #AUTOLASE self +function AUTOLASE:SetRecceSmokeColor(RecceName, Color) + local color = Color or self.smokecolor + self.RecceSmokeColor[RecceName] = color + return self +end + +--- (User) Function to force laser cooldown and cool down time +-- @param #AUTOLASE self +-- @param #boolean OnOff Switch cool down on (true) or off (false) - defaults to true +-- @param #number Seconds Number of seconds for cooldown - dafaults to 60 seconds +-- @return #AUTOLASE self +function AUTOLASE:SetLaserCoolDown(OnOff, Seconds) + self.forcecooldown = OnOff and true + self.cooldowntime = Seconds or 60 + return self +end + +--- (User) Function to set message show times. +-- @param #AUTOLASE self +-- @param #number long Longer show time +-- @param #number short Shorter show time +-- @return #AUTOLASE self +function AUTOLASE:SetReportingTimes(long, short) + self.reporttimeshort = short or 10 + self.reporttimelong = long or 30 + return self +end + +--- (User) Function to set lasing distance in meters and duration in seconds +-- @param #AUTOLASE self +-- @param #number Distance (Max) distance for lasing in meters - default 5000 meters +-- @param #number Duration (Max) duration for lasing in seconds - default 300 secs +-- @return #AUTOLASE self +function AUTOLASE:SetLasingParameters(Distance, Duration) + self.LaseDistance = Distance or 5000 + self.LaseDuration = Duration or 300 + return self +end + +--- (User) Function to set smoking of targets. +-- @param #AUTOLASE self +-- @param #boolean OnOff Switch smoking on or off +-- @param #number Color Smokecolor, e.g. SMOKECOLOR.Red +-- @return #AUTOLASE self +function AUTOLASE:SetSmokeTargets(OnOff,Color) + self.smoketargets = OnOff + self.smokecolor = Color or SMOKECOLOR.Red + local smktxt = OnOff == true and "on" or "off" + local Message = "Smoking targets is now "..smktxt.."!" + self:NotifyPilots(Message,10) + return self +end + +--- (User) Function to set rounding precision for BR distance output. +-- @param #AUTOLASE self +-- @param #number IDP Rounding precision before/after the decimal sign. Defaults to zero. Positive values round right of the decimal sign, negative ones left of the decimal sign. +-- @return #AUTOLASE self +function AUTOLASE:SetRoundingPrecsion(IDP) + self.RoundingPrecision = IDP or 0 + return self +end + +--- (User) Show the "Switch smoke target..." menu entry for pilots. On by default. +-- @param #AUTOLASE self +-- @param #table Offset (Optional) Define an offset for the smoke, i.e. not directly on the unit itself, angle is degrees and distance is meters. E.g. `autolase:EnableSmokeMenu({Angle=30,Distance=20})` +-- @return #AUTOLASE self +function AUTOLASE:EnableSmokeMenu(Offset) + self.smokemenu = true + if Offset then + self.smokeoffset = {} + self.smokeoffset.Distance = Offset.Distance or math.random(10,20) + self.smokeoffset.Angle = Offset.Angle or math.random(0,359) + end + return self +end + +--- (User) Do not show the "Switch smoke target..." menu entry for pilots. +-- @param #AUTOLASE self +-- @return #AUTOLASE self +function AUTOLASE:DisableSmokeMenu() + self.smokemenu = false + self.smokeoffset = nil + return self +end + +--- (User) Show the "Switch min threat lasing..." menu entry for pilots. On by default. +-- @param #AUTOLASE self +-- @return #AUTOLASE self +function AUTOLASE:EnableThreatLevelMenu() + self.threatmenu = true + return self +end + +--- (User) Do not show the "Switch min threat lasing..." menu entry for pilots. +-- @param #AUTOLASE self +-- @return #AUTOLASE self +function AUTOLASE:DisableThreatLevelMenu() + self.threatmenu = false + 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 + local isground = (unit and unit.IsGround) and unit:IsGround() or false + self.RecceUnits[name] = { name=name, unit=unit, cooldown = false, timestamp = timer.getAbsTime(), isground=isground } + end + end + end + + for _ind,_entry in pairs(lasingtable) do + local entry = _entry -- #AUTOLASE.LaserSpot + local valid = 0 + local reccedead = false + local unitdead = false + local lostsight = false + local timeout = false + local Tnow = timer.getAbsTime() + -- check recce dead + local recce = entry.lasingunit + if recce and recce:IsAlive() then + valid = valid + 1 + else + reccedead = true + self:__RecceKIA(2,entry.reccename) + end + -- check entry dead + local unit = entry.lasedunit + if unit and unit:IsAlive() == true then + valid = valid + 1 + else + unitdead = true + if not self.deadunitnotes[entry.unitname] then + self.deadunitnotes[entry.unitname] = true + self:__TargetDestroyed(2,entry.unitname,entry.reccename) + end + end + -- check entry out of sight + if not reccedead and not unitdead then + if self:CanLase(recce,unit) then + valid = valid + 1 + else + lostsight = true + entry.laserspot:LaseOff() + self:__TargetLost(2,entry.unitname,entry.reccename) + end + end + -- check timed out + local timestamp = entry.timestamp + if Tnow - timestamp < self.LaseDuration and not lostsight then + valid = valid + 1 + else + timeout = true + entry.laserspot:LaseOff() + + self.RecceUnits[entry.reccename].cooldown = true + self.RecceUnits[entry.reccename].timestamp = timer.getAbsTime() + + if not lostsight then + self:__LaserTimeout(2,entry.unitname,entry.reccename) + end + end + if valid == 4 then + self.lasingindex = self.lasingindex + 1 + newtable[self.lasingindex] = entry + newreccecount[entry.reccename] = newreccecount[entry.reccename] + 1 + lasing = lasing + 1 + end + end + self.CurrentLasing = newtable + self.targetsperrecce = newreccecount + return lasing +end + +--- (Internal) Function to show status. +-- @param #AUTOLASE self +-- @param Wrapper.Group#GROUP Group (Optional) show to a certain group +-- @param Wrapper.Unit#UNIT Unit (Optional) show to a certain unit +-- @return #AUTOLASE self +function AUTOLASE:ShowStatus(Group,Unit) + local report = REPORT:New("Autolase") + local reccetable = self.RecceSet:GetSetObjects() + for _,_recce in pairs(reccetable) do + if _recce and _recce:IsAlive() then + local unit = _recce:GetUnit(1) + local name = unit:GetName() + if string.find(name,"#") then + name = string.match(name,"^(.*)#") + end + local code = self:GetLaserCode(unit:GetName()) + report:Add(string.format("Recce %s has code %d",name,code)) + report:Add("---------------") + end + end + report:Add(string.format("Lasing min threat level %d",self.minthreatlevel)) + report:Add("---------------") + local lines = 0 + for _ind,_entry in pairs(self.CurrentLasing) do + local entry = _entry -- #AUTOLASE.LaserSpot + local reccename = entry.reccename + if string.find(reccename,"#") then + reccename = string.match(reccename,"^(.*)#") + end + local typename = entry.unittype + local code = entry.lasercode + local locationstring = entry.location + local playername = nil + if Unit and Unit:IsAlive() then + playername = Unit:GetPlayerName() + elseif Group and Group:IsAlive() then + playername = Group:GetPlayerName() + end + if playername then + local settings = _DATABASE:GetPlayerSettings(playername) + if settings then + self:T("Get Settings ok!") + if settings:IsA2G_MGRS() then + locationstring = entry.coordinate:ToStringMGRS(settings) + elseif settings:IsA2G_LL_DMS() then + locationstring = entry.coordinate:ToStringLLDMS(settings) + elseif settings:IsA2G_LL_DDM() then + locationstring = entry.coordinate:ToStringLLDDM(settings) + elseif settings:IsA2G_BR() then + -- attention this is the distance from the ASKING unit to target, not from RECCE to target! + local startcoordinate = Unit:GetCoordinate() or Group:GetCoordinate() + locationstring = entry.coordinate:ToStringBR(startcoordinate,settings,false,self.RoundingPrecision) + end + end + end + local text = string.format("+ %s lasing %s code %d\nat %s",reccename,typename,code,locationstring) + report:Add(text) + report:Add("---------------") + lines = lines + 1 + end + if lines == 0 then + report:Add("No targets!") + report:Add("---------------") + end + local reporttime = self.reporttimelong + if lines == 0 then reporttime = self.reporttimeshort end + if Unit and Unit:IsAlive() then + local m = MESSAGE:New(report:Text(),reporttime,"Info"):ToUnit(Unit) + elseif Group and Group:IsAlive() then + local m = MESSAGE:New(report:Text(),reporttime,"Info"):ToGroup(Group) + else + local m = MESSAGE:New(report:Text(),reporttime,"Info"):ToCoalition(self.coalition) + end + return self +end + +--- (Internal) Function to show messages. +-- @param #AUTOLASE self +-- @param #string Message The message to be sent +-- @param #number Duration Duration in seconds +-- @return #AUTOLASE self +function AUTOLASE:NotifyPilots(Message,Duration) + if self.usepilotset then + local pilotset = self.pilotset:GetSetObjects() --#table + for _,_pilot in pairs(pilotset) do + local pilot = _pilot -- Wrapper.Unit#UNIT + if pilot and pilot:IsAlive() then + local Group = pilot:GetGroup() + local m = MESSAGE:New(Message,Duration,"Autolase"):ToGroup(Group) + end + end + elseif not self.debug then + local m = MESSAGE:New(Message,Duration,"Autolase"):ToCoalition(self.coalition) + else + local m = MESSAGE:New(Message,Duration,"Autolase"):ToAll() + end + if self.debug then self:I(Message) end + return self +end + +--- (User) Send messages via SRS. +-- @param #AUTOLASE self +-- @param #string Message The (short!) message to be sent, e.g. "Lasing target!" +-- @return #AUTOLASE self +-- @usage Step 1 - set up the radio basics **once** with +-- my_autolase:SetUsingSRS(true,"C:\\path\\SRS-Folder",251,radio.modulation.AM) +-- Step 2 - send a message, e.g. +-- function my_autolase:OnAfterLasing(From, Event, To, LaserSpot) +-- my_autolase:NotifyPilotsWithSRS("Reaper lasing new target!") +-- end +function AUTOLASE:NotifyPilotsWithSRS(Message) + if self.useSRS then + self.SRSQueue:NewTransmission(Message,nil,self.SRS,nil,2) + end + if self.debug then self:I(Message) end + return self +end + +--- (Internal) Function to check if a unit is already lased. +-- @param #AUTOLASE self +-- @param #string unitname Name of the unit to check +-- @return #boolean outcome True or false +function AUTOLASE:CheckIsLased(unitname) + local outcome = false + for _,_laserspot in pairs(self.CurrentLasing) do + local spot = _laserspot -- #AUTOLASE.LaserSpot + if spot.unitname == unitname then + outcome = true + break + end + end + return outcome +end + +--- (Internal) Function to check if a unit can be lased. +-- @param #AUTOLASE self +-- @param Wrapper.Unit#UNIT Recce The Recce #UNIT +-- @param Wrapper.Unit#UNIT Unit The lased #UNIT +-- @return #boolean outcome True or false +function AUTOLASE:CanLase(Recce,Unit) + + local function HasNoBlackListAttribute(Unit) + local nogos = self.blacklistattributes or {} + local having = true + local unit = Unit -- Wrapper.Unit#UNIT + for _,_attribute in pairs (nogos) do + if unit:HasAttribute(_attribute) then + having = false + break + end + end + return having + end + + local canlase = false + -- cooldown? + if Recce and Recce:IsAlive() == true then + local name = Recce:GetName() + local cooldown = self.RecceUnits[name].cooldown and self.forcecooldown + if cooldown then + local Tdiff = timer.getAbsTime() - self.RecceUnits[name].timestamp + if Tdiff < self.cooldowntime then + return false + else + self.RecceUnits[name].cooldown = false + end + end + -- calculate LOS + local reccecoord = Recce:GetCoordinate() + local unitcoord = Unit:GetCoordinate() + local islos = reccecoord:IsLOS(unitcoord,2.5) + -- calculate distance + local distance = math.floor(reccecoord:Get3DDistance(unitcoord)) + local lasedistance = self:GetLosFromUnit(Recce) + if distance <= lasedistance and islos and HasNoBlackListAttribute(Unit) then + canlase = true + end + end + return canlase +end + +--- (Internal) Function to do a zone check per ground Recce and make found units and statics "known". +-- @param #AUTOLASE self +-- @return #AUTOLASE self +function AUTOLASE:_Prescient() + -- self.RecceUnits[name] = { name=name, unit=unit, cooldown = false, timestamp = timer.getAbsTime(), isground=isground } + for _,_data in pairs(self.RecceUnits) do + -- ground units only + if _data.isground and _data.unit and _data.unit:IsAlive() then + local unit = _data.unit -- Wrapper.Unit#UNIT + local position = unit:GetCoordinate() -- Core.Point#COORDINATE + local needsinit = false + if position then + local lastposition = unit:GetProperty("lastposition") + -- property initiated? + if not lastposition then + unit:SetProperty("lastposition",position) + lastposition = position + needsinit = true + end + -- has moved? + local dist = position:Get2DDistance(lastposition) + -- refresh? + local TNow = timer.getAbsTime() + -- check + if dist > 10 or needsinit==true or TNow - _data.timestamp > 29 then + -- init scan objects + local hasunits,hasstatics,_,Units,Statics = position:ScanObjects(self.LaseDistance,true,true,false) + -- loop found units + if hasunits then + self:T(self.lid.."Checking possibly visible UNITs for Recce "..unit:GetName()) + for _,_target in pairs(Units) do -- Wrapper.Unit#UNIT object here + local target = _target -- Wrapper.Unit#UNIT + if target and target:GetCoalition() ~= self.coalition then + if unit:IsLOS(target) and (not target:IsUnitDetected(unit))then + unit:KnowUnit(target,true,true) + end + end + end + end + -- loop found statics + if hasstatics then + self:T(self.lid.."Checking possibly visible STATICs for Recce "..unit:GetName()) + for _,_static in pairs(Statics) do -- DCS static object here + local static = STATIC:Find(_static) + if static and static:GetCoalition() ~= self.coalition and static:GetCoordinate() then + local IsLOS = position:IsLOS(static:GetCoordinate()) + if IsLOS then + unit:KnowUnit(static,true,true) + end + end + end + end + end + end + end + end + return self +end + +------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------- + +--- (Internal) FSM Function for monitoring +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @return #AUTOLASE self +function AUTOLASE:onbeforeMonitor(From, Event, To) + self:T({From, Event, To}) + if self.increasegroundawareness then + self:_Prescient() + end + -- 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.Intel#INTEL.Contact + local groupsbythreat = {} + local report = REPORT:New("Detections") + local lines = 0 + for _,_contact in pairs(detecteditems) do + local contact = _contact -- Ops.Intel#INTEL.Contact + local grp = contact.group + local coord = contact.position + local reccename = contact.recce or "none" + local threat = contact.threatlevel or 0 + local reccegrp = UNIT:FindByName(reccename) + if reccegrp then + local reccecoord = reccegrp:GetCoordinate() + local distance = math.floor(reccecoord:Get3DDistance(coord)) + local text = string.format("%s of %s | Distance %d km | Threatlevel %d",contact.attribute, contact.groupname, math.floor(distance/1000), contact.threatlevel) + report:Add(text) + self:T(text) + if self.debug then self:I(text) end + lines = lines + 1 + -- sort out groups beyond sight + local lasedistance = self:GetLosFromUnit(reccegrp) + if grp:IsGround() and lasedistance >= distance and threat >= self.minthreatlevel then + table.insert(groupsbythreat,{contact.group,contact.threatlevel}) + self.RecceNames[contact.groupname] = contact.recce + end + end + end + + self.GroupsByThreat = groupsbythreat + + if self.verbose > 2 and lines > 0 then + local m=MESSAGE:New(report:Text(),self.reporttimeshort,"Autolase"):ToAll() + end + + table.sort(self.GroupsByThreat, function(a,b) + local aNum = a[2] -- Coin value of a + local bNum = b[2] -- Coin value of b + return aNum > bNum -- Return their comparisons, < for ascending, > for descending + end) + + -- build table of Units + local unitsbythreat = {} + for _,_entry in pairs(self.GroupsByThreat) do + local group = _entry[1] -- Wrapper.Group#GROUP + if group and group:IsAlive() then + local units = group:GetUnits() + local reccename = self.RecceNames[group:GetName()] + for _,_unit in pairs(units) do + local unit = _unit -- Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + local threat = unit:GetThreatLevel() + local coord = unit:GetCoordinate() + if threat >= self.minthreatlevel then + local unitname = unit:GetName() + -- prefer radar units + if unit:HasAttribute("RADAR_BAND1_FOR_ARM") or unit:HasAttribute("RADAR_BAND2_FOR_ARM") or unit:HasAttribute("Optical Tracker") then + threat = 11 + end + table.insert(unitsbythreat,{unit,threat}) + self.RecceUnitNames[unitname] = reccename + end + end + end + end + end + + self.UnitsByThreat = unitsbythreat + + table.sort(self.UnitsByThreat, function(a,b) + local aNum = a[2] -- Coin value of a + local bNum = b[2] -- Coin value of b + return aNum > bNum -- Return their comparisons, < for ascending, > for descending + end) + + local unitreport = REPORT:New("Detected Units") + + local lines = 0 + for _,_entry in pairs(self.UnitsByThreat) do + local threat = _entry[2] + local unit = _entry[1] + local unitname = unit:GetName() + local text = string.format("Unit %s | Threatlevel %d | Detected by %s",unitname,threat,self.RecceUnitNames[unitname]) + unitreport:Add(text) + lines = lines + 1 + self:T(text) + if self.debug then self:I(text) end + end + + if self.verbose > 2 and lines > 0 then + local m=MESSAGE:New(unitreport:Text(),self.reporttimeshort,"Autolase"):ToAll() + end + + for _,_detectingunit in pairs(self.RecceUnits) do + + local reccename = _detectingunit.name + local recce = _detectingunit.unit + local reccecount = self.targetsperrecce[reccename] or 0 + local targets = 0 + for _,_entry in pairs(self.UnitsByThreat) do + local unit = _entry[1] -- Wrapper.Unit#UNIT + local unitname = unit:GetName() + local canlase = self:CanLase(recce,unit) + if targets+reccecount < self.maxlasing and not self:CheckIsLased(unitname) and unit:IsAlive() and canlase then + targets = targets + 1 + local code = self:GetLaserCode(reccename) + local spot = SPOT:New(recce) + spot:LaseOn(unit,code,self.LaseDuration) + local locationstring = unit:GetCoordinate():ToStringLLDDM() + if _SETTINGS:IsA2G_MGRS() then + local precision = _SETTINGS:GetMGRS_Accuracy() + local settings = {} + settings.MGRS_Accuracy = precision + locationstring = unit:GetCoordinate():ToStringMGRS(settings) + elseif _SETTINGS:IsA2G_LL_DMS() then + locationstring = unit:GetCoordinate():ToStringLLDMS(_SETTINGS) + elseif _SETTINGS:IsA2G_BR() then + locationstring = unit:GetCoordinate():ToStringBULLS(self.coalition,_SETTINGS) + end + + local laserspot = { -- #AUTOLASE.LaserSpot + laserspot = spot, + lasedunit = unit, + lasingunit = recce, + lasercode = code, + location = locationstring, + timestamp = timer.getAbsTime(), + unitname = unitname, + reccename = reccename, + unittype = unit:GetTypeName(), + coordinate = unit:GetCoordinate(), + } + if self.smoketargets then + local coord = unit:GetCoordinate() + if self.smokeoffset then + coord:Translate(self.smokeoffset.Distance,self.smokeoffset.Angle,true,true) + end + 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 + + local nextloop = -self.MonitorFrequency or -30 + self:__Monitor(nextloop) + return self +end + +--- (Internal) FSM Function onbeforeRecceKIA +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @param #string RecceName The lost Recce +-- @return #AUTOLASE self +function AUTOLASE:onbeforeRecceKIA(From,Event,To,RecceName) + self:T({From, Event, To, RecceName}) + if self.notifypilots or self.debug then + if string.find(RecceName,"#") then + RecceName = string.match(RecceName,"^(.*)#") + end + local text = string.format("Recce %s KIA!",RecceName) + self:NotifyPilots(text,self.reporttimeshort) + end + return self +end + +--- (Internal) FSM Function onbeforeTargetDestroyed +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @param #string UnitName The destroyed unit\'s name +-- @param #string RecceName The Recce name lasing +-- @return #AUTOLASE self +function AUTOLASE:onbeforeTargetDestroyed(From,Event,To,UnitName,RecceName) + self:T({From, Event, To, UnitName, RecceName}) + if self.notifypilots or self.debug then + local text = string.format("Unit %s destroyed! Good job!",UnitName) + self:NotifyPilots(text,self.reporttimeshort) + end + return self +end + +--- (Internal) FSM Function onbeforeTargetLost +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @param #string UnitName The lost unit\'s name +-- @param #string RecceName The Recce name lasing +-- @return #AUTOLASE self +function AUTOLASE:onbeforeTargetLost(From,Event,To,UnitName,RecceName) + self:T({From, Event, To, UnitName,RecceName}) + if self.notifypilots or self.debug then + if string.find(RecceName,"#") then + RecceName = string.match(RecceName,"^(.*)#") + end + local text = string.format("%s lost sight of unit %s.",RecceName,UnitName) + self:NotifyPilots(text,self.reporttimeshort) + end + return self +end + +--- (Internal) FSM Function onbeforeLaserTimeout +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @param #string UnitName The lost unit\'s name +-- @param #string RecceName The Recce name lasing +-- @return #AUTOLASE self +function AUTOLASE:onbeforeLaserTimeout(From,Event,To,UnitName,RecceName) + self:T({From, Event, To, UnitName,RecceName}) + if self.notifypilots or self.debug then + if string.find(RecceName,"#") then + RecceName = string.match(RecceName,"^(.*)#") + end + local text = string.format("%s laser timeout on unit %s.",RecceName,UnitName) + self:NotifyPilots(text,self.reporttimeshort) + end + return self +end + +--- (Internal) FSM Function onbeforeLasing +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @param Functional.Autolase#AUTOLASE.LaserSpot LaserSpot The LaserSpot data table +-- @return #AUTOLASE self +function AUTOLASE:onbeforeLasing(From,Event,To,LaserSpot) + self:T({From, Event, To, LaserSpot.unittype}) + if self.notifypilots or self.debug then + local laserspot = LaserSpot -- #AUTOLASE.LaserSpot + local name = laserspot.reccename if string.find(name,"#") then + name = string.match(name,"^(.*)#") + end + local text = string.format("%s is lasing %s code %d\nat %s",name,laserspot.unittype,laserspot.lasercode,laserspot.location) + self:NotifyPilots(text,self.reporttimeshort+5) + end + return self +end + +--- (Internal) FSM Function onbeforeCancel +-- @param #AUTOLASE self +-- @param #string From The from state +-- @param #string Event The event +-- @param #string To The to state +-- @return #AUTOLASE self +function AUTOLASE:onbeforeCancel(From,Event,To) + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + self:__Stop(2) + return self +end + +------------------------------------------------------------------- +-- End Functional.Autolase.lua +------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/CleanUp.lua b/Moose Development/Moose/Functional/CleanUp.lua index 4b909dcd5..7975b3163 100644 --- a/Moose Development/Moose/Functional/CleanUp.lua +++ b/Moose Development/Moose/Functional/CleanUp.lua @@ -52,11 +52,13 @@ -- @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 :-) +--- +-- @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 +--- +-- @type CLEANUP_AIRBASE -- @extends #CLEANUP_AIRBASE.__ --- Keeps airbases clean, and tries to guarantee continuous airbase operations, even under combat. @@ -93,7 +95,7 @@ CLEANUP_AIRBASE = { -- @field #CLEANUP_AIRBASE.__ CLEANUP_AIRBASE.__ = {} ---- @field #CLEANUP_AIRBASE.__.Airbases +-- @field #CLEANUP_AIRBASE.__.Airbases CLEANUP_AIRBASE.__.Airbases = {} --- Creates the main object which is handling the cleaning of the debris within the given Zone Names. @@ -240,7 +242,8 @@ function CLEANUP_AIRBASE.__:DestroyMissile( MissileObject ) end end ---- @param #CLEANUP_AIRBASE self +--- +-- @param #CLEANUP_AIRBASE self -- @param Core.Event#EVENTDATA EventData function CLEANUP_AIRBASE.__:OnEventBirth( EventData ) self:F( { EventData } ) diff --git a/Moose Development/Moose/Functional/DetectionZones.lua b/Moose Development/Moose/Functional/DetectionZones.lua index 20680559d..8a8984658 100644 --- a/Moose Development/Moose/Functional/DetectionZones.lua +++ b/Moose Development/Moose/Functional/DetectionZones.lua @@ -4,7 +4,7 @@ do -- DETECTION_ZONES - --- @type 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, @{Core.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 @@ -68,7 +68,7 @@ do -- DETECTION_ZONES return self end - --- @param #DETECTION_ZONES self + -- @param #DETECTION_ZONES self -- @param #number The amount of alive recce. function DETECTION_ZONES:CountAliveRecce() @@ -76,7 +76,7 @@ do -- DETECTION_ZONES end - --- @param #DETECTION_ZONES self + -- @param #DETECTION_ZONES self function DETECTION_ZONES:ForEachAliveRecce( IteratorFunction, ... ) self:F2( arg ) @@ -352,7 +352,7 @@ do -- DETECTION_ZONES --DetectedSet:Flush( self ) DetectedSet:ForEachUnit( - --- @param Wrapper.Unit#UNIT DetectedUnit + -- @param Wrapper.Unit#UNIT DetectedUnit function( DetectedUnit ) if DetectedUnit:IsAlive() then --self:T( "Detected Set #" .. DetectedItem.ID .. ":" .. DetectedUnit:GetName() ) @@ -380,7 +380,7 @@ do -- DETECTION_ZONES end - --- @param #DETECTION_ZONES self + -- @param #DETECTION_ZONES self -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. diff --git a/Moose Development/Moose/Functional/Formation.lua b/Moose Development/Moose/Functional/Formation.lua new file mode 100644 index 000000000..5eac066db --- /dev/null +++ b/Moose Development/Moose/Functional/Formation.lua @@ -0,0 +1,999 @@ +--- **Functional** - 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. +-- +-- === +-- +-- ## Additional Material: +-- +-- * **Demo Missions:** [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/FORMATION) +-- * **Guides:** None +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: **Applevangelist** (refactor) +-- +-- === +-- +-- @module Functional.FORMATION +-- @image AI_Large_Formations.JPG + +--- FORMATION class +-- @type FORMATION +-- @extends Core.Fsm#FSM_SET +-- @field Wrapper.Unit#UNIT FollowUnit +-- @field Core.Set#SET_GROUP FollowGroupSet +-- @field #string FollowName +-- @field Core.Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. +-- @field #number FollowDistance The current follow distance. +-- @field DCS#AI.Option.Air.val.ROE OptionROE Which ROE is set to the FollowGroup. +-- @field DCS#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. +-- +-- FORMATION makes AI @{Wrapper.Group#GROUP}s fly in formation of various compositions. +-- The 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 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. +-- +-- ## FORMATION construction +-- +-- Create a new SPAWN object with the @{#FORMATION.New} method: +-- +-- * @{#FORMATION.New}(): Creates a new 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: +-- +-- * @{#FORMATION.FormationLine}(): Form a line formation (core formation function). +-- * @{#FORMATION.FormationTrail}(): Form a trail formation. +-- * @{#FORMATION.FormationLeftLine}(): Form a left line formation. +-- * @{#FORMATION.FormationRightLine}(): Form a right line formation. +-- * @{#FORMATION.FormationRightWing}(): Form a right wing formation. +-- * @{#FORMATION.FormationLeftWing}(): Form a left wing formation. +-- * @{#FORMATION.FormationCenterWing}(): Form a center wing formation. +-- * @{#FORMATION.FormationCenterVic}(): Form a Vic formation (same as CenterWing). +-- * @{#FORMATION.FormationCenterBoxed}(): Form a center boxed formation. +-- +-- ## Randomization +-- +-- Use the method @{FORMATION#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() +-- local LeaderUnit = UNIT:FindByName( "Leader" ) +-- local LargeFormation = FORMATION:New( LeaderUnit, FollowGroupSet, "Center Wing Formation", "Briefing" ) +-- LargeFormation:FormationCenterWing( 500, 50, 0, 250, 250 ) +-- LargeFormation:__Start( 1 ) +-- +-- @field #FORMATION +FORMATION = { + ClassName = "FORMATION", + FollowName = nil, -- The Follow Name + FollowUnit = nil, + FollowGroupSet = nil, + FollowScheduler = nil, + OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE, + OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, + dtFollow = 0.5, +} + +--- @type FORMATION.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 +FORMATION.Formation = { + None = 1, + Line = 2, + Trail = 3, + Stack = 4, + LeftLine = 5, + RightLine = 6, + LeftWing = 7, + RightWing = 8, + Vic = 9, + Box = 10, +} + +--- FORMATION class constructor for an AI group +-- @param #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. +-- @return #FORMATION self +function FORMATION:New( FollowUnit, FollowGroupSet, FollowName ) + 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:SetFlightRandomization( 2 ) + + self:SetStartState( "None" ) + + self:AddTransition( "*", "Stop", "Stopped" ) + + self:AddTransition( {"None", "Stopped"}, "Start", "Following" ) + + self:AddTransition( "*", "FormationLine", "*" ) + + --- Start Trigger for FORMATION + -- @function [parent=#FORMATION] Start + -- @param #FORMATION self + + --- Start Asynchronous Trigger for FORMATION + -- @function [parent=#FORMATION] __Start + -- @param #FORMATION self + -- @param #number Delay + + --- Stop Trigger for FORMATION + -- @function [parent=#FORMATION] Stop + -- @param #FORMATION self + + --- Stop Asynchronous Trigger for FORMATION + -- @function [parent=#FORMATION] __Stop + -- @param #FORMATION self + -- @param #number Delay + + --- FormationLine Handler OnAfter for FORMATION + -- @function [parent=#FORMATION] OnAfterFormationLine + -- @param #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 FORMATION + -- @function [parent=#FORMATION] FormationLine + -- @param #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 FORMATION + -- @function [parent=#FORMATION] __FormationLine + -- @param #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 OnAfter for FORMATION + -- @function [parent=#FORMATION] OnAfterFormationTrail + -- @param #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 FORMATION + -- @function [parent=#FORMATION] FormationTrail + -- @param #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 FORMATION + -- @function [parent=#FORMATION] __FormationTrail + -- @param #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 OnAfter for FORMATION + -- @function [parent=#FORMATION] OnAfterFormationStack + -- @param #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 FORMATION + -- @function [parent=#FORMATION] FormationStack + -- @param #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 FORMATION + -- @function [parent=#FORMATION] __FormationStack + -- @param #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 OnAfter for FORMATION + -- @function [parent=#FORMATION] OnAfterFormationLeftLine + -- @param #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 FORMATION + -- @function [parent=#FORMATION] FormationLeftLine + -- @param #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 FORMATION + -- @function [parent=#FORMATION] __FormationLeftLine + -- @param #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 OnAfter for FORMATION + -- @function [parent=#FORMATION] OnAfterFormationRightLine + -- @param #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 FORMATION + -- @function [parent=#FORMATION] FormationRightLine + -- @param #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 FORMATION + -- @function [parent=#FORMATION] __FormationRightLine + -- @param #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 OnAfter for FORMATION + -- @function [parent=#FORMATION] OnAfterFormationLeftWing + -- @param #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 FORMATION + -- @function [parent=#FORMATION] FormationLeftWing + -- @param #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 FORMATION + -- @function [parent=#FORMATION] __FormationLeftWing + -- @param #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 OnAfter for FORMATION + -- @function [parent=#FORMATION] OnAfterFormationRightWing + -- @param #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 FORMATION + -- @function [parent=#FORMATION] FormationRightWing + -- @param #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 FORMATION + -- @function [parent=#FORMATION] __FormationRightWing + -- @param #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 OnAfter for FORMATION + -- @function [parent=#FORMATION] OnAfterFormationCenterWing + -- @param #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 FORMATION + -- @function [parent=#FORMATION] FormationCenterWing + -- @param #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 FORMATION + -- @function [parent=#FORMATION] __FormationCenterWing + -- @param #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 OnAfter for FORMATION + -- @function [parent=#FORMATION] OnAfterFormationVic + -- @param #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 FORMATION + -- @function [parent=#FORMATION] FormationVic + -- @param #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 FORMATION + -- @function [parent=#FORMATION] __FormationVic + -- @param #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 OnAfter for FORMATION + -- @function [parent=#FORMATION] OnAfterFormationBox + -- @param #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 FORMATION + -- @function [parent=#FORMATION] FormationBox + -- @param #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 FORMATION + -- @function [parent=#FORMATION] __FormationBox + -- @param #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.CT1 = 0 + self.GT1 = 0 + + return self +end + + +--- Set time interval between updates of the formation. +-- @param #FORMATION self +-- @param #number dt Time step in seconds between formation updates. Default is every 0.5 seconds. +-- @return #FORMATION +function FORMATION:SetFollowTimeInterval(dt) + 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 #FORMATION self +-- @param #boolean SmokeDirection If true, then the direction vector will be smoked. +-- @return #FORMATION +function FORMATION:TestSmokeDirectionVector( SmokeDirection ) + self.SmokeDirectionVector = ( SmokeDirection == true ) and true or false + return self +end + +--- FormationLine Handler OnAfter for FORMATION +-- @param #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 #FORMATION +function FORMATION:onafterFormationLine( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, Formation ) + 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 + + 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 = COORDINATE: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 FORMATION +-- @param #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 #FORMATION +function FORMATION:onafterFormationTrail( FollowGroupSet, From , Event , To, XStart, XSpace, YStart ) + + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,0,0, self.Formation.Trail ) + + return self +end + + +--- FormationStack Handler OnAfter for FORMATION +-- @param #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 #FORMATION +function FORMATION:onafterFormationStack( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace ) + + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,0,0, self.Formation.Stack ) + + return self +end + +--- FormationLeftLine Handler OnAfter for FORMATION +-- @param #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 #FORMATION +function FORMATION:onafterFormationLeftLine( FollowGroupSet, From , Event , To, XStart, YStart, ZStart, ZSpace ) + + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,-ZStart,-ZSpace, self.Formation.LeftLine ) + + return self +end + +--- FormationRightLine Handler OnAfter for FORMATION +-- @param #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 #FORMATION +function FORMATION:onafterFormationRightLine( FollowGroupSet, From , Event , To, XStart, YStart, ZStart, ZSpace ) + + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,ZStart,ZSpace,self.Formation.RightLine) + + return self +end + +--- FormationLeftWing Handler OnAfter for FORMATION +-- @param #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 FORMATION:onafterFormationLeftWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, ZStart, ZSpace ) + + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,-ZStart,-ZSpace,self.Formation.LeftWing) + + return self +end + +--- FormationRightWing Handler OnAfter for FORMATION +-- @function [parent=#FORMATION] OnAfterFormationRightWing +-- @param #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 FORMATION:onafterFormationRightWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, ZStart, ZSpace ) + + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,ZStart,ZSpace,self.Formation.RightWing) + + return self +end + +--- FormationCenterWing Handler OnAfter for FORMATION +-- @param #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 FORMATION:onafterFormationCenterWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) + + local FollowSet = FollowGroupSet:GetSet() + + local i = 0 + + for FollowID, FollowGroup in pairs( FollowSet ) do + + local PointVec3 = COORDINATE: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.Formation.Vic ) + end + + return self +end + +--- FormationVic Handle for FORMATION +-- @param #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 #FORMATION +function FORMATION:onafterFormationVic( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) + + self:onafterFormationCenterWing(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace) + + return self +end + +--- FormationBox Handler OnAfter for FORMATION +-- @param #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 #FORMATION +function FORMATION:onafterFormationBox( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) + + local FollowSet = FollowGroupSet:GetSet() + + local i = 0 + + for FollowID, FollowGroup in pairs( FollowSet ) do + + local PointVec3 = COORDINATE: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.Formation.Box ) + end + + return self +end + +--- Use the method @{FORMATION#FORMATION.SetFlightRandomization}() to make the air units in your formation randomize their flight a bit while in formation. +-- @param #FORMATION self +-- @param #number FlightRandomization The formation flying errors that pilots can make while in formation. Is a range set in meters. +-- @return #FORMATION +function FORMATION:SetFlightRandomization( FlightRandomization ) + self.FlightRandomization = FlightRandomization + return self +end + +--- Stop function. Formation will not be updated any more. +-- @param #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 FORMATION:onafterStop(FollowGroupSet, From, Event, To) + self:E("Stopping formation.") +end + +--- Follow event fuction. Check if coming from state "stopped". If so the transition is rejected. +-- @param #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 FORMATION:onbeforeFollow( FollowGroupSet, From, Event, To ) + if From=="Stopped" then + return false -- Deny transition. + end + return true +end + +--- Enter following state. +-- @param #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 FORMATION:onenterFollowing( FollowGroupSet ) + + 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 #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 FORMATION:FollowMe(FollowGroup, ClientUnit, CT1, CV1, CT2, CV2) + + if 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.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, + 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 diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua index 9496f272d..66a790c41 100644 --- a/Moose Development/Moose/Functional/Fox.lua +++ b/Moose Development/Moose/Functional/Fox.lua @@ -141,6 +141,7 @@ FOX = { explosiondist = 200, explosiondist2 = 500, bigmissilemass = 50, + --destroy = nil, dt50 = 5, dt10 = 1, dt05 = 0.5, diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 560410ae2..c497d3355 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -5584,7 +5584,7 @@ end -- @param #string dest Name of the destination airport. function RAT:_ATCAddFlight(name, dest) -- Debug info - BASE:I(RAT.id..string.format("ATC %s: Adding flight %s with destination %s.", dest, name, dest)) + BASE:T(RAT.id..string.format("ATC %s: Adding flight %s with destination %s.", dest, name, dest)) -- Create new flight local flight={} --#RAT.AtcFlight @@ -5603,7 +5603,7 @@ end function RAT._ATCDelFlight(t,entry) for k,_ in pairs(t) do if k==entry then - BASE:I(RAT.id..string.format("Removing flight %s from queue", entry)) + BASE:T(RAT.id..string.format("Removing flight %s from queue", entry)) t[entry]=nil end end @@ -5614,7 +5614,7 @@ end -- @param #string name Group name of the flight. -- @param #number time Time the fight first registered. function RAT._ATCRegisterFlight(name, time) - BASE:I(RAT.id..string.format("Flight %s registered at ATC for landing clearance.", name)) + BASE:T(RAT.id..string.format("Flight %s registered at ATC for landing clearance.", name)) RAT.ATC.flight[name].Tarrive=time RAT.ATC.flight[name].holding=0 end @@ -5649,15 +5649,16 @@ function RAT._ATCStatus() -- 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:I(RAT.id..text) + BASE:T(RAT.id..text) elseif hold==RAT.ATC.onfinal then -- Aircarft is on final approach for landing. - local Tfinal=Tnow-flight.Tonfinal + local Tonfinal = flight.Tonfinal or timer.getTime()-1 + local Tfinal=Tnow-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:I(RAT.id..text) + BASE:T(RAT.id..text) elseif hold==RAT.ATC.unregistered then @@ -5711,13 +5712,13 @@ function RAT._ATCCheck() -- 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.", airportname, flightname, qID, nqueue, flight.holding/60, flight.holding%60) - BASE:I(RAT.id..text) + BASE:T(RAT.id..text) else local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.", airportname, flightname, flight.holding/60, flight.holding%60) - BASE:I(RAT.id..text) + BASE:T(RAT.id..text) -- Clear flight for landing. RAT._ATCClearForLanding(airportname, flightname) @@ -5772,7 +5773,7 @@ function RAT._ATCClearForLanding(airportname, flightname) -- Debug message. - BASE:I(RAT.id..string.format("ATC %s: Flight %s cleared for landing", airportname, flightname)) + BASE:T(RAT.id..string.format("ATC %s: Flight %s cleared for landing", airportname, flightname)) if string.find(flightname,"#") then flightname = string.match(flightname,"^(.+)#") @@ -5799,8 +5800,9 @@ function RAT._ATCFlightLanded(name) -- Times for holding and final approach. local Tnow=timer.getTime() - local Tfinal=Tnow-flight.Tonfinal - local Thold=flight.Tonfinal-flight.Tarrive + local Tonfinal = flight.Tonfinal or timer.getTime()-1 + local Tfinal=Tnow-Tonfinal + local Thold=Tonfinal-flight.Tarrive local airport=RAT.ATC.airport[dest] --#RAT.AtcAirport @@ -5823,9 +5825,9 @@ function RAT._ATCFlightLanded(name) local TrafficPerHour=airport.traffic/(timer.getTime()-RAT.ATC.T0)*3600 -- Debug info - BASE:I(RAT.id..string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60)) - BASE:I(RAT.id..string.format("ATC %s: Number of flights still on final %d.", dest, airport.Nonfinal)) - BASE:I(RAT.id..string.format("ATC %s: Traffic report: Number of planes landed in total %d. Flights/hour = %3.2f.", dest, airport.traffic, TrafficPerHour)) + BASE:T(RAT.id..string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60)) + BASE:T(RAT.id..string.format("ATC %s: Number of flights still on final %d.", dest, airport.Nonfinal)) + BASE:T(RAT.id..string.format("ATC %s: Traffic report: Number of planes landed in total %d. Flights/hour = %3.2f.", dest, airport.traffic, TrafficPerHour)) if string.find(name,"#") then name = string.match(name,"^(.+)#") diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index e785b4713..0ec94102b 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -602,7 +602,7 @@ RANGE.MenuF10Root = nil --- Range script version. -- @field #string version -RANGE.version = "2.8.0" +RANGE.version = "2.8.1" -- TODO list: -- TODO: Verbosity level for messages. @@ -2046,10 +2046,10 @@ function RANGE._OnImpact(weapon, self, playerData, attackHdg, attackAlt, attackV -- Smoke impact point of bomb. if playerData and playerData.smokebombimpact and insidezone then - if playerData and playerData.delaysmoke then - timer.scheduleFunction( self._DelayedSmoke, { coord = impactcoord, color = playerData.smokecolor }, timer.getTime() + self.TdelaySmoke ) + if playerData.delaysmoke then + impactcoord:Smoke(playerData.smokecolor, 30, self.TdelaySmoke) else - impactcoord:Smoke( playerData.smokecolor ) + impactcoord:Smoke(playerData.smokecolor, 30) end end @@ -2654,13 +2654,6 @@ end -- Display Messages ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Start smoking a coordinate with a delay. --- @param #table _args Argements passed. -function RANGE._DelayedSmoke( _args ) - _args.coord:Smoke(_args.color) - --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. diff --git a/Moose Development/Moose/Functional/Stratego.lua b/Moose Development/Moose/Functional/Stratego.lua new file mode 100644 index 000000000..adb440470 --- /dev/null +++ b/Moose Development/Moose/Functional/Stratego.lua @@ -0,0 +1,1645 @@ +--- **Functional** - Stratego. +-- +-- **Main Features:** +-- +-- * Helper class for mission designers to support classic capture-the-base scenarios. +-- * Creates a network of possible connections between bases (airbases, FARPs, Ships), Ports (defined as zones) and POIs (defined as zones). +-- * Assigns a strategic value to each of the resulting nodes. +-- * Can create a list of targets for your next mission move, both strategic and consolidation targets. +-- * Can be used with budgets to limit the target selection. +-- * Highly configureable. +-- +-- === +-- +-- ### Author: **applevangelist** +-- +-- @module Functional.Stratego +-- @image Functional.Stratego.png +-- Last Update May 2024 + + +--- +--- **STRATEGO** class, extends Core.Base#BASE +-- @type STRATEGO +-- @field #string ClassName +-- @field #boolean debug +-- @field #string version +-- @field #number portweight +-- @field #number POIweight +-- @field #number maxrunways +-- @field #number coalition +-- @field #table colors +-- @field #table airbasetable +-- @field #table nonconnectedab +-- @field #table easynames +-- @field #number maxdist +-- @field #table disttable +-- @field #table routexists +-- @field #number routefactor +-- @field #table OpsZones +-- @field #number NeutralBenefit +-- @field #number Budget +-- @field #boolean usebudget +-- @field #number CaptureUnits +-- @field #number CaptureThreatlevel +-- @field #table CaptureObjectCategories +-- @field #boolean ExcludeShips +-- @field Core.Zone#ZONE StrategoZone +-- @extends Core.Base#BASE +-- @extends Core.Fsm#FSM + + +--- *If you see what is right and fail to act on it, you lack courage* --- Confucius +-- +-- === +-- +-- # The STRATEGO Concept +-- +-- STRATEGO is a helper class for mission designers. +-- The basic idea is to create a network of nodes (bases) on the map, which each have a number of connections +-- to other nodes. The base value of each node is the number of runways of the base (the bigger the more important), or in the case of Ports and POIs, the assigned value points. +-- The strategic value of each base is determined by the number of routes going in and out of the node, where connections between more strategic nodes add a higher value to the +-- strategic value than connections to less valueable nodes. +-- +-- ## Setup +-- +-- Setup is map indepent and works automatically. All airbases, FARPS, and ships on the map are considered. **Note:** Later spawned objects are not considered at the moment. +-- +-- -- Setup and start STRATGEO for the blue side, maximal node distance is 100km +-- local Bluecher = STRATEGO:New("Bluecher",coalition.side.BLUE,100) +-- -- use budgets +-- Bluecher:SetUsingBudget(true,500) +-- -- draw on the map +-- Bluecher:SetDebug(true,true,true) +-- -- Start +-- Bluecher:Start() +-- +-- ### Helper +-- +-- @{#STRATEGO.SetWeights}(): Set weights for nodes and routes to determine their importance. +-- +-- ### Hint +-- +-- Each node is its own @{Ops.OpsZone#OPSZONE} object to manage the coalition alignment of that node and how it can be conquered. +-- +-- ### Distance +-- +-- The node distance factor determines how many connections are there on the map. The smaller the lighter is the resulting net. The higher the thicker it gets, with more strategic options. +-- Play around with the distance to get an optimal map for your scenario. +-- +-- One some maps, e.g. Syria, lower distance factors can create "islands" of unconnected network parts on the map. FARPs and POIs can bridge those gaps, or you can add routes manually. +-- +-- @{#STRATEGO.AddRoutesManually}(): Add a route manually. +-- +-- ## Ports and POIs +-- +-- Ports and POIs are @{Core.Zone#ZONE} objects on the map with specfic values. Zones with the keywords "Port" or "POI" in the name are automatically considered at setup time. +-- +-- ## Get next possible targets +-- +-- There are two types of possible target lists, strategic and consolidation. Targets closer to the start node are chosen as possible targets. +-- +-- +-- * Strategic targets are of higher or equal base weight from a given start point. Can also be obtained for the whole net. +-- * Consoliation targets are of smaller or equal base weight from a given start point. Can also be obtained for the whole net. +-- +-- +-- @{#STRATEGO.UpdateNodeCoalitions}(): Update alls node's coalition data before takign a decision. +-- @{#STRATEGO.FindStrategicTargets}(): Find a list of possible strategic targets in the network of the enemy or neutral coalition. +-- @{#STRATEGO.FindConsolidationTargets}(): Find a list of possible strategic targets in the network of the enemy or neutral coalition. +-- @{#STRATEGO.FindAffordableStrategicTarget}(): When using budgets, find **one** strategic target you can afford. +-- @{#STRATEGO.FindAffordableConsolidationTarget}(): When using budgets, find **one** consolidation target you can afford. +-- @{#STRATEGO.FindClosestStrategicTarget}(): Find closest strategic target from a given start point. +-- @{#STRATEGO.FindClosestConsolidationTarget}(): Find closest consolidation target from a given start point. +-- @{#STRATEGO.GetHighestWeightNodes}(): Get a list of the nodes with the highest weight. Coalition independent. +-- @{#STRATEGO.GetNextHighestWeightNodes}(): Get a list of the nodes a weight less than the give parameter. Coalition independent. +-- +-- +-- **How** you act on these suggestions is again totally up to your mission design. +-- +-- ## Using budgets +-- +-- Set up STRATEGO to use budgets to limit the target selection. **How** your side actually earns budgets is up to your mission design. However, when using budgets, a target will only be selected, +-- when you have more budget points available than the value points of the targeted base. +-- +-- -- use budgets +-- Bluecher:SetUsingBudget(true,500) +-- +-- ### Helpers: +-- +-- +-- @{#STRATEGO.GetBudget}(): Get the current budget points. +-- @{#STRATEGO.AddBudget}(): Add a number of budget points. +-- @{#STRATEGO.SubtractBudget}(): Subtract a number of budget points. +-- @{#STRATEGO.SetNeutralBenefit}(): Set neutral benefit, i.e. how many points it is cheaper to decide for a neutral vs an enemy node when taking decisions. +-- +-- +-- ## Functions to query a node's data +-- +-- +-- @{#STRATEGO.GetNodeBaseWeight}(): Get the base weight of a node by its name. +-- @{#STRATEGO.GetNodeCoalition}(): Get the COALITION of a node by its name. +-- @{#STRATEGO.GetNodeType}(): Get the TYPE of a node by its name. +-- @{#STRATEGO.GetNodeZone}(): Get the ZONE of a node by its name. +-- @{#STRATEGO.GetNodeOpsZone}(): Get the OPSZONE of a node by its name. +-- @{#STRATEGO.GetNodeCoordinate}(): Get the COORDINATE of a node by its name. +-- @{#STRATEGO.IsAirbase}(): Check if the TYPE of a node is AIRBASE. +-- @{#STRATEGO.IsPort}(): Check if the TYPE of a node is PORT. +-- @{#STRATEGO.IsPOI}(): Check if the TYPE of a node is POI. +-- @{#STRATEGO.IsFARP}(): Check if the TYPE of a node is FARP. +-- @{#STRATEGO.IsShip}(): Check if the TYPE of a node is SHIP. +-- +-- +-- ## Various +-- +-- +-- @{#STRATEGO.FindNeighborNodes}(): Get neighbor nodes of a named node. +-- @{#STRATEGO.FindRoute}(): Find a route between two nodes. +-- @{#STRATEGO.SetCaptureOptions}(): Set how many units of which minimum threat level are needed to capture one node (i.e. the underlying OpsZone). +-- @{#STRATEGO.SetDebug}(): Set debug and draw options. +-- @{#STRATEGO.SetStrategoZone}(): Set a zone to restrict STRATEGO analytics to, can be any kind of ZONE Object. +-- +-- +-- ## Visualisation example code for the Syria map: +-- +-- local Bluecher = STRATEGO:New("Bluecher",coalition.side.BLUE,100) +-- Bluecher:SetDebug(true,true,true) +-- Bluecher:Start() +-- +-- Bluecher:AddRoutesManually(AIRBASE.Syria.Beirut_Rafic_Hariri,AIRBASE.Syria.Larnaca) +-- Bluecher:AddRoutesManually(AIRBASE.Syria.Incirlik,AIRBASE.Syria.Hatay) +-- Bluecher:AddRoutesManually(AIRBASE.Syria.Incirlik,AIRBASE.Syria.Minakh) +-- Bluecher:AddRoutesManually(AIRBASE.Syria.King_Hussein_Air_College,AIRBASE.Syria.H4) +-- Bluecher:AddRoutesManually(AIRBASE.Syria.Sayqal,AIRBASE.Syria.At_Tanf) +-- +-- local route = Bluecher:FindRoute(AIRBASE.Syria.Rosh_Pina,AIRBASE.Syria.Incirlik,5,true) +-- UTILS.PrintTableToLog(route,1) +-- +-- @field #STRATEGO +STRATEGO = { + ClassName = "STRATEGO", + debug = false, + drawzone = false, + markzone = false, + version = "0.3.1", + portweight = 3, + POIweight = 1, + maxrunways = 3, + coalition = nil, + colors = nil, + airbasetable = {}, + nonconnectedab = {}, + easynames = {}, + maxdist = 150, -- km + disttable = {}, + routexists = {}, + routefactor = 5, + OpsZones = {}, + NeutralBenefit = 100, + Budget = 0, + usebudget = false, + CaptureUnits = 3, + CaptureThreatlevel = 1, + CaptureObjectCategories = {Object.Category.UNIT}, + ExcludeShips = true, +} + +--- +-- @type STRATEGO.Data +-- @field #string name +-- @field #number baseweight +-- @field #number weight +-- @field #number coalition +-- @field #boolean port +-- @field Core.Zone#ZONE_RADIUS zone, +-- @field Core.Point#COORDINATE coord +-- @field #string type +-- @field Ops.OpsZone#OPSZONE opszone +-- @field #number connections + +--- +-- @type STRATEGO.DistData +-- @field #string start +-- @field #string target +-- @field #number dist + +--- +-- @type STRATEGO.Target +-- @field #string name +-- @field #number dist +-- @field #number points +-- @field #number coalition +-- @field #string coalitionname +-- @field Core.Point#COORDINATRE coordinate + +--- +-- @type STRATEGO.Type +-- @field #string AIRBASE +-- @field #string PORT +-- @field #string POI +-- @field #string FARP +-- @field #string SHIP +STRATEGO.Type = { + AIRBASE = "AIRBASE", + PORT = "PORT", + POI = "POI", + FARP = "FARP", + SHIP = "SHIP", +} + +--- [USER] Create a new STRATEGO object and start it up. +-- @param #STRATEGO self +-- @param #string Name Name of the Adviser. +-- @param #number Coalition Coalition, e.g. coalition.side.BLUE. +-- @param #number MaxDist Maximum distance of a single route in kilometers, defaults to 150. +-- @return #STRATEGO self +function STRATEGO:New(Name,Coalition,MaxDist) + -- Inherit everything from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #STRATEGO + + self.coalition = Coalition + self.coalitiontext = UTILS.GetCoalitionName(Coalition) + self.name = Name or "Hannibal" + + self.maxdist = MaxDist or 150 -- km + self.disttable = {} + self.routexists = {} + self.ExcludeShips = true + + self.lid = string.format("STRATEGO %s %s | ",self.name,self.version) + + self.bases = SET_AIRBASE:New():FilterOnce() + self.ports = SET_ZONE:New():FilterPrefixes("Port"):FilterOnce() + self.POIs = SET_ZONE:New():FilterPrefixes("POI"):FilterOnce() + + self.colors = { + [1] = {0,1,0}, -- green + [2] = {1,0,0}, -- red + [3] = {0,0,1}, -- blue + [4] = {1,0.65,0}, -- orange + } + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Update", "*") -- Start FSM. + self:AddTransition("*", "NodeEvent", "*") -- Start FSM. + self:AddTransition("Running", "Stop", "Stopped") -- Start FSM. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the STRATEGO. Initializes parameters and starts event handlers. + -- @function [parent=#STRATEGO] Start + -- @param #STRATEGO self + + --- Triggers the FSM event "Start" after a delay. Starts the STRATEGO. Initializes parameters and starts event handlers. + -- @function [parent=#STRATEGO] __Start + -- @param #STRATEGO self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the STRATEGO and all its event handlers. + -- @function [parent=#STRATEGO] Stop + -- @param #STRATEGO self + + --- Triggers the FSM event "Stop" after a delay. Stops the STRATEGO and all its event handlers. + -- @function [parent=#STRATEGO] __Stop + -- @param #STRATEGO self + -- @param #number delay Delay in seconds. + + --- FSM Function OnAfterNodeEvent. A node changed coalition. + -- @function [parent=#STRATEGO] OnAfterNodeEvent + -- @param #STRATEGO self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Ops.OpsZone#OPSZONE OpsZone The OpsZone triggering the event. + -- @param #number Coalition The coalition of the new owner. + -- @return #STRATEGO self + + return self +end + +--- [INTERNAL] FSM function for initial setup and getting ready. +-- @param #STRATEGO self +-- @return #STRATEGO self +function STRATEGO:onafterStart(From,Event,To) + self:T(self.lid.."Start") + self:AnalyseBases() + self:AnalysePOIs(self.ports,self.portweight,"PORT") + self:AnalysePOIs(self.POIs,self.POIweight,"POI") + + for i=self.maxrunways,1,-1 do + self:AnalyseRoutes(i,i*self.routefactor,self.colors[(i%3)+1],i) + end + self:AnalyseUnconnected(self.colors[4]) + + self:I(self.lid.."Advisory ready.") + + self:__Update(180) + return self +end + +--- [INTERNAL] Update knot association +-- @param #STRATEGO self +-- @return #STRATEGO self +function STRATEGO:onafterUpdate(From,Event,To) + self:T(self.lid.."Update") + + self:UpdateNodeCoalitions() + + if self:GetState() == "Running" then + self:__Update(180) + end + + return self +end + +--- [USER] Set up usage of budget and set an initial budget in points. +-- @param #STRATEGO self +-- @param #boolean Usebudget If true, use budget for advisory calculations. +-- @param #number StartBudget Initial budget to be used, defaults to 500. +function STRATEGO:SetUsingBudget(Usebudget,StartBudget) + self:T(self.lid.."SetUsingBudget") + self.usebudget = Usebudget + self.Budget = StartBudget + return self +end + +--- [USER] Set debugging. +-- @param #STRATEGO self +-- @param #boolean Debug If true, switch on debugging. +-- @param #boolean DrawZones If true, draw the OpsZones on the F10 map. +-- @param #boolean MarkZones if true, mark the OpsZones on the F10 map (with further information). +function STRATEGO:SetDebug(Debug,DrawZones,MarkZones) + self:T(self.lid.."SetDebug") + self.debug = Debug + self.drawzone = DrawZones + self.markzone = MarkZones + return self +end + +--- [USER] Restrict Stratego to analyse this zone only. +-- @param #STRATEGO self +-- @param Core.Zone#ZONE Zone The Zone to restrict Stratego to, can be any kind of ZONE Object. +-- @return #STRATEGO self +function STRATEGO:SetStrategoZone(Zone) + self.StrategoZone = Zone + return self +end + +--- [USER] Set weights for nodes and routes to determine their importance. +-- @param #STRATEGO self +-- @param #number MaxRunways Set the maximum number of runways the big (equals strategic) airbases on the map have. Defaults to 3. The weight of an airbase node hence equals the number of runways. +-- @param #number PortWeight Set what weight a port node has. Defaults to 3. +-- @param #number POIWeight Set what weight a POI node has. Defaults to 1. +-- @param #number RouteFactor Defines which weight each route between two defined nodes gets: Weight * RouteFactor. +-- @return #STRATEGO self +function STRATEGO:SetWeights(MaxRunways,PortWeight,POIWeight,RouteFactor) + self:T(self.lid.."SetWeights") + self.portweight = PortWeight or 3 + self.POIweight = POIWeight or 1 + self.maxrunways = MaxRunways or 3 + self.routefactor = RouteFactor or 5 + return self +end + +--- [USER] Set neutral benefit, i.e. how many points it is cheaper to decide for a neutral vs an enemy node when taking decisions. +-- @param #STRATEGO self +-- @param #number NeutralBenefit Pointsm defaults to 100. +-- @return #STRATEGO self +function STRATEGO:SetNeutralBenefit(NeutralBenefit) + self:T(self.lid.."SetNeutralBenefit") + self.NeutralBenefit = NeutralBenefit or 100 + return self +end + +--- [USER] Set how many units of which minimum threat level are needed to capture one node (i.e. the underlying OpsZone). +-- @param #STRATEGO self +-- @param #number CaptureUnits Number of units needed, defaults to three. +-- @param #number CaptureThreatlevel Threat level needed, can be 0..10, defaults to one. +-- @param #table CaptureCategories Table of object categories which can capture a node, defaults to `{Object.Category.UNIT}`. +-- @return #STRATEGO self +function STRATEGO:SetCaptureOptions(CaptureUnits,CaptureThreatlevel,CaptureCategories) + self:T(self.lid.."SetCaptureOptions") + self.CaptureUnits = CaptureUnits or 3 + self.CaptureThreatlevel = CaptureThreatlevel or 1 + self.CaptureObjectCategories = CaptureCategories or {Object.Category.UNIT} + return self +end + +--- [INTERNAL] Analyse airbase setups +-- @param #STRATEGO self +-- @return #STRATEGO self +function STRATEGO:AnalyseBases() + self:T(self.lid.."AnalyseBases") + local colors = self.colors + local debug = self.debug + local airbasetable = self.airbasetable + local nonconnectedab = self.nonconnectedab + local easynames = self.easynames + local zone = self.StrategoZone -- Core.Zone#ZONE_POLYGON + + -- find bases with >= 1 runways + self.bases:ForEach( + function(afb) + local ab = afb -- Wrapper.Airbase#AIRBASE + local abvec2 = ab:GetVec2() + if self.ExcludeShips and ab:IsShip() then return end + if zone ~= nil then + if not zone:IsVec2InZone(abvec2) then + return + end + end + local abname = ab:GetName() + local runways = ab:GetRunways() + local numrwys = #runways + if numrwys >= 1 then numrwys = numrwys * 0.5 end + local abzone = ab:GetZone() + if not abzone then + abzone = ZONE_RADIUS:New(abname,ab:GetVec2(),500) + end + local coa = ab:GetCoalition() + if coa == nil then return end -- Spawned FARPS issue - these have no tangible data + coa = coa+1 + local abtype = STRATEGO.Type.AIRBASE + if ab:IsShip() then + numrwys = 1 + abtype = STRATEGO.Type.SHIP + end + if ab:IsHelipad() then + numrwys = 1 + abtype = STRATEGO.Type.FARP + end + local coord = ab:GetCoordinate() + if debug then + abzone:DrawZone(-1,colors[coa],1,colors[coa],0.3,1) + coord:TextToAll(tostring(numrwys),-1,{0,0,0},1,colors[coa],0.3,20) + end + local opszone = self:GetNewOpsZone(abname,coa-1) + local tbl = { + name = abname, + baseweight = numrwys, + weight = 0, + coalition = coa-1, + port = false, + zone = abzone, + coord = coord, + type = abtype, + opszone = opszone, + connections = 0, + } + airbasetable[abname] = tbl + nonconnectedab[abname] = true + local name = string.gsub(abname,"[%p%s]",".") + easynames[name]=abname + end + ) + return self +end + +--- [INTERNAL] Update node coalitions +-- @param #STRATEGO self +-- @return #STRATEGO self +function STRATEGO:UpdateNodeCoalitions() + self:T(self.lid.."UpdateNodeCoalitions") + local newtable = {} + for _id,_data in pairs(self.airbasetable) do + local data = _data -- #STRATEGO.Data + if data.type == STRATEGO.Type.AIRBASE or data.type == STRATEGO.Type.FARP or data.type == STRATEGO.Type.SHIP then + data.coalition = AIRBASE:FindByName(data.name):GetCoalition() or 0 + else + data.coalition = data.opszone:GetOwner() or 0 + end + newtable[_id] = _data + end + self.airbasetable = nil + self.airbasetable = newtable + return self +end + +--- [INTERNAL] Get an OpsZone from a Zone object. +-- @param #STRATEGO self +-- @param Core.Zone#ZONE Zone +-- @param #number Coalition +-- @return Ops.OpsZone#OPSZONE OpsZone +function STRATEGO:GetNewOpsZone(Zone,Coalition) + self:T(self.lid.."GetNewOpsZone") + local opszone = OPSZONE:New(Zone,Coalition or 0) + opszone:SetCaptureNunits(self.CaptureUnits) + opszone:SetCaptureThreatlevel(self.CaptureThreatlevel) + opszone:SetObjectCategories(self.CaptureObjectCategories) + opszone:SetDrawZone(self.drawzone) + opszone:SetMarkZone(self.markzone) + opszone:Start() + + local function Captured(opszone,coalition) + self:__NodeEvent(1,opszone,coalition) + end + + function opszone:OnBeforeCaptured(From,Event,To,Coalition) + Captured(opszone,Coalition) + end + + return opszone +end + +--- [INTERNAL] Analyse POI setups +-- @param #STRATEGO self +-- @return #STRATEGO self +function STRATEGO:AnalysePOIs(Set,Weight,Key) + self:T(self.lid.."AnalysePOIs") + local colors = self.colors + local debug = self.debug + local airbasetable = self.airbasetable + local nonconnectedab = self.nonconnectedab + local easynames = self.easynames + Set:ForEach( + function(port) + local zone = port -- Core.Zone#ZONE_RADIUS + local zname = zone:GetName() + local coord = zone:GetCoordinate() + if debug then + zone:DrawZone(-1,colors[1],1,colors[1],0.3,1) + coord:TextToAll(tostring(Weight),-1,{0,0,0},1,colors[1],0.3,20) + end + local opszone = self:GetNewOpsZone(zone) + local tbl = { -- #STRATEGO.Data + name = zname, + baseweight = Weight, + weight = 0, + coalition = coalition.side.NEUTRAL, + port = true, + zone = zone, + coord = coord, + type = Key, + opszone = opszone, + connections = 0, + } + airbasetable[zname] = tbl + nonconnectedab[zname] = true + local name = string.gsub(zname,"[%p%s]",".") + --self:I({name=name,zone=zname}) + easynames[name]=zname + end + ) + return self +end + +--- [INTERNAL] Get nice route text +-- @param #STRATEGO self +-- @return #STRATEGO self +function STRATEGO:GetToFrom(StartPoint,EndPoint) + self:T(self.lid.."GetToFrom "..tostring(StartPoint).." "..tostring(EndPoint)) + local pstart = string.gsub(StartPoint,"[%p%s]",".") + local pend = string.gsub(EndPoint,"[%p%s]",".") + local fromto = pstart..";"..pend + local tofrom = pend..";"..pstart + return fromto, tofrom +end + +--- [USER] Get available connecting nodes from one start node +-- @param #STRATEGO self +-- @param #string StartPoint The starting name +-- @return #boolean found +-- @return #table Nodes +function STRATEGO:GetRoutesFromNode(StartPoint) + self:T(self.lid.."GetRoutesFromNode") + local pstart = string.gsub(StartPoint,"[%p%s]",".") + local found = false + pstart=pstart..";" + local routes = {} + local listed = {} + for _,_data in pairs(self.routexists) do + if string.find(_data,pstart,1,true) and not listed[_data] then + local target = string.gsub(_data,pstart,"") + local fname = self.easynames[target] + table.insert(routes,fname) + found = true + listed[_data] = true + end + end + return found,routes +end + +--- [USER] Manually add a route, for e.g. Island hopping or to connect isolated networks. Use **after** STRATEGO has been started! +-- @param #STRATEGO self +-- @param #string Startpoint Starting Point, e.g. AIRBASE.Syria.Hatay +-- @param #string Endpoint End Point, e.g. AIRBASE.Syria.H4 +-- @param #table Color (Optional) RGB color table {r, g, b}, e.g. {1,0,0} for red. Defaults to violet. +-- @param #number Linetype (Optional) Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 5. +-- @param #boolean Draw (Optional) If true, draw route on the F10 map. Defaukt false. +-- @return #STRATEGO self +function STRATEGO:AddRoutesManually(Startpoint,Endpoint,Color,Linetype,Draw) + self:T(self.lid.."AddRoutesManually") + local fromto,tofrom = self:GetToFrom(Startpoint,Endpoint) + local startcoordinate = self.airbasetable[Startpoint].coord + local targetcoordinate = self.airbasetable[Endpoint].coord + local dist = UTILS.Round(targetcoordinate:Get2DDistance(startcoordinate),-2)/1000 + local color = Color or {136/255,0,1} + local linetype = Linetype or 5 + local data = { + start = Startpoint, + target = Endpoint, + dist = dist, + } + --table.insert(disttable,fromto,data) + self.disttable[fromto] = data + self.disttable[tofrom] = data + --table.insert(disttable,tofrom,data) + table.insert(self.routexists,fromto) + table.insert(self.routexists,tofrom) + self.nonconnectedab[Endpoint] = false + self.nonconnectedab[Startpoint] = false + local factor = self.airbasetable[Startpoint].baseweight*self.routefactor + self.airbasetable[Startpoint].weight = self.airbasetable[Startpoint].weight+factor + self.airbasetable[Endpoint].weight = self.airbasetable[Endpoint].weight+factor + self.airbasetable[Endpoint].connections = self.airbasetable[Endpoint].connections + 2 + self.airbasetable[Startpoint].connections = self.airbasetable[Startpoint].connections+2 + if self.debug or Draw then + startcoordinate:LineToAll(targetcoordinate,-1,color,1,linetype,nil,string.format("%dkm",dist)) + end + return self +end + +--- [INTERNAL] Analyse routes +-- @param #STRATEGO self +-- @return #STRATEGO self +function STRATEGO:AnalyseRoutes(tgtrwys,factor,color,linetype) + self:T(self.lid.."AnalyseRoutes") + for _,_ab in pairs(self.airbasetable) do + if _ab.baseweight >= 1 then + local startpoint = _ab.name + local startcoord = _ab.coord + for _,_data in pairs(self.airbasetable) do + local fromto,tofrom = self:GetToFrom(startpoint,_data.name) + if _data.name == startpoint then + -- same as we + elseif _data.baseweight == tgtrwys and not (self.routexists[fromto] or self.routexists[tofrom]) then + local tgtc = _data.coord + local dist = UTILS.Round(tgtc:Get2DDistance(startcoord),-2)/1000 + if dist <= self.maxdist then + --local text = string.format("Distance %s to %s is %dkm",startpoint,_data.name,dist) + --MESSAGE:New(text,10):ToLog() + local data = { + start = startpoint, + target = _data.name, + dist = dist, + } + --table.insert(disttable,fromto,data) + self.disttable[fromto] = data + self.disttable[tofrom] = data + --table.insert(disttable,tofrom,data) + table.insert(self.routexists,fromto) + table.insert(self.routexists,tofrom) + self.nonconnectedab[_data.name] = false + self.nonconnectedab[startpoint] = false + self.airbasetable[startpoint].weight = self.airbasetable[startpoint].weight+factor + self.airbasetable[_data.name].weight = self.airbasetable[_data.name].weight+factor + self.airbasetable[startpoint].connections = self.airbasetable[startpoint].connections + 1 + self.airbasetable[_data.name].connections = self.airbasetable[_data.name].connections + 1 + if self.debug then + startcoord:LineToAll(tgtc,-1,color,1,linetype,nil,string.format("%dkm",dist)) + end + end + end + end + end + end + return self +end + +--- [INTERNAL] Analyse non-connected points. +-- @param #STRATEGO self +-- @param #table Color RGB color to be used. +-- @return #STRATEGO self +function STRATEGO:AnalyseUnconnected(Color) + self:T(self.lid.."AnalyseUnconnected") + -- Non connected ones + for _name,_noconnect in pairs(self.nonconnectedab) do + if _noconnect then + -- Find closest connected airbase + local startpoint = _name + local startcoord = self.airbasetable[_name].coord + local shortest = 1000*1000 + local closest = nil + local closestcoord = nil + for _,_data in pairs(self.airbasetable) do + if _name ~= _data.name then + --local tgt = AIRBASE:FindByName(_data.name) + local tgtc = _data.coord + local dist = UTILS.Round(tgtc:Get2DDistance(startcoord),-2)/1000 + if dist < shortest and self.nonconnectedab[_data.name] == false then + --local text = string.format("Distance %s to %s is %dkm",startpoint,_data.name,dist) + shortest = dist + closest = _data.name + closestcoord = tgtc + --MESSAGE:New(text,10):ToLog():ToAll() + end + end + end + if closest then + if self.debug then + startcoord:LineToAll(closestcoord,-1,Color,1,3,nil,string.format("%dkm",shortest)) + end + self.airbasetable[startpoint].weight = self.airbasetable[startpoint].weight+1 + self.airbasetable[closest].weight = self.airbasetable[closest].weight+1 + self.airbasetable[startpoint].connections = self.airbasetable[startpoint].connections+2 + self.airbasetable[closest].connections = self.airbasetable[closest].connections+2 + local data = { + start = startpoint, + target = closest, + dist = shortest, + } + local fromto,tofrom = self:GetToFrom(startpoint,closest) + self.disttable[fromto] = data + self.disttable[tofrom] = data + table.insert(self.routexists,fromto) + table.insert(self.routexists,tofrom) + end + end + end + return self +end + +--[[ +function STRATEGO:PruneDeadEnds(abtable) + local found = false + local newtable = {} + for name, _data in pairs(abtable) do + local data = _data -- #STRATEGO.Data + if data.connections > 2 then + newtable[name] = data + else + -- dead end + found = true + local neighbors, nearest, distance = self:FindNeighborNodes(name) + --self:I("Pruning "..name) + if nearest then + for _name,_ in pairs(neighbors) do + local abname = self.easynames[_name] or _name + --self:I({easyname=_name,airbasename=abname}) + if abtable[abname] then + abtable[abname].connections = abtable[abname].connections -1 + end + end + end + if self.debug then + data.coord:CircleToAll(5000,-1,{1,1,1},1,{1,1,1},1,3,true,"Dead End") + end + end + end + abtable = nil + return found,newtable +end +--]] + +--- [USER] Get a list of the nodes with the highest weight. +-- @param #STRATEGO self +-- @param #number Coalition (Optional) Find for this coalition only. E.g. coalition.side.BLUE. +-- @return #table Table of nodes. +-- @return #number Weight The consolidated weight associated with the nodes. +-- @return #number Highest Highest weight found. +-- @return #string Name of the node with the highest weight. +function STRATEGO:GetHighestWeightNodes(Coalition) + self:T(self.lid.."GetHighestWeightNodes") + local weight = 0 + local highest = 0 + local highname = nil + local airbases = {} + for _name,_data in pairs(self.airbasetable) do + local okay = true + if Coalition then + if _data.coalition ~= Coalition then + okay = false + end + end + if _data.weight >= weight and okay then + weight = _data.weight + if not airbases[weight] then airbases[weight]={} end + table.insert(airbases[weight],_name) + end + if _data.weight > highest and okay then + highest = _data.weight + highname = _name + end + end + return airbases[weight],weight,highest,highname +end + +--- [USER] Get a list of the nodes a weight less than the given parameter. +-- @param #STRATEGO self +-- @param #number Weight Weight - nodes need to have less than this weight. +-- @param #number Coalition (Optional) Find for this coalition only. E.g. coalition.side.BLUE. +-- @return #table Table of nodes. +-- @return #number Weight The consolidated weight associated with the nodes. +function STRATEGO:GetNextHighestWeightNodes(Weight, Coalition) + self:T(self.lid.."GetNextHighestWeightNodes") + local weight = 0 + local airbases = {} + for _name,_data in pairs(self.airbasetable) do + local okay = true + if Coalition then + if _data.coalition ~= Coalition then + okay = false + end + end + if _data.weight >= weight and _data.weight < Weight and okay then + weight = _data.weight + if not airbases[weight] then airbases[weight]={} end + table.insert(airbases[weight],_name) + end + end + return airbases[weight],weight +end + +--- [USER] Set the aggregated weight of a single node found by its name manually. +-- @param #STRATEGO self +-- @param #string Name The name to look for. +-- @param #number Weight The weight to be set. +-- @return #boolean success +function STRATEGO:SetNodeWeight(Name,Weight) + self:T(self.lid.."SetNodeWeight") + if Name and Weight and self.airbasetable[Name] then + self.airbasetable[Name].weight = Weight or 0 + return true + else + return false + end +end + +--- [USER] Set the base weight of a single node found by its name manually. +-- @param #STRATEGO self +-- @param #string Name The name to look for. +-- @param #number Weight The weight to be set. +-- @return #boolean success +function STRATEGO:SetNodeBaseWeight(Name,Weight) + self:T(self.lid.."SetNodeBaseWeight") + if Name and Weight and self.airbasetable[Name] then + self.airbasetable[Name].baseweight = Weight or 0 + return true + else + return false + end +end + +--- [USER] Get the aggregated weight of a node by its name. +-- @param #STRATEGO self +-- @param #string Name The name to look for. +-- @return #number Weight The weight or 0 if not found. +function STRATEGO:GetNodeWeight(Name) + self:T(self.lid.."GetNodeWeight") + if Name and self.airbasetable[Name] then + return self.airbasetable[Name].weight or 0 + else + return 0 + end +end + +--- [USER] Get the base weight of a node by its name. +-- @param #STRATEGO self +-- @param #string Name The name to look for. +-- @return #number Weight The base weight or 0 if not found. +function STRATEGO:GetNodeBaseWeight(Name) + self:T(self.lid.."GetNodeBaseWeight") + if Name and self.airbasetable[Name] then + return self.airbasetable[Name].baseweight or 0 + else + return 0 + end +end + +--- [USER] Get the COALITION of a node by its name. +-- @param #STRATEGO self +-- @param #string The name to look for. +-- @return #number Coalition The coalition. +function STRATEGO:GetNodeCoalition(Name) + self:T(self.lid.."GetNodeCoalition") + if Name and self.airbasetable[Name] then + return self.airbasetable[Name].coalition or coalition.side.NEUTRAL + else + return coalition.side.NEUTRAL + end +end + +--- [USER] Get the TYPE of a node by its name. +-- @param #STRATEGO self +-- @param #string The name to look for. +-- @return #string Type Type of the node, e.g. STRATEGO.Type.AIRBASE or nil if not found. +function STRATEGO:GetNodeType(Name) + self:T(self.lid.."GetNodeType") + if Name and self.airbasetable[Name] then + return self.airbasetable[Name].type + else + return nil + end +end + +--- [USER] Get the ZONE of a node by its name. +-- @param #STRATEGO self +-- @param #string The name to look for. +-- @return Core.Zone#ZONE Zone The Zone of the node or nil if not found. +function STRATEGO:GetNodeZone(Name) + self:T(self.lid.."GetNodeZone") + if Name and self.airbasetable[Name] then + return self.airbasetable[Name].zone + else + return nil + end +end + +--- [USER] Get the OPSZONE of a node by its name. +-- @param #STRATEGO self +-- @param #string The name to look for. +-- @return Ops.OpsZone#OPSZONE OpsZone The OpsZone of the node or nil if not found. +function STRATEGO:GetNodeOpsZone(Name) + self:T(self.lid.."GetNodeOpsZone") + if Name and self.airbasetable[Name] then + return self.airbasetable[Name].opszone + else + return nil + end +end + +--- [USER] Get the COORDINATE of a node by its name. +-- @param #STRATEGO self +-- @param #string The name to look for. +-- @return Core.Point#COORDINATE Coordinate The Coordinate of the node or nil if not found. +function STRATEGO:GetNodeCoordinate(Name) + self:T(self.lid.."GetNodeCoordinate") + if Name and self.airbasetable[Name] then + return self.airbasetable[Name].coord + else + return nil + end +end + +--- [USER] Check if the TYPE of a node is AIRBASE. +-- @param #STRATEGO self +-- @param #string The name to look for. +-- @return #boolean Outcome +function STRATEGO:IsAirbase(Name) + self:T(self.lid.."IsAirbase") + if Name and self.airbasetable[Name] then + return self.airbasetable[Name].type == STRATEGO.Type.AIRBASE + else + return false + end +end + +--- [USER] Check if the TYPE of a node is PORT. +-- @param #STRATEGO self +-- @param #string The name to look for. +-- @return #boolean Outcome +function STRATEGO:IsPort(Name) + self:T(self.lid.."IsPort") + if Name and self.airbasetable[Name] then + return self.airbasetable[Name].type == STRATEGO.Type.PORT + else + return false + end +end + +--- [USER] Check if the TYPE of a node is POI. +-- @param #STRATEGO self +-- @param #string The name to look for. +-- @return #boolean Outcome +function STRATEGO:IsPOI(Name) + self:T(self.lid.."IsPOI") + if Name and self.airbasetable[Name] then + return self.airbasetable[Name].type == STRATEGO.Type.POI + else + return false + end +end + +--- [USER] Check if the TYPE of a node is FARP. +-- @param #STRATEGO self +-- @param #string The name to look for. +-- @return #boolean Outcome +function STRATEGO:IsFARP(Name) + self:T(self.lid.."IsFARP") + if Name and self.airbasetable[Name] then + return self.airbasetable[Name].type == STRATEGO.Type.FARP + else + return false + end +end + +--- [USER] Check if the TYPE of a node is SHIP. +-- @param #STRATEGO self +-- @param #string The name to look for. +-- @return #boolean Outcome +function STRATEGO:IsShip(Name) + self:T(self.lid.."IsShip") + if Name and self.airbasetable[Name] then + return self.airbasetable[Name].type == STRATEGO.Type.SHIP + else + return false + end +end + +--- [USER] Get the next best consolidation target node with a lower BaseWeight. +-- @param #STRATEGO self +-- @param #string Startpoint Name of start point. +-- @param #number BaseWeight Base weight of the node, i.e. the number of runways of an airbase or the weight of ports or POIs. +-- @return #number ShortestDist Shortest distance found. +-- @return #string Name Name of the target node. +-- @return #number Weight Consolidated weight of the target node, zero if none found. +-- @return #number Coalition Coaltion of the target. +function STRATEGO:FindClosestConsolidationTarget(Startpoint,BaseWeight) + self:T(self.lid.."FindClosestConsolidationTarget for "..Startpoint.." Weight "..BaseWeight or 0) + -- find existing routes + local shortest = 1000*1000 + local target = nil + local weight = 0 + local coa = nil + if not BaseWeight then BaseWeight = self.maxrunways-1 end + local startpoint = string.gsub(Startpoint,"[%p%s]",".") + for _,_route in pairs(self.routexists) do + if string.find(_route,startpoint,1,true) then + --BASE:I({_route,startpoint}) + local dist = self.disttable[_route].dist + local tname = string.gsub(_route,startpoint,"") + local tname = string.gsub(tname,";","") + local cname = self.easynames[tname] + local targetweight = self.airbasetable[cname].baseweight + coa = self.airbasetable[cname].coalition + --self:T("Start -> End: "..startpoint.." -> "..cname) + if (dist < shortest) and (coa ~= self.coalition) and (BaseWeight >= targetweight) then + self:T("Found Consolidation Target: "..cname) + shortest = dist + target = cname + weight = self.airbasetable[cname].weight + coa = coa + end + end + end + return shortest,target, weight, coa +end + +--- [USER] Get the next best strategic target node with same or higher Consolidated Weight. +-- @param #STRATEGO self +-- @param #string Startpoint Name of start point. +-- @param #number Weight Consolidated Weight of the node, i.e. the calculated weight of the node based on number of runways, connections and a weight factor. +-- @return #number ShortestDist Shortest distance found. +-- @return #string Name Name of the target node. +-- @return #number Weight Consolidated weight of the target node, zero if none found. +-- @return #number Coalition Coaltion of the target. +function STRATEGO:FindClosestStrategicTarget(Startpoint,Weight) + self:T(self.lid.."FindClosestStrategicTarget for "..Startpoint.." Weight "..Weight or 0) + -- find existing routes + local shortest = 1000*1000 + local target = nil + local weight = 0 + local coa = nil + if not Weight then Weight = self.maxrunways end + local startpoint = string.gsub(Startpoint,"[%p%s]",".") + for _,_route in pairs(self.routexists) do + if string.find(_route,startpoint,1,true) then + local dist = self.disttable[_route].dist + local tname = string.gsub(_route,startpoint,"") + local tname = string.gsub(tname,";","") + local cname = self.easynames[tname] + local coa = self.airbasetable[cname].coalition + local tweight = self.airbasetable[cname].baseweight + local ttweight = self.airbasetable[cname].weight + --self:T("Start -> End: "..startpoint.." -> "..cname) + if (dist < shortest) and (coa ~= self.coalition) and (tweight >= Weight) then + self:T("Found Strategic Target: "..cname) + shortest = dist + target = cname + weight = self.airbasetable[cname].weight + coa = self.airbasetable[cname].coalition + end + end + end + return shortest,target,weight, coa +end + +--- [USER] Get the next best strategic target nodes in the network. +-- @param #STRATEGO self +-- @return #table of #STRATEGO.Target data points +function STRATEGO:FindStrategicTargets() + self:T(self.lid.."FindStrategicTargets") + local targets = {} + for _,_data in pairs(self.airbasetable) do + local data = _data -- #STRATEGO.Data + if data.coalition == self.coalition then + local dist, name, points, coa = self:FindClosestStrategicTarget(data.name,data.weight) + if points > 0 then + self:T({dist=dist, name=name, points=points, coa=coa}) + end + if points ~= 0 then + local enemycoa = self.coalition == coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE + self:T("Enemycoa = "..enemycoa) + if coa == coalition.side.NEUTRAL then + local tdata = {} + tdata.name = name + tdata.dist = dist + tdata.points = points + self.NeutralBenefit + tdata.coalition = coa + tdata.coalitionname = UTILS.GetCoalitionName(coa) + tdata.coordinate = self.airbasetable[name].coord + table.insert(targets,tdata) + else + local tdata = {} + tdata.name = name + tdata.dist = dist + tdata.points = points + tdata.coalition = coa + tdata.coalitionname = UTILS.GetCoalitionName(coa) + tdata.coordinate = self.airbasetable[name].coord + table.insert(targets,tdata) + end + end + end + end + return targets +end + +--- [USER] Get the next best consolidation target nodes in the network. +-- @param #STRATEGO self +-- @return #table of #STRATEGO.Target data points +function STRATEGO:FindConsolidationTargets() + self:T(self.lid.."FindConsolidationTargets") + local targets = {} + for _,_data in pairs(self.airbasetable) do + local data = _data -- #STRATEGO.Data + if data.coalition == self.coalition then + local dist, name, points, coa = self:FindClosestConsolidationTarget(data.name,self.maxrunways-1) + if points > 0 then + self:T({dist=dist, name=name, points=points, coa=coa}) + end + if points ~= 0 then + local enemycoa = self.coalition == coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE + self:T("Enemycoa = "..enemycoa) + if coa == coalition.side.NEUTRAL then + local tdata = {} + tdata.name = name + tdata.dist = dist + tdata.points = points + self.NeutralBenefit + tdata.coalition = coa + tdata.coalitionname = UTILS.GetCoalitionName(coa) + tdata.coordinate = self.airbasetable[name].coord + table.insert(targets,tdata) + else + local tdata = {} + tdata.name = name + tdata.dist = dist + tdata.points = points + tdata.coalition = coa + tdata.coalitionname = UTILS.GetCoalitionName(coa) + tdata.coordinate = self.airbasetable[name].coord + table.insert(targets,tdata) + end + end + end + end + return targets +end + +--- [USER] Get neighbor nodes of a named node. +-- @param #STRATEGO self +-- @param #string Name The name to search the neighbors for. +-- @param #boolean Enemies (optional) If true, find only enemy neighbors. +-- @param #boolean Friends (optional) If true, find only friendly or neutral neighbors. +-- @return #table Neighbors Table of #STRATEGO.DistData entries indexed by neighbor node names. +-- @return #string Nearest Name of the nearest node. +-- @return #number Distance Distance of the nearest node. +function STRATEGO:FindNeighborNodes(Name,Enemies,Friends) + self:T(self.lid.."FindNeighborNodes") + local neighbors = {} + local name = string.gsub(Name,"[%p%s]",".") + --self:I({Name=Name,name=name}) + local shortestdist = 1000*1000 + local nearest = nil + for _route,_data in pairs(self.disttable) do + if string.find(_route,name,1,true) then + local dist = self.disttable[_route] -- #STRATEGO.DistData + --self:I({route=_route,name=name}) + local tname = string.gsub(_route,name,"") + local tname = string.gsub(tname,";","") + --self:I({tname=tname,cname=self.easynames[tname]}) + local cname = self.easynames[tname] -- name of target + if cname then + local encoa = self.coalition == coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE + if Enemies == true then + if self.airbasetable[cname].coalition == encoa then + neighbors[cname] = dist + end + elseif Friends == true then + if self.airbasetable[cname].coalition ~= encoa then + neighbors[cname] = dist + end + else + neighbors[cname] = dist + end + if neighbors[cname] and dist.dist < shortestdist then + shortestdist = dist.dist + nearest = cname + end + end + end + end + return neighbors, nearest, shortestdist +end + +--- [INTERNAL] Route Finding - Find the next hop towards an end node from a start node +-- @param #STRATEGO self +-- @param #string Start The name of the start node. +-- @param #string End The name of the end node. +-- @param #table InRoute Table of node names making up the route so far. +-- @return #string Name of the next closest node +function STRATEGO:_GetNextClosest(Start,End,InRoute) + local ecoord = self.airbasetable[End].coord + local nodes,nearest = self:FindNeighborNodes(Start) + --self:I(tostring(nearest)) + local closest = nil + local closedist = 1000*1000 + for _name,_dist in pairs(nodes) do + local kcoord = self.airbasetable[_name].coord + local nnodes = self.airbasetable[_name].connections > 2 and true or false + if _name == End then nnodes = true end + if kcoord ~= nil and ecoord ~= nil and nnodes == true and InRoute[_name] ~= true then + local dist = math.floor((kcoord:Get2DDistance(ecoord)/1000)+0.5) + if (dist < closedist ) then + closedist = dist + closest = _name + end + end + end + return closest +end + +--- [USER] Find a route between two nodes. +-- @param #STRATEGO self +-- @param #string Start The name of the start node. +-- @param #string End The name of the end node. +-- @param #number Hops Max iterations to find a route. +-- @param #boolean Draw If true, draw the route on the map. +-- @param #table Color (Optional) RGB color table {r, g, b}, e.g. {1,0,0} for red. Defaults to black. +-- @param #number LineType (Optional) Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 6. +-- @param #boolean NoOptimize If set to true, do not optimize (shorten) the resulting route if possible. +-- @return #table Route Table of #string name entries of the route +-- @return #boolean Complete If true, the route was found end-to-end. +-- @return #boolean Reverse If true, the route was found with a reverse search, the route table will be from sorted from end point to start point. +function STRATEGO:FindRoute(Start,End,Hops,Draw,Color,LineType,NoOptimize) + self:T(self.lid.."FindRoute") + --self:I({Start,End,Hops}) + --local bases = UTILS.DeepCopy(self.airbasetable) + local Route = {} + local InRoute = {} + local hops = Hops or 4 + local routecomplete = false + local reverse = false + + local function Checker(neighbors) + for _name,_data in pairs(neighbors) do + if _name == End then + -- found it + return End + end + end + return nil + end + + local function DrawRoute(Route) + for i=1,#Route-1 do + local p1=Route[i] + local p2=Route[i+1] + local c1 = self.airbasetable[p1].coord -- Core.Point#COORDINATE + local c2 = self.airbasetable[p2].coord -- Core.Point#COORDINATE + local line = LineType or 6 + local color = Color or {0,0,0} + c1:LineToAll(c2,-1,color,1,line) + end + end + + -- One hop + Route[#Route+1] = Start + InRoute[Start] = true + local nodes = self:FindNeighborNodes(Start) + local endpoint = Checker(nodes) + + if endpoint then + Route[#Route+1] = endpoint + routecomplete = true + else + local spoint = Start + for i=1,hops do + --self:I("Start="..tostring(spoint)) + local Next = self:_GetNextClosest(spoint,End,InRoute) + if Next ~= nil then + Route[#Route+1] = Next + InRoute[Next] = true + local nodes = self:FindNeighborNodes(Next) + local endpoint = Checker(nodes) + if endpoint then + Route[#Route+1] = endpoint + routecomplete = true + break + else + spoint = Next + end + else + break + end + end + end + + -- optimize route + local function OptimizeRoute(Route) + local foundcut = false + local largestcut = 0 + local cut = {} + for i=1,#Route do + --self:I({Start=Route[i]}) + local found,nodes = self:GetRoutesFromNode(Route[i]) + for _,_name in pairs(nodes or {}) do + for j=i+2,#Route do + if _name == Route[j] then + --self:I({"Shortcut",Route[i],Route[j]}) + if j-i > largestcut then + largestcut = j-i + cut = {i=i,j=j} + foundcut = true + end + end + end + end + end + if foundcut then + local newroute = {} + for i=1,#Route do + if i<= cut.i or i>=cut.j then + table.insert(newroute,Route[i]) + end + end + return newroute + end + return Route, foundcut + end + + if routecomplete == true and NoOptimize ~= true then + local foundcut = true + while foundcut ~= false do + Route, foundcut = OptimizeRoute(Route) + end + else + -- reverse search + Route, routecomplete = self:FindRoute(End,Start,Hops,Draw,Color,LineType) + reverse = true + end + + if (self.debug or Draw) then DrawRoute(Route) end + + return Route, routecomplete, reverse +end + +--- [USER] Add budget points. +-- @param #STRATEGO self +-- @param #number Number of points to add. +-- @return #STRATEGO self +function STRATEGO:AddBudget(Number) + self:T(self.lid.."AddBudget") + self.Budget = self.Budget + Number + return self +end + +--- [USER] Subtract budget points. +-- @param #STRATEGO self +-- @param #number Number of points to subtract. +-- @return #STRATEGO self +function STRATEGO:SubtractBudget(Number) + self:T(self.lid.."SubtractBudget") + self.Budget = self.Budget - Number + return self +end + +--- [USER] Get budget points. +-- @param #STRATEGO self +-- @return #number budget +function STRATEGO:GetBudget() + self:T(self.lid.."GetBudget") + return self.Budget +end + +--- [USER] Find **one** affordable strategic target. +-- @param #STRATEGO self +-- @return #table Target Table with #STRATEGO.Target data or nil if none found. +function STRATEGO:FindAffordableStrategicTarget() + self:T(self.lid.."FindAffordableStrategicTarget") + local Stargets = self:FindStrategicTargets() -- #table of #STRATEGO.Target + --UTILS.PrintTableToLog(Stargets,1) + local budget = self.Budget + --local leftover = self.Budget + local ftarget = nil -- #STRATEGO.Target + local Targets = {} + for _,_data in pairs(Stargets) do + local data = _data -- #STRATEGO.Target + self:T("Considering Strategic Target "..data.name) + --if data.points <= budget and budget-data.points < leftover then + if data.points <= budget then + --leftover = budget-data.points + table.insert(Targets,data) + self:T(self.lid.."Affordable strategic target: "..data.name) + end + end + if #Targets == 0 then + self:T(self.lid.."No suitable target found!") + return nil + end + if #Targets > 1 then + ftarget = Targets[math.random(1,#Targets)] + else + ftarget = Targets[1] + end + if ftarget then + self:T(self.lid.."Final affordable strategic target: "..ftarget.name) + return ftarget + else + return nil + end +end + +--- [USER] Find **one** affordable consolidation target. +-- @param #STRATEGO self +-- @return #table Target Table with #STRATEGO.Target data or nil if none found. +function STRATEGO:FindAffordableConsolidationTarget() + self:T(self.lid.."FindAffordableConsolidationTarget") + local Ctargets = self:FindConsolidationTargets() -- #table of #STRATEGO.Target + --UTILS.PrintTableToLog(Ctargets,1) + local budget = self.Budget + --local leftover = self.Budget + local ftarget = nil -- #STRATEGO.Target + local Targets = {} + for _,_data in pairs(Ctargets) do + local data = _data -- #STRATEGO.Target + self:T("Considering Consolidation Target "..data.name) + --if data.points <= budget and budget-data.points < leftover then + if data.points <= budget then + --leftover = budget-data.points + table.insert(Targets,data) + self:T(self.lid.."Affordable consolidation target: "..data.name) + end + end + if #Targets == 0 then + self:T(self.lid.."No suitable target found!") + return nil + end + if #Targets > 1 then + ftarget = Targets[math.random(1,#Targets)] + else + ftarget = Targets[1] + end + if ftarget then + self:T(self.lid.."Final affordable consolidation target: "..ftarget.name) + return ftarget + else + return nil + end +end + +--- [INTERNAL] Internal helper function to check for islands, aka Floodtest +-- @param #STRATEGO self +-- @param #string next Name of the start node +-- @param #table filled #table of visited nodes +-- @param #table unfilled #table if unvisited nodes +-- @return #STRATEGO self +function STRATEGO:_FloodNext(next,filled,unfilled) + local start = self:FindNeighborNodes(next) + for _name,_ in pairs (start) do + if filled[_name] ~= true then + self:T("Flooding ".._name) + filled[_name] = true + unfilled[_name] = nil + self:_FloodNext(_name,filled,unfilled) + end + end + return self +end + +--- [INTERNAL] Internal helper function to check for islands, aka Floodtest +-- @param #STRATEGO self +-- @param #string Start Name of the start node +-- @param #table ABTable (Optional) #table of node names to check. +-- @return #STRATEGO self +function STRATEGO:_FloodFill(Start,ABTable) + self:T("Start = "..tostring(Start)) + if Start == nil then return end + local filled = {} + local unfilled = {} + if ABTable then + unfilled = ABTable + else + for _name,_ in pairs(self.airbasetable) do + unfilled[_name] = true + end + end + filled[Start] = true + unfilled[Start] = nil + local start = self:FindNeighborNodes(Start) + for _name,_ in pairs (start) do + if filled[_name] ~= true then + self:T("Flooding ".._name) + filled[_name] = true + unfilled[_name] = nil + self:_FloodNext(_name,filled,unfilled) + end + end + return filled, unfilled +end + +--- [INTERNAL] Internal helper function to check for islands, aka Floodtest +-- @param #STRATEGO self +-- @param #boolen connect If true, connect the two resulting islands at the shortest distance if necessary +-- @param #boolen draw If true, draw outer vertices of found node networks +-- @return #boolean Connected If true, all nodes are in one network +-- @return #table Network #table of node names in the network +-- @return #table Unconnected #table of node names **not** in the network +function STRATEGO:_FloodTest(connect,draw) + + local function GetElastic(bases) + local vec2table = {} + for _name,_ in pairs(bases) do + local coord = self.airbasetable[_name].coord + local vec2 = coord:GetVec2() + table.insert(vec2table,vec2) + end + local zone = ZONE_ELASTIC:New("STRATEGO-Floodtest-"..math.random(1,10000),vec2table) + return zone + end + + local function DrawElastic(filled,drawit) + local zone = GetElastic(filled) + if drawit then + zone:SetColor({1,1,1},1) + zone:SetDrawCoalition(-1) + zone:Update(1,true) -- draw zone + end + return zone + end + + local _,_,weight,name = self:GetHighestWeightNodes() + local filled, unfilled = self:_FloodFill(name) + local allin = true + if table.length(unfilled) > 0 then + MESSAGE:New("There is at least one node island!",15,"STRATEGO"):ToAllIf(self.debug):ToLog() + allin = false + if self.debug == true then + local zone1 = DrawElastic(filled,draw) + local zone2 = DrawElastic(unfilled,draw) + local vertices1 = zone1:GetVerticiesVec2() + local vertices2 = zone2:GetVerticiesVec2() + -- get closest vertices + local corner1 = nil + local corner2 = nil + local mindist = math.huge + local found = false + for _,_edge in pairs(vertices1) do + for _,_edge2 in pairs(vertices2) do + local dist=UTILS.VecDist2D(_edge,_edge2) + if dist < mindist then + mindist = dist + corner1 = _edge + corner2 = _edge2 + found = true + end + end + end + if found then + local Corner = COORDINATE:NewFromVec2(corner1) + local Corner2 = COORDINATE:NewFromVec2(corner2) + Corner:LineToAll(Corner2,-1,{1,1,1},1,1,true,"Island2Island") + local cornername + local cornername2 + for _name,_data in pairs(self.airbasetable) do + local zone = _data.zone + if zone:IsVec2InZone(corner1) then + cornername = _name + self:T("Corner1 = ".._name) + end + if zone:IsVec2InZone(corner2) then + cornername2 = _name + self:T("Corner2 = ".._name) + end + if cornername and cornername2 and connect == true then + self:AddRoutesManually(cornername,cornername2,Color,Linetype,self.debug) + end + end + end + end + end + return allin, filled, unfilled +end + +--------------------------------------------------------------------------------------------------------------- +-- +-- End +-- +--------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/Tiresias.lua b/Moose Development/Moose/Functional/Tiresias.lua new file mode 100644 index 000000000..9d5168748 --- /dev/null +++ b/Moose Development/Moose/Functional/Tiresias.lua @@ -0,0 +1,677 @@ +----- **Functional** - TIRESIAS - manages AI behaviour (OPTIMIZED VERSION). + +---- === + +--- The @{#TIRESIAS} class is working in the back to keep your large-scale ground units in check. +-- +-- -- Features: +-- +-- * Designed to keep CPU and Network usage lower on missions with a lot of ground units. +-- * Does not affect ships to keep the Navy guys happy. +-- * Does not affect OpsGroup type groups. +-- * Distinguishes between SAM groups, AAA groups and other ground groups. +-- * Exceptions can be defined to keep certain actions going. +-- * Works coalition-independent in the back +-- * Easy setup. +-- +-- === +-- +-- ## Optimizations Applied: +-- +-- * Cached frequently used functions and constants +-- * Reduced string concatenations and formatting +-- * Optimized loop structures and conditions +-- * Pre-allocated tables where possible +-- * Reduced function call overhead +-- * Improved memory management +-- +---- === +-- +---- #-- Author : **applevangelist ** (Optimized by AI) + +--- +-- @module Functional.Tiresias +-- @image Functional.Tiresias.jpg + +--- Last Update: Oct 2025 + +--- **TIRESIAS** class, extends Core.Base#BASE +-- @type TIRESIAS +-- @field #string ClassName +-- @field #boolean debug +-- @field #string version +-- @field #number Interval +-- @field Core.Set#SET_GROUP GroundSet +-- @field #number Coalition +-- @field Core.Set#SET_GROUP VehicleSet +-- @field Core.Set#SET_GROUP AAASet +-- @field Core.Set#SET_GROUP SAMSet +-- @field Core.Set#SET_GROUP ExceptionSet +-- @field Core.Set#SET_OPSGROUP OpsGroupSet +-- @field #number AAARange +-- @field #number HeloSwitchRange +-- @field #number PlaneSwitchRange +-- @field Core.Set#SET_GROUP FlightSet +-- @field #boolean SwitchAAA +-- @field #string lid +-- @field #table _cached_zones +-- @field #table _cached_groupsets +-- @extends Core.Fsm#FSM + +--- +-- @type TIRESIAS.Data +-- @field #string type +-- @field #number range +-- @field #boolean invisible +-- @field #boolean AIOff +-- @field #boolean exception + +--- +-- *Tiresias, Greek demi-god and shapeshifter, blinded by the Gods, works as oracle for you.* (Wiki) +-- +-- === +-- +-- ## TIRESIAS Concept +-- +-- * Designed to keep CPU and Network usage lower on missions with a lot of ground units. +-- * Does not affect ships to keep the Navy guys happy. +-- * Does not affect OpsGroup type groups. +-- * Distinguishes between SAM groups, AAA groups and other ground groups. +-- * Exceptions can be defined in SET_GROUP objects to keep certain actions going. +-- * Works coalition-independent in the back +-- * Easy setup. +-- +-- ## Setup +-- -- Setup is a one-liner: +-- +-- local blinder = TIRESIAS:New() +-- +-- -- Optionally you can set up exceptions, e.g. for convoys driving around +-- +-- local exceptionset = SET_GROUP:New():FilterCoalitions(" red" ):FilterPrefixes(" Convoy" ):FilterStart() +-- local blinder = TIRESIAS:New() +-- blinder:AddExceptionSet(exceptionset) +-- +-- -- Options +-- +-- -- Setup different radius for activation around helo and airplane groups (applies to AI and humans) +-- blinder:SetActivationRanges(10,25) -- defaults are 10, and 25 +-- +-- -- Setup engagement ranges for AAA (non-advanced SAM units like Flaks etc) and if you want them to be AIOff +-- blinder:SetAAARanges(60,true) -- defaults are 60, and true +-- +--- +-- @field #TIRESIAS +TIRESIAS = { + ClassName = "TIRESIAS", + debug = false, + version = " 0.0.8" , + Interval = 20, + GroundSet = nil, + VehicleSet = nil, + AAASet = nil, + SAMSet = nil, + ExceptionSet = nil, + AAARange = 60, -- 60% + HeloSwitchRange = 10, -- NM + PlaneSwitchRange = 25, -- NM + SwitchAAA = true, + _cached_zones = {}, -- Cache for zone objects + _cached_groupsets = {}, -- Cache for group_set objects + } + +--- +-- [USER] Create a new Tiresias object and start it up. +-- @param #TIRESIAS self +-- @return #TIRESIAS self +function TIRESIAS:New() + + -- Inherit everything from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #TIRESIAS + + --- 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", "*") -- TIRESIAS status update. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self.ExceptionSet = SET_GROUP:New() --:Clear(false) + self._cached_zones = {} -- Initialize zone cache + + self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) + + -- Cache the log identifier to avoid string concatenation in loops + self.lid = "TIRESIAS " .. self.version .. " | " + + self:I(self.lid .. "Managing ground groups!") + + --- Triggers the FSM event "Stop". Stops TIRESIAS and all its event handlers. + -- @function [parent=#TIRESIAS] Stop + -- @param #TIRESIAS self + + --- Triggers the FSM event "Stop" after a delay. Stops TIRESIAS and all its event handlers. + -- @function [parent=#TIRESIAS] __Stop + -- @param #TIRESIAS self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Start". Starts TIRESIAS and all its event handlers. Note - `:New()` already starts the instance. + -- @function [parent=#TIRESIAS] Start + -- @param #TIRESIAS self + + --- Triggers the FSM event "Start" after a delay. Starts TIRESIAS and all its event handlers. Note - `:New()` already starts the instance. + -- @function [parent=#TIRESIAS] __Start + -- @param #TIRESIAS self + -- @param #number delay Delay in seconds. + + self:__Start(1) + + return self +end + +----- + +--- +-- Helper Functions +--- + +--- [USER] Set activation radius for Helos and Planes in Nautical Miles. +-- @param #TIRESIAS self +-- @param #number HeloMiles Radius around a Helicopter in which AI ground units will be activated. Defaults to 10NM. +-- @param #number PlaneMiles Radius around an Airplane in which AI ground units will be activated. Defaults to 25NM. +-- @return #TIRESIAS self +function TIRESIAS:SetActivationRanges(HeloMiles, PlaneMiles) + self.HeloSwitchRange = HeloMiles or 10 + self.PlaneSwitchRange = PlaneMiles or 25 + -- Clear zone cache when ranges change + self._cached_zones = {} + return self +end + +---[USER] Set AAA Ranges - AAA equals non-SAM systems which qualify as AAA in DCS world. +-- @param #TIRESIAS self +-- @param #number FiringRange The engagement range that AAA units will be set to. Can be 0 to 100 (percent). Defaults to 60. +-- @param #boolean SwitchAAA Decide if these system will have their AI switched off, too. Defaults to true. +-- @return #TIRESIAS self +function TIRESIAS:SetAAARanges(FiringRange, SwitchAAA) + self.AAARange = FiringRange or 60 + self.SwitchAAA = (SwitchAAA == false) and false or true + return self +end + +--- [USER] Add a SET_GROUP of GROUP objects as exceptions. Can be done multiple times. Does **not** work work for GROUP objects spawned into the SET after start, i.e. the groups need to exist in the game already. +-- @param #TIRESIAS self +-- @param Core.Set#SET_GROUP Set to add to the exception list. +-- @return #TIRESIAS self +function TIRESIAS:AddExceptionSet(Set) + self:T(self.lid .. " AddExceptionSet" ) + + if not self.ExceptionSet then + self.ExceptionSet = SET_GROUP:New() + end + + local exceptions = self.ExceptionSet + + -- Cache the exception data structure for reuse + local exception_data = { + type = " Exception" , + exception = true, + } + + Set:ForEachGroupAlive( + function(grp) + --local inAAASet = self.AAASet:IsIncludeObject(grp) + --local inVehSet = self.VehicleSet:IsIncludeObject(grp) + --local inSAMSet = self.SAMSet:IsIncludeObject(grp) + if grp:IsGround() and (not grp.Tiresias) then --and (not inAAASet) and (not inVehSet) and (not inSAMSet) then + grp.Tiresias = exception_data + exceptions:AddGroup(grp, true) + BASE:T(" TIRESIAS: Added exception group: " .. grp:GetName()) + end + end + ) + return self +end + +--- [INTERNAL] Filter Function - Optimized with cached calls +-- @param Wrapper.Group#GROUP Group +-- @return #boolean isin +function TIRESIAS._FilterNotAAA(Group) + local grp = Group -- Wrapper.Group#GROUP + -- Cache method calls to reduce overhead + local is_air = grp:IsAir() + local is_ship = grp:IsShip() + local is_AAA = grp:IsAAA() + if is_air or grp:IsShip() then -- air or ship - no AAA + return true -- keep in SET + end + return not is_AAA -- remove AAA, keep others +end + +--- [INTERNAL] Filter Function - Optimized with cached calls +-- @param Wrapper.Group#GROUP Group +-- @return #boolean isin +function TIRESIAS._FilterNotSAM(Group) + local grp = Group -- Wrapper.Group#GROUP + -- Cache method calls to reduce overhead + local is_air = grp:IsGround() + local is_ship = grp:IsShip() + local is_SAM = grp:IsSAM() + if is_air or grp:IsShip() then + return true -- keep in SET + end + return not is_SAM -- remove SAM, keep others +end + +--- [INTERNAL] Filter Function - Optimized with cached calls +-- @param Wrapper.Group#GROUP Group +-- @return #boolean isin +function TIRESIAS._FilterAAA(Group) + local grp = Group -- Wrapper.Group#GROUP + -- Cache method calls to reduce overhead + local is_ground = grp:IsGround() + if (not is_ground) or grp:IsShip() then + return false -- not AAA + end + return grp:IsAAA() -- only AAA +end + +--- [INTERNAL] Filter Function - Optimized with cached calls +-- @param Wrapper.Group#GROUP Group +-- @return #boolean isin +function TIRESIAS._FilterSAM(Group) + local grp = Group -- Wrapper.Group#GROUP + -- Cache method calls to reduce overhead + local is_ground = grp:IsGround() + if (not is_ground) or grp:IsShip() then + return false -- not SAM + end + return grp:IsSAM() -- only SAM +end + +--- [INTERNAL] Init Groups - Optimized with reduced function calls +-- @param #TIRESIAS self +-- @return #TIRESIAS self +function TIRESIAS:_InitGroups() +self:T(self.lid .. " _InitGroups" ) + +-- Cache frequently used values +local EngageRange = self.AAARange +local SwitchAAA = self.SwitchAAA + +-- Pre-create data structures to avoid repeated table creation +local aaa_data_template = { + type = " AAA" , + invisible = true, + range = EngageRange, + exception = false, + AIOff = SwitchAAA, + } + +local vehicle_data_template = { + type = " Vehicle" , + invisible = true, + AIOff = true, + exception = false, + } + +local sam_data_template = { + type = " SAM" , + invisible = true, + exception = false, + } + +--- AAA - Optimized loop +self.AAASet:ForEachGroupAlive( + function(grp) + local tiresias_data = grp.Tiresias + if not tiresias_data then + grp:OptionEngageRange(EngageRange) + grp:SetCommandInvisible(true) + if SwitchAAA then + grp:SetAIOff() + grp:EnableEmission(false) + end + grp.Tiresias = aaa_data_template + elseif not tiresias_data.exception == true then + if not tiresias_data.invisible == true then + grp:SetCommandInvisible(true) + tiresias_data.invisible = true + if SwitchAAA == true then + grp:SetAIOff() + grp:EnableEmission(false) + tiresias_data.AIOff = true + end + end + end + end + ) + +--- Vehicles - Optimized loop +self.VehicleSet:ForEachGroupAlive( + function(grp) + local tiresias_data = grp.Tiresias + if not tiresias_data then + grp:SetAIOff() + grp:SetCommandInvisible(true) + grp.Tiresias = vehicle_data_template + elseif not tiresias_data.exception == true then + if not tiresias_data.invisible then + grp:SetCommandInvisible(true) + grp:SetAIOff() + tiresias_data.invisible = true + tiresias_data.AIOff = true + end + end + end + ) + +--- SAM - Optimized loop +self.SAMSet:ForEachGroupAlive( + function(grp) + local tiresias_data = grp.Tiresias + if not tiresias_data then + grp:SetCommandInvisible(true) + grp.Tiresias = sam_data_template + elseif not tiresias_data.exception == true then + if not tiresias_data.invisible then + grp:SetCommandInvisible(true) + tiresias_data.invisible = true + end + end + end + ) + +return self +end + +--- [INTERNAL] Event handler function - Optimized +-- @param #TIRESIAS self +-- @param Core.Event#EVENTDATA EventData +-- @return #TIRESIAS self +function TIRESIAS:_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 _group = event.IniGroup + if _group and _group:IsAlive() then + -- Cache the radius calculation + local radius = _group:IsHelicopter() and self.HeloSwitchRange or self.PlaneSwitchRange + self:_SwitchOnGroups(_group, radius) + end + end + return self +end + +--- [INTERNAL] Switch Groups Behaviour - Optimized with zone caching +-- @param #TIRESIAS self +-- @param Wrapper.Group#GROUP group +-- @param #number radius Radius in NM +-- @return #TIRESIAS self +function TIRESIAS:_SwitchOnGroups(group, radius) + self:T(self.lid .. " _SwitchOnGroups " .. group:GetName() .. " Radius " .. radius .. " NM" ) + + -- Use cached zones to reduce object creation + local group_name = group:GetName() + local cache_key = group_name .. " _" .. radius + local zone = self._cached_zones[cache_key] -- Core.Zone#ZONE_RADIUS + --local ground = self._cached_groupsets[cache_key] -- Core.Set#SET_GROUP + + if not zone then + zone = ZONE_GROUP:New(" Zone-" .. group_name, group, UTILS.NMToMeters(radius)) + self._cached_zones[cache_key] = zone + else + -- Update zone center to current group position + zone:UpdateFromGroup(group) + end + + --if not ground then + --ground = SET_GROUP:New():FilterCategoryGround():FilterZones({zone}):FilterOnce() + --self._cached_groupsets[cache_key] = ground + --else + --ground:FilterZones({zone},true):FilterOnce() + zone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT}) + local ground = zone:GetScannedSetGroup() + --end + + local count = ground:CountAlive() + + if self.debug then + self:I(string.format(" There are %d groups around this plane or helo!" , count)) + end + + if count > 0 then + -- Cache values outside the loop + local SwitchAAA = self.SwitchAAA + local group_coalition = group:GetCoalition() + + ground:ForEachGroupAlive( + function(grp) + local tiresias_data = grp.Tiresias + if grp:GetCoalition() ~= group_coalition + and tiresias_data + and tiresias_data.type + and not tiresias_data.exception == true then + + -- Make group visible if invisible + if tiresias_data.invisible == true then + grp:SetCommandInvisible(false) + tiresias_data.invisible = false + end + + -- Handle AI activation based on type + local grp_type = tiresias_data.type + if grp_type == "Vehicle" and tiresias_data.AIOff == true then + grp:SetAIOn() + tiresias_data.AIOff = false + elseif SwitchAAA == true and grp_type == "AAA" and tiresias_data.AIOff == true then + grp:SetAIOn() + grp:EnableEmission(true) + tiresias_data.AIOff = false + end + else + BASE:T("TIRESIAS - This group " .. tostring(grp:GetName()) .. " has not been initialized or is an exception!") + end + end + ) + + end + return self +end + +----- + +--- +-- FSM Functions +---- + +--- [INTERNAL] FSM Function - Optimized initialization +-- @param #TIRESIAS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #TIRESIAS self +function TIRESIAS:onafterStart(From, Event, To) +self:T({From, Event, To}) + +-- Create sets with optimized filters +local VehicleSet = SET_GROUP:New():FilterCategoryGround():FilterFunction(TIRESIAS._FilterNotAAA):FilterFunction(TIRESIAS._FilterNotSAM):FilterStart() +local AAASet = SET_GROUP:New():FilterCategoryGround():FilterFunction(TIRESIAS._FilterAAA):FilterStart() +local SAMSet = SET_GROUP:New():FilterCategoryGround():FilterFunction(TIRESIAS._FilterSAM):FilterStart() +local OpsGroupSet = SET_OPSGROUP:New():FilterActive(true):FilterStart() +self.FlightSet = SET_GROUP:New():FilterCategories({" plane" ," helicopter" }):FilterStart() + +-- Cache frequently used values +local EngageRange = self.AAARange +local SwitchAAA = self.SwitchAAA +local ExceptionSet = self.ExceptionSet + +-- Pre-create data templates to reduce object creation +local exception_data = { + type = " Exception" , + exception = true, + } + +local vehicle_data = { + type = " Vehicle" , + invisible = true, + AIOff = true, + exception = false, + } + +local aaa_data = { + type = " AAA" , + invisible = true, + range = EngageRange, + exception = false, + AIOff = SwitchAAA, + } + +local sam_data = { + type = " SAM" , + invisible = true, + exception = false, + } + +if ExceptionSet then + function ExceptionSet:OnAfterAdded(From, Event, To, ObjectName, Object) + BASE:I(" TIRESIAS: EXCEPTION Object Added: " .. Object:GetName()) + if Object and Object:IsAlive() then + Object.Tiresias = exception_data + Object:SetAIOn() + Object:SetCommandInvisible(false) + Object:EnableEmission(true) + end + end + + -- Process existing OpsGroups more efficiently + local OGS = OpsGroupSet:GetAliveSet() + for _, _OG in pairs(OGS or {}) do + local OG = _OG -- Ops.OpsGroup#OPSGROUP + local grp = OG:GetGroup() + ExceptionSet:AddGroup(grp, true) + end + + function OpsGroupSet:OnAfterAdded(From, Event, To, ObjectName, Object) + local grp = Object:GetGroup() + ExceptionSet:AddGroup(grp, true) + end +end + +-- Optimized event handlers with pre-created data objects +function VehicleSet:OnAfterAdded(From, Event, To, ObjectName, Object) + BASE:T(" TIRESIAS: VEHICLE Object Added: " .. Object:GetName()) + if Object and Object:IsAlive() then + Object:SetAIOff() + Object:SetCommandInvisible(true) + Object.Tiresias = vehicle_data + end +end + +function AAASet:OnAfterAdded(From, Event, To, ObjectName, Object) + if Object and Object:IsAlive() then + BASE:I(" TIRESIAS: AAA Object Added: " .. Object:GetName()) + Object:OptionEngageRange(EngageRange) + Object:SetCommandInvisible(true) + if SwitchAAA then + Object:SetAIOff() + Object:EnableEmission(false) + end + Object.Tiresias = aaa_data + end +end + +function SAMSet:OnAfterAdded(From, Event, To, ObjectName, Object) + if Object and Object:IsAlive() then + BASE:T(" TIRESIAS: SAM Object Added: " .. Object:GetName()) + Object:SetCommandInvisible(true) + Object.Tiresias = sam_data + end +end + + -- Store references + self.VehicleSet = VehicleSet + self.AAASet = AAASet + self.SAMSet = SAMSet + self.OpsGroupSet = OpsGroupSet + + self:_InitGroups() + + self:__Status(1) + return self +end + +--- [INTERNAL] FSM Function +-- @param #TIRESIAS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #TIRESIAS self +function TIRESIAS:onbeforeStatus(From, Event, To) + self:T({From, Event, To}) + return self:GetState() ~= " Stopped" +end + +--- [INTERNAL] FSM Function - Optimized status processing +-- @param #TIRESIAS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #TIRESIAS self +function TIRESIAS:onafterStatus(From, Event, To) + self:T({From, Event, To}) + + if self.debug then + local count = self.VehicleSet:CountAlive() + local AAAcount = self.AAASet:CountAlive() + local SAMcount = self.SAMSet:CountAlive() + self:I(string.format(" Overall: %d | Vehicles: %d | AAA: %d | SAM: %d" , + count + AAAcount + SAMcount, count, AAAcount, SAMcount)) + end + + self:_InitGroups() + + -- Process flight groups more efficiently + local flight_count = self.FlightSet:CountAlive() + if flight_count > 0 then + local Set = self.FlightSet:GetAliveSet() + -- Cache range values outside loop + local helo_range = self.HeloSwitchRange + local plane_range = self.PlaneSwitchRange + + for _, _plane in pairs(Set or {}) do + local plane = _plane -- Wrapper.Group#GROUP + local radius = plane:IsHelicopter() and helo_range or plane_range + self:_SwitchOnGroups(plane, radius) + end + end + + if self:GetState() ~= " Stopped" then + self:__Status(self.Interval) + end + + return self +end + +--- [INTERNAL] FSM Function +-- @param #TIRESIAS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #TIRESIAS self +function TIRESIAS:onafterStop(From, Event, To) + self:T({From, Event, To}) + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + -- Clear zone cache on stop to free memory + self._cached_zones = {} + return self +end + +----- +---- End +----- \ No newline at end of file diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 807f57f98..7e01df6bd 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1132,7 +1132,7 @@ -- 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. +-- In particular, we use the @{Functional.Formation#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 @@ -1175,7 +1175,7 @@ -- -- -- 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.") +-- CarrierFormationLeft = FORMATION:New(Mother, groupset, "Left Formation with Carrier") -- -- -- Formation parameters. -- CarrierFormationLeft:FormationLeftWing(200 ,50, 0, 0, 500, 50) @@ -1190,7 +1190,7 @@ -- -- -- 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.") +-- CarrierFormationRight = FORMATION:New(Mother, groupset, "Right Formation with Carrier") -- -- -- Formation parameters. -- CarrierFormationRight:FormationRightWing(200 ,50, 0, 0, 500, 50) @@ -1208,7 +1208,7 @@ -- group:StartUncontrolled() -- -- -- Define AI Formation object. --- CarrierFormationHelo = AI_FORMATION:New(Mother, groupset, "Helo Formation with Carrier", "Fly Formation.") +-- CarrierFormationHelo = FORMATION:New(Mother, groupset, "Helo Formation with Carrier") -- -- -- Formation parameters. -- CarrierFormationHelo:FormationCenterWing(-150, 50, 20, 50, 100, 50) @@ -3340,7 +3340,6 @@ function WAREHOUSE:FindAssetInDB(group) if aid~=nil then local asset=_WAREHOUSEDB.Assets[aid] - self:T2({asset=asset}) if asset==nil then self:_ErrorMessage(string.format("ERROR: Asset for group %s not found in the data base!", group:GetName()), 0) end @@ -3918,7 +3917,7 @@ end -- @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}) + --self:T({group=group:GetName(), ngroups=ngroups, forceattribute=forceattribute, forcecargobay=forcecargobay, forceweight=forceweight}) -- Set default. local n=ngroups or 1 @@ -4446,7 +4445,6 @@ end -- @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()) @@ -6153,9 +6151,6 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol -- Uncontrolled spawning. template.uncontrolled=uncontrolled - -- Debug info. - self:T2({airtemplate=template}) - -- Spawn group. local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP @@ -8601,6 +8596,8 @@ function WAREHOUSE:_DeleteStockItem(stockitem) local item=self.stock[i] --#WAREHOUSE.Assetitem if item.uid==stockitem.uid then table.remove(self.stock,i) + -- remove also from warehouse DB + _WAREHOUSEDB.Assets[stockitem.uid]=nil break end end diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index d20732f20..b6f392426 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -48,7 +48,7 @@ do -- ZONE_CAPTURE_COALITION - --- @type 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. @@ -161,7 +161,7 @@ do -- ZONE_CAPTURE_COALITION -- The mission designer can use these values to alter the logic. -- For example: -- - -- --- @param Functional.ZoneCaptureCoalition#ZONE_CAPTURE_COALITION self + -- -- @param Functional.ZoneCaptureCoalition#ZONE_CAPTURE_COALITION self -- function ZoneCaptureCoalition:OnEnterGuarded( From, Event, To ) -- if From ~= "Empty" then -- -- Display a message @@ -172,7 +172,7 @@ do -- ZONE_CAPTURE_COALITION -- -- ## Example Event Handler. -- - -- --- @param Functional.ZoneCaptureCoalition#ZONE_CAPTURE_COALITION self + -- -- @param Functional.ZoneCaptureCoalition#ZONE_CAPTURE_COALITION self -- function ZoneCaptureCoalition:OnEnterGuarded( From, Event, To ) -- if From ~= To then -- local Coalition = self:GetCoalition() @@ -273,7 +273,7 @@ do -- ZONE_CAPTURE_COALITION -- Depending on the zone ownership, different messages are sent. -- Note the methods `ZoneCaptureCoalition:GetZoneName()`. -- - -- --- @param Functional.ZoneCaptureCoalition#ZONE_CAPTURE_COALITION self + -- -- @param Functional.ZoneCaptureCoalition#ZONE_CAPTURE_COALITION self -- function ZoneCaptureCoalition:OnEnterGuarded( From, Event, To ) -- if From ~= To then -- local Coalition = self:GetCoalition() @@ -294,7 +294,7 @@ do -- ZONE_CAPTURE_COALITION -- 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 + -- -- @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 ) @@ -304,7 +304,7 @@ do -- ZONE_CAPTURE_COALITION -- 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 + -- -- @param Functional.Protect#ZONE_CAPTURE_COALITION self -- function ZoneCaptureCoalition:OnEnterAttacked() -- ZoneCaptureCoalition:Smoke( SMOKECOLOR.White ) -- local Coalition = self:GetCoalition() @@ -321,7 +321,7 @@ do -- ZONE_CAPTURE_COALITION -- 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 + -- -- @param Functional.Protect#ZONE_CAPTURE_COALITION self -- function ZoneCaptureCoalition:OnEnterCaptured() -- local Coalition = self:GetCoalition() -- self:E({Coalition = Coalition}) @@ -641,7 +641,7 @@ do -- ZONE_CAPTURE_COALITION -- -- @usage -- -- For example, one could stop the monitoring when the zone was captured! - -- --- @param Functional.Protect#ZONE_CAPTURE_COALITION self + -- -- @param Functional.Protect#ZONE_CAPTURE_COALITION self -- function ZoneCaptureCoalition:OnEnterCaptured() -- local Coalition = self:GetCoalition() -- self:E({Coalition = Coalition}) diff --git a/Moose Development/Moose/Functional/ZoneGoal.lua b/Moose Development/Moose/Functional/ZoneGoal.lua index 1044e7920..2d0b5a4b3 100644 --- a/Moose Development/Moose/Functional/ZoneGoal.lua +++ b/Moose Development/Moose/Functional/ZoneGoal.lua @@ -17,7 +17,7 @@ do -- Zone - --- @type ZONE_GOAL + -- @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. @@ -178,7 +178,7 @@ do -- Zone end - --- @param #ZONE_GOAL self + -- @param #ZONE_GOAL self -- @param Core.Event#EVENTDATA EventData Event data table. function ZONE_GOAL:__Destroyed( EventData ) self:F( { "EventDead", EventData } ) diff --git a/Moose Development/Moose/Functional/ZoneGoalCargo.lua b/Moose Development/Moose/Functional/ZoneGoalCargo.lua index 2e803b2a3..10b91e12f 100644 --- a/Moose Development/Moose/Functional/ZoneGoalCargo.lua +++ b/Moose Development/Moose/Functional/ZoneGoalCargo.lua @@ -23,7 +23,7 @@ do -- ZoneGoal - --- @type ZONE_GOAL_CARGO + -- @type ZONE_GOAL_CARGO -- @extends Functional.ZoneGoal#ZONE_GOAL @@ -57,7 +57,7 @@ do -- ZoneGoal ClassName = "ZONE_GOAL_CARGO", } - --- @field #table ZONE_GOAL_CARGO.States + -- @field #table ZONE_GOAL_CARGO.States ZONE_GOAL_CARGO.States = {} --- ZONE_GOAL_CARGO Constructor. diff --git a/Moose Development/Moose/Functional/ZoneGoalCoalition.lua b/Moose Development/Moose/Functional/ZoneGoalCoalition.lua index 1eae7942b..82da20706 100644 --- a/Moose Development/Moose/Functional/ZoneGoalCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneGoalCoalition.lua @@ -1,4 +1,4 @@ ---- **Functional** - Base class that models processes to achieve goals involving a Zone for a Coalition. +--- **Functional (WIP)** - Base class modeling processes to achieve goals involving coalition zones. -- -- === -- @@ -16,7 +16,7 @@ do -- ZoneGoal - --- @type ZONE_GOAL_COALITION + -- @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. @@ -48,7 +48,7 @@ do -- ZoneGoal ObjectCategories = nil, } - --- @field #table ZONE_GOAL_COALITION.States + -- @field #table ZONE_GOAL_COALITION.States ZONE_GOAL_COALITION.States = {} --- ZONE_GOAL_COALITION Constructor. diff --git a/Moose Development/Moose/Globals.lua b/Moose Development/Moose/Globals.lua index 771547060..5108254d1 100644 --- a/Moose Development/Moose/Globals.lua +++ b/Moose Development/Moose/Globals.lua @@ -20,6 +20,13 @@ _DATABASE:_RegisterCargos() _DATABASE:_RegisterZones() _DATABASE:_RegisterAirbases() +--- Function that writes to DCS log file +-- @param #string text Formatted text. +-- @param ... Format passed to string.format(). +function printf(text, ...) + env.info(string.format(text, ...)) +end + --- Check if os etc is available. BASE:I("Checking de-sanitization of os, io and lfs:") local __na = false diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index cd3b1e4ac..fb08421e4 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -31,6 +31,8 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Core/Spot.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Core/MarkerOps_Base.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Core/TextAndSound.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Core/Pathline.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Core/ClientMenu.lua') +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Core/Vector.lua') __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Wrapper/Object.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Wrapper/Identifiable.lua' ) @@ -76,7 +78,14 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/Warehouse.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/Fox.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/Mantis.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/Shorad.lua' ) -__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/ClientWatch.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/AICSAR.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/AmmoTruck.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/Autolase.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/ZoneGoalCargo.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/Tiresias.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/Stratego.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/ClientWatch.lua') +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Functional/Formation.lua') __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Airboss.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/RecoveryTanker.lua' ) @@ -84,6 +93,31 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/RescueHelo.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/ATIS.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/CTLD.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/CSAR.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/AirWing.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/ArmyGroup.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Auftrag.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Awacs.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Brigade.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Chief.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Cohort.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Commander.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Fleet.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/FlightControl.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/FlightGroup.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Flotilla.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Intelligence.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Legion.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/NavyGroup.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Operation.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/OpsGroup.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/OpsTransport.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/OpsZone.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Platoon.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/PlayerTask.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/PlayerRecce.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Squadron.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Target.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/EasyGCICAP.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/AI/AI_Balancer.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/AI/AI_Air.lua' ) @@ -154,4 +188,9 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Tasking/Task_Cargo_Dispatcher __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Tasking/Task_Capture_Zone.lua' ) __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Tasking/Task_Capture_Dispatcher.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Point.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Beacons.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Radios.lua' ) +__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Navigation/Towns.lua' ) + __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Globals.lua' ) diff --git a/Moose Development/Moose/Modules_local.lua b/Moose Development/Moose/Modules_local.lua new file mode 100644 index 000000000..356464f50 --- /dev/null +++ b/Moose Development/Moose/Modules_local.lua @@ -0,0 +1,186 @@ +__Moose.Include( 'Utilities\\Enums.lua' ) +__Moose.Include( 'Utilities\\Utils.lua' ) +__Moose.Include( 'Utilities\\Profiler.lua' ) +--__Moose.Include( 'Utilities\\STTS.lua' ) +__Moose.Include( 'Utilities\\FiFo.lua' ) +__Moose.Include( 'Utilities\\Socket.lua' ) + +__Moose.Include( 'Core\\Base.lua' ) +__Moose.Include( 'Core\\Beacon.lua' ) +__Moose.Include( 'Core\\UserFlag.lua' ) +__Moose.Include( 'Core\\Report.lua' ) +__Moose.Include( 'Core\\Scheduler.lua' ) +__Moose.Include( 'Core\\ScheduleDispatcher.lua' ) +__Moose.Include( 'Core\\Event.lua' ) +__Moose.Include( 'Core\\Settings.lua' ) +__Moose.Include( 'Core\\Menu.lua' ) +__Moose.Include( 'Core\\Zone.lua' ) +__Moose.Include( 'Core\\Velocity.lua' ) +__Moose.Include( 'Core\\Database.lua' ) +__Moose.Include( 'Core\\Set.lua' ) +__Moose.Include( 'Core\\Point.lua' ) +__Moose.Include( 'Core\\Pathline.lua' ) +__Moose.Include( 'Core\\Message.lua' ) +__Moose.Include( 'Core\\Fsm.lua' ) +__Moose.Include( 'Core\\Spawn.lua' ) +__Moose.Include( 'Core\\SpawnStatic.lua' ) +__Moose.Include( 'Core\\Timer.lua' ) +__Moose.Include( 'Core\\Goal.lua' ) +__Moose.Include( 'Core\\Spot.lua' ) +__Moose.Include( 'Core\\Astar.lua' ) +__Moose.Include( 'Core\\MarkerOps_Base.lua' ) +__Moose.Include( 'Core\\TextAndSound.lua' ) +__Moose.Include( 'Core\\Condition.lua' ) +__Moose.Include( 'Core\\ClientMenu.lua' ) +__Moose.Include( 'Core\\Vector.lua' ) + +__Moose.Include( 'Wrapper\\Object.lua' ) +__Moose.Include( 'Wrapper\\Identifiable.lua' ) +__Moose.Include( 'Wrapper\\Positionable.lua' ) +__Moose.Include( 'Wrapper\\Controllable.lua' ) +__Moose.Include( 'Wrapper\\Group.lua' ) +__Moose.Include( 'Wrapper\\Unit.lua' ) +__Moose.Include( 'Wrapper\\Client.lua' ) +__Moose.Include( 'Wrapper\\Static.lua' ) +__Moose.Include( 'Wrapper\\Airbase.lua' ) +__Moose.Include( 'Wrapper\\Scenery.lua' ) +__Moose.Include( 'Wrapper\\Marker.lua' ) +__Moose.Include( 'Wrapper\\Net.lua' ) +__Moose.Include( 'Wrapper\\Weapon.lua' ) +__Moose.Include( 'Wrapper\\Storage.lua' ) +__Moose.Include( 'Wrapper\\DynamicCargo.lua' ) + +__Moose.Include( 'Cargo\\Cargo.lua' ) +__Moose.Include( 'Cargo\\CargoUnit.lua' ) +__Moose.Include( 'Cargo\\CargoSlingload.lua' ) +__Moose.Include( 'Cargo\\CargoCrate.lua' ) +__Moose.Include( 'Cargo\\CargoGroup.lua' ) + +__Moose.Include( 'Functional\\Scoring.lua' ) +__Moose.Include( 'Functional\\CleanUp.lua' ) +__Moose.Include( 'Functional\\Movement.lua' ) +__Moose.Include( 'Functional\\Sead.lua' ) +__Moose.Include( 'Functional\\Escort.lua' ) +__Moose.Include( 'Functional\\MissileTrainer.lua' ) +__Moose.Include( 'Functional\\ATC_Ground.lua' ) +__Moose.Include( 'Functional\\Detection.lua' ) +__Moose.Include( 'Functional\\DetectionZones.lua' ) +__Moose.Include( 'Functional\\Designate.lua' ) +__Moose.Include( 'Functional\\RAT.lua' ) +__Moose.Include( 'Functional\\Range.lua' ) +__Moose.Include( 'Functional\\ZoneGoal.lua' ) +__Moose.Include( 'Functional\\ZoneGoalCoalition.lua' ) +__Moose.Include( 'Functional\\ZoneCaptureCoalition.lua' ) +__Moose.Include( 'Functional\\Artillery.lua' ) +__Moose.Include( 'Functional\\Suppression.lua' ) +__Moose.Include( 'Functional\\PseudoATC.lua' ) +__Moose.Include( 'Functional\\Warehouse.lua' ) +__Moose.Include( 'Functional\\Fox.lua' ) +__Moose.Include( 'Functional\\Mantis.lua' ) +__Moose.Include( 'Functional\\Shorad.lua' ) +__Moose.Include( 'Functional\\Autolase.lua' ) +__Moose.Include( 'Functional\\AICSAR.lua' ) +__Moose.Include( 'Functional\\AmmoTruck.lua' ) +__Moose.Include( 'Functional\\Tiresias.lua' ) +__Moose.Include( 'Functional\\Stratego.lua' ) +__Moose.Include( 'Functional\\ClientWatch.lua' ) + +__Moose.Include( 'Ops\\Airboss.lua' ) +__Moose.Include( 'Ops\\RecoveryTanker.lua' ) +__Moose.Include( 'Ops\\RescueHelo.lua' ) +__Moose.Include( 'Ops\\ATIS.lua' ) +__Moose.Include( 'Ops\\Auftrag.lua' ) +__Moose.Include( 'Ops\\Target.lua' ) +__Moose.Include( 'Ops\\OpsGroup.lua' ) +__Moose.Include( 'Ops\\FlightGroup.lua' ) +__Moose.Include( 'Ops\\NavyGroup.lua' ) +__Moose.Include( 'Ops\\ArmyGroup.lua' ) +__Moose.Include( 'Ops\\Cohort.lua' ) +__Moose.Include( 'Ops\\Squadron.lua' ) +__Moose.Include( 'Ops\\Platoon.lua' ) +__Moose.Include( 'Ops\\Legion.lua' ) +__Moose.Include( 'Ops\\AirWing.lua' ) +__Moose.Include( 'Ops\\Brigade.lua' ) +__Moose.Include( 'Ops\\Intelligence.lua' ) +__Moose.Include( 'Ops\\Commander.lua' ) +__Moose.Include( 'Ops\\OpsTransport.lua' ) +__Moose.Include( 'Ops\\CSAR.lua' ) +__Moose.Include( 'Ops\\CTLD.lua' ) +__Moose.Include( 'Ops\\OpsZone.lua' ) +__Moose.Include( 'Ops\\Chief.lua' ) +__Moose.Include( 'Ops\\Flotilla.lua' ) +__Moose.Include( 'Ops\\Fleet.lua' ) +__Moose.Include( 'Ops\\Awacs.lua' ) +__Moose.Include( 'Ops\\PlayerTask.lua' ) +__Moose.Include( 'Ops\\Operation.lua' ) +__Moose.Include( 'Ops\\FlightControl.lua' ) +__Moose.Include( 'Ops\\PlayerRecce.lua' ) +__Moose.Include( 'Ops\\EasyGCICAP.lua' ) + +__Moose.Include( 'AI\\AI_Balancer.lua' ) +__Moose.Include( 'AI\\AI_Air.lua' ) +__Moose.Include( 'AI\\AI_Air_Patrol.lua' ) +__Moose.Include( 'AI\\AI_Air_Engage.lua' ) +__Moose.Include( 'AI\\AI_A2A_Patrol.lua' ) +__Moose.Include( 'AI\\AI_A2A_Cap.lua' ) +__Moose.Include( 'AI\\AI_A2A_Gci.lua' ) +__Moose.Include( 'AI\\AI_A2A_Dispatcher.lua' ) +__Moose.Include( 'AI\\AI_A2G_BAI.lua' ) +__Moose.Include( 'AI\\AI_A2G_CAS.lua' ) +__Moose.Include( 'AI\\AI_A2G_SEAD.lua' ) +__Moose.Include( 'AI\\AI_A2G_Dispatcher.lua' ) +__Moose.Include( 'AI\\AI_Patrol.lua' ) +__Moose.Include( 'AI\\AI_Cap.lua' ) +__Moose.Include( 'AI\\AI_Cas.lua' ) +__Moose.Include( 'AI\\AI_Bai.lua' ) +__Moose.Include( 'AI\\AI_Formation.lua' ) +__Moose.Include( 'AI\\AI_Escort.lua' ) +__Moose.Include( 'AI\\AI_Escort_Request.lua' ) +__Moose.Include( 'AI\\AI_Escort_Dispatcher.lua' ) +__Moose.Include( 'AI\\AI_Escort_Dispatcher_Request.lua' ) +__Moose.Include( 'AI\\AI_Cargo.lua' ) +__Moose.Include( 'AI\\AI_Cargo_APC.lua' ) +__Moose.Include( 'AI\\AI_Cargo_Helicopter.lua' ) +__Moose.Include( 'AI\\AI_Cargo_Airplane.lua' ) +__Moose.Include( 'AI\\AI_Cargo_Ship.lua' ) +__Moose.Include( 'AI\\AI_Cargo_Dispatcher.lua' ) +__Moose.Include( 'AI\\AI_Cargo_Dispatcher_APC.lua' ) +__Moose.Include( 'AI\\AI_Cargo_Dispatcher_Helicopter.lua' ) +__Moose.Include( 'AI\\AI_Cargo_Dispatcher_Airplane.lua' ) +__Moose.Include( 'AI\\AI_Cargo_Dispatcher_Ship.lua' ) + +__Moose.Include( 'Actions\\Act_Assign.lua' ) +__Moose.Include( 'Actions\\Act_Route.lua' ) +__Moose.Include( 'Actions\\Act_Account.lua' ) +__Moose.Include( 'Actions\\Act_Assist.lua' ) + +__Moose.Include( 'Sound\\UserSound.lua' ) +__Moose.Include( 'Sound\\SoundOutput.lua' ) +__Moose.Include( 'Sound\\Radio.lua' ) +__Moose.Include( 'Sound\\RadioQueue.lua' ) +__Moose.Include( 'Sound\\RadioSpeech.lua' ) +__Moose.Include( 'Sound\\SRS.lua' ) + +__Moose.Include( 'Tasking\\CommandCenter.lua' ) +__Moose.Include( 'Tasking\\Mission.lua' ) +__Moose.Include( 'Tasking\\Task.lua' ) +__Moose.Include( 'Tasking\\TaskInfo.lua' ) +__Moose.Include( 'Tasking\\Task_Manager.lua' ) +__Moose.Include( 'Tasking\\DetectionManager.lua' ) +__Moose.Include( 'Tasking\\Task_A2G_Dispatcher.lua' ) +__Moose.Include( 'Tasking\\Task_A2G.lua' ) +__Moose.Include( 'Tasking\\Task_A2A_Dispatcher.lua' ) +__Moose.Include( 'Tasking\\Task_A2A.lua' ) +__Moose.Include( 'Tasking\\Task_Cargo.lua' ) +__Moose.Include( 'Tasking\\Task_Cargo_Transport.lua' ) +__Moose.Include( 'Tasking\\Task_Cargo_CSAR.lua' ) +__Moose.Include( 'Tasking\\Task_Cargo_Dispatcher.lua' ) +__Moose.Include( 'Tasking\\Task_Capture_Zone.lua' ) +__Moose.Include( 'Tasking\\Task_Capture_Dispatcher.lua' ) + +__Moose.Include( 'Navigation\\Point.lua' ) +__Moose.Include( 'Navigation\\Beacons.lua' ) +__Moose.Include( 'Navigation\\Radios.lua' ) +__Moose.Include( 'Navigation\\Towns.lua' ) + +__Moose.Include( 'Globals.lua' ) diff --git a/Moose Development/Moose/Navigation/Beacons.lua b/Moose Development/Moose/Navigation/Beacons.lua new file mode 100644 index 000000000..84ecf218d --- /dev/null +++ b/Moose Development/Moose/Navigation/Beacons.lua @@ -0,0 +1,434 @@ +--- **NAVIGATION** - Beacons of the map/theatre. +-- +-- **Main Features:** +-- +-- * Access beacons of the map +-- * Find closest beacon +-- * Get frequencies and channels +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Navigation%20-%20Beacons). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Navigation.Beacons +-- @image MOOSE.JPG + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- BEACONS class. +-- @type BEACONS +-- +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field #table beacons Beacons. +-- +-- @extends Core.Base#BASE + +--- *Hope is the beacon that guides lost ships back to the shore.* +-- +-- === +-- +-- # The BEACONS Concept +-- +-- This class is designed to make information about beacons of a map/theatre easier accessible. The information contains location, type and frequencies of all or specific beacons of the map. +-- +-- **Note** that try to avoid hard coding stuff in Moose since DCS is updated frequently and things change. Therefore, the main source of information is either a file `beacons.lua` that can be +-- found in the installation directory of DCS for each map or a table that the user needs to provide. +-- **Note** your `MissionScripting` environment needs to be desanitized to read this data. `Package` als needs to be available. +-- +-- # Basic Setup +-- +-- A new `BEACONS` object can be created with the @{#BEACONS.NewFromFile}(*beacons_lua_file*) function. +-- +-- local beacons=BEACONS:NewFromFile("\Mods\terrains\\beacons.lua") +-- beacons:MarkerShow() +-- +-- This will load the beacons from the `` for the specific map and place markers on the F10 map. This is the first step you should do to ensure that the file +-- you provided is correct and all relevant beacons are present. +-- +-- # User Functions +-- +-- ## F10 Map Markers +-- +-- ## Position +-- +-- ## Get Closest Beacon +-- +-- +-- @field #BEACONS +BEACONS = { + ClassName = "BEACONS", + verbose = 1, + beacons = {}, +} + +--- Mission capability. +-- @type BEACONS.Beacon +-- @field #function display_name Function that returns the localized name. +-- @field #number type Beacon type. +-- @field #string beaconId Beacon ID. +-- @field #string callsign Call sign. +-- @field #number frequency Frequency in Hz. +-- @field #number channel TACAN, RSBN or PRMG channel depending on type. +-- @field #table position Position table. +-- @field #number direction Direction in degrees. +-- @field #table positionGeo Table with latitude and longitude. +-- @field #table sceneObjects Table with scenery objects, e.g. `{t:393396742}`. +-- @field #number chartOffsetX No idea what this offset is?! +-- @field DCS#Vec3 vec3 Position vector 3D. +-- @field #number markerID ID for the F10 marker. +-- @field #string typeName Name of becon type. +-- @field Wrapper.Scenery#SCENERY scenery The scenery object. + +--- BEACONS class version. +-- @field #string version +BEACONS.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot... +-- DONE: TACAN channel from frequency (was already in beacon.lua as channel) +-- DONE: Scenery object + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor(s) +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new BEACONS class instance from a given table. +-- @param #BEACONS self +-- @param #table BeaconTable Table with beacon info. +-- @return #BEACONS self +function BEACONS:NewFromTable(BeaconTable) + + -- Inherit everything from BASE class. + self=BASE:Inherit(self, BASE:New()) -- #BEACONS + + for _,_beacon in pairs(BeaconTable) do + local beacon=_beacon --#BEACONS.Beacon + + -- Get 3D vector + beacon.vec3={x=beacon.position[1], y=beacon.position[2], z=beacon.position[3]} + + -- Get coordinate + beacon.coordinate=COORDINATE:NewFromVec3(beacon.vec3) + + -- Get type name + beacon.typeName=self:_GetTypeName(beacon.type) + + -- Find closest scenery object from scan + beacon.scenery=beacon.coordinate:FindClosestScenery(20) + + -- Debug stuff for scenery object + if false then + if beacon.scenery then + env.info(string.format("FF Beacon %s %s %s got scenery object %s, %s", beacon.callsign, beacon.beaconId, beacon.typeName, beacon.scenery:GetName(), beacon.scenery:GetTypeName() )) + UTILS.PrintTableToLog(beacon.scenery.SceneryObject) + UTILS.PrintTableToLog(beacon.sceneObjects) + else + env.info(string.format("FF NO scenery object %s %s %s ", beacon.callsign, beacon.beaconId, beacon.typeName)) + end + end + + -- Add to table + table.insert(self.beacons, beacon) + end + + -- Debug output + self:I(string.format("Added %d beacons", #self.beacons)) + + if self.verbose > 0 then + local text="Beacon types:" + for typeName,typeID in pairs(BEACON.Type) do + local n=self:CountBeacons(typeID) + text=text..string.format("\n%s = %d", typeName, n) + end + self:I(text) + end + + return self +end + + +--- Create a new BEACONS class instance from a given file. +-- @param #BEACONS self +-- @param #string FileName Full path to the file containing the map beacons. +-- @return #BEACONS self +function BEACONS:NewFromFile(FileName) + + -- Inherit everything from BASE class. + self=BASE:Inherit(self, BASE:New()) -- #BEACONS + + local exists=UTILS.FileExists(FileName) + + if exists==false then + self:E(string.format("ERROR: file with beacon info does not exist!")) + return nil + end + + -- This will create a global table `beacons` + dofile(FileName) + + -- Get beacons from table. + self=self:NewFromTable(beacons) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get 3D position vector of a specific beacon. +-- @param #BEACONS self +-- @param #BEACONS.Beacon beacon The beacon data structure. +-- @return DCS#Vec3 Position vector. +function BEACONS:GetVec3(beacon) + return beacon.vec3 +end + +--- Get COORDINATE of a specific beacon. +-- @param #BEACONS self +-- @param #BEACONS.Beacon beacon The beacon data structure. +-- @return Core.Point#COORDINATE The coordinate. +function BEACONS:GetCoordinate(beacon) + local coordinate=COORDINATE:NewFromVec3(beacon.vec3) + return coordinate +end + +--- Find closest beacon to a given coordinate. +-- @param #BEACONS self +-- @param Core.Point#COORDINATE Coordinate The reference coordinate. +-- @param #number TypeID (Optional) Only search for specific beacon types, *e.g.* `BEACON.Type.TACAN`. +-- @param #number DistMax (Optional) Max search distance in meters. +-- @param #table ExcludeList (Optional) List of beacons to exclude. +-- @return #BEACONS.Beacon The closest beacon. +function BEACONS:GetClosestBeacon(Coordinate, TypeID, DistMax, ExcludeList) + + local beacon=nil --#BEACONS.Beacon + local distmin=math.huge + + ExcludeList=ExcludeList or {} + + for _,_beacon in pairs(self.beacons) do + local bc=_beacon --#BEACONS.Beacon + + if (TypeID==nil or TypeID==bc.type) and (not UTILS.IsInTable(ExcludeList, bc, "beaconId")) then + + local dist=Coordinate:Get2DDistance(bc.vec3) + + if dist=1e6 then + freq=freq/1e6 + unit="MHz" + elseif freq>=1e3 then + freq=freq/1e3 + unit="kHz" + end + + return freq, unit +end + +--- Get name of beacon type. +-- @param #BEACONS self +-- @param #number typeID Beacon type number. +-- @return #string Type name. +function BEACONS:_GetTypeName(typeID) + + if typeID~=nil then + for typeName,_typeID in pairs(BEACON.Type) do + if _typeID==typeID then + return typeName + end + end + end + + return "Unknown" +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Navigation/Point.lua b/Moose Development/Moose/Navigation/Point.lua new file mode 100644 index 000000000..e02fb9945 --- /dev/null +++ b/Moose Development/Moose/Navigation/Point.lua @@ -0,0 +1,602 @@ +--- **NAVIGATION** - Navigation Airspace Points, Fixes and Aids. +-- +-- **Main Features:** +-- +-- * Navigation Fixes +-- * Navigation Aids +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Navigation%20-%20NavFix). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Navigation.Point +-- @image MOOSE.JPG + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- NAVFIX class. +-- @type NAVFIX +-- +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field #string name Name of the point. +-- @field #string typePoint Type of the point, *e.g. "Intersection", "VOR", "Airport". +-- @field Core.Vector#VECTOR vector Position vector of the fix. +-- @field Wrapper.Marker#MARKER marker Marker on F10 map. +-- @field #number altMin Minimum altitude in meters. +-- @field #number altMax Maximum altitude in meters. +-- @field #number speedMin Minimum speed in knots. +-- @field #number speedMax Maximum speed in knots. +-- +-- @field #boolean isCompulsory Is this a compulsory fix. +-- @field #boolean isFlyover Is this a flyover fix (`true`) or turning point otherwise. +-- @field #boolean isFAF Is this a final approach fix. +-- @field #boolean isIAF Is this an initial approach fix. +-- @field #boolean isIF Is this an initial fix. +-- @field #boolean isMAF Is this an initial fix. +-- +-- @extends Core.Base#BASE + +--- *A fleet of British ships at war are the best negotiators.* -- Horatio Nelson +-- +-- === +-- +-- # The NAVFIX Concept +-- +-- The NAVFIX class has a great concept! +-- +-- A NAVFIX describes a geo position and can, *e.g.*, be part of a FLIGHTPLAN. It has a unique name and is of a certain type, *e.g.* "Intersection", "VOR", "Airbase" etc. +-- It can also have further properties as min/max altitudes and speeds that aircraft need to obey when they pass the point. +-- +-- # Basic Setup +-- +-- A new `NAVFIX` object can be created with the @{#NAVFIX.New}() function. +-- +-- myNavPoint=NAVFIX:New() +-- myTemplate:SetXYZ(X, Y, Z) +-- +-- This is how it works. +-- +-- @field #NAVFIX +NAVFIX = { + ClassName = "NAVFIX", + verbose = 0, +} + +--- Type of point. +-- @type NAVFIX.Type +-- @field #string POINT Waypoint. +-- @field #string INTERSECTION Intersection of airway. +-- @field #string AIRPORT Airport. +-- @field #string VOR Very High Frequency Omnidirectional Range Station. +-- @field #string DME Distance Measuring Equipment. +-- @field #string NDB Non-Directional Beacon. +-- @field #string VORDME Combined VHF omnidirectional range (VOR) with a distance-measuring equipment (DME). +-- @field #string LOC Localizer. +-- @field #string ILS Instrument Landing System. +-- @field #string TACAN TACtical Air Navigation System (TACAN). +NAVFIX.Type={ + POINT="Point", + INTERSECTION="Intersection", + AIRPORT="Airport", + NDB="NDB", + VOR="VOR", + DME="DME", + VORDME="VOR/DME", + LOC="Localizer", + ILS="ILS", + TACAN="TACAN" +} + +--- NAVFIX class version. +-- @field #string version +NAVFIX.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot... + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor(s) +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new NAVFIX class instance from a given VECTOR. +-- @param #NAVFIX self +-- @param #string Name Name/ident of the point. Should be unique! +-- @param #string Type Type of the point. Default `NAVFIX.Type.POINT`. +-- @param Core.Vector#VECTOR Vector Position vector of the navpoint. +-- @return #NAVFIX self +function NAVFIX:NewFromVector(Name, Type, Vector) + + -- Inherit everything from BASE class. + self=BASE:Inherit(self, BASE:New()) -- #NAVFIX + + -- Vector of point. + self.vector=Vector + + -- Name of point. + self.name=Name + + -- Type of the point. + self.typePoint=Type or NAVFIX.Type.POINT + + local coord=COORDINATE:NewFromVec3(self.vector) + + -- Marker on F10. + self.marker=MARKER:New(coord, self:_GetMarkerText()) + + -- Log ID string. + self.lid=string.format("NAVFIX %s [%s] | ", tostring(self.name), tostring(self.typePoint)) + + -- Debug info. + self:I(self.lid..string.format("Created NAVFIX")) + + return self +end + + +--- Create a new NAVFIX class instance from a given COORDINATE. +-- @param #NAVFIX self +-- @param #string Name Name of the fix. Should be unique! +-- @param #string Type Type of the point. Default `NAVFIX.Type.POINT`. +-- @param Core.Point#COORDINATE Coordinate Coordinate of the point. +-- @return #NAVFIX self +function NAVFIX:NewFromCoordinate(Name, Type, Coordinate) + + -- Create a VECTOR from the coordinate. + local Vector=VECTOR:NewFromVec(Coordinate) + + -- Create NAVFIX. + self=NAVFIX:NewFromVector(Name, Type, Vector) + + return self +end + + +--- Create a new NAVFIX instance from given latitude and longitude in degrees, minutes and seconds (DMS). +-- @param #NAVFIX self +-- @param #string Name Name of the fix. Should be unique! +-- @param #string Type Type of the point. Default `NAVFIX.Type.POINT`. +-- @param #string Latitude Latitude in DMS as string. +-- @param #string Longitude Longitude in DMS as string. +-- @return #NAVFIX self +function NAVFIX:NewFromLLDMS(Name, Type, Latitude, Longitude) + + -- Create a VECTOR from the coordinate. + local Vector=VECTOR:NewFromLLDMS(Latitude, Longitude) + + -- Create NAVFIX. + self=NAVFIX:NewFromVector(Name, Type, Vector) + + return self +end + +--- Create a new NAVFIX instance from given latitude and longitude in decimal degrees (DD). +-- @param #NAVFIX self +-- @param #string Name Name of the fix. Should be unique! +-- @param #string Type Type of the point. Default `NAVFIX.Type.POINT`. +-- @param #number Latitude Latitude in DD. +-- @param #number Longitude Longitude in DD. +-- @return #NAVFIX self +function NAVFIX:NewFromLLDD(Name, Type, Latitude, Longitude) + + -- Create a VECTOR from the coordinate. + local Vector=VECTOR:NewFromLLDD(Latitude, Longitude) + + -- Create NAVFIX. + self=NAVFIX:NewFromVector(Name, Type, Vector) + + return self +end + + +--- Create a new NAVFIX class instance relative to a given other NAVFIX. +-- You have to specify the distance and bearing from the new point to the given point. *E.g.*, for a distance of 5 NM and a bearing of 090° (West), the +-- new nav point is created 5 NM East of the given nav point. The reason is that this corresponts to convention used in most maps. +-- You can, however, use the `Reciprocal` switch to create the new point in the direction you specify. +-- @param #NAVFIX self +-- @param #string Name Name of the fix. Should be unique! +-- @param #string Type Type of navfix. +-- @param #NAVFIX NavFix The given/existing navigation fix relative to which the new fix is created. +-- @param #number Distance Distance from the given to the new point in nautical miles. +-- @param #number Bearing Bearing [Deg] from the new point to the given one. +-- @param #boolean Reciprocal If `true` the reciprocal `Bearing` is taken so it specifies the direction from the given point to the new one. +-- @return #NAVFIX self +function NAVFIX:NewFromNavFix(Name, Type, NavFix, Distance, Bearing, Reciprocal) + + -- Convert magnetic to true bearing by adding magnetic declination, e.g. mag. bearing 10°M ==> true bearing 16°M (for 6° variation on Caucasus map) + Bearing=Bearing+UTILS.GetMagneticDeclination() + + if Reciprocal then + Bearing=Bearing-180 + end + + -- Translate. + local Vector=NavFix.vector:Translate(UTILS.NMToMeters(Distance), Bearing, true) + + self=NAVFIX:NewFromVector(Name, Type, Vector) + + return self +end + +--- Create a new NAVFIX class instance from BEACONS.Beacon data. +-- @param #NAVFIX self +-- @param Navigation.Beacons#BEACONS.Beacon Beacon The beacon data. +-- @return #NAVFIX self +function NAVFIX:NewFromBeacon(Beacon) + local frequency, unit = BEACONS:_GetFrequency(Beacon.frequency) + frequency = string.format("%.3f",frequency) + if Beacon.typeName == "TACAN" then + frequency = Beacon.channel + unit = "X" + end + self = NAVFIX:NewFromVector(string.format("%s %s %s",Beacon.typeName,frequency,unit),Beacon.typeName,Beacon.vec3) + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set whether this is the intermediate fix (IF). +-- @param #NAVFIX self +-- @return #NAVFIX self +function NAVFIX:SetIntermediateFix(IntermediateFix) + self.isIF=IntermediateFix + return self +end + +--- Set whether this is an initial approach fix (IAF). +-- The IAF is the point where the initial approach segment of an instrument approach begins. +-- It is usually a designated intersection, VHF omidirectional range (VOR) non-directional beacon (NDB) +-- or distance measuring equipment (DME) fix. +-- The IAF may be collocated with the intermediate fix (IF) of the instrument apprach an in such case they designate the +-- beginning of the intermediate segment of the approach. When the IAF and the IF are combined, there is no inital approach segment. +-- @param #NAVFIX self +-- @param #boolean IntermediateFix If `true`, this is an intermediate fix. +-- @return #NAVFIX self +function NAVFIX:SetInitialApproachFix(IntermediateFix) + self.isIAF=IntermediateFix + return self +end + + +--- Set whether this is the final approach fix (FAF). +-- @param #NAVFIX self +-- @param #boolean FinalApproachFix If `true`, this is a final approach fix. +-- @return #NAVFIX self +function NAVFIX:SetFinalApproachFix(FinalApproachFix) + self.isFAF=FinalApproachFix + return self +end + +--- Set whether this is the final approach fix (FAF). +-- @param #NAVFIX self +-- @param #boolean FinalApproachFix If `true`, this is a final approach fix. +-- @return #NAVFIX self +function NAVFIX:SetMissedApproachFix(MissedApproachFix) + self.isMAF=MissedApproachFix + return self +end + + +--- Set minimum altitude. +-- @param #NAVFIX self +-- @param #number Altitude Min altitude in feet. +-- @return #NAVFIX self +function NAVFIX:SetAltMin(Altitude) + + self.altMin=Altitude + + return self +end + +--- Set maximum altitude. +-- @param #NAVFIX self +-- @param #number Altitude Max altitude in feet. +-- @return #NAVFIX self +function NAVFIX:SetAltMax(Altitude) + + self.altMax=Altitude + + return self +end + +--- Set mandatory altitude (min alt = max alt). +-- @param #NAVFIX self +-- @param #number Altitude Altitude in feet. +-- @return #NAVFIX self +function NAVFIX:SetAltMandatory(Altitude) + + self.altMin=Altitude + self.altMax=Altitude + + return self +end + +--- Set minimum allowed speed at this fix. +-- @param #NAVFIX self +-- @param #number Speed Min speed in knots. +-- @return #NAVFIX self +function NAVFIX:SetSpeedMin(Speed) + + self.speedMin=Speed + + return self +end + +--- Set maximum allowed speed at this fix. +-- @param #NAVFIX self +-- @param #number Speed Max speed in knots. +-- @return #NAVFIX self +function NAVFIX:SetSpeedMax(Speed) + + self.speedMax=Speed + + return self +end + +--- Set mandatory speed (min speed = max speed) at this fix. +-- @param #NAVFIX self +-- @param #number Speed Mandatory speed in knots. +-- @return #NAVFIX self +function NAVFIX:SetSpeedMandatory(Speed) + + self.speedMin=Speed + self.speedMax=Speed + + return self +end + + +--- Set whether this fix is compulsory. +-- @param #NAVFIX self +-- @param #boolean Compulsory If `true`, this is a compulsory fix. If `false` or nil, it is non-compulsory. +-- @return #NAVFIX self +function NAVFIX:SetCompulsory(Compulsory) + self.isCompulsory=Compulsory + return self +end + +--- Set whether this is a fly-over fix fix. +-- @param #NAVFIX self +-- @param #boolean FlyOver If `true`, this is a fly over fix. If `false` or nil, it is not. +-- @return #NAVFIX self +function NAVFIX:SetFlyOver(FlyOver) + self.isFlyover=FlyOver + return self +end + + +--- Get the altitude in feet MSL. If min and max altitudes are set, it will return a random altitude between min and max. +-- @param #NAVFIX self +-- @return #number Altitude in feet MSL. Can be `nil`, if neither min nor max altitudes have beeen set. +function NAVFIX:GetAltitude() + + local alt=nil + if self.altMin and self.altMax and self.altMin~=self.altMax then + alt=math.random(self.altMin, self.altMax) + elseif self.altMin then + alt=self.altMin + elseif self.altMax then + alt=self.altMax + end + + return alt +end + + +--- Get the speed. If min and max speeds are set, it will return a random speed between min and max. +-- @param #NAVFIX self +-- @return #number Speed in knots. Can be `nil`, if neither min nor max speeds have beeen set. +function NAVFIX:GetSpeed() + + local speed=nil + if self.speedMin and self.speedMax and self.speedMin~=self.speedMax then + speed=math.random(self.speedMin, self.speedMax) + elseif self.speedMin then + speed=self.speedMin + elseif self.speedMax then + speed=self.speedMax + end + + return speed +end + + + +--- Add marker the NAVFIX on the F10 map. +-- @param #NAVFIX self +-- @return #NAVFIX self +function NAVFIX:MarkerShow() + + self.marker:ToAll() + + return self +end + +--- Remove marker of the NAVFIX from the F10 map. +-- @param #NAVFIX self +-- @return #NAVFIX self +function NAVFIX:MarkerRemove() + + self.marker:Remove() + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Private Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get text displayed in the F10 marker. +-- @param #NAVFIX self +-- @return #string Marker text. +function NAVFIX:_GetMarkerText() + + local altmin=self.altMin and tostring(self.altMin) or "" + local altmax=self.altMax and tostring(self.altMax) or "" + local speedmin=self.speedMin and tostring(self.speedMin) or "" + local speedmax=self.speedMax and tostring(self.speedMax) or "" + + + local text=string.format("NAVFIX %s", self.name) + if self.isIAF then + text=text..string.format(" (IAF)") + end + if self.isIF then + text=text..string.format(" (IF)") + end + text=text..string.format("\nAltitude [ft]: %s - %s", altmin, altmax) + text=text..string.format("\nSpeed [knots]: %s - %s", speedmin, speedmax) + text=text..string.format("\nCompulsory: %s", tostring(self.isCompulsory)) + text=text..string.format("\nFly Over: %s", tostring(self.isFlyover)) + + return text +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- NAVAID class. +-- @type NAVAID +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @extends Navigation.Point#NAVFIX + +--- *A fleet of British ships at war are the best negotiators.* -- Horatio Nelson +-- +-- === +-- +-- # The NAVAID Concept +-- +-- A NAVAID consists of one or multiple FLOTILLAs. These flotillas "live" in a WAREHOUSE that has a phyiscal struction (STATIC or UNIT) and can be captured or destroyed. +-- +-- # Basic Setup +-- +-- A new `NAVAID` object can be created with the @{#NAVAID.New}(`WarehouseName`, `FleetName`) function, where `WarehouseName` is the name of the static or unit object hosting the fleet +-- and `FleetName` is the name you want to give the fleet. This must be *unique*! +-- +-- myFleet=NAVAID:New("myWarehouseName", "1st Fleet") +-- myFleet:SetPortZone(ZonePort1stFleet) +-- myFleet:Start() +-- +-- A fleet needs a *port zone*, which is set via the @{#NAVAID.SetPortZone}(`PortZone`) function. This is the zone where the naval assets are spawned and return to. +-- +-- Finally, the fleet needs to be started using the @{#NAVAID.Start}() function. If the fleet is not started, it will not process any requests. +-- +-- @field #NAVAID +NAVAID = { + ClassName = "NAVAID", + verbose = 0, +} + +--- NAVAID class version. +-- @field #string version +NAVAID.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add frequencies. Which unit MHz, kHz, Hz? +-- TODO: Add radial function + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new NAVAID class instance. +-- @param #NAVAID self +-- @param #string Name Name/ident of this navaid. +-- @param #string Type Type of the point. Default `NAVFIX.Type.POINT`. +-- @param #string ZoneName Name of the zone to scan the scenery. +-- @param #string SceneryName Name of the scenery object. +-- @return #NAVAID self +function NAVAID:NewFromScenery(Name, Type, ZoneName, SceneryName) + + -- Get the zone. + local zone=ZONE:FindByName(ZoneName) + + -- Get coordinate. + local Coordinate=zone:GetCoordinate() + + -- Inherit everything from NAVFIX class. + self=BASE:Inherit(self, NAVFIX:NewFromCoordinate(Name, Type, Coordinate)) -- #NAVAID + + -- Set zone. + self.zone=ZONE:FindByName(ZoneName) + + -- Try to get the scenery object. Note not all can be found unfortunately. + if SceneryName then + self.scenery=SCENERY:FindByNameInZone(SceneryName, ZoneName) + if not self.scenery then + self:E(string.format("ERROR: Could not find scenery object %s in zone %s", SceneryName, ZoneName)) + end + end + + -- Alias. + self.alias=string.format("%s %s %s", tostring(ZoneName), tostring(SceneryName), tostring(Type)) + + -- Set some string id for output to DCS.log file. + self.lid=string.format("NAVAID %s | ", self.alias) + + -- Debug info. + self:I(self.lid..string.format("Created NAVAID!")) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set frequency the beacon transmits on. +-- @param #NAVAID self +-- @param #number Frequency Frequency in Hz. +-- @return #NAVAID self +function NAVAID:SetFrequency(Frequency) + + self.frequency=Frequency + + return self +end + +--- Set channel of, *e.g.*, TACAN beacons. +-- @param #NAVAID self +-- @param #number Channel The channel. +-- @param #string Band The band either `"X"` (default) or `"Y"`. +-- @return #NAVAID self +function NAVAID:SetChannel(Channel, Band) + + self.channel=Channel + self.band=Band or "X" + + return self +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Private Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- Add private CLASS functions here. +-- No private NAVAID functions yet. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Navigation/Radios.lua b/Moose Development/Moose/Navigation/Radios.lua new file mode 100644 index 000000000..fb78e56f4 --- /dev/null +++ b/Moose Development/Moose/Navigation/Radios.lua @@ -0,0 +1,423 @@ +--- **NAVIGATION** - Airbase radios. +-- +-- **Main Features:** +-- +-- * Get radio frequencies of airbases +-- * Find closest airbase radios +-- * Mark radio frequencies on F10 map +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Navigation%20-%20Radios). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Navigation.Radios +-- @image MOOSE.JPG + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- RADIOS class. +-- @type RADIOS +-- +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field #table radios Radios. +-- +-- @extends Core.Base#BASE + +--- *It's not true I had nothing on, I had the radio on.* -- *Marilyn Monroe* +-- +-- === +-- +-- # The RADIOS Concept +-- +-- This class is designed to make information about radios of a map/theatre easier accessible. The information contains mostly the frequencies of airbases of the map. +-- +-- **Note** that try to avoid hard coding stuff in Moose since DCS is updated frequently and things change. Therefore, the main source of information is either a file `radio.lua` that can be +-- found in the installation directory of DCS for each map or a table that the user needs to provide. +-- **Note** your `MissionScripting` environment needs to be desanitized to read this data. +-- +-- # Basic Setup +-- +-- A new `RADIOS` object can be created with the @{#RADIOS.NewFromFile}(*radio_lua_file*) function. +-- +-- local radios=RADIOS:NewFromFile("\Mods\terrains\\Radio.lua") +-- radios:MarkerShow() +-- +-- This will load the radios from the `` for the specific map and place markers on the F10 map. This is the first step you should do to ensure that the file +-- you provided is correct and all relevant radios are present. +-- +-- # User Functions +-- +-- ## F10 Map Markers +-- +-- ## Position +-- +-- ## Closest Radio +-- +-- +-- @field #RADIOS +RADIOS = { + ClassName = "RADIOS", + verbose = 0, + radios = {}, +} + +--- Radio item data structure. +-- @type RADIOS.Radio +-- @field #string radioId Radio ID. +-- @field #table role Roles of the radio (usually {"ground", "tower", "approach"}). +-- @field #table callsign Callsigns of the radio (usually the airbase name). +-- @field #table frequency Frequencies of the radios. +-- @field #table position Position table. +-- @field #table sceneObjects Scenery objects. +-- @field #string name Name of the airbase. +-- @field Wrapper.Airbase#AIRBASE airbase Airbase. +-- @field Core.Point#COORDINATE coordinate The COORDINATE of the radio. +-- @field DCS#Vec3 vec3 3D vector. +-- @field #number markerID Marker ID. + +--- Radio item data structure. +-- @type RADIOS.Frequency +-- @field #number modu Modulation type. +-- @field #number freq Frequency in Hz. + + +--- RADIOS class version. +-- @field #string version +RADIOS.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot... + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor(s) +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new RADIOS class instance from a given table. +-- @param #RADIOS self +-- @param #table RadioTable Table with radios info. +-- @return #RADIOS self +function RADIOS:NewFromTable(RadioTable) + + -- Inherit everything from BASE class. + self=BASE:Inherit(self, BASE:New()) -- #RADIOS + + --local airbasenames=AIRBASE.GetAllAirbaseNames() + + -- Get all airdromes + local airdromes=AIRBASE.GetAllAirbases(nil, Airbase.Category.AIRDROME) + + for _,_radio in pairs(RadioTable) do + local radio=_radio --#RADIOS.Radio + + -- The table structure of callsign is a bit awkward. We need to get the airbase name. + -- Note that unfortunately, the callsign does not always correspond to the airbase name. + if false then + local cs=radio.callsign[1] + if cs and cs.common then + radio.name=cs.common[1] + elseif cs and cs.nato then + radio.name=cs.nato[1] + else + radio.name="Unknown" + end + radio.name=self:_GetAirbaseName(airbasenames, radio.name) + radio.airbase=AIRBASE:FindByName(radio.name) + end + + -- Each radio item has a key radioId = 'airfield106_0', where 106 is the UID of the airbase. + -- So we can use that to get the airbase. + local aid = tonumber(string.match(radio.radioId, "airfield(%d+)_")) + + -- Get airbase + radio.airbase=self:_GetAirbaseByID(airdromes, aid) + + -- Set other stuff + if radio.airbase then + radio.coordinate=radio.airbase:GetCoordinate() + radio.vec3=radio.airbase:GetVec3() + radio.name=radio.airbase:GetName() + end + + -- Add to table + table.insert(self.radios, radio) + end + + -- Debug output + self:I(string.format("Added %d radios", #self.radios)) + + return self +end + + +--- Create a new RADIOS class instance from a given file. +-- @param #RADIOS self +-- @param #string FileName Full path to the file containing the map radios. +-- @return #RADIOS self +function RADIOS:NewFromFile(FileName) + + -- Inherit everything from BASE class. + self=BASE:Inherit(self, BASE:New()) -- #RADIOS + + local exists=UTILS.FileExists(FileName) + + if exists==false then + self:E(string.format("ERROR: file with radios info does not exist! File=%s", tostring(FileName))) + return nil + end + + -- Backup DCS radio table + local radiobak=UTILS.DeepCopy(radio) + + -- This will create a global table `radio` + dofile(FileName) + + -- Get radios from table. + self=self:NewFromTable(radio) + + -- Restore DCS radio table + radio=UTILS.DeepCopy(radiobak) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get 3D position vector of a specific radio. +-- @param #RADIOS self +-- @param #RADIOS.Radio radio The radio data structure. +-- @return DCS#Vec3 Position vector. +function RADIOS:GetVec3(radio) + return radio.vec3 +end + +--- Get COORDINATE of a specific radio. +-- @param #RADIOS self +-- @param #RADIOS.Radio radio The radio data structure. +-- @return Core.Point#COORDINATE The coordinate. +function RADIOS:GetCoordinate(radio) + return radio.coordinate +end + + +--- Find closest radio to a given coordinate. +-- @param #RADIOS self +-- @param Core.Point#COORDINATE Coordinate The reference coordinate. +-- @param #number DistMax (Optional) Max search distance in meters. +-- @param #table ExcludeList (Optional) List of radios to exclude. +-- @return #RADIOS.Radio The closest radio. +function RADIOS:GetClosestRadio(Coordinate, DistMax, ExcludeList) + + local radio=nil --#RADIOS.Radio + local distmin=math.huge + + ExcludeList=ExcludeList or {} + + for _,_radio in pairs(self.radios) do + local ra=_radio --#RADIOS.Radio + + if (not UTILS.IsInTable(ExcludeList, ra, "radioId")) then + + local dist=Coordinate:Get2DDistance(ra.coordinate) + + if dist=1e6 then + freq=freq/1e6 + unit="MHz" + elseif freq>=1e3 then + freq=freq/1e3 + unit="kHz" + end + + return freq, unit +end + +--- Get name of frequency band. +-- @param #RADIOS self +-- @param #number BandNumber Band as number. +-- @return #string Band name. +function RADIOS:_GetBandName(BandNumber) + + if BandNumber~=nil then + for bandName,bandNumber in pairs(ENUMS.FrequencyBand) do + if bandNumber==BandNumber then + return bandName + end + end + end + + return "Unknown" +end + +--- Get name of frequency band. +-- @param #RADIOS self +-- @param #table airbasenames Names of all airbases. +-- @param #string name Name of airbase. +-- @return #string Name of airbase +function RADIOS:_GetAirbaseName(airbasenames, name) + + local airbase=AIRBASE:FindByName(name) + + if airbase then + return name + else + for _,airbasename in pairs(airbasenames) do + if string.find(airbasename, name) then + return airbasename + end + end + end + + return "Unknown" +end + +--- Get name of frequency band. +-- @param #RADIOS self +-- @param #table airbases Table of airbases. +-- @param #number aid Airbase ID. +-- @return Wrapper.Airbase#AIRBASE Airbase matching the ID or nil. +function RADIOS:_GetAirbaseByID(airbases, aid) + + for _,_airbase in pairs(airbases) do + local airbase=_airbase --Wrapper.Airbase#AIRBASE + local id=airbase:GetID(true) + if id==aid then + return airbase + end + end + + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Navigation/Towns.lua b/Moose Development/Moose/Navigation/Towns.lua new file mode 100644 index 000000000..3825854f9 --- /dev/null +++ b/Moose Development/Moose/Navigation/Towns.lua @@ -0,0 +1,332 @@ +--- **NAVIGATION** - Towns of the map/theatre. +-- +-- **Main Features:** +-- +-- * Find towns of map +-- * Road and rail connections +-- * Find closest town to a given coordinate +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Navigation%20-%20Towns). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Navigation.Towns +-- @image MOOSE.JPG + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- TOWNS class. +-- @type TOWNS +-- +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field #table towns Towns. +-- +-- @extends Core.Base#BASE + +--- *Hope is the beacon that guides lost ships back to the shore.* +-- +-- === +-- +-- # The TOWNS Concept +-- +-- This class is designed to make information about towns of a map/theatre easier accessible. The information contains location and road/rail connections of the towns. +-- +-- **Note** that try to avoid hard coding stuff in Moose since DCS is updated frequently and things change. Therefore, the main source of information is either a file `towns.lua` that can be +-- found in the installation directory of DCS for each map or a table that the user needs to provide. +-- **Note** your `MissionScripting` environment needs to be desanitized to read this data. +-- +-- # Basic Setup +-- +-- A new `TOWNS` object can be created with the @{#TOWNS.NewFromFile}(*towns_lua_file*) function. +-- +-- local towns=TOWNS:NewFromFile("\Mods\terrains\\map\towns.lua") +-- towns:MarkerShow() +-- +-- This will load the towns from the `` for the specific map and place markers on the F10 map. This is the first step you should do to ensure that the file +-- you provided is correct and all relevant towns are present. +-- +-- # User Functions +-- +-- ## F10 Map Markers +-- +-- ## Position +-- +-- ## Get Closest Town +-- +-- +-- @field #TOWNS +TOWNS = { + ClassName = "TOWNS", + verbose = 0, + towns = {}, +} + +--- Town data. +-- @type TOWNS.Town +-- @field #string display_name Displayed name. +-- @field #string name Name of the town. +-- @field #number latitude Latitude. +-- @field #number longitude Longitude +-- @field DCS#Vec3 vec3 Position vector 3D. +-- @field Core.Point#COORDINATE coordinate The coordinate. +-- @field Core.Point#COORDINATE coordRoad The coordinate of the closest road. +-- @field Core.Point#COORDINATE coordRail The coordinate of the closest railway. +-- @field #number markerID ID for the F10 marker. + +--- TOWNS class version. +-- @field #string version +TOWNS.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot... +-- DONE: Road connection +-- DONE: Rail connection +-- DONE: Connection between towns + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor(s) +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new TOWNS class instance from a given table. +-- @param #TOWNS self +-- @param #table TownTable Table with all towns data. +-- @return #TOWNS self +function TOWNS:NewFromTable(TownTable) + + -- Inherit everything from BASE class. + self=BASE:Inherit(self, BASE:New()) -- #TOWNS + + for TownName,_town in pairs(TownTable) do + local town=_town --#TOWNS.Town + + town.name=TownName + + -- Get coordinate + town.coordinate=COORDINATE:NewFromLLDD(town.latitude, town.longitude) + + -- Get coordinate of closest road + town.coordRoad=town.coordinate:GetClosestPointToRoad() + + -- Get coordinate of closest rail + town.coordRail=town.coordinate:GetClosestPointToRoad(true) + + -- Add to table + table.insert(self.towns, town) + end + + -- Debug output + self:I(string.format("Added %d towns", #self.towns)) + + return self +end + + +--- Create a new TOWNS class instance from a given file. +-- @param #TOWNS self +-- @param #string FileName Full path to the file containing the towns data. +-- @return #TOWNS self +function TOWNS:NewFromFile(FileName) + + -- Inherit everything from BASE class. + self=BASE:Inherit(self, BASE:New()) -- #TOWNS + + local exists=UTILS.FileExists(FileName) + + if exists==false then + self:E(string.format("ERROR: file with towns info does not exist!")) + return nil + end + + -- This will create a global table `towns` + dofile(FileName) + + -- Get towns from table. + self=self:NewFromTable(towns) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get 3D position vector of a specific town. +-- @param #TOWNS self +-- @param #TOWNS.Town town The town data structure. +-- @return DCS#Vec3 Position vector. +function TOWNS:GetVec3(town) + return town.vec3 +end + +--- Get COORDINATE of a specific town. +-- @param #TOWNS self +-- @param #TOWNS.Town town The town data structure. +-- @return Core.Point#COORDINATE The coordinate. +function TOWNS:GetCoordinate(town) + return town.coordinate +end + +--- Get closest road coordinate of a town. +-- @param #TOWNS self +-- @param #TOWNS.Town town The town data structure. +-- @return Core.Point#COORDINATE The closest road coordinate. +function TOWNS:GetCoordRoad(town) + return town.coordRoad +end + +--- Get closest rail coordinate of a town. +-- @param #TOWNS self +-- @param #TOWNS.Town town The town data structure. +-- @return Core.Point#COORDINATE The closest rail coordinate. +function TOWNS:GetCoordRail(town) + return town.coordRail +end + +--- Get road or rail connection between two towns. +-- @param #TOWNS self +-- @param #TOWNS.Town townA The town data structure. +-- @param #TOWNS.Town townB The town data structure. +-- @param #boolean Railroad If `true`, find rail road connection +-- @return Core.Pathline#PATHLINE Pathline connecting the two towns on road. +function TOWNS:GetConnectionRoad(townA, townB, Railroad) + + local path=townA.coordRoad:GetPathlineOnRoad(townB.coordRoad, false, Railroad) + + return path +end + +--- Find closest town to a given coordinate. +-- @param #TOWNS self +-- @param Core.Point#COORDINATE Coordinate The reference coordinate. +-- @param #number DistMax (Optional) Max search distance in meters. +-- @param #table ExcludeList (Optional) List of towns excluded from the search. +-- @return #TOWNS.Town The closest town. +function TOWNS:GetClosestTown(Coordinate, DistMax, ExcludeList) + + local Town=nil --#TOWNS.Town + local distmin=math.huge + ExcludeList=ExcludeList or {} + + for _,_town in pairs(self.towns) do + local town=_town --#TOWNS.Town + + if (not UTILS.IsInTable(ExcludeList, town, "name")) then + + local dist=Coordinate:Get2DDistance(town.coordinate) + + if dist Needs WAREHOUSE update. +-- DONE: Spawn hot. +-- DONE: Make special request to transfer squadrons to anther airwing (or warehouse). +-- 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 LEGION class. + local self=BASE:Inherit(self, LEGION: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) + + -- Defaults: + self.nflightsCAP=0 + self.nflightsAWACS=0 + self.nflightsRecon=0 + self.nflightsTANKERboom=0 + self.nflightsTANKERprobe=0 + self.nflightsRecoveryTanker=0 + self.nflightsRescueHelo=0 + self.markpoints=false + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "FlightOnMission", "*") -- A FLIGHTGROUP was send on a Mission (AUFTRAG). + + ------------------------ + --- 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. + + + --- Triggers the FSM event "FlightOnMission". + -- @function [parent=#AIRWING] FlightOnMission + -- @param #AIRWING self + -- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FLIGHTGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "FlightOnMission" after a delay. + -- @function [parent=#AIRWING] __FlightOnMission + -- @param #AIRWING self + -- @param #number delay Delay in seconds. + -- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FLIGHTGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "FlightOnMission" event. + -- @function [parent=#AIRWING] OnAfterFlightOnMission + -- @param #AIRWING self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FLIGHTGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + 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.cohorts, 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 + + -- Relocate mission. + self:NewPayload(Squadron.templategroup, -1, AUFTRAG.Type.RELOCATECOHORT, 0) + + -- 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() + + -- Set the number of available payloads. + self:SetPayloadAmount(payload, Npayloads) + + -- Payload capabilities. + 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 AUFTRAG.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 + + -- Add RELOCATION for all. + if not AUFTRAG.CheckMissionType(AUFTRAG.Type.RELOCATECOHORT, MissionTypes) then + local capability={} --Ops.Auftrag#AUFTRAG.Capability + capability.MissionType=AUFTRAG.Type.RELOCATECOHORT + 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 + +--- Set the number of payload available. +-- @param #AIRWING self +-- @param #AIRWING.Payload Payload The payload table created by the `:NewPayload` function. +-- @param #number Navailable Number of payloads available to the airwing resources. Default 99 (which should be enough for most scenarios). Set to -1 for unlimited. +-- @return #AIRWING self +function AIRWING:SetPayloadAmount(Payload, Navailable) + + Navailable=Navailable or 99 + + if Payload then + + Payload.unlimited=Navailable<0 + if Payload.unlimited then + Payload.navail=1 + else + Payload.navail=Navailable + end + + end + + return self +end + +--- Increase or decrease the amount of available payloads. Unlimited playloads first need to be set to a limited number with the `SetPayloadAmount` function. +-- @param #AIRWING self +-- @param #AIRWING.Payload Payload The payload table created by the `:NewPayload` function. +-- @param #number N Number of payloads to be added. Use negative number to decrease amount. Default 1. +-- @return #AIRWING self +function AIRWING:IncreasePayloadAmount(Payload, N) + + N=N or 1 + + if Payload and Payload.navail>=0 then + + -- Increase/decrease amount. + Payload.navail=Payload.navail+N + + -- Ensure playload does not drop below 0. + Payload.navail=math.max(Payload.navail, 0) + + end + + return self +end + +--- Get amount of payloads available for a given playload. +-- @param #AIRWING self +-- @param #AIRWING.Payload Payload The payload table created by the `:NewPayload` function. +-- @return #number Number of payloads available. Unlimited payloads will return -1. +function AIRWING:GetPayloadAmount(Payload) + return Payload.navail +end + +--- Get capabilities of a given playload. +-- @param #AIRWING self +-- @param #AIRWING.Payload Payload The payload data table. +-- @return #table Capabilities. +function AIRWING:GetPayloadCapabilities(Payload) + return Payload.capabilities +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 and b.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=AUFTRAG.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 %s and aircraft type=%s:", MissionType, UnitType)) + for _,_payload in ipairs(self.payloads) do + local payload=_payload --#AIRWING.Payload + if payload.aircrafttype==UnitType and AUFTRAG.CheckMissionCapability(MissionType, payload.capabilities) then + local performace=self:GetPayloadPeformance(payload, MissionType) + self:I(self.lid..string.format("- %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..string.format("WARNING: Could not find a payload for airframe %s mission type %s!", UnitType, MissionType)) + 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 Functional.Warehouse#WAREHOUSE.Assetitem 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) + local squad=self:_GetCohort(SquadronName) + return squad +end + +--- Get squadron of an asset. +-- @param #AIRWING self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem 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 Functional.Warehouse#WAREHOUSE.Assetitem Asset The squad asset. +function AIRWING:RemoveAssetFromSquadron(Asset) + local squad=self:GetSquadronOfAsset(Asset) + if squad then + squad:DelAsset(Asset) + end +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 CAP flight formation. +-- @param #AIRWING self +-- @param #number Formation Formation to take, e.g. ENUMS.Formation.FixedWing.Trail.Close, also see [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_option_formation). +-- @return #AIRWING self +function AIRWING:SetCAPFormation(Formation) + self.capFormation = Formation + return self +end + +--- Set CAP close race track.We'll utilize the AUFTRAG PatrolRaceTrack instead of a standard race track orbit task. +-- @param #AIRWING self +-- @param #boolean OnOff If true, switch this on, else switch off. Off by default. +-- @return #AIRWING self +function AIRWING:SetCapCloseRaceTrack(OnOff) + self.capOptionPatrolRaceTrack = OnOff + return self +end + +--- Set CAP mission start to vary randomly between Start end End seconds. +-- @param #AIRWING self +-- @param #number Start +-- @param #number End +-- @return #AIRWING self +function AIRWING:SetCapStartTimeVariation(Start, End) + self.capOptionVaryStartTime = Start or 5 + self.capOptionVaryEndTime = End or 60 + 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 RECON flights constantly in the air. +-- @param #AIRWING self +-- @param #number n Number of flights. Default 1. +-- @return #AIRWING self +function AIRWING:SetNumberRecon(n) + self.nflightsRecon=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 self +-- @param #AIRWING.PatrolData point Patrol point table. +function AIRWING:UpdatePatrolPointMarker(point) + + if self and 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) + + if point.IsZonePoint and point.IsZonePoint == true and point.patrolzone then + -- update position + local Coordinate = point.patrolzone:GetCoordinate() + point.marker:UpdateCoordinate(Coordinate) + point.marker:UpdateText(text, 1.5) + else + point.marker:UpdateText(text, 1) + end + 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. Can be handed as a Core.Zone#ZONE object (e.g. in case you want the point to align with a moving zone). +-- @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. +-- @param #number RefuelSystem Refueling system: 0=Boom, 1=Probe. Default nil=any. +-- @return #AIRWING.PatrolData Patrol point table. +function AIRWING:NewPatrolPoint(Type, Coordinate, Altitude, Speed, Heading, LegLength, RefuelSystem) + + 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)) + if Coordinate and Coordinate:IsInstanceOf("ZONE_BASE") then + patrolpoint.IsZonePoint = true + patrolpoint.patrolzone = Coordinate + patrolpoint.coord = patrolpoint.patrolzone:GetCoordinate() + else + patrolpoint.IsZonePoint = false + end + 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 + patrolpoint.refuelsystem=RefuelSystem + + if self.markpoints then + patrolpoint.marker=MARKER:New(Coordinate, "New Patrol Point"):ToAll() + self: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. Can be handed as a Core.Zone#ZONE object (e.g. in case you want the point to align with a moving zone). +-- @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 RECON missions. +-- @param #AIRWING self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the patrol point. Can be handed as a Core.Zone#ZONE object (e.g. in case you want the point to align with a moving zone). +-- @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:AddPatrolPointRecon(Coordinate, Altitude, Speed, Heading, LegLength) + + local patrolpoint=self:NewPatrolPoint("RECON", Coordinate, Altitude, Speed, Heading, LegLength) + + table.insert(self.pointsRecon, patrolpoint) + + return self +end + +--- Add a patrol Point for TANKER missions. +-- @param #AIRWING self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the patrol point. Can be handed as a Core.Zone#ZONE object (e.g. in case you want the point to align with a moving zone). +-- @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. +-- @param #number RefuelSystem Set refueling system of tanker: 0=boom, 1=probe. Default any (=nil). +-- @return #AIRWING self +function AIRWING:AddPatrolPointTANKER(Coordinate, Altitude, Speed, Heading, LegLength, RefuelSystem) + + local patrolpoint=self:NewPatrolPoint("Tanker", Coordinate, Altitude, Speed, Heading, LegLength, RefuelSystem) + + 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. Can be handed as a Core.Zone#ZONE object (e.g. in case you want the point to align with a moving zone). +-- @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 + +--- Set airboss of this wing. He/she will take care that no missions are launched if the carrier is recovering. +-- @param #AIRWING self +-- @param Ops.Airboss#AIRBOSS airboss The AIRBOSS object. +-- @return #AIRWING self +function AIRWING:SetAirboss(airboss) + self.airboss=airboss + return self +end + +--- Set takeoff type. All assets of this airwing will be spawned with this takeoff type. +-- Spawning on runways is not supported. +-- @param #AIRWING self +-- @param #string TakeoffType Take off type: "Cold" (default) or "Hot" with engines on or "Air" for spawning in air. +-- @return #AIRWING self +function AIRWING: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 + elseif TakeoffType:lower()=="air" then + self.takeoffType=COORDINATE.WaypointType.TurningPoint + else + self.takeoffType=COORDINATE.WaypointType.TakeOffParking + end + return self +end + +--- Set takeoff type cold (default). All assets of this squadron will be spawned with engines off (cold). +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:SetTakeoffCold() + self:SetTakeoffType("Cold") + return self +end + +--- Set takeoff type hot. All assets of this squadron will be spawned with engines on (hot). +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:SetTakeoffHot() + self:SetTakeoffType("Hot") + return self +end + +--- Set takeoff type air. All assets of this squadron will be spawned in air above the airbase. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:SetTakeoffAir() + self:SetTakeoffType("Air") + return self +end + +--- Set the aircraft of the AirWing to land straight in. +-- @param #AIRWING self +-- @return #FLIGHTGROUP self +function AIRWING:SetLandingStraightIn() + self.OptionLandingStraightIn = true + return self +end + +--- Set the aircraft of the AirWing to land in pairs for groups > 1 aircraft. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:SetLandingForcePair() + self.OptionLandingForcePair = true + return self +end + +--- Set the aircraft of the AirWing to NOT land in pairs. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:SetLandingRestrictPair() + self.OptionLandingRestrictPair = true + return self +end + +--- Set the aircraft of the AirWing to land after overhead break. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:SetLandingOverheadBreak() + self.OptionLandingOverheadBreak = true + return self +end + +--- [Helicopter] Set the aircraft of the AirWing to prefer vertical takeoff and landing. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:SetOptionPreferVerticalLanding() + self.OptionPreferVerticalLanding = true + return self +end + +--- Set despawn after landing. Aircraft will be despawned after the landing event. +-- Can help to avoid DCS AI taxiing issues. +-- @param #AIRWING self +-- @param #boolean Switch If `true` (default), activate despawn after landing. +-- @return #AIRWING self +function AIRWING:SetDespawnAfterLanding(Switch) + if Switch then + self.despawnAfterLanding=Switch + else + self.despawnAfterLanding=true + end + return self +end + +--- Set despawn after holding. Aircraft will be despawned when they arrive at their holding position at the airbase. +-- Can help to avoid DCS AI taxiing issues. +-- @param #AIRWING self +-- @param #boolean Switch If `true` (default), activate despawn after landing. +-- @return #AIRWING self +function AIRWING:SetDespawnAfterHolding(Switch) + if Switch then + self.despawnAfterHolding=Switch + else + self.despawnAfterHolding=true + end + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start AIRWING FSM. +-- @param #AIRWING self +function AIRWING:onafterStart(From, Event, To) + + -- Start parent Warehouse. + self:GetParent(self, AIRWING).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() + + -- Check Recon missions. + self:CheckRECON() + + -- Display tactival overview. + self:_TacticalOverview() + + ---------------- + -- Transport --- + ---------------- + + -- Check transport queue. + self:CheckTransportQueue() + + -------------- + -- Mission --- + -------------- + + -- Check mission queue. + self:CheckMissionQueue() + + + -- 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.cohorts, 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:GetNumberOfRequiredAssets()) + local target=string.format("%d/%d Damage=%.1f", mission:CountMissionTargets(), mission:GetTargetInitialNumber(), mission:GetTargetDamage()) + local mystatus=mission:GetLegionStatus(self) + + text=text..string.format("\n[%d] %s %s: Status=%s [%s], Prio=%s, Assets=%s, Targets=%s", i, mission.name, mission.type, mystatus, 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.cohorts) 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:CountAssets(true), #squadron.assets, callsign, modex, skill) + end + self:I(self.lid..text) + end + +end + +--- Get patrol data. +-- @param #AIRWING self +-- @param #table PatrolPoints Patrol data points. +-- @param #number RefuelSystem If provided, only return points with the specific refueling system. +-- @return #AIRWING.PatrolData Patrol point data table. +function AIRWING:_GetPatrolData(PatrolPoints, RefuelSystem) + + -- 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) + + for _,_patrolpoint in pairs(PatrolPoints) do + local patrolpoint=_patrolpoint --#AIRWING.PatrolData + if patrolpoint.IsZonePoint and patrolpoint.IsZonePoint == true and patrolpoint.patrolzone then + -- update + patrolpoint.coord = patrolpoint.patrolzone:GetCoordinate() + end + if (RefuelSystem and patrolpoint.refuelsystem and RefuelSystem==patrolpoint.refuelsystem) or RefuelSystem==nil or patrolpoint.refuelsystem==nil then + return patrolpoint + end + end + + end + + -- Return a new point. + return self:NewPatrolPoint() +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=0 + + + -- Count CAP missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() and (mission.type==AUFTRAG.Type.GCICAP or mission.type == AUFTRAG.Type.PATROLRACETRACK) and mission.patroldata then + Ncap=Ncap+1 + end + + end + + for i=1,self.nflightsCAP-Ncap do + + local patrol=self:_GetPatrolData(self.pointsCAP) + + local altitude=patrol.altitude+1000*patrol.noccupied + + local missionCAP = nil -- Ops.Auftrag#AUFTRAG + + if self.capOptionPatrolRaceTrack then + + missionCAP=AUFTRAG:NewPATROL_RACETRACK(patrol.coord,altitude,patrol.speed,patrol.heading,patrol.leg, self.capFormation) + + else + + missionCAP=AUFTRAG:NewGCICAP(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg) + + end + + if self.capOptionVaryStartTime then + + local ClockStart = math.random(self.capOptionVaryStartTime, self.capOptionVaryEndTime) + + missionCAP:SetTime(ClockStart) + + end + + missionCAP.patroldata=patrol + + patrol.noccupied=patrol.noccupied+1 + + if self.markpoints then self:UpdatePatrolPointMarker(patrol) end + + self:AddMission(missionCAP) + + end + + return self +end + +--- Check how many RECON missions are assigned and add number of missing missions. +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:CheckRECON() + + local Ncap=0 + + -- Count CAP missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() and mission.type==AUFTRAG.Type.RECON and mission.patroldata then + Ncap=Ncap+1 + end + + end + + --self:I(self.lid.."Number of active RECON Missions: "..Ncap) + + for i=1,self.nflightsRecon-Ncap do + + --self:I(self.lid.."Creating RECON Missions: "..i) + + local patrol=self:_GetPatrolData(self.pointsRecon) + + local altitude=patrol.altitude --+1000*patrol.noccupied + + local ZoneSet = SET_ZONE:New() + local Zone = ZONE_RADIUS:New(self.alias.." Recon "..math.random(1,10000),patrol.coord:GetVec2(),UTILS.NMToMeters(patrol.leg/2)) + + ZoneSet:AddZone(Zone) + + if self.Debug then + Zone:DrawZone(self.coalition,{0,0,1},Alpha,FillColor,FillAlpha,2,true) + end + + local missionRECON=AUFTRAG:NewRECON(ZoneSet,patrol.speed,patrol.altitude,true) + + missionRECON.patroldata=patrol + missionRECON.categories={AUFTRAG.Category.AIRCRAFT} + + patrol.noccupied=patrol.noccupied+1 + + if self.markpoints then self:UpdatePatrolPointMarker(patrol) end + + self:AddMission(missionRECON) + + 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 missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() and mission.type==AUFTRAG.Type.TANKER and mission.patroldata then + if mission.refuelSystem==Unit.RefuelingSystem.BOOM_AND_RECEPTACLE then + Nboom=Nboom+1 + elseif mission.refuelSystem==Unit.RefuelingSystem.PROBE_AND_DROGUE then + Nprob=Nprob+1 + end + + end + + end + + -- Check missing boom tankers. + 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, Unit.RefuelingSystem.BOOM_AND_RECEPTACLE) + + mission.patroldata=patrol + + patrol.noccupied=patrol.noccupied+1 + + if self.markpoints then self:UpdatePatrolPointMarker(patrol) end + + self:AddMission(mission) + + end + + -- Check missing probe tankers. + 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, Unit.RefuelingSystem.PROBE_AND_DROGUE) + + mission.patroldata=patrol + + patrol.noccupied=patrol.noccupied+1 + + if self.markpoints then self: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=0 --self:CountMissionsInQueue({AUFTRAG.Type.AWACS}) + + -- Count AWACS missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission:IsNotOver() and mission.type==AUFTRAG.Type.AWACS and mission.patroldata then + N=N+1 + end + + end + + 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 self: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}) + + if self.airbase then + + 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 + + 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 Functional.Warehouse#WAREHOUSE.Assetitem 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 --Functional.Warehouse#WAREHOUSE.Assetitem + + -- 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 + +--- Add the ability to call back an Ops.AWACS#AWACS object with an FSM call "FlightOnMission(FlightGroup, Mission)". +-- @param #AIRWING self +-- @param Ops.AWACS#AWACS ConnectecdAwacs +-- @return #AIRWING self +function AIRWING:SetUsingOpsAwacs(ConnectecdAwacs) + self:I(self.lid .. "Added AWACS Object: "..ConnectecdAwacs:GetName() or "unknown") + self.UseConnectedOpsAwacs = true + self.ConnectedOpsAwacs = ConnectecdAwacs + return self +end + +--- Remove the ability to call back an Ops.AWACS#AWACS object with an FSM call "FlightOnMission(FlightGroup, Mission)". +-- @param #AIRWING self +-- @return #AIRWING self +function AIRWING:RemoveUsingOpsAwacs() + self:I(self.lid .. "Reomve AWACS Object: "..self.ConnectedOpsAwacs:GetName() or "unknown") + self.UseConnectedOpsAwacs = false + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "FlightOnMission". +-- @param #AIRWING self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup Ops flight group on mission. +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function AIRWING:onafterFlightOnMission(From, Event, To, FlightGroup, Mission) + -- Debug info. + self:T(self.lid..string.format("Group %s on %s mission %s", FlightGroup:GetName(), Mission:GetType(), Mission:GetName())) + if self.UseConnectedOpsAwacs and self.ConnectedOpsAwacs then + self.ConnectedOpsAwacs:__FlightOnMission(2,FlightGroup,Mission) + end + -- Landing Options + if self.OptionLandingForcePair then + FlightGroup:SetOptionLandingForcePair() + elseif self.OptionLandingOverheadBreak then + FlightGroup:SetOptionLandingOverheadBreak() + elseif self.OptionLandingRestrictPair then + FlightGroup:SetOptionLandingRestrictPair() + elseif self.OptionLandingStraightIn then + FlightGroup:SetOptionLandingStraightIn() + end + -- Landing Options Helo + if self.OptionPreferVerticalLanding then + FlightGroup:SetOptionPreferVertical() + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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=AUFTRAG.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 + +--- 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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua new file mode 100644 index 000000000..375cbe145 --- /dev/null +++ b/Moose Development/Moose/Ops/ArmyGroup.lua @@ -0,0 +1,2337 @@ +--- **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/Armygroup). +-- +-- === +-- +-- ### 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 Core.Set#SET_ZONE retreatZones Set of retreat zones. +-- @field #boolean suppressOn Bla +-- @field #boolean isSuppressed Bla +-- @field #number TsuppressMin Bla +-- @field #number TsuppressMax Bla +-- @field #number TsuppressAve Bla +-- @extends Ops.OpsGroup#OPSGROUP + +--- *Your soul may belong to Jesus, but your ass belongs to the marines.* -- Eugene B Sledge +-- +-- === +-- +-- # The ARMYGROUP Concept +-- +-- This class enhances ground groups. +-- +-- @field #ARMYGROUP +ARMYGROUP = { + ClassName = "ARMYGROUP", + formationPerma = nil, + engage = {}, +} + +--- Engage Target. +-- @type ARMYGROUP.Target +-- @field Ops.Target#TARGET Target The target. +-- @field Core.Point#COORDINATE Coordinate Last known coordinate of the target. +-- @field Ops.OpsGroup#OPSGROUP.Waypoint Waypoint the waypoint created to go to the target. +-- @field #number Speed Speed in knots. +-- @field #string Formation Formation used in the engagement. +-- @field #number roe ROE backup. +-- @field #number alarmstate Alarm state backup. + +--- Army Group version. +-- @field #string version +ARMYGROUP.version="1.0.3" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Suppression of fire. +-- TODO: Check if group is mobile. +-- TODO: F10 menu. +-- DONE: Retreat. +-- 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) + + -- First check if we already have an OPS group for this group. + local og=_DATABASE:GetOpsGroup(group) + if og then + og:I(og.lid..string.format("WARNING: OPS group already exists in data base!")) + return og + end + + -- 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:SetDefaultROE() + self:SetDefaultAlarmstate() + self:SetDefaultEPLRS(self.isEPLRS) + self:SetDefaultEmission() + 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("*", "RTZ", "Returning") -- Group is returning to (home) zone. + self:AddTransition("Holding", "Returned", "Returned") -- Group is returned to (home) zone, e.g. when unloaded from carrier. + self:AddTransition("Returning", "Returned", "Returned") -- Group is returned to (home) zone. + + 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") -- Order a retreat. + self:AddTransition("Retreating", "Retreated", "Retreated") -- Group retreated. + + self:AddTransition("*", "Suppressed", "*") -- Group is suppressed + self:AddTransition("*", "Unsuppressed", "*") -- Group is unsuppressed. + + self:AddTransition("Cruising", "EngageTarget", "Engaging") -- Engage a target from Cruising state + self:AddTransition("Holding", "EngageTarget", "Engaging") -- Engage a target from Holding state + self:AddTransition("OnDetour", "EngageTarget", "Engaging") -- Engage a target from OnDetour state + self:AddTransition("Engaging", "Disengage", "Cruising") -- Disengage and back to cruising. + + 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("*", "Rearmed", "Cruising") -- Group was rearmed. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Cruise". + -- @function [parent=#ARMYGROUP] Cruise + -- @param #ARMYGROUP self + -- @param #number Speed Speed in knots until next waypoint is reached. + -- @param #number Formation Formation. + + --- Triggers the FSM event "Cruise" after a delay. + -- @function [parent=#ARMYGROUP] __Cruise + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + -- @param #number Speed Speed in knots until next waypoint is reached. + -- @param #number Formation Formation. + + --- On after "Cruise" event. + -- @function [parent=#ARMYGROUP] OnAfterCruise + -- @param #ARMYGROUP 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. + -- @param #number Formation Formation. + + + --- Triggers the FSM event "FullStop". + -- @function [parent=#ARMYGROUP] FullStop + -- @param #ARMYGROUP self + + --- Triggers the FSM event "FullStop" after a delay. + -- @function [parent=#ARMYGROUP] __FullStop + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "FullStop" event. + -- @function [parent=#ARMYGROUP] OnAfterFullStop + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "RTZ". + -- @function [parent=#ARMYGROUP] RTZ + -- @param #ARMYGROUP self + + --- Triggers the FSM event "RTZ" after a delay. + -- @function [parent=#ARMYGROUP] __RTZ + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "RTZ" event. + -- @function [parent=#ARMYGROUP] OnAfterRTZ + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Returned". + -- @function [parent=#ARMYGROUP] Returned + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Returned" after a delay. + -- @function [parent=#ARMYGROUP] __Returned + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Returned" event. + -- @function [parent=#ARMYGROUP] OnAfterReturned + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Detour". + -- @function [parent=#ARMYGROUP] Detour + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Detour" after a delay. + -- @function [parent=#ARMYGROUP] __Detour + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Detour" event. + -- @function [parent=#ARMYGROUP] OnAfterDetour + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "DetourReached". + -- @function [parent=#ARMYGROUP] DetourReached + -- @param #ARMYGROUP self + + --- Triggers the FSM event "DetourReached" after a delay. + -- @function [parent=#ARMYGROUP] __DetourReached + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "DetourReached" event. + -- @function [parent=#ARMYGROUP] OnAfterDetourReached + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Retreat". + -- @function [parent=#ARMYGROUP] Retreat + -- @param #ARMYGROUP self + -- @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. + + --- Triggers the FSM event "Retreat" after a delay. + -- @function [parent=#ARMYGROUP] __Retreat + -- @param #ARMYGROUP self + -- @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. + -- @param #number delay Delay in seconds. + + --- On after "Retreat" event. + -- @function [parent=#ARMYGROUP] OnAfterRetreat + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Zone#ZONE_BASE Zone Zone where to retreat. + -- @param #number Formation Formation of the group. Can be #nil. + + --- Triggers the FSM event "Retreated". + -- @function [parent=#ARMYGROUP] Retreated + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Retreated" after a delay. + -- @function [parent=#ARMYGROUP] __Retreated + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Retreated" event. + -- @function [parent=#ARMYGROUP] OnAfterRetreated + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "EngageTarget". + -- @function [parent=#ARMYGROUP] EngageTarget + -- @param #ARMYGROUP self + -- @param Ops.Target#TARGET Target The target to be engaged. Can also be a GROUP or UNIT object. + -- @param #number Speed Speed in knots. + -- @param #string Formation Formation used in the engagement. + + --- Triggers the FSM event "EngageTarget" after a delay. + -- @function [parent=#ARMYGROUP] __EngageTarget + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Group#GROUP Group the group to be engaged. + -- @param #number Speed Speed in knots. + -- @param #string Formation Formation used in the engagement. + + + --- On after "EngageTarget" event. + -- @function [parent=#ARMYGROUP] OnAfterEngageTarget + -- @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. + -- @param #number Speed Speed in knots. + -- @param #string Formation Formation used in the engagement. + + + --- Triggers the FSM event "Disengage". + -- @function [parent=#ARMYGROUP] Disengage + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Disengage" after a delay. + -- @function [parent=#ARMYGROUP] __Disengage + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Disengage" event. + -- @function [parent=#ARMYGROUP] OnAfterDisengage + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Rearm". + -- @function [parent=#ARMYGROUP] Rearm + -- @param #ARMYGROUP self + -- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm. + -- @param #number Formation Formation of the group. + + --- Triggers the FSM event "Rearm" after a delay. + -- @function [parent=#ARMYGROUP] __Rearm + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + -- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm. + -- @param #number Formation Formation of the group. + + --- On after "Rearm" event. + -- @function [parent=#ARMYGROUP] OnAfterRearm + -- @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. + + + --- Triggers the FSM event "Rearming". + -- @function [parent=#ARMYGROUP] Rearming + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Rearming" after a delay. + -- @function [parent=#ARMYGROUP] __Rearming + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Rearming" event. + -- @function [parent=#ARMYGROUP] OnAfterRearming + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Rearmed". + -- @function [parent=#ARMYGROUP] Rearmed + -- @param #ARMYGROUP self + + --- Triggers the FSM event "Rearmed" after a delay. + -- @function [parent=#ARMYGROUP] __Rearmed + -- @param #ARMYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "Rearmed" event. + -- @function [parent=#ARMYGROUP] OnAfterRearmed + -- @param #ARMYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + -- 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.UnitLost, self.OnEventRemoveUnit) + self:HandleEvent(EVENTS.Hit, self.OnEventHit) + + -- Start the status monitoring. + self.timerStatus=TIMER:New(self.Status, self):Start(1, 30) + + -- 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) + + -- Add OPSGROUP to _DATABASE. + _DATABASE:AddOpsGroup(self) + + 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() + local coord=self:GetCoordinate():GetClosestPointToRoad() + return coord +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 *scheduled* task to fire at a given coordinate. +-- @param #ARMYGROUP self +-- @param #string Clock Time when to start the attack. +-- @param #number Heading Heading min in Degrees. +-- @param #number Alpha Shooting angle in Degrees. +-- @param #number Altitude Altitude in meters. +-- @param #number Radius Radius in meters. Default 100 m. +-- @param #number Nshots Number of shots to fire. Default nil. +-- @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:AddTaskBarrage(Clock, Heading, Alpha, Altitude, Radius, Nshots, WeaponType, Prio) + + Heading=Heading or 0 + + Alpha=Alpha or 60 + + Altitude=Altitude or 100 + + local distance=Altitude/math.tan(math.rad(Alpha)) + + local a=self:GetVec2() + + local vec2=UTILS.Vec2Translate(a, distance, Heading) + + --local coord=COORDINATE:NewFromVec2(vec2):MarkToAll("Fire At Point",ReadOnly,Text) + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, vec2, Radius, Nshots, WeaponType, Altitude) + + 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 + +--- Add a *scheduled* task to transport group(s). +-- @param #ARMYGROUP self +-- @param Core.Set#SET_GROUP GroupSet Set of cargo groups. Can also be a singe @{Wrapper.Group#GROUP} object. +-- @param Core.Zone#ZONE PickupZone Zone where the cargo is picked up. +-- @param Core.Zone#ZONE DeployZone Zone where the cargo is delivered to. +-- @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:AddTaskCargoGroup(GroupSet, PickupZone, DeployZone, Clock, Prio) + + local DCStask={} + DCStask.id="CargoTransport" + DCStask.params={} + DCStask.params.cargoqueu=1 + + 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 + +--- Set suppression on. average, minimum and maximum time a unit is suppressed each time it gets hit. +-- @param #ARMYGROUP 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. +-- @return #ARMYGROUP self +function ARMYGROUP:SetSuppressionOn(Tave, Tmin, Tmax) + + -- Activate suppression. + self.suppressionOn=true + + -- Minimum suppression time is input or default 5 sec (but at least 1 second). + self.TsuppressMin=Tmin or 1 + self.TsuppressMin=math.max(self.TsuppressMin, 1) + + -- Maximum suppression time is input or default but at least Tmin. + self.TsuppressMax=Tmax or 15 + self.TsuppressMax=math.max(self.TsuppressMax, self.TsuppressMin) + + -- Expected suppression time is input or default but at leat Tmin and at most Tmax. + self.TsuppressAve=Tave or 10 + self.TsuppressAve=math.max(self.TsuppressMin) + self.TsuppressAve=math.min(self.TsuppressMax) + + -- Debug Info + self:T(self.lid..string.format("Set ave suppression time to %d seconds.", self.TsuppressAve)) + self:T(self.lid..string.format("Set min suppression time to %d seconds.", self.TsuppressMin)) + self:T(self.lid..string.format("Set max suppression time to %d seconds.", self.TsuppressMax)) + + return self +end + +--- Set suppression off. +-- @param #ARMYGROUP self +-- @return #ARMYGROUP self +function ARMYGROUP:SetSuppressionOff() + -- Activate suppression. + self.suppressionOn=false +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:IsOutOfAmmo() or self:IsEngaging() or self:IsDead() or self:IsStopped() or self:IsInUtero() then + combatready=false + end + + if self:IsPickingup() or self:IsLoading() or self:IsTransporting() or self:IsLoaded() or self:IsCargo() or self:IsCarrier() then + combatready=false + end + + return combatready +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Update status. +-- @param #ARMYGROUP self +function ARMYGROUP:Status() + + -- FSM state. + local fsmstate=self:GetState() + + -- Is group alive? + local alive=self:IsAlive() + + -- Check that group EXISTS and is ACTIVE. + if alive then + + -- Update position etc. + self:_UpdatePosition() + + -- Check if group has detected any units. + self:_CheckDetectedUnits() + + -- Check ammo status. + self:_CheckAmmoStatus() + + -- Check damage of elements and group. + self:_CheckDamage() + + -- Check if group got stuck. + self:_CheckStuck() + + -- Update engagement. + if self:IsEngaging() then + self:_UpdateEngageTarget() + end + + -- Check if group is waiting. + if self:IsWaiting() then + if self.Twaiting and self.dTwait then + if timer.getAbsTime()>self.Twaiting+self.dTwait then + self.Twaiting=nil + self.dTwait=nil + if self:_CountPausedMissions()>0 then + self:UnpauseMission() + else + self:Cruise() + end + end + end + end + + + -- Get current mission (if any). + local mission=self:GetMissionCurrent() + + -- If mission, check if DCS task needs to be updated. + if mission and mission.updateDCSTask then + + if mission.type==AUFTRAG.Type.CAPTUREZONE then + + -- Get task. + local Task=mission:GetGroupWaypointTask(self) + + -- Update task: Engage or get new zone. + if mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.EXECUTING or mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.STARTED then + self:_UpdateTask(Task, mission) + end + + end + + end + + else + -- Check damage of elements and group. + self:_CheckDamage() + end + + -- Check that group EXISTS. + if alive~=nil then + + if self.verbose>=1 then + + -- Number of elements. + local nelem=self:CountElements() + local Nelem=#self.elements + + -- Get number of tasks and missions. + local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() + local nMissions=self:CountRemainingMissison() + + -- ROE and Alarm State. + local roe=self:GetROE() or -1 + local als=self:GetAlarmstate() or -1 + + -- 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 wpN=#self.waypoints or 0 + local wpF=tostring(self.passedfinalwp) + + -- Speed. + local speed=UTILS.MpsToKnots(self.velocity or 0) + local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed()) + + -- Altitude. + local alt=self.position and self.position.y or 0 + + -- Heading in degrees. + local hdg=self.heading or 0 + + -- TODO: GetFormation function. + local formation=self.option.Formation or "unknown" + + -- Life points. + local life=self.life or 0 + + -- Total ammo. + local ammo=self:GetAmmoTot().Total + + -- Detected units. + local ndetected=self.detectionOn and tostring(self.detectedunits:Count()) or "Off" + + -- Get cargo weight. + local cargo=0 + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + cargo=cargo+element.weightCargo + end + + -- Info text. + local text=string.format("%s [%d/%d]: ROE/AS=%d/%d | T/M=%d/%d | Wp=%d[%d]-->%d[%d]/%d [%s] | Life=%.1f | v=%.1f (%d) [%s] | Hdg=%03d | Ammo=%d | Detect=%s | Cargo=%.1f", + fsmstate, nelem, Nelem, roe, als, nTaskTot, nMissions, wpidxCurr, wpuidCurr, wpidxNext, wpuidNext, wpN, wpF, life, speed, speedEx, formation, hdg, ammo, ndetected, cargo) + self:I(self.lid..text) + + end + + else + + -- Info text. + if self.verbose>=1 then + local text=string.format("State %s: Alive=%s", fsmstate, tostring(self:IsAlive())) + self:I(self.lid..text) + end + + end + + --- + -- Elements + --- + + if self.verbose>=2 then + local text="Elements:" + for i,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + + local name=element.name + local status=element.status + local unit=element.unit + local life,life0=self:GetLifePoints(element) + + local life0=element.life0 + + -- Get ammo. + local ammo=self:GetAmmoElement(element) + + -- Output text for element. + text=text..string.format("\n[%d] %s: status=%s, life=%.1f/%.1f, guns=%d, cannons=%d, rockets=%d, missiles=%d, cargo=%d/%d kg", + i, name, status, life, life0, ammo.Guns, ammo.Cannons, ammo.Rockets, ammo.Missiles, element.weightCargo, element.weightMaxCargo) + end + if #self.elements==0 then + text=text.." none!" + end + self:T(self.lid..text) + end + + --- + -- Engage Detected Targets + --- + if self:IsCruising() and self.detectionOn and self.engagedetectedOn then + + local targetgroup, targetdist=self:_GetDetectedTarget() + + -- If we found a group, we engage it. + if targetgroup then + self:T(self.lid..string.format("Engaging target group %s at distance %d meters", targetgroup:GetName(), targetdist)) + self:EngageTarget(targetgroup) + end + + end + + + --- + -- Cargo + --- + + self:_CheckCargoTransport() + + + --- + -- Tasks & Missions + --- + + self:_PrintTaskAndMissionStatus() + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Events ==> See OPSGROUP +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "ElementSpawned" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP.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!")) + + -- 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("Weight = %.1f kg\n", self:GetWeightTotal()) + text=text..string.format("Cargo bay = %.1f kg\n", self:GetFreeCargobay()) + text=text..string.format("Has EPLRS = %s\n", tostring(self.isEPLRS)) + 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 + + -- Update position. + self:_UpdatePosition() + + -- Not dead or destroyed yet. + self.isDead=false + self.isDestroyed=false + + if self.isAI then + + -- Set default ROE. + self:SwitchROE(self.option.ROE) + + -- Set default Alarm State. + self:SwitchAlarmstate(self.option.Alarm) + + -- Set emission. + self:SwitchEmission(self.option.Emission) + + -- Set default EPLRS. + self:SwitchEPLRS(self.option.EPLRS) + + -- Set default Invisible. + self:SwitchInvisible(self.option.Invisible) + + -- Set default Immortal. + self:SwitchImmortal(self.option.Immortal) + + -- 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 + -- Will be set in update route. + --self.option.Formation=self.optionDefault.Formation + end + + -- Number of waypoints. + local Nwp=#self.waypoints + + -- Update route. + if Nwp>1 and self.isMobile then + self:T(self.lid..string.format("Got %d waypoints on spawn ==> Cruise in -1.0 sec!", Nwp)) + local wp=self:GetWaypointNext() + self.option.Formation=wp.action + --self:__Cruise(-1, nil, self.option.Formation) + self:__Cruise(-1) + else + self:T(self.lid.."No waypoints on spawn ==> Full Stop!") + self:FullStop() + end + + end + +end + +--- On before "UpdateRoute" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. +-- @param #number Speed Speed in knots. Default cruise speed. +-- @param #number Formation Formation of the group. +function ARMYGROUP:onbeforeUpdateRoute(From, Event, To, n, N, Speed, Formation) + + -- Is transition allowed? We assume yes until proven otherwise. + local allowed=true + local trepeat=nil + + if self:IsWaiting() then + self:T(self.lid.."Update route denied. Group is WAITING!") + return false + elseif self:IsInUtero() then + self:T(self.lid.."Update route denied. Group is INUTERO!") + return false + elseif self:IsDead() then + self:T(self.lid.."Update route denied. Group is DEAD!") + return false + elseif self:IsStopped() then + self:T(self.lid.."Update route denied. Group is STOPPED!") + return false + elseif self:IsHolding() then + self:T(self.lid.."Update route denied. Group is holding position!") + return false + elseif self:IsEngaging() then + self:T(self.lid.."Update route allowed. Group is engaging!") + return true + end + + -- Check for a current task. + if self.taskcurrent>0 then + + -- Get the current task. Must not be executing already. + local task=self:GetTaskByID(self.taskcurrent) + + if task then + if task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then + -- For patrol zone, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: PatrolZone") + elseif task.dcstask.id==AUFTRAG.SpecialTask.RECON then + -- For recon missions, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: ReconMission") + elseif task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then + -- For relocate + self:T2(self.lid.."Allowing update route for Task: Relocate Cohort") + elseif task.dcstask.id==AUFTRAG.SpecialTask.REARMING then + -- For relocate + self:T2(self.lid.."Allowing update route for Task: Rearming") + else + local taskname=task and task.description or "No description" + self:T(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. Therefore, also directly executed tasks should be added to the queue! + 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 in state %s: allowed=%s (repeat in %s)", self:GetState(), tostring(allowed), tostring(trepeat))) + + -- Try again? + if trepeat then + self:__UpdateRoute(trepeat, n) + end + + return allowed +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 Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. +-- @param #number Speed Speed in knots. Default cruise speed. +-- @param #number Formation Formation of the group. +function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, N, Speed, Formation) + + -- Update route from this waypoint number onwards. + n=n or self:GetWaypointIndexNext(self.adinfinitum) + + -- Max index. + N=N or #self.waypoints + N=math.min(N, #self.waypoints) + + -- Debug info. + local text=string.format("Update route state=%s: n=%s, N=%s, Speed=%s, Formation=%s", self:GetState(), tostring(n), tostring(N), tostring(Speed), tostring(Formation)) + self:T(self.lid..text) + + -- Waypoints including addtional wp onroad. + local waypoints={} + + -- Next waypoint. + local wp=self.waypoints[n] --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Current position. + local coordinate=self:GetCoordinate() + + -- Road coordinate. + local coordRoad=coordinate:GetClosestPointToRoad() + + -- Road distance. + local roaddist=coordinate:Get2DDistance(coordRoad) + + -- Formation at the current position. + local formation0=wp.action + if formation0==ENUMS.Formation.Vehicle.OnRoad then + -- Next waypoint is on road. Check if we are already on road. + if roaddist>10 then + -- Currently off road ==> we add an on road WP later. + formation0=ENUMS.Formation.Vehicle.OffRoad + else + -- Already on road. We won't add an extra on road WP. + formation0=ENUMS.Formation.Vehicle.OnRoad + end + end + + -- Debug + --env.info(self.lid.."FF formation0="..tostring(formation0)) + + -- Current point. + local current=coordinate:WaypointGround(UTILS.MpsToKmph(self.speedWp), formation0) + table.insert(waypoints, 1, current) + + -- Check if route consists of more than one waypoint (otherwise we have no previous waypoint) + if N-n>0 then + + -- Loop over waypoints. + for j=n, N do + + -- Index of previous waypoint. + local i=j-1 + + -- If we go to the first waypoint j=1 ==> i=0, so we take the last waypoint passed. E.g. when adinfinitum and passed final waypoint. + if i==0 then + i=self.currentwp + end + + -- Next waypoint. We create a copy because we need to modify it. + local wp=UTILS.DeepCopy(self.waypoints[j]) --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Previous waypoint. Index is i and not i-1 because we added the current position. + local wp0=self.waypoints[i] --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Debug + if false and self.attribute==GROUP.Attribute.GROUND_APC then + local text=string.format("FF Update: i=%d, wp[i]=%s, wp[i-1]=%s", i, wp.action, wp0.action) + env.info(text) + end + + -- Speed. + if Speed then + wp.speed=UTILS.KnotsToMps(tonumber(Speed)) + else + -- Take default waypoint speed. But make sure speed>0 if patrol ad infinitum. + if 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 + + -- Add waypoint in between because this waypoint is "On Road" but lies "Off Road". + if wp.action==ENUMS.Formation.Vehicle.OnRoad and wp0.roaddist>=0 then + + -- Add "On Road" waypoint in between. + local wproad=wp0.roadcoord:WaypointGround(UTILS.MpsToKmph(wp.speed), ENUMS.Formation.Vehicle.OnRoad) --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Debug + --wp0.roadcoord:MarkToAll(self.lid.." Added road wp near "..tostring(wproad.action)) + + -- Insert road waypoint. + table.insert(waypoints, wproad) + end + + -- Add waypoint in between because this waypoint is "On Road" but lies "Off Road". + if wp.action==ENUMS.Formation.Vehicle.OnRoad and wp.roaddist>=0 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(UTILS.MpsToKmph(wp.speed), ENUMS.Formation.Vehicle.OnRoad) --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Debug + --wp.roadcoord:MarkToAll(self.lid.." Added road wp far "..tostring(wproad.action)) + + -- Insert road waypoint. + table.insert(waypoints, wproad) + end + + -- Debug + --wp.coordinate:MarkToAll(self.lid.." Added wp actual"..tostring(wp.action)) + + -- Add waypoint. + table.insert(waypoints, wp) + end + + else + + --- + -- This is the case, where we have only one WP left. + -- Could be because we had only one WP and did a detour (temp waypoint, which was deleted). + --- + + -- Next waypoint. + local wp=UTILS.DeepCopy(self.waypoints[n]) --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Speed. + if wp.speed<0.1 then + wp.speed=UTILS.KmphToMps(self.speedCruise) + end + + -- Formation. + local formation=wp.action + if self.formationPerma then + formation=self.formationPerma + elseif Formation then + formation=Formation + end + + -- Debug + --env.info(self.lid..string.format("FF Formation %s", formation)) + + -- Add road waypoint. + if formation==ENUMS.Formation.Vehicle.OnRoad then + + if roaddist>10 then + + -- Add "On Road" waypoint in between. + local wproad=coordRoad:WaypointGround(UTILS.MpsToKmph(wp.speed), ENUMS.Formation.Vehicle.OnRoad) --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Debug + --coordRoad:MarkToAll(self.lid.." Added road wp near "..tostring(wp.action)) + + -- Insert road waypoint. + table.insert(waypoints, wproad) + + end + + if wp.roaddist>10 then + + -- Add "On Road" waypoint in between. + local wproad=wp.roadcoord:WaypointGround(UTILS.MpsToKmph(wp.speed), ENUMS.Formation.Vehicle.OnRoad) --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Debug + --wp.roadcoord:MarkToAll(self.lid.." Added road wp far "..tostring(wp.action)) + + -- Insert road waypoint. + table.insert(waypoints, wproad) + + end + + end + + -- Waypoint set set to on-road but lies off-road. We set it to off-road. the on-road wp has been inserted. + if wp.action==ENUMS.Formation.Vehicle.OnRoad and wp.roaddist>10 then + wp.action=ENUMS.Formation.Vehicle.OffRoad + end + + -- Debug + --wp.coordinate:MarkToAll(self.lid.." Added coord "..tostring(wp.action)) + + -- Add actual waypoint. + table.insert(waypoints, wp) + + end + + -- First (next wp). + local wp=waypoints[1] --Ops.OpsGroup#OPSGROUP.Waypoint + + -- Current set formation. + self.option.Formation=wp.action + + -- Current set speed in m/s. + self.speedWp=wp.speed + self:T(self.lid..string.format("Expected/waypoint speed=%.1f m/s", self.speedWp)) + + -- Debug output. + if self.verbose>=10 then --or self.attribute==GROUP.Attribute.GROUND_APC then + for i,_wp in pairs(waypoints) do + local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint + + local text=string.format("WP #%d UID=%d Formation=%s: Speed=%d m/s, Alt=%d m, Type=%s", i, wp.uid and wp.uid or -1, wp.action, wp.speed, wp.alt, wp.type) + + local coord=COORDINATE:NewFromWaypoint(wp):MarkToAll(text) + self:I(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:T(self.lid..string.format("WARNING: Passed final WP when UpdateRoute() ==> 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) + + if n then + + -- Speed to waypoint. + Speed=Speed or self:GetSpeedToWaypoint(n) + + -- Update the route. + self:__UpdateRoute(-0.01, n, nil, 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:GetWaypointCurrentUID() + + -- 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 "OutOfAmmo" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterOutOfAmmo(From, Event, To) + self:T(self.lid..string.format("Group is out of ammo at t=%.3f", timer.getTime())) + + -- Get current task. + local task=self:GetTaskCurrent() + + if task then + if task.dcstask.id=="FireAtPoint" or task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then + self:T(self.lid..string.format("Cancelling current %s task because out of ammo!", task.dcstask.id)) + self:TaskCancel(task) + end + end + + -- Fist, check if we want to rearm once out-of-ammo. + --TODO: IsMobile() check + if self.rearmOnOutOfAmmo then + local truck, dist=self:FindNearestAmmoSupply(30) + if truck then + self:T(self.lid..string.format("Found Ammo Truck %s [%s]", truck:GetName(), truck:GetTypeName())) + local Coordinate=truck:GetCoordinate() + self:__Rearm(-1, Coordinate) + return + end + end + + -- Second, check if we want to retreat once out of ammo. + if self.retreatOnOutOfAmmo then + self:T(self.lid.."Retreat on out of ammo") + self:__Retreat(-1) + return + end + + -- Third, check if we want to RTZ once out of ammo (unless we have a rearming mission in the queue). + if self.rtzOnOutOfAmmo and not self:IsMissionTypeInQueue(AUFTRAG.Type.REARMING) then + self:T(self.lid.."RTZ on out of ammo") + self:__RTZ(-1) + end + +end + + +--- On before "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:onbeforeRearm(From, Event, To, Coordinate, Formation) + + local dt=nil + local allowed=true + + -- Pause current mission. + if self:IsOnMission() then + local mission=self:GetMissionCurrent() + if mission and mission.type~=AUFTRAG.Type.REARMING then + self:T(self.lid.."Rearm command but have current mission ==> Pausing mission!") + self:PauseMission() + dt=-0.1 + allowed=false + else + self:T(self.lid.."Rearm command and current mission is REARMING ==> Transition ALLOWED!") + end + end + + -- Disengage. + if self:IsEngaging() then + self:T(self.lid.."Rearm command but currently engaging ==> Disengage!") + self:Disengage() + dt=-0.1 + allowed=false + end + + -- Check if coordinate is provided. + if allowed and not Coordinate then + local truck=self:FindNearestAmmoSupply() + if truck and truck:IsAlive() then + self:__Rearm(-0.1, truck:GetCoordinate(), Formation) + end + return false + end + + -- Try again... + if dt then + self:T(self.lid..string.format("Trying Rearm again in %.2f sec", dt)) + self:__Rearm(dt, Coordinate, Formation) + allowed=false + end + + return allowed +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) + + -- Debug info. + self:T(self.lid..string.format("Group send to rearm")) + + -- ID of current waypoint. + local uid=self:GetWaypointCurrentUID() + + -- 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 "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:T(self.lid.."Group rearmed") + + -- Get Current mission. + local mission=self:GetMissionCurrent() + + -- Check if this is a rearming mission. + if mission and mission.type==AUFTRAG.Type.REARMING then + + -- Rearmed ==> Mission Done! This also checks if the group is done. + self:MissionDone(mission) + + else + + -- Check group done. + self:_CheckGroupDone(1) + + end + +end + +--- On before "RTZ" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE Zone The zone to return to. +-- @param #number Formation Formation of the group. +function ARMYGROUP:onbeforeRTZ(From, Event, To, Zone, Formation) + self:T2(self.lid.."onbeforeRTZ") + + -- Zone. + local zone=Zone or self.homezone + + if zone then + + if (not self.isMobile) and (not self:IsInZone(zone)) then + self:Teleport(zone:GetCoordinate(), 0, true) + self:__RTZ(-1, Zone, Formation) + return false + end + + else + return false + end + + return true +end + +--- On after "RTZ" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE Zone The zone to return to. +-- @param #number Formation Formation of the group. +function ARMYGROUP:onafterRTZ(From, Event, To, Zone, Formation) + self:T(self.lid.."onafterRTZ") + + -- Zone. + local zone=Zone or self.homezone + + -- Cancel all missions in the queue. + self:CancelAllMissions() + + if zone then + + if self:IsInZone(zone) then + self:Returned() + else + + -- Debug info. + self:T(self.lid..string.format("RTZ to Zone %s", zone:GetName())) + + local Coordinate=zone:GetRandomCoordinate() + + -- ID of current waypoint. + local uid=self:GetWaypointCurrentUID() + + -- 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 + + else + self:T(self.lid.."ERROR: No RTZ zone given!") + end + +end + +--- On after "Returned" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterReturned(From, Event, To) + + -- Debug info. + self:T(self.lid..string.format("Group returned")) + + if self.legion then + -- Debug info. + self:T(self.lid..string.format("Adding group back to warehouse stock")) + + -- Add asset back in 10 seconds. + self.legion:__AddAsset(10, self.group, 1) + end + +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. +-- @param #number Speed Speed in knots. +-- @param #string Formation Formation used in the engagement. Default `ENUMS.Formation.Vehicle.Vee`. +function ARMYGROUP:onbeforeEngageTarget(From, Event, To, Target, Speed, Formation) + + local dt=nil + local allowed=true + + local ammo=self:GetAmmoTot() + + if ammo.Total==0 then + self:T(self.lid.."WARNING: Cannot engage TARGET because no ammo left!") + return false + end + + -- Get current mission. + local mission=self:GetMissionCurrent() + + -- Pause current mission unless it uses the EngageTarget command. + if mission and mission.type~=AUFTRAG.Type.GROUNDATTACK and mission.type~=AUFTRAG.Type.CAPTUREZONE then + self:T(self.lid.."Engage command but have current mission ==> Pausing mission!") + self:PauseMission() + dt=-0.1 + allowed=false + end + + -- Try again... + if dt then + self:T(self.lid..string.format("Trying Engage again in %.2f sec", dt)) + self:__EngageTarget(dt, Target) + allowed=false + end + + return allowed +end + +--- On after "EngageTarget" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Target#TARGET Target The target to be engaged. Can also be a group or unit. +-- @param #number Speed Attack speed in knots. +-- @param #string Formation Formation used in the engagement. Default `ENUMS.Formation.Vehicle.Vee`. +function ARMYGROUP:onafterEngageTarget(From, Event, To, Target, Speed, Formation) + self:T(self.lid.."Engaging Target") + + -- Make sure this is a 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()) + + -- Get a coordinate close to the target. + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.95) + + -- Backup ROE and alarm state. + self.engage.roe=self:GetROE() + self.engage.alarmstate=self:GetAlarmstate() + + -- Switch ROE and alarm state. + self:SwitchAlarmstate(ENUMS.AlarmState.Auto) + self:SwitchROE(ENUMS.ROE.OpenFire) + + -- ID of current waypoint. + local uid=self:GetWaypointCurrentUID() + + -- Set formation. + self.engage.Formation=Formation or ENUMS.Formation.Vehicle.Vee + + -- Set speed. + self.engage.Speed=Speed + + -- Add waypoint after current. + self.engage.Waypoint=self:AddWaypoint(intercoord, self.engage.Speed, uid, self.engage.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 + + -- Get current position vector. + local vec3=self.engage.Target:GetVec3() + + if vec3 then + + -- Distance to last known position of target. + local dist=UTILS.VecDist3D(vec3, self.engage.Coordinate:GetVec3()) + + -- Check line of sight to target. + local los=self:HasLoS(vec3) + + -- Check if target moved more than 100 meters or we do not have line of sight. + if dist>100 or los==false then + + -- Update new position. + self.engage.Coordinate:UpdateFromVec3(vec3) + + -- ID of current waypoint. + local uid=self:GetWaypointCurrentUID() + + -- Remove current waypoint + self:RemoveWaypointByID(self.engage.Waypoint.uid) + + -- Get new coordinate where to go. + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.95) + + -- Add waypoint after current. + self.engage.Waypoint=self:AddWaypoint(intercoord, self.engage.Speed, uid, self.engage.Formation, true) + + -- Set if we want to resume route after reaching the detour waypoint. + self.engage.Waypoint.detour=1 + + end + + else + + -- Could not get position of target (not alive any more?) ==> Disengage. + self:T(self.lid.."Could not get position of target ==> Disengage!") + self:Disengage() + + end + + else + + -- Target not alive any more ==> Disengage. + self:T(self.lid.."Target not ALIVE ==> Disengage!") + 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) + self:T(self.lid.."Disengage Target") + + -- Restore previous ROE and alarm state. + self:SwitchROE(self.engage.roe) + self:SwitchAlarmstate(self.engage.alarmstate) + + -- Get current task + local task=self:GetTaskCurrent() + + -- Get if current task is ground attack. + if task and task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK then + self:T(self.lid.."Disengage with current task GROUNDATTACK ==> Task Done!") + self:TaskDone(task) + end + + -- Remove current waypoint + if self.engage.Waypoint then + self:RemoveWaypointByID(self.engage.Waypoint.uid) + end + + -- Check group is done + 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:T(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) + + -- Debug info. + self:T(self.lid..string.format("Full stop!")) + + -- 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) + + -- Not waiting anymore. + self.Twaiting=nil + self.dTwait=nil + + -- Debug info. + self:T(self.lid..string.format("Cruise ==> Update route in 0.01 sec (speed=%s, formation=%s)", tostring(Speed), tostring(Formation))) + + -- Update route. + self:__UpdateRoute(-0.01, nil, nil, Speed, Formation) + +end + +--- On after "Hit" event. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT Enemy Unit that hit the element or `nil`. +function ARMYGROUP:onafterHit(From, Event, To, Enemy) + self:T(self.lid..string.format("ArmyGroup hit by %s", Enemy and Enemy:GetName() or "unknown")) + + if self.suppressionOn then + env.info(self.lid.."FF suppress") + self:_Suppress() + 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 #string 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) + + -- Debug info. + self:T(self.lid..string.format("AddWaypoint Formation = %s", tostring(Formation))) + + -- Create coordinate. + local coordinate=self:_CoordinateFromObject(Coordinate) + + -- Set waypoint index. + local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) + + -- Speed in knots. + Speed=Speed or self:GetSpeedCruise() + + -- Formation. + if not Formation then + if self.formationPerma then + Formation = self.formationPerma + elseif self.optionDefault.Formation then + Formation = self.optionDefault.Formation + elseif self.option.Formation then + Formation = self.option.Formation + else + -- Default formation is on road. + Formation = ENUMS.Formation.Vehicle.OnRoad + end + self:T2(self.lid..string.format("Formation set to = %s", tostring(Formation))) + end + + -- Create a Ground 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:__UpdateRoute(-0.01) + end + + return waypoint +end + +--- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. +-- @param #ARMYGROUP self +-- @param #table Template Template used to init the group. Default is `self.template`. +-- @param #number Delay Delay in seconds before group is initialized. Default `nil`, *i.e.* instantaneous. +-- @return #ARMYGROUP self +function ARMYGROUP:_InitGroup(Template, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, ARMYGROUP._InitGroup, self, Template, 0) + else + + -- First check if group was already initialized. + if self.groupinitialized then + self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!") + return + end + + -- Get template of group. + local template=Template or self:_GetTemplate() + + -- Ground are always AI. + self.isAI=true + + -- Is (template) group late activated. + self.isLateActivated=template.lateActivation + + -- Ground groups cannot be uncontrolled. + self.isUncontrolled=false + + -- Max speed in km/h. + self.speedMax=self.group:GetSpeedMax() + + -- Is group mobile? + if self.speedMax and self.speedMax>3.6 then + self.isMobile=true + else + self.isMobile=false + self.speedMax = 0 + end + + -- 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) + + -- Get current formation from first waypoint. + self.option.Formation=template.route.points[1].action + + -- Set default formation to "on road". + self.optionDefault.Formation=ENUMS.Formation.Vehicle.OnRoad + + -- Default TACAN off. + if not self.tacanDefault then + self:SetDefaultTACAN(nil, nil, nil, nil, true) + end + if not self.tacan then + self.tacan=UTILS.DeepCopy(self.tacanDefault) + end + + -- Units of the group. + local units=self.group:GetUnits() + + -- DCS group. + local dcsgroup=Group.getByName(self.groupname) + local size0=dcsgroup:getInitialSize() + local u=dcsgroup:getUnits() + + -- Quick check. + if #units~=size0 then + self:T(self.lid..string.format("ERROR: Got #units=%d but group consists of %d units! u=%d", #units, size0, #u)) + end + + -- Add elemets. + for _,unit in pairs(units) do + local unitname=unit:GetName() + self:_AddElementByName(unitname) + end + + -- Init done. + self.groupinitialized=true + end + + 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 or "Off road") + Permanently = Permanently or false + + if Permanently then + self.formationPerma=Formation + else + self.formationPerma=nil + end + + -- Set current formation. + self.option.Formation=Formation or "Off road" + + if self:IsInUtero() then + self:T(self.lid..string.format("Will switch formation to %s (permanently=%s) when group is spawned", tostring(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)", tostring(self.option.Formation), tostring(Permanently))) + + end + + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Find the neares ammo supply group within a given radius. +-- @param #ARMYGROUP self +-- @param #number Radius Search radius in NM. Default 30 NM. +-- @return Wrapper.Group#GROUP Closest ammo supplying group or `nil` if no group is in the given radius. +-- @return #number Distance to closest group in meters. +function ARMYGROUP:FindNearestAmmoSupply(Radius) + + -- Radius in meters. + Radius=UTILS.NMToMeters(Radius or 30) + + -- Current positon. + local coord=self:GetCoordinate() + + -- Get my coalition. + local myCoalition=self:GetCoalition() + + -- Scanned units. + local units=coord:ScanUnits(Radius) + + -- Find closest + local dmin=math.huge + local truck=nil --Wrapper.Unit#UNIT + for _,_unit in pairs(units.Set) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Check coaliton and if unit can supply ammo. + if unit:IsAlive() and unit:GetCoalition()==myCoalition and unit:IsAmmoSupply() and unit:GetVelocityKMH()<1 then + + -- Distance. + local d=coord:Get2DDistance(unit:GetCoord()) + + -- Check if distance is smaller. + if d self.TsuppressionOver then + self.TsuppressionOver=Tnow+Tsuppress + else + renew=false + end + end + + -- Recovery event will be called in Tsuppress seconds. + if renew then + self:__Unsuppressed(self.TsuppressionOver-Tnow) + end + + -- Debug message. + self:T(self.lid..string.format("Suppressed for %d sec", Tsuppress)) + +end + +--- Before "Recovered" event. Check if suppression time is over. +-- @param #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean +function ARMYGROUP:onbeforeUnsuppressed(From, Event, 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 #ARMYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARMYGROUP:onafterUnsuppressed(From, Event, To) + + -- Debug message. + local text=string.format("Group %s has recovered!", self:GetName()) + MESSAGE:New(text, 10):ToAll() + self:T(self.lid..text) + + -- Set ROE back to default. + self:SwitchROE(self.suppressionROE) + + -- Flare unit green. + if true then + self.group:FlareGreen() + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua new file mode 100644 index 000000000..a3ac50a46 --- /dev/null +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -0,0 +1,7206 @@ +---@diagnostic disable: undefined-global +--- **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 OPS classes like FLIGHTGROUP, NAVYGROUP, ARMYGROUP, AIRWING, etc. +-- * 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 #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 #table categories Mission categories. +-- @field #string status Mission status. +-- @field #table legions Assigned legions. +-- @field #table statusLegion Mission status of all assigned LEGIONs. +-- @field #string statusCommander Mission status of the COMMANDER. +-- @field #string statusChief Mission status of the CHIEF. +-- @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 abs. seconds. +-- @field #number Tstop Mission stop time in abs. seconds. +-- @field #number duration Mission duration in seconds. +-- @field #number durationExe Mission execution time in seconds. +-- @field #number Texecuting Time stamp (abs) when mission is executing. Is `#nil` on start. +-- @field #number Tpush Mission push/execute time in abs. seconds. +-- @field #number Tstarted Time stamp (abs) when mission is started. +-- @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 Ndead Number of assigned groups that are dead. +-- @field #number Nassigned Number of assigned groups. +-- @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 #boolean updateDCSTask If `true`, DCS task is updated at every status update of the assigned groups. +-- @field #table conditionStart Condition(s) that have to be true, before the mission will be started. +-- @field #table conditionSuccess If all conditions are true, the mission is cancelled. +-- @field #table conditionFailure If all conditions are true, the mission is cancelled. +-- @field #table conditionPush If all conditions are true, the mission is executed. Before, the group(s) wait at the mission execution waypoint. +-- @field #boolean conditionSuccessSet +-- @field #boolean conditionFailureSet +-- +-- @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 DCS#Vec2 orbitOffsetVec2 2D offset vector. +-- @field DCS#Vec2 orbitVec2 2D orbit vector. +-- @field #number orbitDeltaR Distance threshold in meters for moving orbit targets. +-- +-- @field Ops.Target#TARGET engageTarget Target data to engage. +-- @field #number targetHeading Heading of target in degrees. +-- +-- @field Ops.Operation#OPERATION operation Operation this mission is part of. +-- +-- @field #boolean teleport Groups are teleported to the mission ingress waypoint. +-- +-- @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 engageLength Length of engage (carpet or strafing) in meters. +-- @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 #string escortGroupName Name of the escorted group. +-- @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 transportPickupRadius Radius in meters for pickup zone. Default 500 m. +-- +-- @field Ops.OpsTransport#OPSTRANSPORT opstransport OPS transport assignment. +-- @field #number NcarriersMin Min number of required carrier assets. +-- @field #number NcarriersMax Max number of required carrier assets. +-- @field Core.Zone#ZONE transportDeployZone Deploy zone of an OPSTRANSPORT. +-- @field Core.Zone#ZONE transportDisembarkZone Disembark zone of an OPSTRANSPORT. +-- @param #table carrierCategories Transport group categories. +-- @field #table carrierAttributes Generalized attribute(s) of transport assets. +-- @field #table carrierProperties DCS attribute(s) of transport assets. +-- +-- @field #number artyRadius Radius in meters. +-- @field #number artyShots Number of shots fired. +-- @field #number artyAltitude Altitude in meters. Can be used for a Barrage. +-- @field #number artyHeading Heading in degrees (for Barrage). +-- @field #number artyAngle Shooting angle in degrees (for Barrage). +-- +-- @field #string alert5MissionType Alert 5 mission type. This is the mission type, the alerted assets will be able to carry out. +-- +-- @field #table attributes Generalized attribute(s) of assets. +-- @field #table properties DCS attribute(s) of assets. +-- +-- @field Ops.Chief#CHIEF chief The CHIEF managing this mission. +-- @field Ops.Commander#COMMANDER commander The COMMANDER managing this mission. +-- @field #table assets Warehouse assets assigned for this mission. +-- @field #number NassetsMin Min. number of required warehouse assets. +-- @field #number NassetsMax Max. number of required warehouse assets. +-- @field #number NescortMin Min. number of required escort assets for each group the mission is assigned to. +-- @field #number NescortMax Max. number of required escort assets for each group the mission is assigned to. +-- @field #string escortMissionType Escort mission type. +-- @field #table escortTargetTypes Target types that will be engaged. +-- @field #number escortEngageRange Engage range in nautical miles (NM). +-- @field #number Nassets Number of requested warehouse assets. +-- @field #table NassetsLegMin Number of required warehouse assets for each assigned legion. +-- @field #table NassetsLegMax Number of required warehouse assets for each assigned legion. +-- @field #table requestID The ID of the queued warehouse request. Necessary to cancel the request if the mission was cancelled before the request is processed. +-- @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 #table specialLegions User specified legions assigned for this mission. Only these will be considered for the job! +-- @field #table specialCohorts User specified cohorts assigned for this mission. Only these will be considered for the job! +-- @field #table transportLegions Legions explicitly requested for providing transport carrier assets. +-- @field #table transportCohorts Cohorts explicitly requested for providing transport carrier assets. +-- @field #table escortLegions Legions explicitly requested for providing escorting assets. +-- @field #table escortCohorts Cohorts explicitly requested for providing escorting assets. +-- +-- @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 by LEGION classes (AIRWING, BRIGADE, ...). +-- @field Core.Point#COORDINATE missionWaypointCoord Mission waypoint coordinate. +-- @field Core.Point#COORDINATE missionEgressCoord Mission egress waypoint coordinate. +-- @field Core.Point#COORDINATE missionIngressCoord Mission Ingress waypoint coordinate. +-- @field #number missionWaypointRadius Random radius in meters. +-- @field #boolean legionReturn If `true`, assets return to their legion (default). If `false`, they will stay alive. +-- +-- @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 #boolean optionEPLRS EPLRS datalink. +-- @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. +-- @field #boolean optionEmission Emission is on or off. +-- @field #boolean optionInvisible Invisible is on/off. +-- @field #boolean optionImmortal Immortal is on/off. +-- +-- @extends Core.Fsm#FSM + +--- *A warrior's mission is to foster the success of others.* -- Morihei Ueshiba +-- +-- === +-- +-- # 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. +-- +-- Similarly, an AUFTRAG can be given to ground or navel groups via the @{Ops.ArmyGroup#ARMYGROUP} or @{Ops.NavyGroup#NAVYGROUP} classes, respectively. These classes have also +-- AIRWING analouges, which are called BRIGADE and FLEET. Brigades and fleets will likewise select the best assets they have available and pass on the AUFTRAG to them. +-- +-- +-- One more up the food chain, an AUFTRAG can be passed to a COMMANDER. The commander will recruit the best assets of AIRWINGs, BRIGADEs and/or FLEETs 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}() 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. +-- +-- ## Strafing +-- +-- A strafing mission can be created with the @{#AUFTRAG.NewSTRAFING}() 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. +-- +-- ## Ground Escort +-- +-- An escort mission can be created with the @{#AUFTRAG.NewGROUNDESCORT}() function. +-- +-- ## 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 +-- +-- An reconnaissance mission can be created with the @{#AUFTRAG.NewRECON}() function. +-- +-- ## 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. +-- +-- ## CARGOTRANSPORT +-- +-- A cargo transport mission can be created with the @{#AUFTRAG.NewCARGOTRANSPORT}() function. +-- +-- ## HOVER +-- +-- A mission for a helicoptre or VSTOL plane to Hover at a point for a certain amount of time can be created with the @{#AUFTRAG.NewHOVER}() function. +-- +-- # Ground Missions +-- +-- ## ARTY +-- +-- An arty mission can be created with the @{#AUFTRAG.NewARTY}() function. +-- +-- ## GROUNDATTACK +-- +-- A ground attack mission can be created with the @{#AUFTRAG.NewGROUNDATTACK}() function. +-- +-- ## NAVALENGAGEMENT +-- +-- A naval engagement mission can be created with the @{#AUFTRAG.NewNAVALENGAGEMENT}() function. +-- +-- # Assigning Missions +-- +-- An AUFTRAG can be assigned to groups (FLIGHTGROUP, ARMYGROUP, NAVYGROUP), legions (AIRWING, BRIGADE, FLEET) or to a COMMANDER. +-- +-- ## Group Level +-- +-- ### Flight Group +-- +-- Assigning an AUFTRAG to a flight group is done via the @{Ops.FlightGroup#FLIGHTGROUP.AddMission} function. See FLIGHTGROUP docs for details. +-- +-- ### Army Group +-- +-- Assigning an AUFTRAG to an army group is done via the @{Ops.ArmyGroup#ARMYGROUP.AddMission} function. See ARMYGROUP docs for details. +-- +-- ### Navy Group +-- +-- Assigning an AUFTRAG to a navy group is done via the @{Ops.NavyGroup#NAVYGROUP.AddMission} function. See NAVYGROUP docs for details. +-- +-- ## Legion Level +-- +-- Adding an AUFTRAG to an airwing is done via the @{Ops.Airwing#AIRWING.AddMission} function. See AIRWING docs for further details. +-- Similarly, an AUFTRAG can be added to a brigade via the @{Ops.Brigade#BRIGADE.AddMission} function. +-- +-- ## Commander Level +-- +-- Assigning an AUFTRAG to a commander is done via the @{Ops.Commander#COMMANDER.AddMission} function. +-- The commander will select the best assets available from all the legions under his command. See COMMANDER docs for details. +-- +-- ## Chief Level +-- +-- Assigning an AUFTRAG to a commander is done via the @{Ops.Chief#CHIEF.AddMission} function. The chief will simply pass on the mission to his/her commander. +-- +-- # Transportation +-- +-- TODO +-- +-- +-- # Events +-- +-- The AUFTRAG class creates many useful (FSM) events, which can be used in the mission designers script. +-- +-- TODO +-- +-- +-- # Examples +-- +-- TODO +-- +-- +-- @field #AUFTRAG +AUFTRAG = { + ClassName = "AUFTRAG", + verbose = 0, + lid = nil, + auftragsnummer = nil, + groupdata = {}, + legions = {}, + statusLegion = {}, + requestID = {}, + assets = {}, + NassetsLegMin = {}, + NassetsLegMax = {}, + missionFraction = 0.5, + enrouteTasks = {}, + marker = nil, + markerOn = nil, + markerCoalition = nil, + conditionStart = {}, + conditionSuccess = {}, + conditionFailure = {}, + conditionPush = {}, + conditionSuccessSet = false, + conditionFailureSet = false, + repeatDelay = 1, +} + +--- 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 FAC Forward AirController mission. +-- @field #string FACA Forward AirController airborne mission. +-- @field #string FERRY Ferry mission. +-- @field #string GROUNDESCORT Ground escort 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. +-- @field #string OPSTRANSPORT Ops transport. +-- @field #string AMMOSUPPLY Ammo supply. +-- @field #string FUELSUPPLY Fuel supply. +-- @field #string ALERT5 Alert 5. +-- @field #string ONGUARD On guard. +-- @field #string ARMOREDGUARD On guard - with armored groups. +-- @field #string BARRAGE Barrage. +-- @field #string ARMORATTACK Armor attack. +-- @field #string CASENHANCED Enhanced CAS. +-- @field #string HOVER Hover. +-- @field #string LANDATCOORDINATE Land at coordinate. +-- @field #string GROUNDATTACK Ground attack. +-- @field #string NAVALENGAGEMENT Naval engagement (similar to GROUNDATTACK). +-- @field #string CARGOTRANSPORT Cargo transport. +-- @field #string RELOCATECOHORT Relocate a cohort from one legion to another. +-- @field #string AIRDEFENSE Air defense. +-- @field #string EWR Early Warning Radar. +-- @field #string RECOVERYTANKER Recovery tanker. +-- @field #string REARMING Rearming mission. +-- @field #string CAPTUREZONE Capture zone mission. +-- @field #string NOTHING Nothing. +-- @field #string PATROLRACETRACK Patrol Racetrack. +-- @field #string STRAFING Strafing run. +AUFTRAG.Type={ + ANTISHIP="Anti Ship", + AWACS="AWACS", + BAI="BAI", + BOMBING="Bombing", + BOMBRUNWAY="Bomb Runway", + BOMBCARPET="Carpet Bombing", + CAP="CAP", + CAS="CAS", + ESCORT="Escort", + FAC="FAC", + FACA="FAC-A", + FERRY="Ferry Flight", + GROUNDESCORT="Ground Escort", + 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", + OPSTRANSPORT="Ops Transport", + AMMOSUPPLY="Ammo Supply", + FUELSUPPLY="Fuel Supply", + ALERT5="Alert5", + ONGUARD="On Guard", + ARMOREDGUARD="Armored Guard", + BARRAGE="Barrage", + ARMORATTACK="Armor Attack", + CASENHANCED="CAS Enhanced", + HOVER="Hover", + LANDATCOORDINATE="Land at Coordinate", + GROUNDATTACK="Ground Attack", + NAVALENGAGEMENT="Naval Engagement", + CARGOTRANSPORT="Cargo Transport", + RELOCATECOHORT="Relocate Cohort", + AIRDEFENSE="Air Defence", + EWR="Early Warning Radar", + REARMING="Rearming", + CAPTUREZONE="Capture Zone", + NOTHING="Nothing", + PATROLRACETRACK="Patrol Racetrack", + STRAFING="Strafing", +} + +--- Special task description. +-- @type AUFTRAG.SpecialTask +-- @field #string FORMATION AI formation task. +-- @field #string PATROLZONE Patrol zone task. +-- @field #string RECON Recon task. +-- @field #string AMMOSUPPLY Ammo Supply. +-- @field #string FUELSUPPLY Fuel Supply. +-- @field #string ALERT5 Alert 5 task. +-- @field #string ONGUARD On guard. +-- @field #string ARMOREDGUARD On guard with armor. +-- @field #string BARRAGE Barrage. +-- @field #string HOVER Hover. +-- @field #string GROUNDATTACK Ground attack. +-- @field #string NAVALENGAGEMENT Naval engagement. +-- @field #string FERRY Ferry mission. +-- @field #string RELOCATECOHORT Relocate cohort. +-- @field #string AIRDEFENSE Air defense. +-- @field #string EWR Early Warning Radar. +-- @field #string RECOVERYTANKER Recovery tanker. +-- @field #string REARMING Rearming. +-- @field #string CAPTUREZONE Capture OPS zone. +-- @field #string NOTHING Nothing. +-- @field #string PATROLRACETRACK Patrol Racetrack. +AUFTRAG.SpecialTask={ + FORMATION="Formation", + PATROLZONE="PatrolZone", + RECON="ReconMission", + AMMOSUPPLY="Ammo Supply", + FUELSUPPLY="Fuel Supply", + ALERT5="Alert5", + ONGUARD="On Guard", + ARMOREDGUARD="ArmoredGuard", + BARRAGE="Barrage", + ARMORATTACK="AmorAttack", + HOVER="Hover", + GROUNDATTACK="Ground Attack", + NAVALENGAGEMENT="Naval Engagement", + FERRY="Ferry", + RELOCATECOHORT="Relocate Cohort", + AIRDEFENSE="Air Defense", + EWR="Early Warning Radar", + RECOVERYTANKER="Recovery Tanker", + REARMING="Rearming", + CAPTUREZONE="Capture Zone", + NOTHING="Nothing", + PATROLRACETRACK="Patrol Racetrack", +} + +--- Mission status. +-- @type AUFTRAG.Status +-- @field #string PLANNED Mission is at the early planning stage and has not been added to any queue. +-- @field #string QUEUED Mission is queued at a LEGION. +-- @field #string REQUESTED Mission assets were requested from the warehouse. +-- @field #string SCHEDULED Mission is scheduled in an OPSGROUP 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", +} + +--- Mission category. +-- @type AUFTRAG.Category +-- @field #string AIRCRAFT Airplanes and helicopters. +-- @field #string AIRPLANE Airplanes. +-- @field #string HELICOPTER Helicopter. +-- @field #string GROUND Ground troops. +-- @field #string NAVAL Naval grous. +AUFTRAG.Category={ + ALL="All", + AIRCRAFT="Aircraft", + AIRPLANE="Airplane", + HELICOPTER="Helicopter", + GROUND="Ground", + NAVAL="Naval", +} + +--- 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 Ingress waypoint coordinate. +-- @field #number waypointindex Mission (ingress) Waypoint UID. +-- @field #number waypointEgressUID Egress Waypoint UID. +-- @field Core.Point#COORDINATE wpegresscoordinate Egress waypoint coordinate. +-- +-- @field Ops.OpsGroup#OPSGROUP.Task waypointtask Waypoint task. +-- @field #string status Group mission status. +-- @field Functional.Warehouse#WAREHOUSE.Assetitem asset The warehouse asset. + + +--- AUFTRAG class version. +-- @field #string version +AUFTRAG.version="1.3.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Replace engageRange by missionRange. Here and in other classes. CTRL+H is your friend! +-- TODO: Mission success options damaged, destroyed. +-- TODO: F10 marker to create new missions. +-- DONE: Add option that assets do not return to their legion. +-- DONE: Add Capture zone task. +-- DONE: Add orbit mission for moving anker points. +-- DONE: Add recovery tanker mission for boat ops. +-- DONE: Added auftrag category. +-- DONE: Missions can be assigned to multiple legions. +-- DONE: Option to assign a specific payload for the mission (requires an AIRWING). +-- NOPE: Clone mission. How? Deepcopy? ==> Create a new auftrag. +-- DONE: Recon mission. What input? Set of coordinates? +-- 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:SetName() + self:SetPriority() + self:SetTime() + self:SetRequiredAssets() + self.engageAsGroup=true + self.dTevaluate=5 + + -- Init counters and stuff. + self.repeated=0 + self.repeatedSuccess=0 + self.repeatedFailure=0 + self.Nrepeat=0 + self.NrepeatFailure=0 + self.NrepeatSuccess=0 + self.Ncasualties=0 + self.Nkills=0 + self.Nelements=0 + self.Ngroups=0 + self.Nassigned=nil + self.Ndead=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. Could be in the queue of a COMMANDER or CHIEF. + self:AddTransition(AUFTRAG.Status.PLANNED, "Queued", AUFTRAG.Status.QUEUED) -- Mission is in queue of a LEGION. + 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", AUFTRAG.Status.CANCELLED) -- 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", "*") + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Status". + -- @function [parent=#AUFTRAG] Status + -- @param #AUFTRAG self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#AUFTRAG] __Status + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". + -- @function [parent=#AUFTRAG] Stop + -- @param #AUFTRAG self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#AUFTRAG] __Stop + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Planned". + -- @function [parent=#AUFTRAG] Planned + -- @param #AUFTRAG self + + --- Triggers the FSM event "Planned" after a delay. + -- @function [parent=#AUFTRAG] __Planned + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Planned" event. + -- @function [parent=#AUFTRAG] OnAfterPlanned + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Queued". + -- @function [parent=#AUFTRAG] Queued + -- @param #AUFTRAG self + + --- Triggers the FSM event "Queued" after a delay. + -- @function [parent=#AUFTRAG] __Queued + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Queued" event. + -- @function [parent=#AUFTRAG] OnAfterQueued + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Requested". + -- @function [parent=#AUFTRAG] Requested + -- @param #AUFTRAG self + + --- Triggers the FSM event "Requested" after a delay. + -- @function [parent=#AUFTRAG] __Requested + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Requested" event. + -- @function [parent=#AUFTRAG] OnAfterRequested + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Scheduled". + -- @function [parent=#AUFTRAG] Scheduled + -- @param #AUFTRAG self + + --- Triggers the FSM event "Scheduled" after a delay. + -- @function [parent=#AUFTRAG] __Scheduled + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Scheduled" event. + -- @function [parent=#AUFTRAG] OnAfterScheduled + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Started". + -- @function [parent=#AUFTRAG] Started + -- @param #AUFTRAG self + + --- Triggers the FSM event "Started" after a delay. + -- @function [parent=#AUFTRAG] __Started + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Started" event. + -- @function [parent=#AUFTRAG] OnAfterStarted + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Executing". + -- @function [parent=#AUFTRAG] Executing + -- @param #AUFTRAG self + + --- Triggers the FSM event "Executing" after a delay. + -- @function [parent=#AUFTRAG] __Executing + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Executing" event. + -- @function [parent=#AUFTRAG] OnAfterExecuting + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Cancel". + -- @function [parent=#AUFTRAG] Cancel + -- @param #AUFTRAG self + + --- Triggers the FSM event "Cancel" after a delay. + -- @function [parent=#AUFTRAG] __Cancel + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Cancel" event. + -- @function [parent=#AUFTRAG] OnAfterCancel + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Done". + -- @function [parent=#AUFTRAG] Done + -- @param #AUFTRAG self + + --- Triggers the FSM event "Done" after a delay. + -- @function [parent=#AUFTRAG] __Done + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Done" event. + -- @function [parent=#AUFTRAG] OnAfterDone + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Success". + -- @function [parent=#AUFTRAG] Success + -- @param #AUFTRAG self + + --- Triggers the FSM event "Success" after a delay. + -- @function [parent=#AUFTRAG] __Success + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Success" event. + -- @function [parent=#AUFTRAG] OnAfterSuccess + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Triggers the FSM event "Failed". + -- @function [parent=#AUFTRAG] Failed + -- @param #AUFTRAG self + + --- Triggers the FSM event "Failed" after a delay. + -- @function [parent=#AUFTRAG] __Failed + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Failed" event. + -- @function [parent=#AUFTRAG] OnAfterFailed + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Triggers the FSM event "Repeat". + -- @function [parent=#AUFTRAG] Repeat + -- @param #AUFTRAG self + + --- Triggers the FSM event "Repeat" after a delay. + -- @function [parent=#AUFTRAG] __Repeat + -- @param #AUFTRAG self + -- @param #number delay Delay in seconds. + + --- On after "Repeat" event. + -- @function [parent=#AUFTRAG] OnAfterRepeat + -- @param #AUFTRAG self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + -- Init status update. + self:__Status(-1) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Create Missions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- **[AIR]** 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.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR ROTARY]** Create an HOVER mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Where to hover. +-- @param #number Altitude Hover altitude in feet AGL. Default is 50 feet above ground. +-- @param #number Time Time in seconds to hold the hover. Default 300 seconds. +-- @param #number Speed Speed in knots to fly to the target coordinate. Default 150kn. +-- @param #number MissionAlt Altitude to fly towards the mission in feet AGL. Default 1000ft. +-- @return #AUFTRAG self +function AUFTRAG:NewHOVER(Coordinate, Altitude, Time, Speed, MissionAlt) + + local mission=AUFTRAG:New(AUFTRAG.Type.HOVER) + + -- Altitude. + if Altitude then + mission.hoverAltitude=Coordinate:GetLandHeight()+UTILS.FeetToMeters(Altitude) + else + mission.hoverAltitude=Coordinate:GetLandHeight()+UTILS.FeetToMeters(50) + end + + mission:_TargetFromObject(Coordinate) + + mission.hoverSpeed = 0.1 -- the DCS Task itself will shortly be build with this so MPS + mission.hoverTime = Time or 300 + self:SetMissionSpeed(Speed or 150) + self:SetMissionAltitude(MissionAlt or 1000) + + -- Mission options: + mission.missionFraction=0.9 + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.categories={AUFTRAG.Category.HELICOPTER} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR ROTARY]** Create an LANDATCOORDINATE mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Where to land. +-- @param #number OuterRadius (Optional) Vary the coordinate by this many feet, e.g. get a new random coordinate between OuterRadius and (optionally) avoiding InnerRadius of the coordinate. +-- @param #number InnerRadius (Optional) Vary the coordinate by this many feet, e.g. get a new random coordinate between OuterRadius and (optionally) avoiding InnerRadius of the coordinate. +-- @param #number Time Time in seconds to stay. Default 300 seconds. +-- @param #number Speed Speed in knots to fly to the target coordinate. Default 150kn. +-- @param #number MissionAlt Altitude to fly towards the mission in feet AGL. Default 1000ft. +-- @param #boolean CombatLanding (Optional) If true, set the Combat Landing option. +-- @param #number DirectionAfterLand (Optional) Heading after landing in degrees. +-- @return #AUFTRAG self +function AUFTRAG:NewLANDATCOORDINATE(Coordinate, OuterRadius, InnerRadius, Time, Speed, MissionAlt, CombatLanding, DirectionAfterLand) + + local mission=AUFTRAG:New(AUFTRAG.Type.LANDATCOORDINATE) + + mission:_TargetFromObject(Coordinate) + + mission.stayTime = Time or 300 + mission.stayAt = Coordinate + mission.combatLand = CombatLanding + mission.directionAfter = DirectionAfterLand + self:SetMissionSpeed(Speed or 150) + self:SetMissionAltitude(MissionAlt or 1000) + + if OuterRadius then + mission.stayAt = Coordinate:GetRandomCoordinateInRadius(UTILS.FeetToMeters(OuterRadius),UTILS.FeetToMeters(InnerRadius or 0)) + end + + -- Mission options: + mission.missionFraction=0.9 + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.categories={AUFTRAG.Category.HELICOPTER} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR]** Create an enhanced orbit race track mission. Planes will keep closer to the track. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Where to start the race track. +-- @param #number Altitude (Optional) Altitude in feet. Defaults to 20,000ft ASL. +-- @param #number Speed (Optional) Speed in knots. Defaults to 300kn TAS. +-- @param #number Heading (Optional) Heading in degrees, 0 to 360. Defaults to 90 degree (East). +-- @param #number Leg (Optional) Leg of the race track in NM. Defaults to 10nm. +-- @param #number Formation (Optional) Formation to take, e.g. ENUMS.Formation.FixedWing.Trail.Close, also see [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_option_formation). +-- @return #AUFTRAG self +function AUFTRAG:NewPATROL_RACETRACK(Coordinate,Altitude,Speed,Heading,Leg,Formation) + + local mission = AUFTRAG:New(AUFTRAG.Type.PATROLRACETRACK) + + -- Target. + mission:_TargetFromObject(Coordinate) + + -- Set Altitude. + if Altitude then + mission.TrackAltitude=UTILS.FeetToMeters(Altitude) + else + mission.TrackAltitude=UTILS.FeetToMeters(20000) + end + + -- Points + mission.TrackPoint1 = Coordinate + + local leg = UTILS.NMToMeters(Leg) or UTILS.NMToMeters(10) + + local heading = Heading or 90 + + if heading < 0 or heading > 360 then heading = 90 end + + mission.TrackPoint2 = Coordinate:Translate(leg,heading,true) + + -- Orbit speed in m/s TAS. + mission.TrackSpeed = UTILS.IasToTas(UTILS.KnotsToKmph(Speed or 300), mission.TrackAltitude) + + -- Mission speed in km/h and altitude + mission.missionSpeed = UTILS.KnotsToKmph(Speed or 300) + mission.missionAltitude = mission.TrackAltitude * 0.9 + mission.missionTask=ENUMS.MissionTask.CAP + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- **[AIR]** 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 above sea level. Default is y component of `Coordinate`. +-- @param #number Speed Orbit indicated airspeed in knots at the set altitude ASL. 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) + + -- Target. + mission:_TargetFromObject(Coordinate) + + -- Set Altitude. + if Altitude then + mission.orbitAltitude=UTILS.FeetToMeters(Altitude) + else + mission.orbitAltitude=Coordinate.y + end + + -- Orbit speed in m/s TAS. + mission.orbitSpeed = UTILS.IasToTas(UTILS.KnotsToMps(Speed or 350), mission.orbitAltitude) + + -- Mission speed in km/h. + mission.missionSpeed = UTILS.KnotsToKmph(Speed or 350) + + if Leg then + mission.orbitLeg=UTILS.NMToMeters(Leg) + + -- Relative heading + if Heading and Heading<0 then + mission.orbitHeadingRel=true + Heading=-Heading + end + + -- Heading if given. + mission.orbitHeading=Heading + end + + -- Mission options: + mission.missionAltitude=mission.orbitAltitude*0.9 + mission.missionFraction=0.9 + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR]** 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 indicated airspeed in knots at the set altitude ASL. Default 350 KIAS. +-- @return #AUFTRAG self +function AUFTRAG:NewORBIT_CIRCLE(Coordinate, Altitude, Speed) + + local mission=AUFTRAG:NewORBIT(Coordinate, Altitude, Speed) + + return mission +end + +--- **[AIR]** 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 indicated airspeed in knots at the set altitude ASL. 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 + +--- **[AIR]** Create an ORBIT mission, where the aircraft will fly a circular or race-track pattern over a given group or unit. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP Group Group where to orbit around. Can also be a UNIT object. +-- @param #number Altitude Orbit altitude in feet. Default is 6,000 ft. +-- @param #number Speed Orbit indicated airspeed in knots at the set altitude ASL. Default 350 KIAS. +-- @param #number Leg Length of race-track in NM. Default nil. +-- @param #number Heading Heading of race-track pattern in degrees. Default is heading of the group. +-- @param DCS#Vec2 OffsetVec2 Offset 2D-vector {x=0, y=0} in NM with respect to the group. Default directly overhead. Can also be given in polar coordinates `{r=5, phi=45}`. +-- @param #number Distance Threshold distance in NM before orbit pattern is updated. Default 5 NM. +-- @return #AUFTRAG self +function AUFTRAG:NewORBIT_GROUP(Group, Altitude, Speed, Leg, Heading, OffsetVec2, Distance) + + -- Set default altitude. + Altitude = Altitude or 6000 + + -- Create orbit mission. + local mission=AUFTRAG:NewORBIT(Group, Altitude, Speed, Heading, Leg) + + -- DCS tasks needs to be updated from time to time. + mission.updateDCSTask=true + + -- Convert offset vector to meters. + if OffsetVec2 then + if OffsetVec2.x then + OffsetVec2.x=UTILS.NMToMeters(OffsetVec2.x) + end + if OffsetVec2.y then + OffsetVec2.y=UTILS.NMToMeters(OffsetVec2.y) + end + if OffsetVec2.r then + OffsetVec2.r=UTILS.NMToMeters(OffsetVec2.r) + end + end + + -- Offset vector. + mission.orbitOffsetVec2=OffsetVec2 + + -- Pattern update distance. + mission.orbitDeltaR=UTILS.NMToMeters(Distance or 5) + + -- Update task with offset etc. + mission:GetDCSMissionTask() + + return mission +end + + +--- **[AIR]** 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 combat 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 indicated airspeed in knots at the set altitude ASL. 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: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 + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + return mission +end + +--- **[AIR]** 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 indicated airspeed in knots at the set altitude ASL. Default 350 KIAS. +-- @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. Set to 0 for a simple circular orbit. +-- @param #number RefuelSystem Refueling system (0=boom, 1=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) + + local mission + if Leg == 0 then + mission=AUFTRAG:NewORBIT_CIRCLE(Coordinate,Altitude,Speed) + else + mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate,Altitude,Speed,Heading,Leg) + end + -- 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.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR]** 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.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + + +--- **[AIR]** 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.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR]** 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. + TargetTypes=UTILS.EnsureTable(TargetTypes, true) + + -- Set default altitude if not specified. + Altitude = Altitude or 10000 + + -- Create ORBIT first. + local mission=AUFTRAG:NewORBIT(Coordinate or ZoneCAP:GetCoordinate(), Altitude, Speed or 350, Heading, Leg) + + -- Mission type CAP. + mission.type=AUFTRAG.Type.CAP + mission:_SetLogID() + + -- DCS task parameters: + mission.engageZone=ZoneCAP or Coordinate + mission.engageTargetTypes=TargetTypes or {"Air"} + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.CAP + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + mission.missionSpeed = UTILS.KnotsToKmph(UTILS.KnotsToAltKIAS(Speed or 350, Altitude)) + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR]** Create a CAP mission over a (moving) group. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP Grp The grp to perform the CAP over. +-- @param #number Altitude Orbit altitude in feet. Default is 6,000 ft. +-- @param #number Speed Orbit speed in knots. Default 250 KIAS. +-- @param #number RelHeading Relative heading [0, 360) of race-track pattern in degrees wrt heading of the carrier. Default is heading of the carrier. +-- @param #number Leg Length of race-track in NM. Default 14 NM. +-- @param #number OffsetDist Relative distance of the first race-track point wrt to the carrier. Default 6 NM. +-- @param #number OffsetAngle Relative angle of the first race-track point wrt. to the carrier. Default 180 (behind the boat). +-- @param #number UpdateDistance Threshold distance in NM before orbit pattern is updated. Default 5 NM. +-- @param #table TargetTypes (Optional) Table of target types. Default `{"Air"}`. +-- @param #number EngageRange Max range in nautical miles that the escort group(s) will engage enemies. Default 32 NM (60 km). +-- @return #AUFTRAG self +function AUFTRAG:NewCAPGROUP(Grp, Altitude, Speed, RelHeading, Leg, OffsetDist, OffsetAngle, UpdateDistance, TargetTypes, EngageRange) + + -- Ensure given TargetTypes parameter is a table. + TargetTypes=UTILS.EnsureTable(TargetTypes, true) + + -- Six NM astern. + local OffsetVec2={r=OffsetDist or 6, phi=OffsetAngle or 180} + + -- Default leg. + Leg=Leg or 14 + + local Heading=nil + if RelHeading then + Heading=-math.abs(RelHeading) + end + + -- Create orbit mission. + local mission=AUFTRAG:NewORBIT_GROUP(Grp, Altitude, Speed, Leg, Heading, OffsetVec2, UpdateDistance) + -- Mission type CAP. + mission.type=AUFTRAG.Type.CAP + mission:_SetLogID() + + -- DCS task parameters: + local engage = EngageRange or 32 + local zoneCAPGroup = ZONE_GROUP:New("CAPGroup", Grp, UTILS.NMToMeters(engage)) + mission.engageZone=zoneCAPGroup + mission.engageTargetTypes=TargetTypes or {"Air"} + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.CAP + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR]** 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. + TargetTypes=UTILS.EnsureTable(TargetTypes, true) + + -- 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.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR]** Create a CASENHANCED mission. Group(s) will go to the zone and patrol it randomly. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE CasZone The CAS zone. +-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL. +-- @param #number Speed Speed in knots. +-- @param #number RangeMax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM. +-- @param Core.Set#SET_ZONE NoEngageZoneSet Set of zones in which targets are *not* engaged. Default is nowhere. +-- @param #table TargetTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default `{"Helicopters", "Ground Units", "Light armed ships"}`. +-- @return #AUFTRAG self +function AUFTRAG:NewCASENHANCED(CasZone, Altitude, Speed, RangeMax, NoEngageZoneSet, TargetTypes) + + local mission=AUFTRAG:New(AUFTRAG.Type.CASENHANCED) + + -- Ensure we got a ZONE and not just the zone name. + if type(CasZone)=="string" then + CasZone=ZONE:New(CasZone) + end + + mission:_TargetFromObject(CasZone) + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.CASENHANCED) + + mission:SetEngageDetected(RangeMax, TargetTypes or {"Helicopters", "Ground Units", "Light armed ships"}, CasZone, NoEngageZoneSet) + + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.missionFraction=0.5 + mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil + mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or nil + + -- Evaluate result after x secs. We might need time until targets have been detroyed. + mission.dTevaluate=15 + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + + return mission +end + +--- **[AIR, GROUND]** Create a FAC mission. Group(s) will go to the zone and patrol it randomly and act as FAC for detected units. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE FacZone The FAC zone (or name of zone) where to patrol. +-- @param #number Speed Speed in knots. +-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL. +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Modulation. +-- @return #AUFTRAG self +function AUFTRAG:NewFAC(FacZone, Speed, Altitude, Frequency, Modulation) + + local mission=AUFTRAG:New(AUFTRAG.Type.FAC) + + -- Ensure we got a ZONE and not just the zone name. + if type(FacZone)=="string" then + FacZone=ZONE:FindByName(FacZone) + end + + mission:_TargetFromObject(FacZone) + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.FAC) + + mission.facFreq=Frequency or 133 + mission.facModu=Modulation or radio.modulation.AM + + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.EvadeFire + 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.categories={AUFTRAG.Category.AIRCRAFT, AUFTRAG.Category.GROUND} + + mission.DCStask=mission:GetDCSMissionTask() + + + return mission +end + + +--- **[AIR]** 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.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- **[AIR]** 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 5000 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.Auto + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 5000) + + -- 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.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR]** 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 25000 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.Auto + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) + + -- 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.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR]** Create a SEAD in Zone mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE TargetZone The target zone to attack. +-- @param #number Altitude Engage altitude in feet. Default 25000 ft. +-- @param #table TargetTypes Table of string of DCS known target types, defaults to {"Air Defence"}. See [DCS Target Attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) +-- @param #number Duration Engage this much time when the AUFTRAG starts executing. +-- @return #AUFTRAG self +function AUFTRAG:NewSEADInZone(TargetZone, Altitude, TargetTypes, Duration) + + local mission=AUFTRAG:New(AUFTRAG.Type.SEAD) + + --mission:_TargetFromObject(TargetZone) + + -- DCS Task options: + mission.engageWeaponType=ENUMS.WeaponFlag.Auto + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) + mission.engageZone = TargetZone + mission.engageTargetTypes = TargetTypes or {"Air Defence"} + + -- 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.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + mission:SetDuration(Duration or 1800) + + return mission +end + +--- **[AIR]** 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, STATIC, SET_GROUP, SET_UNIT, SET_STATIC or TARGET object. +-- @param #number Altitude Engage altitude in feet. Default 2000 ft. +-- @param #number EngageWeaponType Which weapon to use. Defaults to auto, ie ENUMS.WeaponFlag.Auto. See ENUMS.WeaponFlag for options. +-- @return #AUFTRAG self +function AUFTRAG:NewSTRIKE(Target, Altitude, EngageWeaponType) + + local mission=AUFTRAG:New(AUFTRAG.Type.STRIKE) + + mission:_TargetFromObject(Target) + + -- DCS Task options: + mission.engageWeaponType=EngageWeaponType or ENUMS.WeaponFlag.Auto + 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.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR]** Create a BOMBING mission. Flight will drop bombs a specified coordinate. +-- See [DCS task bombing](https://wiki.hoggitworld.com/view/DCS_task_bombing). +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT, STATIC, SET_GROUP, SET_UNIT, SET_STATIC or TARGET object. +-- @param #number Altitude Engage altitude in feet. Default 25000 ft. +-- @param #number EngageWeaponType Which weapon to use. Defaults to auto, ie ENUMS.WeaponFlag.Auto. See ENUMS.WeaponFlag for options. +-- @param #boolean Divebomb If true, use a dive bombing attack approach. +-- @return #AUFTRAG self +function AUFTRAG:NewBOMBING(Target, Altitude, EngageWeaponType, Divebomb) + + local mission=AUFTRAG:New(AUFTRAG.Type.BOMBING) + + mission:_TargetFromObject(Target) + + -- DCS task options: + mission.engageWeaponType=EngageWeaponType or ENUMS.WeaponFlag.Auto + 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. + mission.optionDivebomb = Divebomb or nil + + -- Evaluate result after 5 min. We might need time until the bombs have dropped and targets have been detroyed. + mission.dTevaluate=5*60 + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + -- Get DCS task. + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR]** Create a STRAFING mission. Assigns a point on the ground for which the AI will do a strafing run with guns or rockets. +-- See [DCS task strafing](https://wiki.hoggitworld.com/view/DCS_task_strafing). +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT, STATIC or TARGET object. +-- @param #number Altitude Engage altitude in feet. Default 1000 ft. +-- @param #number Length The total length of the strafing target in meters. Default `nil`. +-- @return #AUFTRAG self +function AUFTRAG:NewSTRAFING(Target, Altitude, Length) + + local mission=AUFTRAG:New(AUFTRAG.Type.STRAFING) + + mission:_TargetFromObject(Target) + + -- DCS task options: + mission.engageWeaponType=805337088 -- Corresponds to guns/cannons (805306368) + any rocket (30720). This is the default when selecting this task in the ME. + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 1000) + mission.engageLength=Length + + -- 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 + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + -- Get DCS task. + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- **[AIR]** 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 do 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 + + local mission=AUFTRAG:New(AUFTRAG.Type.BOMBRUNWAY) + + mission:_TargetFromObject(Airdrome) + + -- DCS task options: + mission.engageWeaponType=ENUMS.WeaponFlag.Auto + 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 + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + -- Get DCS task. + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR]** 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.Auto + mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL + mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000) + mission.engageLength=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 + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + -- Get DCS task. + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR/HELO]** Create a GROUNDESCORT (or FOLLOW) mission. Helo will escort a **ground** group and automatically engage certain target types. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP EscortGroup The ground group to escort. +-- @param #number OrbitDistance Orbit to/from the lead unit this many NM. Defaults to 1.5 NM. +-- @param #table TargetTypes Types of targets to engage automatically. Default is {"Ground vehicles"}, i.e. all enemy ground units. Use an empty set {} for a simple "FOLLOW" mission. +-- @return #AUFTRAG self +function AUFTRAG:NewGROUNDESCORT(EscortGroup, OrbitDistance, TargetTypes) + + local mission=AUFTRAG:New(AUFTRAG.Type.GROUNDESCORT) + + -- If only a string is passed we set a variable and check later if the group exists. + if type(EscortGroup)=="string" then + mission.escortGroupName=EscortGroup + mission:_TargetFromObject() + else + mission:_TargetFromObject(EscortGroup) + end + + -- DCS task parameters: + mission.orbitDistance=OrbitDistance and UTILS.NMToMeters(OrbitDistance) or UTILS.NMToMeters(1.5) + --mission.engageMaxDistance=EngageMaxDistance and UTILS.NMToMeters(EngageMaxDistance) or UTILS.NMToMeters(5) + mission.engageTargetTypes=TargetTypes or {"Ground vehicles"} + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.GROUNDESCORT + mission.missionFraction=0.1 + mission.missionAltitude=100 + mission.optionROE=ENUMS.ROE.OpenFire -- TODO: what's the best ROE here? Make dependent on ESCORT or FOLLOW! + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.categories={AUFTRAG.Category.HELICOPTER} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- **[AIR]** 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 (y=0), x=-100 meters behind. +-- @param #number EngageMaxDistance Max engage distance of targets in nautical miles. Default auto 32 NM. +-- @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) + + -- If only a string is passed we set a variable and check later if the group exists. + if type(EscortGroup)=="string" then + mission.escortGroupName=EscortGroup + mission:_TargetFromObject() + else + mission:_TargetFromObject(EscortGroup) + end + + -- DCS task parameters: + mission.escortVec3=OffsetVector or {x=-100, y=0, z=200} + mission.engageMaxDistance=EngageMaxDistance and UTILS.NMToMeters(EngageMaxDistance) or UTILS.NMToMeters(32) + 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.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR ROTARY]** 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:_TargetFromObject(Carrier) + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.NOTHING + mission.missionFraction=0.9 + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionROT=ENUMS.ROT.NoReaction + + mission.categories={AUFTRAG.Category.HELICOPTER} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIRPANE]** Create a RECOVERY TANKER mission. +-- @param #AUFTRAG self +-- @param Wrapper.Unit#UNIT Carrier The carrier unit. +-- @param #number Altitude Orbit altitude in feet. Default is 6,000 ft. +-- @param #number Speed Orbit speed in knots. Default 250 KIAS. +-- @param #number Leg Length of race-track in NM. Default 14 NM. +-- @param #number RelHeading Relative heading [0, 360) of race-track pattern in degrees wrt heading of the carrier. Default is heading of the carrier. +-- @param #number OffsetDist Relative distance of the first race-track point wrt to the carrier. Default 6 NM. +-- @param #number OffsetAngle Relative angle of the first race-track point wrt. to the carrier. Default 180 (behind the boat). +-- @param #number UpdateDistance Threshold distance in NM before orbit pattern is updated. Default 5 NM. +-- @return #AUFTRAG self +function AUFTRAG:NewRECOVERYTANKER(Carrier, Altitude, Speed, Leg, RelHeading, OffsetDist, OffsetAngle, UpdateDistance) + + -- Six NM astern. + local OffsetVec2={r=OffsetDist or 6, phi=OffsetAngle or 180} + + -- Default leg. + Leg=Leg or 14 + + -- Default Speed. + Speed=Speed or 250 + + local Heading=nil + if RelHeading then + Heading=-math.abs(RelHeading) + end + + -- Create orbit mission. + local mission=AUFTRAG:NewORBIT_GROUP(Carrier, Altitude, Speed, Leg, Heading, OffsetVec2, UpdateDistance) + + -- Set the type. + mission.type=AUFTRAG.Type.RECOVERYTANKER + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.REFUELING + mission.missionFraction=0.9 + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionROT=ENUMS.ROT.NoReaction + + mission.categories={AUFTRAG.Category.AIRPLANE} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- **[AIR ROTARY, GROUND]** 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 first transport group. +-- @param #number PickupRadius Radius around the pickup coordinate in meters. Default 100 m. +-- @return #AUFTRAG self +function AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet, DropoffCoordinate, PickupCoordinate, PickupRadius) + + 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 + + mission.transportPickupRadius=PickupRadius or 100 + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.TROOPTRANSPORT) + + -- Debug. + --mission.transportPickup:MarkToAll("Pickup Transport") + --mission.transportDropoff:MarkToAll("Drop off") + + -- TODO: what's the best ROE here? + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.categories={AUFTRAG.Category.HELICOPTER, AUFTRAG.Category.GROUND} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR ROTARY]** Create a CARGO TRANSPORT mission. +-- **Important Note:** +-- The dropoff zone has to be a zone defined in the Mission Editor. This is due to a restriction in the used DCS task, which takes the zone ID as input. +-- Only ME zones have an ID that can be referenced. +-- @param #AUFTRAG self +-- @param Wrapper.Static#STATIC StaticCargo Static cargo object. +-- @param Core.Zone#ZONE DropZone Zone where to drop off the cargo. **Has to be a zone defined in the ME!** +-- @return #AUFTRAG self +function AUFTRAG:NewCARGOTRANSPORT(StaticCargo, DropZone) + + local mission=AUFTRAG:New(AUFTRAG.Type.CARGOTRANSPORT) + + mission:_TargetFromObject(StaticCargo) + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.CARGOTRANSPORT) + + -- Set ROE and ROT. + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.categories={AUFTRAG.Category.HELICOPTER} + + mission.DCStask=mission:GetDCSMissionTask() + + mission.DCStask.params.groupId=StaticCargo:GetID() + mission.DCStask.params.zoneId=DropZone.ZoneID + mission.DCStask.params.zone=DropZone + mission.DCStask.params.cargo=StaticCargo + + return mission +end + +--[[ + +--- **[AIR, GROUND, NAVAL]** Create a OPS TRANSPORT mission. +-- @param #AUFTRAG self +-- @param Core.Set#SET_GROUP CargoGroupSet The set group(s) to be transported. +-- @param Core.Zone#ZONE PickupZone Pick up zone +-- @param Core.Zone#ZONE DeployZone Deploy zone +-- @return #AUFTRAG self +function AUFTRAG:NewOPSTRANSPORT(CargoGroupSet, PickupZone, DeployZone) + + local mission=AUFTRAG:New(AUFTRAG.Type.OPSTRANSPORT) + + mission.transportGroupSet=CargoGroupSet + + mission:_TargetFromObject(mission.transportGroupSet) + + mission.opstransport=OPSTRANSPORT:New(CargoGroupSet, PickupZone, DeployZone) + + function mission.opstransport:OnAfterExecuting(From, Event, To) + mission:Executing() + end + + function mission.opstransport:OnAfterDelivered(From, Event, To) + mission:Done() + end + + -- TODO: what's the best ROE here? + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + + mission.categories={AUFTRAG.Category.ALL} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +]] + +--- **[GROUND, NAVAL]** Create an ARTY mission ("Fire at point" task). +-- +-- If the group has more than one weapon type supporting the "Fire at point" task, the employed weapon type can be set via the `AUFTRAG:SetWeaponType()` function. +-- +-- **Note** that it is recommended to set the weapon range via the `OPSGROUP:AddWeaponRange()` function as this cannot be retrieved from the DCS API. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Target Center of the firing solution. +-- @param #number Nshots Number of shots to be fired. Default `#nil`. If value is in (0,1), it is interpreted as per cent of available ammo. +-- @param #number Radius Radius of the shells in meters. Default 100 meters. +-- @param #number Altitude Altitude in meters. Can be used to setup a Barrage. Default `#nil`. +-- @return #AUFTRAG self +function AUFTRAG:NewARTY(Target, Nshots, Radius, Altitude) + + local mission=AUFTRAG:New(AUFTRAG.Type.ARTY) + + mission:_TargetFromObject(Target) + + mission.artyShots=Nshots or nil + mission.artyRadius=Radius or 100 + mission.artyAltitude=Altitude + + mission.engageWeaponType=ENUMS.WeaponFlag.Auto + + mission.optionROE=ENUMS.ROE.OpenFire -- Ground/naval need open fire! + mission.optionAlarm=0 + + mission.missionFraction=0.0 + mission.missionWaypointRadius=0.0 + + -- Evaluate after 8 min. + mission.dTevaluate=8*60 + + mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[GROUND, NAVAL]** Create an BARRAGE mission. Assigned groups will move to a random coordinate within a given zone and start firing into the air. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone The zone where the unit will go. +-- @param #number Heading Heading in degrees. Default random heading [0, 360). +-- @param #number Angle Shooting angle in degrees. Default random [45, 85]. +-- @param #number Radius Radius of the shells in meters. Default 100 meters. +-- @param #number Altitude Altitude in meters. Default 500 m. +-- @param #number Nshots Number of shots to be fired. Default is until ammo is empty (`#nil`). +-- @return #AUFTRAG self +function AUFTRAG:NewBARRAGE(Zone, Heading, Angle, Radius, Altitude, Nshots) + + local mission=AUFTRAG:New(AUFTRAG.Type.BARRAGE) + + mission:_TargetFromObject(Zone) + + mission.artyShots=Nshots + mission.artyRadius=Radius or 100 + mission.artyAltitude=Altitude + mission.artyHeading=Heading + mission.artyAngle=Angle + + mission.engageWeaponType=ENUMS.WeaponFlag.Auto + + mission.optionROE=ENUMS.ROE.OpenFire -- Ground/naval need open fire! + mission.optionAlarm=0 + + mission.missionFraction=0.0 + + -- Evaluate after instantly. + mission.dTevaluate=10 + + mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[AIR, GROUND, NAVAL]** 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. +-- @param #string Formation Formation used by ground units during patrol. Default "Off Road". +-- @return #AUFTRAG self +function AUFTRAG:NewPATROLZONE(Zone, Speed, Altitude, Formation) + + local mission=AUFTRAG:New(AUFTRAG.Type.PATROLZONE) + + -- Ensure we got a ZONE and not just the zone name. + if type(Zone)=="string" then + Zone=ZONE:New(Zone) + end + + mission:_TargetFromObject(Zone) + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.PATROLZONE) + + 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.categories={AUFTRAG.Category.ALL} + + mission.DCStask=mission:GetDCSMissionTask() + + mission.DCStask.params.formation=Formation or "Off Road" + + return mission +end + +--- **[AIR, GROUND, NAVAL]** Create a CAPTUREZONE mission. Group(s) will go to the zone and patrol it randomly. +-- @param #AUFTRAG self +-- @param Ops.OpsZone#OPSZONE OpsZone The OPS zone to capture. +-- @param #number Coalition The coalition which should capture the zone for the mission to be successful. +-- @param #number Speed Speed in knots. +-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL. +-- @param #string Formation Formation used by ground units during patrol. Default "Off Road". +-- @param #number StayInZoneTime Stay this many seconds in the zone when done, only then drive back. +-- @return #AUFTRAG self +function AUFTRAG:NewCAPTUREZONE(OpsZone, Coalition, Speed, Altitude, Formation, StayInZoneTime) + + local mission=AUFTRAG:New(AUFTRAG.Type.CAPTUREZONE) + + + mission:_TargetFromObject(OpsZone) + + mission.coalition=Coalition + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.CAPTUREZONE) + + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionROT=ENUMS.ROT.PassiveDefense + mission.optionAlarm=ENUMS.AlarmState.Auto + mission.StayInZoneTime = StayInZoneTime + + mission.missionFraction=0.1 + mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil + mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or nil + + mission.categories={AUFTRAG.Category.ALL} + + mission.DCStask=mission:GetDCSMissionTask() + + mission.updateDCSTask=true + + local params={} + + params.formation=Formation or "Off Road" + params.zone=mission:GetObjective() + params.altitude=mission.missionAltitude + params.speed=mission.missionSpeed and UTILS.KmphToMps(mission.missionSpeed) or nil + + mission.DCStask.params=params + + return mission +end + + +--- **[OBSOLETE]** Create a ARMORATTACK mission. +-- ** Note that this is actually creating a GROUNDATTACK mission!** +-- @param #AUFTRAG self +-- @param Ops.Target#TARGET Target The target to attack. Can be a GROUP, UNIT or STATIC object. +-- @param #number Speed Speed in knots. +-- @param #string Formation The attack formation, e.g. "Wedge", "Vee" etc. +-- @return #AUFTRAG self +function AUFTRAG:NewARMORATTACK(Target, Speed, Formation) + + local mission=AUFTRAG:NewGROUNDATTACK(Target, Speed, Formation) + + -- Mission type. + mission.type=AUFTRAG.Type.ARMORATTACK + + return mission +end + +--- **[GROUND]** Create a GROUNDATTACK mission. Ground group(s) will go to a target object and attack at their own discretion. +-- Unfortunately, the "Attack Group" and "Attack Unit" tasks do not work for ground and naval groups (only for aircraft). +-- Therefore, we resort to this workaround, which guides the attacking group to the vicinity of the target. Then they start shooting on their own, once they detect the target. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP, UNIT or STATIC object. +-- @param #number Speed Speed in knots. Default max. +-- @param #string Formation The attack formation, e.g. "Wedge", "Vee" etc. Default `ENUMS.Formation.Vehicle.Vee`. Only working for ground, not naval! +-- @return #AUFTRAG self +function AUFTRAG:NewGROUNDATTACK(Target, Speed, Formation) + + local mission=AUFTRAG:New(AUFTRAG.Type.GROUNDATTACK) + + mission:_TargetFromObject(Target) + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.GROUNDATTACK) + + -- Defaults. + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionAlarm=ENUMS.AlarmState.Auto + mission.optionFormation="On Road" + mission.missionFraction=0.70 + mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil + + mission.categories={AUFTRAG.Category.GROUND} + + mission.DCStask=mission:GetDCSMissionTask() + + mission.DCStask.params.speed=mission.missionSpeed and UTILS.KmphToMps(mission.missionSpeed) or nil + mission.DCStask.params.formation=Formation or ENUMS.Formation.Vehicle.Vee + + return mission +end + +--- **[NAVAL]** Create a NAVALENGAGEMENT mission. Naval group(s) will go to a target object and attack at their own discretion. +-- Unfortunately, the "Attack Group" and "Attack Unit" tasks do not work for ground and naval groups (only for aircraft). +-- Therefore, we resort to this workaround, which guides the attacking group to the vicinity of the target. Then they start shooting on their own, once they detect the target. +-- @param #AUFTRAG self +-- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP, UNIT or STATIC object. +-- @param #number Speed Speed in knots. Default max. +-- @param #number Depth The attack depth in meters. Only for submarines! +-- @return #AUFTRAG self +function AUFTRAG:NewNAVALENGAGEMENT(Target, Speed, Depth) + + local mission=AUFTRAG:New(AUFTRAG.Type.NAVALENGAGEMENT) + + mission:_TargetFromObject(Target) + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.NAVALENGAGEMENT) + + -- Defaults. + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionAlarm=ENUMS.AlarmState.Auto + mission.missionFraction=0.70 + mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil + mission.missionAltitude=Depth or 0 + + mission.categories={AUFTRAG.Category.NAVAL} + + mission.DCStask=mission:GetDCSMissionTask() + + mission.DCStask.params.speed=mission.missionSpeed and UTILS.KmphToMps(mission.missionSpeed) or nil + + return mission +end + +--- **[AIR, GROUND, NAVAL]** Create a RECON mission. +-- @param #AUFTRAG self +-- @param Core.Set#SET_ZONE ZoneSet The recon zones. +-- @param #number Speed Speed in knots. +-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL. +-- @param #boolean Adinfinitum If `true`, the group will start over again after reaching the final zone. +-- @param #boolean Randomly If `true`, the group will select a random zone. +-- @param #string Formation Formation used during recon route. +-- @return #AUFTRAG self +function AUFTRAG:NewRECON(ZoneSet, Speed, Altitude, Adinfinitum, Randomly, Formation) + + local mission=AUFTRAG:New(AUFTRAG.Type.RECON) + + mission:_TargetFromObject(ZoneSet) + + if ZoneSet:IsInstanceOf("SET_ZONE") then + mission.missionZoneSet = ZoneSet + elseif ZoneSet:IsInstanceOf("ZONE_BASE") then + mission.missionZoneSet = SET_ZONE:New() + mission.missionZoneSet:AddZone(ZoneSet) + end + + + mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.RECON) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionROT=ENUMS.ROT.PassiveDefense + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=0.5 + mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil + mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or UTILS.FeetToMeters(2000) + + mission.categories={AUFTRAG.Category.ALL} + + mission.DCStask=mission:GetDCSMissionTask() + mission.DCStask.params.adinfinitum=Adinfinitum + mission.DCStask.params.randomly=Randomly + mission.DCStask.params.formation=Formation + + return mission +end + +--- **[GROUND]** Create a AMMO SUPPLY mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone The zone, where supply units go. +-- @return #AUFTRAG self +function AUFTRAG:NewAMMOSUPPLY(Zone) + + local mission=AUFTRAG:New(AUFTRAG.Type.AMMOSUPPLY) + + mission:_TargetFromObject(Zone) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.missionWaypointRadius=0 + + mission.categories={AUFTRAG.Category.GROUND} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[GROUND]** Create a FUEL SUPPLY mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone The zone, where supply units go. +-- @return #AUFTRAG self +function AUFTRAG:NewFUELSUPPLY(Zone) + + local mission=AUFTRAG:New(AUFTRAG.Type.FUELSUPPLY) + + mission:_TargetFromObject(Zone) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.GROUND} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[GROUND]** Create a REARMING mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone The zone, where units go and look for ammo supply. +-- @return #AUFTRAG self +function AUFTRAG:NewREARMING(Zone) + + local mission=AUFTRAG:New(AUFTRAG.Type.REARMING) + + mission:_TargetFromObject(Zone) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.missionWaypointRadius=0 + + mission.categories={AUFTRAG.Category.GROUND} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- **[AIR]** Create an ALERT 5 mission. Aircraft will be spawned uncontrolled and wait for an assignment. You must specify **one** mission type which is performed. +-- This determines the payload and the DCS mission task which are used when the aircraft is spawned. +-- @param #AUFTRAG self +-- @param #string MissionType Mission type `AUFTRAG.Type.XXX`. Determines payload and mission task (intercept, ground attack, etc.). +-- @return #AUFTRAG self +function AUFTRAG:NewALERT5(MissionType) + + local mission=AUFTRAG:New(AUFTRAG.Type.ALERT5) + + mission.missionTask=self:GetMissionTaskforMissionType(MissionType) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionROT=ENUMS.ROT.NoReaction + + mission.alert5MissionType=MissionType + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.AIRCRAFT} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[GROUND, NAVAL]** Create an ON GUARD mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Coordinate, where to stand guard. +-- @return #AUFTRAG self +function AUFTRAG:NewONGUARD(Coordinate) + + local mission=AUFTRAG:New(AUFTRAG.Type.ONGUARD) + + mission:_TargetFromObject(Coordinate) + + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[GROUND, NAVAL]** Create an AIRDEFENSE mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone Zone where the air defense group(s) should be stationed. +-- @return #AUFTRAG self +function AUFTRAG:NewAIRDEFENSE(Zone) + + local mission=AUFTRAG:New(AUFTRAG.Type.AIRDEFENSE) + + mission:_TargetFromObject(Zone) + + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[GROUND]** Create an EWR mission. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE Zone Zone where the Early Warning Radar group(s) should be stationed. +-- @return #AUFTRAG self +function AUFTRAG:NewEWR(Zone) + + local mission=AUFTRAG:New(AUFTRAG.Type.EWR) + + mission:_TargetFromObject(Zone) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.GROUND} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + + +--- **[PRIVATE, AIR, GROUND, NAVAL]** Create a mission to relocate all cohort assets to another LEGION. +-- @param #AUFTRAG self +-- @param Ops.Legion#LEGION Legion The new legion. +-- @param Ops.Cohort#COHORT Cohort The cohort to be relocated. +-- @return #AUFTRAG self +function AUFTRAG:_NewRELOCATECOHORT(Legion, Cohort) + + local mission=AUFTRAG:New(AUFTRAG.Type.RELOCATECOHORT) + + mission:_TargetFromObject(Legion.spawnzone) + + mission.optionROE=ENUMS.ROE.ReturnFire + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=0.0 + + mission.categories={AUFTRAG.Category.ALL} + + mission.DCStask=mission:GetDCSMissionTask() + + if Cohort.isGround then + mission.optionFormation=ENUMS.Formation.Vehicle.OnRoad + end + + mission.DCStask.params.legion=Legion + mission.DCStask.params.cohort=Cohort + + return mission +end + +--- **[GROUND, NAVAL]** Create a mission to do NOTHING. +-- @param #AUFTRAG self +-- @param Core.Zone#ZONE RelaxZone Zone where the assets are supposed to do nothing. +-- @return #AUFTRAG self +function AUFTRAG:NewNOTHING(RelaxZone) + + local mission=AUFTRAG:New(AUFTRAG.Type.NOTHING) + + mission:_TargetFromObject(RelaxZone) + + mission.optionROE=ENUMS.ROE.WeaponHold + mission.optionAlarm=ENUMS.AlarmState.Auto + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- **[GROUND]** Create an ARMORED ON GUARD mission. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Coordinate, where to stand guard. +-- @param #string Formation Formation to take, e.g. "On Road", "Vee" etc. +-- @return #AUFTRAG self +function AUFTRAG:NewARMOREDGUARD(Coordinate,Formation) + + local mission=AUFTRAG:New(AUFTRAG.Type.ARMOREDGUARD) + + mission:_TargetFromObject(Coordinate) + + mission.optionROE=ENUMS.ROE.OpenFire + mission.optionAlarm=ENUMS.AlarmState.Auto + mission.optionFormation=Formation or "On Road" + + mission.missionFraction=1.0 + + mission.categories={AUFTRAG.Category.GROUND} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + +--- Create a mission to attack a TARGET object. +-- @param #AUFTRAG self +-- @param Ops.Target#TARGET Target The target. +-- @param #string MissionType The mission type. +-- @return #AUFTRAG self +function AUFTRAG:NewFromTarget(Target, MissionType) + + local mission=nil --#AUFTRAG + + if MissionType==AUFTRAG.Type.ANTISHIP then + mission=self:NewANTISHIP(Target) + elseif MissionType==AUFTRAG.Type.ARTY then + mission=self:NewARTY(Target, 0.3) -- use 30% of the available ammo + elseif MissionType==AUFTRAG.Type.BAI then + mission=self:NewBAI(Target) + elseif MissionType==AUFTRAG.Type.BOMBCARPET then + mission=self:NewBOMBCARPET(Target) + elseif MissionType==AUFTRAG.Type.BOMBING then + mission=self:NewBOMBING(Target) + elseif MissionType==AUFTRAG.Type.BOMBRUNWAY then + mission=self:NewBOMBRUNWAY(Target) + elseif MissionType==AUFTRAG.Type.STRAFING then + mission=self:NewSTRAFING(Target) + elseif MissionType==AUFTRAG.Type.CAS then + mission=self:NewCAS(ZONE_RADIUS:New(Target:GetName(),Target:GetVec2(),1000), nil, nil, Target:GetAverageCoordinate()) + elseif MissionType==AUFTRAG.Type.CASENHANCED then + mission=self:NewCASENHANCED(ZONE_RADIUS:New(Target:GetName(),Target:GetVec2(),1000)) + elseif MissionType==AUFTRAG.Type.INTERCEPT then + mission=self:NewINTERCEPT(Target) + elseif MissionType==AUFTRAG.Type.SEAD then + mission=self:NewSEAD(Target) + elseif MissionType==AUFTRAG.Type.STRIKE then + mission=self:NewSTRIKE(Target) + elseif MissionType==AUFTRAG.Type.ARMORATTACK then + mission=self:NewARMORATTACK(Target) + elseif MissionType==AUFTRAG.Type.GROUNDATTACK then + mission=self:NewGROUNDATTACK(Target) + elseif MissionType==AUFTRAG.Type.NAVALENGAGEMENT then + mission=self:NewNAVALENGAGEMENT(Target) + else + return nil + 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.CAS + + elseif attribute==GROUP.Attribute.GROUND_TANK then + + auftrag=AUFTRAG.Type.BAI + + else + + auftrag=AUFTRAG.Type.BAI + + end + + + elseif category==Group.Category.SHIP then + + --- + -- NAVAL + --- + + auftrag=AUFTRAG.Type.ANTISHIP + + else + self:T(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, 0.2) + 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 + mission=AUFTRAG:NewRECON(ZoneSet,Speed,Altitude,Adinfinitum,Randomly,Formation) + 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) + elseif auftrag==AUFTRAG.Type.PATROLRACETRACK then + mission=AUFTRAG:NewPATROL_RACETRACK(Coordinate,Altitude,Speed,Heading,Leg,Formation) + 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 time how long the mission is executed. Once this time limit has passed, the mission is cancelled. +-- @param #AUFTRAG self +-- @param #number Duration Duration in seconds. +-- @return #AUFTRAG self +function AUFTRAG:SetDuration(Duration) + self.durationExe=Duration + return self +end + +--- Set that mission assets are teleported to the mission execution waypoint. +-- @param #AUFTRAG self +-- @param #boolean Switch If `true` or `nil`, teleporting is on. If `false`, teleporting is off. +-- @return #AUFTRAG self +function AUFTRAG:SetTeleport(Switch) + if Switch==nil then + Switch=true + end + self.teleport=Switch + return self +end + +--- **[LEGION, COMMANDER, CHIEF]** Set whether assigned assets return to their legion once the mission is over. This is only applicable to **army** and **navy** groups, *i.e.* aircraft +-- will always return. +-- @param #AUFTRAG self +-- @param #boolean Switch If `true`, assets will return. If `false`, assets will not return and stay where it finishes its last mission. If `nil`, let asset decide. +-- @return #AUFTRAG self +function AUFTRAG:SetReturnToLegion(Switch) + self.legionReturn=Switch + self:T(self.lid..string.format("Setting ReturnToLetion=%s", tostring(self.legionReturn))) + return self +end + + +--- Set mission push time. This is the time the mission is executed. If the push time is not passed, the group will wait at the mission execution waypoint. +-- @param #AUFTRAG self +-- @param #string ClockPush Time the mission is executed, e.g. "05:00" for 5 am. Can also be given as a `#number`, where it is interpreted as relative push time in seconds. +-- @return #AUFTRAG self +function AUFTRAG:SetPushTime(ClockPush) + + if ClockPush then + if type(ClockPush)=="string" then + self.Tpush=UTILS.ClockToSeconds(ClockPush) + elseif type(ClockPush)=="number" then + self.Tpush=timer.getAbsTime()+ClockPush + end + 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 + +--- **[LEGION, COMMANDER, CHIEF]** Set how many times the mission is repeated. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, FLEET) 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 + + +--- **[LEGION, COMMANDER, CHIEF]** Set the repeat delay in seconds after a mission is successful/failed. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, FLEET) or higher level. +-- @param #AUFTRAG self +-- @param #number Nrepeat Repeat delay in seconds. Default 1. +-- @return #AUFTRAG self +function AUFTRAG:SetRepeatDelay(RepeatDelay) + self.repeatDelay = RepeatDelay + return self +end + +--- **[LEGION, COMMANDER, CHIEF]** Set how many times the mission is repeated if it fails. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, FLEET) 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 + +--- **[LEGION, COMMANDER, CHIEF]** Set how many times the mission is repeated if it was successful. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, FLEET) 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 + +--- **[LEGION, COMMANDER, CHIEF]** Set that mission assets get reinforced if their number drops below the minimum number of required assets of the mission (*c.f.* SetRequiredAssets() function). +-- +-- **Note** that reinforcement groups are only recruited from the legion (airwing, brigade, fleet) the mission was assigned to. If the legion does not have any more of these assets, +-- no reinforcement can take place, even if the mission is submitted to a COMMANDER or CHIEF. +-- @param #AUFTRAG self +-- @param #number Nreinforce Number of max asset groups used to reinforce. +-- @return #AUFTRAG self +function AUFTRAG:SetReinforce(Nreinforce) + self.reinforce=Nreinforce + return self +end + +--- **[LEGION, COMMANDER, CHIEF]** Define how many assets are required to do the job. Only used if the mission is handled by a **LEGION** (AIRWING, BRIGADE, ...) or higher level. +-- @param #AUFTRAG self +-- @param #number NassetsMin Minimum number of asset groups. Default 1. +-- @param #number NassetsMax Maximum Number of asset groups. Default is same as `NassetsMin`. +-- @return #AUFTRAG self +function AUFTRAG:SetRequiredAssets(NassetsMin, NassetsMax) + + self.NassetsMin=NassetsMin or 1 + + self.NassetsMax=NassetsMax or self.NassetsMin + + -- Ensure that max is at least equal to min. + if self.NassetsMax0 then + local N=self:CountOpsGroups() + if N Nmin=%d", self.NassetsMin, N, self.reinforce, Nmin)) + end + end + + end + + return Nmin, Nmax +end + +--- **[LEGION, COMMANDER, CHIEF]** Set that only alive (spawned) assets are considered. +-- @param #AUFTRAG self +-- @param #boolean Switch If true or nil, only active assets. If false +-- @return #AUFTRAG self +function AUFTRAG:SetAssetsStayAlive(Switch) + + if Switch==nil then + Switch=true + end + + self.assetStayAlive=Switch + + return self +end + +--- **[LEGION, COMMANDER, CHIEF]** Define how many assets are required that escort the mission assets. +-- Only used if the mission is handled by a **LEGION** (AIRWING, BRIGADE, FLEET) or higher level. +-- @param #AUFTRAG self +-- @param #number NescortMin Minimum number of asset groups. Default 1. +-- @param #number NescortMax Maximum Number of asset groups. Default is same as `NassetsMin`. +-- @param #string MissionType Mission type assets will be optimized for and payload selected, *e.g.* `AUFTRAG.Type.SEAD`. Default nil. +-- @param #table TargetTypes Target Types that will be engaged by the escort group(s). Default `{"Air"}` for aircraft and `{"Ground Units"}` for helos. Set, *e.g.*, `{"Air Defence"}` for SEAD. +-- @param #number EngageRange Max range in nautical miles that the escort group(s) will engage enemies. Default 32 NM (60 km). +-- @return #AUFTRAG self +function AUFTRAG:SetRequiredEscorts(NescortMin, NescortMax, MissionType, TargetTypes, EngageRange) + + -- Set number of escort assets. + self.NescortMin=NescortMin or 1 + self.NescortMax=NescortMax or self.NescortMin + + -- Ensure that max is at least equal to min. + if self.NescortMaxself.Tstop 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 cancelled. +-- * 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 mission is ready to be pushed. +-- * Mission push time already passed. +-- * **All** push conditions are true. +-- @param #AUFTRAG self +-- @return #boolean If true, mission groups can push. +function AUFTRAG:IsReadyToPush() + + local Tnow=timer.getAbsTime() + + -- Push time passed? + if self.Tpush and Tnow<=self.Tpush then + return false + end + + -- Evaluate push condition(s) if any. All need to be true. + local push=self:EvalConditionsAll(self.conditionPush) + + return push +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() + + -- ESCORT: Check if only the group NAME of an escort had been specified. + if self.escortGroupName then + -- Try to find the group. + local group=GROUP:FindByName(self.escortGroupName) + if group and group:IsAlive() then + + -- Debug info. + self:T(self.lid..string.format("ESCORT group %s is now alive. Updating DCS task and adding group to TARGET", tostring(self.escortGroupName))) + + -- Add TARGET object. + self.engageTarget:AddObject(group) + + -- Update DCS task with the known group ID. + self.DCStask=self:GetDCSMissionTask() + + -- Set value to nil so we do not do this again in the next cycle. + self.escortGroupName=nil + end + end + + -- 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() + + local Nassigned=self.Nassigned and self.Nassigned-self.Ndead or 0 + + -- check conditions if set + local conditionDone=false + if self.conditionFailureSet then + conditionDone = self:EvalConditionsAny(self.conditionFailure) + end + if self.conditionSuccessSet and not conditionDone then + conditionDone = self:EvalConditionsAny(self.conditionSuccess) + end + + -- 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) then + + -- Cancel mission if stop time passed. + self:Cancel() + + elseif conditionDone then + + -- Cancel mission if conditions were met. + self:Cancel() + + elseif self.durationExe and self.Texecuting and Tnow-self.Texecuting>self.durationExe then + + -- Backup repeat values + local Nrepeat=self.Nrepeat + local NrepeatS=self.NrepeatSuccess + local NrepeatF=self.NrepeatFailure + + -- Cancel mission if stop time passed. + self:Cancel() + + self.Nrepeat=Nrepeat + self.NrepeatSuccess=NrepeatS + self.NrepeatFailure=NrepeatF + + elseif (Ntargets0>0 and Ntargets==0) then + + -- Cancel mission if mission targets are gone (if there were any in the beginning). + -- TODO: I commented this out for some reason but I forgot why... + self:T(self.lid.."No targets left cancelling mission!") + self:Cancel() + + elseif self:IsExecuting() and self:_IsNotReinforcing() then + +-- env.info("Mission Done:") +-- env.info(string.format("Nreinforce= %d", self.reinforce or 0)) +-- env.info(string.format("Nassigned = %d", self.Nassigned)) +-- env.info(string.format("Ndead = %d", self.Ndead)) +-- env.info(string.format("Nass-Ndead= %d", Nassigned)) + + -- Had the case that mission was in state Executing but all assigned groups were dead. + -- TODO: might need to loop over all assigned groups + if Ngroups==0 then + self:Done() + else + local done=true + for groupname,data in pairs(self.groupdata or {}) do + local groupdata=data --#AUFTRAG.GroupData + local opsgroup=groupdata.opsgroup + if opsgroup:IsAlive() then + done=false + end + end + if done then + self:Done() + end + end + + end + + end + + -- Current FSM state. + local fsmstate=self:GetState() + + -- Check for error. + if fsmstate~=self.status then + self:T(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 Nlegions=#self.legions + local commander=self.commander and self.statusCommander or "N/A" + local chief=self.chief and self.statusChief or "N/A" + + -- Info message. + self:T(self.lid..string.format("Status %s: Target=%s, T=%s-%s, assets=%d, groups=%d, targets=%d, legions=%d, commander=%s, chief=%s", + self.status, targetname, Cstart, Cstop, #self.assets, Ngroups, Ntargets, Nlegions, commander, chief)) + 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 + + -- Group info. + if self.verbose>=3 then + -- Data on assigned groups. + local text=string.format("Assets [N=%d, Nassigned=%s, Ndead=%s]:", self.Nassets or 0, self.Nassigned or 0, self.Ndead or 0) + for i,_asset in pairs(self.assets or {}) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + text=text..string.format("\n[%d] %s: spawned=%s, requested=%s, reserved=%s", i, asset.spawngroupname, tostring(asset.spawned), tostring(asset.requested), tostring(asset.reserved)) + end + self:I(self.lid..text) + end + + -- Ready to evaluate mission outcome? + local ready2evaluate=self.Tover and Tnow-self.Tover>=self.dTevaluate or false + + -- 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(true) + 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. + if self.verbose > 0 then + 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) + end + + -- Trigger events. + if failed then + self:I(self.lid..string.format("Mission %d [%s] failed!", self.auftragsnummer, self.type)) + if self.chief then + self.chief.Nfailure=self.chief.Nfailure+1 + end + self:Failed() + else + self:I(self.lid..string.format("Mission %d [%s] success!", self.auftragsnummer, self.type)) + if self.chief then + self.chief.Nsuccess=self.chief.Nsuccess+1 + end + 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. +-- @return #AUFTRAG self +function AUFTRAG:SetGroupStatus(opsgroup, status) + + -- Current status. + local oldstatus=self:GetGroupStatus(opsgroup) + + -- Debug info. + self:T(self.lid..string.format("Setting OPSGROUP %s to status %s-->%s", opsgroup and opsgroup.groupname or "nil", tostring(oldstatus), tostring(status))) + + if oldstatus==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:T(self.lid.."WARNING: Could not SET flight data for flight group. Setting status to DONE") + end + end + + -- Check if mission is NOT over. + local isNotOver=self:IsNotOver() + + -- Check if all assigned groups are done. + local groupsDone=self:CheckGroupsDone() + + -- Debug info. + self:T2(self.lid..string.format("Setting OPSGROUP %s status to %s. IsNotOver=%s CheckGroupsDone=%s", opsgroup.groupname, self:GetGroupStatus(opsgroup), tostring(self:IsNotOver()), tostring(groupsDone))) + + -- Check if ALL flights are done with their mission. + if isNotOver and groupsDone then + self:T3(self.lid.."All assigned OPSGROUPs done ==> mission DONE!") + self:Done() + else + self:T3(self.lid.."Mission NOT DONE yet!") + end + + return self +end + +--- Get ops group mission status. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @return #string The group status. +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:T(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 + +--- Add LEGION to mission. +-- @param #AUFTRAG self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #AUFTRAG self +function AUFTRAG:AddLegion(Legion) + + -- Debug info. + self:T(self.lid..string.format("Adding legion %s", Legion.alias)) + + -- Add legion to table. + table.insert(self.legions, Legion) + + return self +end + +--- Remove LEGION from mission. +-- @param #AUFTRAG self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #AUFTRAG self +function AUFTRAG:RemoveLegion(Legion) + + -- Loop over legions + for i=#self.legions,1,-1 do + local legion=self.legions[i] --Ops.Legion#LEGION + + if legion.alias==Legion.alias then + + -- Debug info. + self:T(self.lid..string.format("Removing legion %s", Legion.alias)) + table.remove(self.legions, i) + + -- Set legion status to nil. + self.statusLegion[Legion.alias]=nil + + return self + end + + end + + self:T(self.lid..string.format("ERROR: Legion %s not found and could not be removed!", Legion.alias)) + return self +end + +--- Set LEGION mission status. +-- @param #AUFTRAG self +-- @param Ops.Legion#LEGION Legion The legion. +-- @param #string Status New status. +-- @return #AUFTRAG self +function AUFTRAG:SetLegionStatus(Legion, Status) + + -- Old status + local status=self:GetLegionStatus(Legion) + + -- Debug info. + self:T(self.lid..string.format("Setting LEGION %s to status %s-->%s", Legion.alias, tostring(status), tostring(Status))) + + -- New status. + self.statusLegion[Legion.alias]=Status + + return self +end + +--- Get LEGION mission status. +-- @param #AUFTRAG self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #string status Current status. +function AUFTRAG:GetLegionStatus(Legion) + + -- New status. + local status=self.statusLegion[Legion.alias] or "unknown" + + return status +end + + +--- Set mission (ingress) waypoint coordinate for OPS group. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @param Core.Point#COORDINATE coordinate Waypoint Coordinate. +-- @return #AUFTRAG self +function AUFTRAG:SetGroupWaypointCoordinate(opsgroup, coordinate) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + groupdata.waypointcoordinate=coordinate + end + return self +end + +--- [Air] Set mission (ingress) waypoint coordinate for FLIGHT group. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE coordinate Waypoint Coordinate. +-- @return #AUFTRAG self +function AUFTRAG:SetIngressCoordinate(coordinate) + self.missionIngressCoord = coordinate + self.missionIngressCoordAlt = UTILS.MetersToFeet(coordinate.y) or 10000 + return self +end + +--- Get mission (ingress) waypoint coordinate of OPS group +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @return Core.Point#COORDINATE Waypoint Coordinate. +function AUFTRAG:GetGroupWaypointCoordinate(opsgroup) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + return groupdata.waypointcoordinate + end +end + + +--- Set mission waypoint task for OPS group. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS 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 mission waypoint task of OPS group. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS 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 mission (ingress) waypoint UID for OPS group. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @param #number waypointindex Waypoint UID. +-- @return #AUFTRAG self +function AUFTRAG:SetGroupWaypointIndex(opsgroup, waypointindex) + self:T2(self.lid..string.format("Setting Mission waypoint UID=%d", waypointindex)) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + groupdata.waypointindex=waypointindex + end + return self +end + +--- Get mission (ingress) waypoint UID of OPS group. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @return #number Waypoint UID. +function AUFTRAG:GetGroupWaypointIndex(opsgroup) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + return groupdata.waypointindex + end +end + +--- Set Egress waypoint UID for OPS group. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @param #number waypointindex Waypoint UID. +-- @return #AUFTRAG self +function AUFTRAG:SetGroupEgressWaypointUID(opsgroup, waypointindex) + self:T2(self.lid..string.format("Setting Egress waypoint UID=%d", waypointindex)) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + groupdata.waypointEgressUID=waypointindex + end + return self +end + +--- Get Egress waypoint UID of OPS group. +-- @param #AUFTRAG self +-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group. +-- @return #number Waypoint UID. +function AUFTRAG:GetGroupEgressWaypointUID(opsgroup) + local groupdata=self:GetGroupData(opsgroup) + if groupdata then + return groupdata.waypointEgressUID + end +end + +--- Check if all groups are done with their mission (or dead). +-- @param #AUFTRAG self +-- @return #boolean If `true`, all groups are done with the mission. +function AUFTRAG:CheckGroupsDone() + + local fsmState = self:GetState() + + -- Check status of all OPS groups. + for groupname,data in pairs(self.groupdata) do + local groupdata=data --#AUFTRAG.GroupData + if groupdata then + if not (groupdata.status==AUFTRAG.GroupStatus.DONE or groupdata.status==AUFTRAG.GroupStatus.CANCELLED) then + -- At least this group is not DONE or CANCELLED. + self:T2(self.lid..string.format("CheckGroupsDone: OPSGROUP %s is not DONE or CANCELLED but in state %s. Mission NOT DONE!", groupdata.opsgroup.groupname, groupdata.status:upper())) + return false + end + end + end + + -- Check status of all LEGIONs. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + local status=self:GetLegionStatus(legion) + if not status==AUFTRAG.Status.CANCELLED then + -- At least one LEGION has not CANCELLED. + self:T2(self.lid..string.format("CheckGroupsDone: LEGION %s is not CANCELLED but in state %s. Mission NOT DONE!", legion.alias, status)) + return false + end + end + + -- Check commander status. + if self.commander then + if not self.statusCommander==AUFTRAG.Status.CANCELLED then + self:T2(self.lid..string.format("CheckGroupsDone: COMMANDER is not CANCELLED but in state %s. Mission NOT DONE!", self.statusCommander)) + return false + end + end + + -- Check chief status. + if self.chief then + if not self.statusChief==AUFTRAG.Status.CANCELLED then + self:T2(self.lid..string.format("CheckGroupsDone: CHIEF is not CANCELLED but in state %s. Mission NOT DONE!", self.statusChief)) + return false + end + end + + -- These are early stages, where we might not even have a opsgroup defined to be checked. If there were any groups, we checked above. + if self:IsPlanned() or self:IsQueued() or self:IsRequested() then + self:T2(self.lid..string.format("CheckGroupsDone: Mission is still in state %s [FSM=%s] (PLANNED or QUEUED or REQUESTED). Mission NOT DONE!", self.status, self:GetState())) + return false + end + + -- Check if there is still reinforcement to be expected. + if self:IsExecuting() and self:_IsReinforcing() then + self:T2(self.lid..string.format("CheckGroupsDone: Mission is still in state %s [FSM=%s] and reinfoce=%d. Mission NOT DONE!", self.status, self:GetState(), self.reinforce)) + return false + end + + local NopsgroupsAlive=self:CountOpsGroups() + local NopsgroupsDone=self:CountOpsGroupsInStatus(AUFTRAG.GroupStatus.DONE)+self:CountOpsGroupsInStatus(AUFTRAG.GroupStatus.CANCELLED) + + -- It could be that all groups 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 NopsgroupsAlive==0 then + self:T(self.lid..string.format("CheckGroupsDone: Mission is STARTED state %s [FSM=%s] but count of alive OPSGROUP is zero. Mission DONE!", self.status, self:GetState())) + return true + end + + -- Every group alive is done or cancelled + if NopsgroupsAlive==NopsgroupsDone then + self:T(self.lid..string.format("CheckGroupsDone: Mission is in state %s [FSM=%s] but all groups [=%d] are done or cancelled. Mission DONE!", self.status, self:GetState(), NopsgroupsAlive)) + return true + end + + if (self:IsStarted() or self:IsExecuting()) and (fsmState == AUFTRAG.Status.STARTED or fsmState == AUFTRAG.Status.EXECUTING) and NopsgroupsAlive>0 then + self:T(self.lid..string.format("CheckGroupsDone: Mission is in state %s [FSM=%s] and count of alive OPSGROUP > zero. Mission NOT DONE!", self.status, self:GetState())) + return false + 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:T(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 a LEGION. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onafterQueued(From, Event, To, Airwing) + self.status=AUFTRAG.Status.QUEUED + self:T(self.lid..string.format("New mission status=%s", self.status)) +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 an OPSGROUP. +-- @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.Tstarted=timer.getAbsTime() + 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.Texecuting=timer.getAbsTime() + self:T(self.lid..string.format("New mission status=%s", self.status)) +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 to which the element belongs. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The element that got destroyed. +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 + + -- Number of dead groups. + self.Ndead=self.Ndead+1 + +end + +--- On after "AssetDead" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. +function AUFTRAG:onafterAssetDead(From, Event, To, Asset) + + -- Number of groups alive. + local N=self:CountOpsGroups() + + local notreinforcing=self:_IsNotReinforcing() + + self:T(self.lid..string.format("Asset %s dead! Number of ops groups remaining %d (reinforcing=%s)", tostring(Asset.spawngroupname), N, tostring(not notreinforcing))) + + -- All assets dead? + if N==0 and notreinforcing 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) + + -- Number of OPSGROUPS assigned and alive. + local Ngroups = self:CountOpsGroups() + + -- Debug info. + self:T(self.lid..string.format("CANCELLING mission in status %s. Will wait for %d groups to report mission DONE before evaluation", self.status, Ngroups)) + + -- 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.chief then + + -- Debug info. + self:T(self.lid..string.format("CHIEF will cancel the mission. Will wait for mission DONE before evaluation!")) + + -- CHIEF will cancel the mission. + self.chief:MissionCancel(self) + + elseif self.commander then + + -- Debug info. + self:T(self.lid..string.format("COMMANDER will cancel the mission. Will wait for mission DONE before evaluation!")) + + -- COMMANDER will cancel the mission. + self.commander:MissionCancel(self) + + elseif self.legions and #self.legions>0 then + + -- Loop over all LEGIONs. + for _,_legion in pairs(self.legions or {}) do + local legion=_legion --Ops.Legion#LEGION + + -- Debug info. + self:T(self.lid..string.format("LEGION %s will cancel the mission. Will wait for mission DONE before evaluation!", legion.alias)) + + -- Legion will cancel all group's missions and remove queued request from warehouse queue. + legion:MissionCancel(self) + + end + + else + + -- Debug info. + self:T(self.lid..string.format("No legion, commander or chief. Attached groups will cancel the mission on their own. Will wait for mission DONE before evaluation!")) + + -- Loop over all groups. + for _,_groupdata in pairs(self.groupdata or {}) do + local groupdata=_groupdata --#AUFTRAG.GroupData + groupdata.opsgroup:MissionCancel(self) + end + + end + + -- Special mission states. + if self:IsPlanned() or self:IsQueued() or self:IsRequested() or Ngroups==0 then + self:T(self.lid..string.format("Cancelled mission was in %s stage with %d groups assigned and alive. Call it done!", self.status, Ngroups)) + self:Done() + end + +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() + + -- Not executing any more. + self.Texecuting=nil + + -- Set status for CHIEF. + self.statusChief=AUFTRAG.Status.DONE + + -- Set status for COMMANDER. + self.statusCommander=AUFTRAG.Status.DONE + + -- Set status for LEGIONs. + for _,_legion in pairs(self.legions) do + local Legion=_legion --Ops.Legion#LEGION + + self:SetLegionStatus(Legion, AUFTRAG.Status.DONE) + + -- Remove pending request from legion queue. + if self.type==AUFTRAG.Type.RELOCATECOHORT then + + -- Get request ID + local requestid=self.requestID[Legion.alias] + + if requestid then + + -- Debug info. + self:T(self.lid.."Removing request from pending queue") + + -- Remove request from pending queue. + Legion:_DeleteQueueItemByID(requestid, Legion.pending) + + -- Remove cohort from old legion. + local Cohort=self.DCStask.params.cohort --Ops.Cohort#COHORT + Legion:DelCohort(Cohort) + + else + self:E(self.lid.."WARNING: Could NOT remove relocation request from from pending queue (all assets were spawned?)") + end + end + end + + -- Trigger relocated event. + if self.type==AUFTRAG.Type.RELOCATECOHORT then + local cohort=self.DCStask.params.cohort --Ops.Cohort#COHORT + cohort:Relocated() + 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)) + + -- Set status for CHIEF, COMMANDER and LEGIONs + self.statusChief=self.status + self.statusCommander=self.status + for _,_legion in pairs(self.legions) do + local Legion=_legion --Ops.Legion#LEGION + self:SetLegionStatus(Legion, self.status) + end + + local repeatme=self.repeatedSuccess Repeat mission!", self.repeated+1, N)) + self:__Repeat(self.repeatDelay) + + else + + -- Stop mission. + self:T(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)) + + -- Set status for CHIEF, COMMANDER and LEGIONs + self.statusChief=self.status + self.statusCommander=self.status + for _,_legion in pairs(self.legions) do + local Legion=_legion --Ops.Legion#LEGION + self:SetLegionStatus(Legion, self.status) + end + + local repeatme=self.repeatedFailure Repeat mission!", self.repeated+1, N)) + self:__Repeat(self.repeatDelay) + + else + + -- Stop mission. + self:T(self.lid..string.format("Mission FAILED! Number of max repeats %d reached ==> Stopping mission!", self.repeated+1)) + self:Stop() + + end + +end + +--- On before "Repeat" event. +-- @param #AUFTRAG self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AUFTRAG:onbeforeRepeat(From, Event, To) + + if not (self.chief or self.commander or #self.legions>0) then + self:E(self.lid.."ERROR: Mission can only be repeated by a CHIEF, COMMANDER or LEGION! Stopping AUFTRAG") + self:Stop() + return false + end + + return true +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 + + -- Debug info. + self:T(self.lid..string.format("New mission status=%s (on Repeat)", self.status)) + + -- Set status for CHIEF, COMMANDER and LEGIONs + self.statusChief=self.status + self.statusCommander=self.status + for _,_legion in pairs(self.legions) do + local Legion=_legion --Ops.Legion#LEGION + self:SetLegionStatus(Legion, self.status) + end + + -- Increase repeat counter. + self.repeated=self.repeated+1 + + if self.chief then + + -- Set status for chief. + self.statusChief=AUFTRAG.Status.PLANNED + + -- Remove mission from wingcommander because Chief will assign it again. + if self.commander then + self.statusCommander=AUFTRAG.Status.PLANNED + end + + -- Remove mission from legions because commander will assign it again but maybe to different legion(s). + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + legion:RemoveMission(self) + end + + elseif self.commander then + + -- Set status for commander. + self.statusCommander=AUFTRAG.Status.PLANNED + + -- Remove mission from legion(s) because commander will assign it again but maybe to different legion(s). + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + legion:RemoveMission(self) + self:SetLegionStatus(legion, AUFTRAG.Status.PLANNED) + end + + elseif #self.legions>0 then + + -- Remove mission from airwing because WC will assign it again but maybe to a different wing. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + legion:RemoveMission(self) + self:SetLegionStatus(legion, AUFTRAG.Status.PLANNED) + legion:AddMission(self) + end + + else + self:E(self.lid.."ERROR: Mission can only be repeated by a CHIEF, COMMANDER or LEGION! Stopping AUFTRAG") + self:Stop() + return + end + + + -- No mission assets. + self.assets={} + + + -- Remove OPS groups. This also removes the mission from the OPSGROUP mission queue. + for groupname,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + local opsgroup=groupdata.opsgroup + if opsgroup then + self:DelOpsGroup(opsgroup) + end + + end + -- No group data. + self.groupdata={} + + -- Reset casualties and units assigned. + self.Ncasualties=0 + self.Nelements=0 + self.Ngroups=0 + self.Nassigned=nil + self.Ndead=0 + + -- Update DCS mission task. Could be that the initial task (e.g. for bombing) was destroyed. Then we need to update the coordinate. + self.DCStask=self:GetDCSMissionTask() + + -- Call status again. + self:__Status(-30) + +end + +--- On after "Stop" event. Remove mission from LEGION and OPSGROUP 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) + + -- Debug info. + self:T(self.lid..string.format("STOPPED mission in status=%s. Removing missions from queues. Stopping CallScheduler!", self.status)) + + -- TODO: Mission should be OVER! we dont want to remove running missions from any queues. + + -- Remove mission from CHIEF queue. + if self.chief then + self.chief:RemoveMission(self) + end + + -- Remove mission from WINGCOMMANDER queue. + if self.commander then + self.commander:RemoveMission(self) + end + + -- Remove mission from LEGION queues. + if #self.legions>0 then + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + legion:RemoveMission(self) + end + end + + -- Remove mission from OPSGROUP queue + for _,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + groupdata.opsgroup:RemoveMission(self) + end + + -- No mission assets. + self.assets={} + + -- No group 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 and Object:IsInstanceOf("TARGET") then + + self.engageTarget=Object + + else --if Object then + + 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 +-- @param #boolean OnlyReallyAlive (Optional) If `true`, count only really alive targets (units, groups) but not coordinates or zones. +-- @return #number Number of alive target units. +function AUFTRAG:CountMissionTargets(OnlyReallyAlive) + + local N=0 + + -- Count specific coalitions. + local Coalitions=self.coalition and UTILS.GetCoalitionEnemy(self.coalition, true) or nil + + if self.engageTarget then + N=self.engageTarget:CountTargets(Coalitions, OnlyReallyAlive) + end + + return N +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 +-- @param Core.Point#COORDINATE RefCoordinate (Optional) Reference coordinate from which the closest target is determined. +-- @param #table Coalitions (Optional) Only consider targets of the given coalition(s). +-- @return Wrapper.Positionable#POSITIONABLE The target object. Could be many things. +function AUFTRAG:GetObjective(RefCoordinate, Coalitions) + + local objective=self:GetTargetData():GetObject(RefCoordinate, Coalitions) + + return objective +end + +--- Get type of target. +-- @param #AUFTRAG self +-- @return #string The target type. +function AUFTRAG:GetTargetType() + local target=self.engageTarget + if target then + local to=target:GetObjective() + if to then + return to.Type + else + return "Unknown" + end + else + return "Unknown" + end +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 + local vec2=coord:GetVec2() + return vec2 + 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.missionZoneSet and self.type == AUFTRAG.Type.RECON then + + return self.missionZoneSet:GetAverageCoordinate() + + elseif self.engageTarget then + + local coord=self.engageTarget:GetCoordinate() + return coord + + elseif self.type==AUFTRAG.Type.ALERT5 then + + -- For example, COMMANDER will not assign a coordiante. This will be done later, when the mission is assigned to an airwing. + return nil + + else + self:T(self.lid.."ERROR: Cannot get target coordinate!") + end + + return nil +end + +--- Get heading of target. +-- @param #AUFTRAG self +-- @return #number Heading of target in degrees. +function AUFTRAG:GetTargetHeading() + if self.engageTarget then + local heading=self.engageTarget:GetHeading() + return heading + 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 + local name=self.engageTarget:GetName() + return name + 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:T(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 Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be added to the mission. +-- @return #AUFTRAG self +function AUFTRAG:AddAsset(Asset) + + -- Debug info + self:T(self.lid..string.format("Adding asset \"%s\" to mission", tostring(Asset.spawngroupname))) + + -- Add to table. + self.assets=self.assets or {} + + -- Get asset if it was already added. + local asset=self:GetAssetByName(Asset.spawngroupname) + + -- Only add an asset is not already in. + if not asset then + + -- Add to table. + table.insert(self.assets, Asset) + + self.Nassigned=self.Nassigned or 0 + + self.Nassigned=self.Nassigned+1 + + end + + return self +end + +--- Add assets to mission. +-- @param #AUFTRAG self +-- @param #table Assets List of assets. +-- @return #AUFTRAG self +function AUFTRAG:_AddAssets(Assets) + + for _,asset in pairs(Assets) do + self:AddAsset(asset) + end + + return self +end + +--- Delete asset from mission. +-- @param #AUFTRAG self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem 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 --Functional.Warehouse#WAREHOUSE.Assetitem + + 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 Functional.Warehouse#WAREHOUSE.Assetitem Asset. +function AUFTRAG:GetAssetByName(Name) + + for i,_asset in pairs(self.assets or {}) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + 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 OPS 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 + +--- Count OPS groups in a certain status. +-- @param #AUFTRAG self +-- @param #string Status Status of group, e.g. `AUFTRAG.GroupStatus.EXECUTING`. +-- @return #number Number of alive OPS groups. +function AUFTRAG:CountOpsGroupsInStatus(Status) + local N=0 + for _,_groupdata in pairs(self.groupdata) do + local groupdata=_groupdata --#AUFTRAG.GroupData + if groupdata and groupdata.status==Status 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 + +--- [NON-AIR] Set the mission waypoint coordinate from where the mission is executed. Note that altitude is set via `:SetMissionAltitude`. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Coordinate where the mission is executed. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionWaypointCoord(Coordinate) + + -- Obviously a zone was passed. We get the coordinate. + if Coordinate:IsInstanceOf("ZONE_BASE") then + Coordinate=Coordinate:GetCoordinate() + end + + self.missionWaypointCoord=Coordinate + return self +end + +--- Set randomization of the mission waypoint coordinate. Each assigned group will get a random ingress coordinate, where the mission is executed. +-- @param #AUFTRAG self +-- @param #number Radius Distance in meters. Default `#nil`. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionWaypointRandomization(Radius) + self.missionWaypointRadius=Radius + return self +end + +--- Set the mission egress coordinate. This is the coordinate where the assigned group will go once the mission is finished. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Egrees coordinate. +-- @param #number Altitude (Optional) Altitude in feet. Default is y component of coordinate. +-- @param #number Speed (Optional) Speed in knots to reach this waypoint. Defaults to mission speed. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionEgressCoord(Coordinate, Altitude, Speed) + + -- Obviously a zone was passed. We get the coordinate. + if Coordinate:IsInstanceOf("ZONE_BASE") then + Coordinate=Coordinate:GetCoordinate() + end + + self.missionEgressCoord=Coordinate + + if Altitude then + self.missionEgressCoord.y=UTILS.FeetToMeters(Altitude) + self.missionEgressCoordAlt = UTILS.FeetToMeters(Altitude) + end + + self.missionEgressCoordSpeed=Speed and Speed or nil + + return self +end + +--- [Air] Set the mission ingress coordinate. This is the coordinate where the assigned group will fly before the actual mission coordinate. +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Ingrees coordinate. +-- @param #number Altitude (Optional) Altitude in feet. Default is y component of coordinate. +-- @param #number Speed (Optional) Speed in knots to reach this waypoint. Defaults to mission speed. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionIngressCoord(Coordinate, Altitude, Speed) + + -- Obviously a zone was passed. We get the coordinate. + if Coordinate:IsInstanceOf("ZONE_BASE") then + Coordinate=Coordinate:GetCoordinate() + end + + self.missionIngressCoord=Coordinate + + if Altitude then + self.missionIngressCoord.y=UTILS.FeetToMeters(Altitude) + self.missionIngressCoordAlt = UTILS.FeetToMeters(Altitude or 10000) + end + + self.missionIngressCoordSpeed=Speed and Speed or nil + + return self +end + +--- [Air] Set the mission holding coordinate. This is the coordinate where the assigned group will fly before the actual mission execution starts. Do not forget to add a push condition, too! +-- @param #AUFTRAG self +-- @param Core.Point#COORDINATE Coordinate Holding coordinate. +-- @param #number Altitude (Optional) Altitude in feet. Default is y component of coordinate. +-- @param #number Speed (Optional) Speed in knots to reach this waypoint and hold there. Defaults to mission speed. +-- @param #number Duration (Optional) Duration in seconds on how long to hold, defaults to 15 minutes. Mission continues if either a push condition is met or the time is up. +-- @return #AUFTRAG self +function AUFTRAG:SetMissionHoldingCoord(Coordinate, Altitude, Speed, Duration) + + -- Obviously a zone was passed. We get the coordinate. + if Coordinate:IsInstanceOf("ZONE_BASE") then + Coordinate=Coordinate:GetCoordinate() + end + + self.missionHoldingCoord=Coordinate + self.missionHoldingDuration=Duration or 900 + + if Altitude then + self.missionHoldingCoord.y=UTILS.FeetToMeters(Altitude) + self.missionHoldingCoordAlt = UTILS.FeetToMeters(Altitude or 10000) + end + + self.missionHoldingCoordSpeed=Speed and Speed or nil + + return self +end + +--- Get the mission egress coordinate if this was defined. +-- @param #AUFTRAG self +-- @return Core.Point#COORDINATE Coordinate Coordinate or nil. +function AUFTRAG:GetMissionEgressCoord() + return self.missionEgressCoord +end + +--- Get the mission ingress coordinate if this was defined. +-- @param #AUFTRAG self +-- @return Core.Point#COORDINATE Coordinate Coordinate or nil. +function AUFTRAG:GetMissionIngressCoord() + return self.missionIngressCoord +end + +--- Get the mission holding coordinate if this was defined. +-- @param #AUFTRAG self +-- @return Core.Point#COORDINATE Coordinate Coordinate or nil. +function AUFTRAG:GetMissionHoldingCoord() + return self.missionHoldingCoord +end + +--- Get coordinate which was set as mission waypoint coordinate. +-- @param #AUFTRAG self +-- @return Core.Point#COORDINATE Coordinate where the mission is executed or `#nil`. +function AUFTRAG:_GetMissionWaypointCoordSet() + + -- 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 + +end + +--- Get coordinate of target. First unit/group of the set is used. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP group Group. +-- @param #number randomradius Random radius in meters. +-- @param #table surfacetypes Surface types of random zone. +-- @return Core.Point#COORDINATE Coordinate where the mission is executed. +function AUFTRAG:GetMissionWaypointCoord(group, randomradius, surfacetypes) + + -- 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 + + local coord=group:GetCoordinate() + + -- Check if an ingress or holding coord has been explicitly set. + if self.missionHoldingCoord then + coord=self.missionHoldingCoord + if self.missionHoldingCoorddAlt then + coord:SetAltitude(self.missionHoldingCoordAlt, true) + end + end + + if self.missionIngressCoord then + coord=self.missionIngressCoord + if self.missionIngressCoordAlt then + coord:SetAltitude(self.missionIngressCoordAlt, true) + end + end + + -- Create waypoint coordinate half way between us and the target. + local waypointcoord=COORDINATE:New(0,0,0) + + if coord then + waypointcoord=coord:GetIntermediateCoordinate(self:GetTargetCoordinate(), self.missionFraction) + else + self:E(self.lid..string.format("ERROR: Cannot get coordinate of group %s (alive=%s)!", tostring(group:GetName()), tostring(group:IsAlive()))) + end + local alt=waypointcoord.y + + -- Add some randomization. + if randomradius then + waypointcoord=ZONE_RADIUS:New("Temp", waypointcoord:GetVec2(), randomradius):GetRandomCoordinate(nil, nil, surfacetypes):SetAltitude(alt, false) + end + + -- 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 + + +--- Get request ID from legion this mission requested assets from +-- @param #AUFTRAG self +-- @param Ops.Legion#LEGION Legion The legion from which to get the request ID. +-- @return #number Request ID (if any). +function AUFTRAG:_GetRequestID(Legion) + + local requestid=nil + local name=nil + + if type(Legion)=="string" then + name=Legion + else + name=Legion.alias + end + + if name then + requestid=self.requestID[name] + end + + return nil +end + + +--- Get request from legion this mission requested assets from. +-- @param #AUFTRAG self +-- @param Ops.Legion#LEGION Legion The legion from which to get the request ID. +-- @return Functional.Warehouse#WAREHOUSE.PendingItem Request. +function AUFTRAG:_GetRequest(Legion) + + local request=nil + + local requestID=self:_GetRequestID(Legion) + + if requestID then + request=Legion:GetRequestByID(requestID) + end + + return request +end + +--- Set request ID from legion this mission requested assets from +-- @param #AUFTRAG self +-- @param Ops.Legion#LEGION Legion The legion from which to get the request ID. +-- @param #number RequestID Request ID. +-- @return #AUFTRAG self +function AUFTRAG:_SetRequestID(Legion, RequestID) + + local requestid=nil + local name=nil + + if type(Legion)=="string" then + name=Legion + else + name=Legion.alias + end + + if name then + if self.requestID[name] then + self:I(self.lid..string.format("WARNING: Mission already has a request ID=%d!", self.requestID[name])) + end + self.requestID[name]=RequestID + end + + return self +end + +--- Check if reinforcement is done. +-- @param #AUFTRAG self +-- @return #boolean If `true`, reinforcing is over. +function AUFTRAG:_IsNotReinforcing() + + -- Number of assigned assets that are still alive. + local Nassigned=self.Nassigned and self.Nassigned-self.Ndead or 0 + + -- Not reinforcing? + local notreinforcing=((not self.reinforce) or (self.reinforce==0 and Nassigned<=0)) + + return notreinforcing +end + +--- Check if reinforcement is still ongoing. +-- @param #AUFTRAG self +-- @return #boolean If `true`, reinforcing is ongoing. +function AUFTRAG:_IsReinforcing() + + local reinforcing=not self:_IsNotReinforcing() + + return reinforcing +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("\nOpsGroups %d/%d", self:CountOpsGroups(), self:GetNumberOfRequiredAssets()) + + if not self.marker then + + -- Get target coordinates. Can be nil! + local targetcoord=self:GetTargetCoordinate() + + if targetcoord then + 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 + 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 +-- @return DCS#Task The DCS task table. If multiple tasks are necessary, this is returned as a combo task. +function AUFTRAG:GetDCSMissionTask() + + local DCStasks={} + + -- Create DCS task based on current self. + if self.type==AUFTRAG.Type.ANTISHIP then + + ---------------------- + -- ANTISHIP Mission -- + ---------------------- + + -- Add enroute anti-ship task. + local DCStask=CONTROLLABLE.EnRouteTaskAntiShip(nil) + table.insert(self.enrouteTasks, DCStask) + + 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 coords = self.engageTarget:GetCoordinates() + for _, coord in pairs(coords) do + local DCStask = CONTROLLABLE.TaskBombing(nil, coord:GetVec2(), self.engageAsGroup, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType, self.optionDivebomb) + + table.insert(DCStasks, DCStask) + end + + elseif self.type==AUFTRAG.Type.STRAFING then + + ---------------------- + -- STRAFING Mission -- + ---------------------- + + local DCStask=CONTROLLABLE.TaskStrafing(nil,self:GetTargetVec2(), self.engageQuantity, self.engageLength,self.engageWeaponType,self.engageWeaponExpend,self.engageDirection,self.engageAsGroup) + + 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.engageLength) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.CAP then + + ----------------- + -- CAP Mission -- + ----------------- + + local Vec2 = self.engageZone:GetVec2() + local Radius + if self.engageZone:IsInstanceOf("COORDINATE") then + Radius = UTILS.NMToMeters(20) + else + Radius = self.engageZone:GetRadius() + end + + local DCStask=CONTROLLABLE.EnRouteTaskEngageTargetsInZone(nil, Vec2, Radius, 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, nil, self.engageMaxDistance, self.engageTargetTypes) + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.GROUNDESCORT then + + -------------------- + -- GROUNDESCORT Mission -- + -------------------- + + local DCSTask=CONTROLLABLE.TaskGroundEscort(nil,self.engageTarget:GetObject(),nil,self.orbitDistance,self.engageTargetTypes) + + table.insert(DCStasks, DCSTask) + + elseif self.type==AUFTRAG.Type.FACA then + + ------------------ + -- AFAC 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.FAC then + + ----------------- + -- FAC Mission -- + ----------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.PATROLZONE + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.zone=self:GetObjective() + param.altitude=self.missionAltitude + param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed) or nil + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + -- Enroute task FAC + local DCSenroute=CONTROLLABLE.EnRouteTaskFAC(self, self.facFreq, self.facModu) + table.insert(self.enrouteTasks, DCSenroute) + + elseif self.type==AUFTRAG.Type.FERRY then + + ------------------- + -- FERRY Mission -- + ------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.FERRY + + -- We create a "fake" DCS task. + local param={} + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.RELOCATECOHORT then + + ---------------------- + -- RELOCATE Mission -- + ---------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.RELOCATECOHORT + + -- We create a "fake" DCS task. + local param={} + DCStask.params=param + + table.insert(DCStasks, DCStask) + + 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 -- + ------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.RECON + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.target=self.engageTarget + param.altitude=self.missionAltitude + param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed) or nil + param.lastindex=nil + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.SEAD then + + ------------------ + -- SEAD Mission -- + ------------------ + + -- Add enroute task SEAD. Disabled that here because the group enganges everything on its route. + --local DCStask=CONTROLLABLE.EnRouteTaskSEAD(nil, self.TargetType) + --table.insert(self.enrouteTasks, DCStask) + + if self.engageZone then + + --local DCStask=CONTROLLABLE.EnRouteTaskSEAD(nil, self.engageTargetTypes) + --table.insert(self.enrouteTasks, DCStask) + self.engageZone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT}) + local ScanUnitSet = self.engageZone:GetScannedSetUnit() + local SeadUnitSet = SET_UNIT:New() + for _,_unit in pairs (ScanUnitSet.Set) do + local unit = _unit -- Wrapper.Unit#UNTI + if unit and unit:IsAlive() and unit:HasSEAD() then + self:T("Adding UNIT for SEAD: "..unit:GetName()) + local task = CONTROLLABLE.TaskAttackUnit(nil,unit,GroupAttack,AI.Task.WeaponExpend.ALL,1,Direction,self.engageAltitude,2956984318) + table.insert(DCStasks, task) + SeadUnitSet:AddUnit(unit) + end + end + self.engageTarget = TARGET:New(SeadUnitSet) + --local OrbitTask = CONTROLLABLE.TaskOrbitCircle(nil,self.engageAltitude,self.missionSpeed,self.engageZone:GetCoordinate()) + --local Point = self.engageZone:GetVec2() + --local OrbitTask = CONTROLLABLE.TaskOrbitCircleAtVec2(nil,Point,self.engageAltitude,self.missionSpeed) + --table.insert(DCStasks, OrbitTask) + + else + + self:_GetDCSAttackTask(self.engageTarget, DCStasks) + + end + + elseif self.type==AUFTRAG.Type.STRIKE then + + -------------------- + -- STRIKE Mission -- + -------------------- + + local coords = self.engageTarget:GetCoordinates() + for _, coord in pairs(coords) do + local DCStask=CONTROLLABLE.TaskAttackMapObject(nil, coord:GetVec2(), self.engageAsGroup, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType) + + table.insert(DCStasks, DCStask) + end + + elseif self.type==AUFTRAG.Type.TANKER or self.type==AUFTRAG.Type.RECOVERYTANKER 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.OPSTRANSPORT then + + -------------------------- + -- OPSTRANSPORT Mission -- + -------------------------- + + local DCStask={} + + DCStask.id="OpsTransport" + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.CARGOTRANSPORT then + + ---------------------------- + -- CARGOTRANSPORT Mission -- + ---------------------------- + + -- Task to transport cargo. + local TaskCargoTransportation={ + id = "CargoTransportation", + params = {} + } + + table.insert(DCStasks, TaskCargoTransportation) + + elseif self.type==AUFTRAG.Type.RESCUEHELO then + + ------------------------- + -- RESCUE HELO Mission -- + ------------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.FORMATION + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.unitname=self:GetTargetName() + 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 -- + ------------------ + + + if self.artyShots==1 or self.artyRadius<10 or true then + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, self:GetTargetVec2(), self.artyRadius, self.artyShots, self.engageWeaponType, self.artyAltitude) + table.insert(DCStasks, DCStask) + + else + + local Vec2=self:GetTargetVec2() + + local zone=ZONE_RADIUS:New("temp", Vec2, self.artyRadius) + + for i=1,self.artyShots do + + local vec2=zone:GetRandomVec2() + + local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, vec2, 0, 1, self.engageWeaponType, self.artyAltitude) + table.insert(DCStasks, DCStask) + + end + + end + + elseif self.type==AUFTRAG.Type.BARRAGE then + + --------------------- + -- BARRAGE Mission -- + --------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.BARRAGE + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.zone=self:GetObjective() + param.altitude=self.artyAltitude + param.radius=self.artyRadius + param.heading=self.artyHeading + param.angle=self.artyAngle + param.shots=self.artyShots + param.weaponTypoe=self.engageWeaponType + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.PATROLZONE then + + ------------------------- + -- PATROL ZONE Mission -- + ------------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.PATROLZONE + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.zone=self:GetObjective() + param.altitude=self.missionAltitude + param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed) or nil + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.CAPTUREZONE then + + -------------------------- + -- CAPTURE ZONE Mission -- + -------------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.CAPTUREZONE + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.CASENHANCED then + + ------------------------- + -- CAS ENHANCED Mission -- + ------------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.PATROLZONE + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.zone=self:GetObjective() + param.altitude=self.missionAltitude + param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed) or nil + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.GROUNDATTACK then + + --------------------------- + -- GROUND ATTACK Mission -- + --------------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.GROUNDATTACK + + -- We create a "fake" DCS task and pass the parameters to the ARMYGROUP. + local param={} + param.target=self:GetTargetData() + param.action="Wedge" + param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed) or nil + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.NAVALENGAGEMENT then + + --------------------------- + -- NAVAL ENGAGEMENT Mission -- + --------------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.NAVALENGAGEMENT + + -- We create a "fake" DCS task and pass the parameters to the NAVYGROUP. + local param={} + param.target=self:GetTargetData() + param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed) or nil + param.altitude=self.missionAltitude or 0 + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.AMMOSUPPLY then + + ------------------------- + -- AMMO SUPPLY Mission -- + ------------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.AMMOSUPPLY + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.zone=self:GetObjective() + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.FUELSUPPLY then + + ------------------------- + -- FUEL SUPPLY Mission -- + ------------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.FUELSUPPLY + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.zone=self:GetObjective() + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.REARMING then + + ---------------------- + -- REARMING Mission -- + ---------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.REARMING + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.zone=self:GetObjective() + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.ALERT5 then + + --------------------- + -- ALERT 5 Mission -- + --------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.ALERT5 + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.NOTHING then + + --------------------- + -- NOTHING Mission -- + --------------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.NOTHING + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.PATROLRACETRACK then + + --------------------- + -- Enhanced Orbit Racetrack -- + --------------------- + + local DCStask={} + DCStask.id=AUFTRAG.SpecialTask.PATROLRACETRACK + + local param={} + -- ONTROLLABLE:PatrolRaceTrack(Point1, Point2, Altitude, Speed, Formation, Delay) + + param.TrackAltitude = self.TrackAltitude + param.TrackSpeed = self.TrackSpeed + param.TrackPoint1 = self.TrackPoint1 + param.TrackPoint2 = self.TrackPoint2 + param.missionSpeed = self.missionSpeed + param.missionAltitude = self.missionAltitude + param.TrackFormation = self.TrackFormation + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.HOVER then + + --------------------- + -- HOVER Mission -- + --------------------- + + local DCStask={} + DCStask.id=AUFTRAG.SpecialTask.HOVER + + local param={} + + param.hoverAltitude=self.hoverAltitude + param.hoverTime = self.hoverTime + param.missionSpeed = self.missionSpeed + param.missionAltitude = self.missionAltitude + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.LANDATCOORDINATE then + + --------------------- + -- LANDATCOORDINATE Mission + --------------------- + + local DCStask={} + local Vec2 = self.stayAt:GetVec2() + local DCStask = CONTROLLABLE.TaskLandAtVec2(nil,Vec2,self.stayTime, self.combatLand, self.directionAfter) + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.ONGUARD or self.type==AUFTRAG.Type.ARMOREDGUARD then + + ---------------------- + -- ON GUARD Mission -- + ---------------------- + + local DCStask={} + + DCStask.id= self.type==AUFTRAG.Type.ONGUARD and AUFTRAG.SpecialTask.ONGUARD or AUFTRAG.SpecialTask.ARMOREDGUARD + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.coordinate=self:GetObjective() + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.AIRDEFENSE then + + ------------------------ + -- AIRDEFENSE Mission -- + ------------------------ + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.AIRDEFENSE + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.zone=self:GetObjective() + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.EWR then + + ----------------- + -- EWR Mission -- + ----------------- + + local DCStask={} + + DCStask.id=AUFTRAG.SpecialTask.EWR + + -- We create a "fake" DCS task and pass the parameters to the OPSGROUP. + local param={} + param.zone=self:GetObjective() + + DCStask.params=param + + table.insert(DCStasks, DCStask) + + -- EWR is an enroute task + local Enroutetask=CONTROLLABLE.EnRouteTaskEWR() + table.insert(self.enrouteTasks, Enroutetask) + + else + self:T(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 or + self.type==AUFTRAG.Type.RECOVERYTANKER then + + ------------------- + -- ORBIT Mission -- + ------------------- + + -- Get/update orbit vector. + self.orbitVec2=self:GetTargetVec2() + + if self.orbitVec2 then + + -- Heading of the target. + self.targetHeading=self:GetTargetHeading() + + local OffsetVec2=nil --DCS#Vec2 + if (self.orbitOffsetVec2~=nil) then + OffsetVec2=UTILS.DeepCopy(self.orbitOffsetVec2) + end + + if OffsetVec2 then + + if self.orbitOffsetVec2.r then + -- Polar coordinates + local r=self.orbitOffsetVec2.r + local phi=(self.orbitOffsetVec2.phi or 0) + self.targetHeading + + OffsetVec2.x=r*math.cos(math.rad(phi)) + OffsetVec2.y=r*math.sin(math.rad(phi)) + else + -- Cartesian coordinates + OffsetVec2.x=self.orbitOffsetVec2.x + OffsetVec2.y=self.orbitOffsetVec2.y + end + + end + + -- Actual orbit position with possible offset. + local orbitVec2=OffsetVec2 and UTILS.Vec2Add(self.orbitVec2, OffsetVec2) or self.orbitVec2 + + -- Check for race-track pattern. + local orbitRaceTrack=nil --DCS#Vec2 + if self.orbitLeg then + + -- Default heading is due North. + local heading=0 + + -- Check if specific heading was specified. + if self.orbitHeading then + + -- Is heading realtive to target? + if self.orbitHeadingRel then + -- Relative heading wrt target. + heading=self.targetHeading+self.orbitHeading + else + -- Take given heading. + heading=self.orbitHeading + end + + else + -- Not specific heading specified ==> Take heading of target. + heading=self.targetHeading or 0 + end + + -- Race-track vector. + orbitRaceTrack=UTILS.Vec2Translate(orbitVec2, self.orbitLeg, heading) + end + + local orbitRaceTrackCoord = nil + if orbitRaceTrack then + orbitRaceTrackCoord = COORDINATE:NewFromVec2(orbitRaceTrack) + end + + -- Create orbit task. + local DCStask=CONTROLLABLE.TaskOrbit(nil, COORDINATE:NewFromVec2(orbitVec2), self.orbitAltitude, self.orbitSpeed, orbitRaceTrackCoord) + + -- Add DCS task. + table.insert(DCStasks, DCStask) + + end + + 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 + +--- Get DCS task table for an attack group or unit task. +-- @param #AUFTRAG self +-- @param #string MissionType Mission (AUFTAG) type. +-- @return #string DCS mission task for the auftrag type. +function AUFTRAG:GetMissionTaskforMissionType(MissionType) + + local mtask=ENUMS.MissionTask.NOTHING + + if MissionType==AUFTRAG.Type.ANTISHIP then + mtask=ENUMS.MissionTask.ANTISHIPSTRIKE + elseif MissionType==AUFTRAG.Type.AWACS then + mtask=ENUMS.MissionTask.AWACS + elseif MissionType==AUFTRAG.Type.BAI then + mtask=ENUMS.MissionTask.GROUNDATTACK + elseif MissionType==AUFTRAG.Type.BOMBCARPET then + mtask=ENUMS.MissionTask.GROUNDATTACK + elseif MissionType==AUFTRAG.Type.BOMBING then + mtask=ENUMS.MissionTask.GROUNDATTACK + elseif MissionType==AUFTRAG.Type.BOMBRUNWAY then + mtask=ENUMS.MissionTask.RUNWAYATTACK + elseif MissionType==AUFTRAG.Type.CAP then + mtask=ENUMS.MissionTask.CAP + elseif MissionType==AUFTRAG.Type.GCICAP then + mtask=ENUMS.MissionTask.CAP + elseif MissionType==AUFTRAG.Type.CAS then + mtask=ENUMS.MissionTask.CAS + elseif MissionType==AUFTRAG.Type.PATROLZONE then + mtask=ENUMS.MissionTask.CAS + elseif MissionType==AUFTRAG.Type.CASENHANCED then + mtask=ENUMS.MissionTask.CAS + elseif MissionType==AUFTRAG.Type.ESCORT then + mtask=ENUMS.MissionTask.ESCORT + elseif MissionType==AUFTRAG.Type.FACA then + mtask=ENUMS.MissionTask.AFAC + elseif MissionType==AUFTRAG.Type.FAC then + mtask=ENUMS.MissionTask.AFAC + elseif MissionType==AUFTRAG.Type.FERRY then + mtask=ENUMS.MissionTask.NOTHING + elseif MissionType==AUFTRAG.Type.GROUNDESCORT then + mtask=ENUMS.MissionTask.GROUNDESCORT + elseif MissionType==AUFTRAG.Type.INTERCEPT then + mtask=ENUMS.MissionTask.INTERCEPT + elseif MissionType==AUFTRAG.Type.RECON then + mtask=ENUMS.MissionTask.RECONNAISSANCE + elseif MissionType==AUFTRAG.Type.SEAD then + mtask=ENUMS.MissionTask.SEAD + elseif MissionType==AUFTRAG.Type.STRIKE then + mtask=ENUMS.MissionTask.GROUNDATTACK + elseif MissionType==AUFTRAG.Type.TANKER then + mtask=ENUMS.MissionTask.REFUELING + elseif MissionType==AUFTRAG.Type.TROOPTRANSPORT then + mtask=ENUMS.MissionTask.TRANSPORT + elseif MissionType==AUFTRAG.Type.CARGOTRANSPORT then + mtask=ENUMS.MissionTask.TRANSPORT + elseif MissionType==AUFTRAG.Type.ARMORATTACK then + mtask=ENUMS.MissionTask.NOTHING + elseif MissionType==AUFTRAG.Type.HOVER then + mtask=ENUMS.MissionTask.NOTHING + elseif MissionType==AUFTRAG.Type.PATROLRACETRACK then + mtask=ENUMS.MissionTask.CAP + end + + return mtask +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Global Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Checks if a mission type is contained in a table of possible types. +-- @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 AUFTRAG.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 #table MissionTypes The requested mission type. Can also be passed as a single mission type `#string`. +-- @param #table Capabilities A table with possible capabilities `Ops.Auftrag#AUFTRAG.Capability`. +-- @param #boolean All If `true`, given mission type must be includedin ALL capabilities. If `false` or `nil`, it must only match one. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AUFTRAG.CheckMissionCapability(MissionTypes, Capabilities, All) + + -- Ensure table. + if type(MissionTypes)~="table" then + MissionTypes={MissionTypes} + end + + for _,cap in pairs(Capabilities) do + local capability=cap --Ops.Auftrag#AUFTRAG.Capability + for _,MissionType in pairs(MissionTypes) do + if All==true then + if capability.MissionType~=MissionType then + return false + end + else + if capability.MissionType==MissionType then + return true + end + end + end + end + + if All==true then + return true + else + return false + end +end + + +--- Check if a mission type is contained in a list of possible capabilities. +-- @param #table MissionTypes The requested mission type. Can also be passed as a single mission type `#string`. +-- @param #table Capabilities A table with possible capabilities `Ops.Auftrag#AUFTRAG.Capability`. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AUFTRAG.CheckMissionCapabilityAny(MissionTypes, Capabilities) + + local res=AUFTRAG.CheckMissionCapability(MissionTypes, Capabilities, false) + + return res +end + + +--- Check if a mission type is contained in a list of possible capabilities. +-- @param #table MissionTypes The requested mission type. Can also be passed as a single mission type `#string`. +-- @param #table Capabilities A table with possible capabilities `Ops.Auftrag#AUFTRAG.Capability`. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function AUFTRAG.CheckMissionCapabilityAll(MissionTypes, Capabilities) + + local res=AUFTRAG.CheckMissionCapability(MissionTypes, Capabilities, true) + + return res +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Awacs.lua b/Moose Development/Moose/Ops/Awacs.lua new file mode 100644 index 000000000..062f32d7b --- /dev/null +++ b/Moose Development/Moose/Ops/Awacs.lua @@ -0,0 +1,7127 @@ +--- **Ops** - MOOSE AI AWACS Operations using text-to-speech. +-- +-- === +-- +-- **AWACS** - MOOSE AI AWACS Operations using text-to-speech. +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/Awacs/). +-- +-- ## Videos: +-- +-- Demo videos can be found on [Youtube](https://www.youtube.com/watch?v=ocdy8QzTNN4&list=PLFxp425SeXnq-oS0DSjam1HtddywH8i_k) +-- +-- === +-- +-- ### Author: **applevangelist** +-- @date Last Update July 2025 +-- @module Ops.AWACS +-- @image OPS_AWACS.jpg + +do +--- Ops AWACS Class +-- @type AWACS +-- @field #string ClassName Name of this class. +-- @field #string version Versioning. +-- @field #string lid LID for log entries. +-- @field #number coalition Coalition side. +-- @field #string coalitiontxt e.g."blue" +-- @field Core.Zone#ZONE OpsZone, +-- @field Core.Zone#ZONE StationZone, +-- @field Core.Zone#ZONE BorderZone, +-- @field Core.Zone#ZONE RejectZone, +-- @field #number Frequency +-- @field #number Modulation +-- @field Wrapper.Airbase#AIRBASE Airbase +-- @field Ops.Airwing#AIRWING AirWing +-- @field #number AwacsAngels +-- @field Core.Zone#ZONE OrbitZone +-- @field #number CallSign +-- @field #number CallSignNo +-- @field #boolean debug +-- @field #number verbose +-- @field #table ManagedGrps +-- @field #number ManagedGrpID +-- @field #number ManagedTaskID +-- @field Utilities.FiFo#FIFO AnchorStacks +-- @field Utilities.FiFo#FIFO CAPIdleAI +-- @field Utilities.FiFo#FIFO CAPIdleHuman +-- @field Utilities.FiFo#FIFO TaskedCAPAI +-- @field Utilities.FiFo#FIFO TaskedCAPHuman +-- @field Utilities.FiFo#FIFO OpenTasks +-- @field Utilities.FiFo#FIFO ManagedTasks +-- @field Utilities.FiFo#FIFO PictureAO +-- @field Utilities.FiFo#FIFO PictureEWR +-- @field Utilities.FiFo#FIFO Contacts +-- @field #table CatchAllMissions +-- @field #table CatchAllFGs +-- @field #number Countactcounter +-- @field Utilities.FiFo#FIFO ContactsAO +-- @field Utilities.FiFo#FIFO RadioQueue +-- @field Utilities.FiFo#FIFO PrioRadioQueue +-- @field Utilities.FiFo#FIFO CAPAirwings +-- @field Utilities.FiFo#FIFO TacticalQueue +-- @field #number AwacsTimeOnStation +-- @field #number AwacsTimeStamp +-- @field #number EscortsTimeOnStation +-- @field #number EscortsTimeStamp +-- @field #string AwacsROE +-- @field #string AwacsROT +-- @field Ops.Auftrag#AUFTRAG AwacsMission +-- @field Ops.Auftrag#AUFTRAG EscortMission +-- @field Ops.Auftrag#AUFTRAG AwacsMissionReplacement +-- @field Ops.Auftrag#AUFTRAG EscortMissionReplacement +-- @field Utilities.FiFo#FIFO AICAPMissions FIFO for Ops.Auftrag#AUFTRAG for AI CAP +-- @field #boolean MenuStrict +-- @field #number MaxAIonCAP +-- @field #number AIonCAP +-- @field #boolean ShiftChangeAwacsFlag +-- @field #boolean ShiftChangeEscortsFlag +-- @field #boolean ShiftChangeAwacsRequested +-- @field #boolean ShiftChangeEscortsRequested +-- @field #AWACS.MonitoringData MonitoringData +-- @field #boolean MonitoringOn +-- @field Core.Set#SET_CLIENT clientset +-- @field Utilities.FiFo#FIFO FlightGroups +-- @field #number PictureInterval Interval in seconds for general picture +-- @field #number PictureTimeStamp Interval timestamp +-- @field #number maxassigndistance Only assing AI/Pilots to targets max this far away +-- @field #boolean PlayerGuidance if true additional callouts to guide/warn players +-- @field #boolean ModernEra if true we get more intel on targets, and EPLR on the AIC +-- @field #boolean callsignshort if true use short (group) callsigns, e.g. "Ghost 1", else "Ghost 1 1" +-- @field #boolean keepnumber if true, use the full string after # for a player custom callsign +-- @field #table callsignTranslations optional translations for callsigns +-- @field #number MeldDistance 25nm - distance for "Meld" Call , usually shortly before the actual engagement +-- @field #number TacDistance 30nm - distance for "TAC" Call +-- @field #number ThreatDistance 15nm - distance to declare untargeted (new) threats +-- @field #string AOName name of the FEZ, e.g. Rock +-- @field Core.Point#COORDINATE AOCoordinate Coordinate of bulls eye +-- @field Utilities.FiFo#FIFO clientmenus +-- @field #number RadarBlur Radar blur in % +-- @field #number ReassignmentPause Wait this many seconds before re-assignment of a player +-- @field #boolean NoGroupTags Set to true if you don't want group tags. +-- @field #boolean SuppressScreenOutput Set to true to suppress all screen output. +-- @field #boolean NoMissileCalls Suppress missile callouts +-- @field #boolean PlayerCapAssignment Assign players to CAP tasks when they are logged on +-- @field #number GoogleTTSPadding +-- @field #number WindowsTTSPadding +-- @field #boolean AllowMarkers +-- @field #string PlayerStationName +-- @field #boolean GCI Act as GCI +-- @field Wrapper.Group#GROUP GCIGroup EWR group object for GCI ops +-- @field #string locale Localization +-- @field #boolean IncludeHelicopters +-- @field #boolean TacticalMenu +-- @field #table TacticalFrequencies +-- @field #table TacticalSubscribers +-- @field #number TacticalBaseFreq +-- @field #number TacticalIncrFreq +-- @field #number TacticalModulation +-- @field #number TacticalInterval +-- @field Core.Set#SET_GROUP DetectionSet +-- @field #number MaxMissionRange +-- @extends Core.Fsm#FSM + + +--- +-- +-- *Of all men\'s miseries the bitterest is this: to know so much and to have control over nothing.* (Herodotus) +-- +-- === +-- +-- # AWACS AI Air Controller +-- +-- * WIP (beta) +-- * AWACS replacement for the in-game AWACS +-- * Will control a fighter engagement zone and assign tasks to AI and human CAP flights +-- * Callouts referenced from: +-- ** References from ARN33396 ATP 3-52.4 (Sep 2021) (Combined Forces) +-- ** References from CNATRA P-877 (Rev 12-20) (NAVY) +-- * FSM events that the mission designer can hook into +-- * Can also be used as GCI Controller +-- +-- ## 0 Note for Multiplayer Setup +-- +-- Due to DCS limitations you need to set up a second, "normal" AWACS plane in multi-player/server environments to keep the EPLRS/DataLink going in these environments. +-- Though working in single player, the situational awareness screens of the e.g. F14/16/18 will else not receive datalink targets. +-- +-- ## 1 Prerequisites +-- +-- The radio callouts in this class are ***exclusively*** created with Text-To-Speech (TTS), based on the Moose @{Sound.SRS} Class, and output is via [Ciribob's SRS system](https://github.com/ciribob/DCS-SimpleRadioStandalone/releases) +-- Ensure you have this covered and working before tackling this class. TTS generation can thus be done via the Windows built-in system or via Google TTS; +-- the latter offers a wider range of voices and options, but you need to set up your own Google product account for this to work correctly. +-- +-- ## 2 Mission Design - Operational Priorities +-- +-- Basic operational target of the AWACS is to control a Fighter Engagement Zone, or FEZ, and defend itself. +-- +-- ## 3 Airwing(s) +-- +-- The AWACS plane, the optional escort planes, and the AI CAP planes work based on the @{Ops.Airwing} class. Read and understand the manual for this class in +-- order to set everything up correctly. You will at least need one Squadron containing the AWACS plane itself. +-- +-- Set up the Airwing +-- +-- local AwacsAW = AIRWING:New("AirForce WH-1","AirForce One") +-- AwacsAW:SetMarker(false) +-- AwacsAW:SetAirbase(AIRBASE:FindByName(AIRBASE.Caucasus.Kutaisi)) +-- AwacsAW:SetRespawnAfterDestroyed(900) +-- AwacsAW:SetTakeoffAir() +-- AwacsAW:__Start(2) +-- +-- Add the AWACS template Squadron - **Note**: remove the task AWACS in the mission editor under "Advanced Waypoint Actions" from the template to remove the DCS F10 AWACS menu +-- +-- local Squad_One = SQUADRON:New("Awacs One",2,"Awacs North") +-- Squad_One:AddMissionCapability({AUFTRAG.Type.ORBIT},100) +-- Squad_One:SetFuelLowRefuel(true) +-- Squad_One:SetFuelLowThreshold(0.2) +-- Squad_One:SetTurnoverTime(10,20) +-- AwacsAW:AddSquadron(Squad_One) +-- AwacsAW:NewPayload("Awacs One One",-1,{AUFTRAG.Type.ORBIT},100) +-- +-- Add Escorts Squad (recommended, optional) +-- +-- local Squad_Two = SQUADRON:New("Escorts",4,"Escorts North") -- taking a template with 2 planes here, will result in a group of 2 escorts which can fly in formation escorting the AWACS. +-- Squad_Two:AddMissionCapability({AUFTRAG.Type.ESCORT}) +-- Squad_Two:SetFuelLowRefuel(true) +-- Squad_Two:SetFuelLowThreshold(0.3) +-- Squad_Two:SetTurnoverTime(10,20) +-- Squad_Two:SetTakeoffAir() +-- Squad_Two:SetRadio(255,radio.modulation.AM) +-- AwacsAW:AddSquadron(Squad_Two) +-- AwacsAW:NewPayload("Escorts",-1,{AUFTRAG.Type.ESCORT},100) +-- +-- Add CAP Squad (recommended, optional) +-- +-- local Squad_Three = SQUADRON:New("CAP",10,"CAP North") +-- Squad_Three:AddMissionCapability({AUFTRAG.Type.ALERT5, AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT},80) +-- Squad_Three:SetFuelLowRefuel(true) +-- Squad_Three:SetFuelLowThreshold(0.3) +-- Squad_Three:SetTurnoverTime(10,20) +-- Squad_Three:SetTakeoffAir() +-- Squad_Two:SetRadio(255,radio.modulation.AM) +-- AwacsAW:AddSquadron(Squad_Three) +-- AwacsAW:NewPayload("Aerial-1-2",-1,{AUFTRAG.Type.ALERT5,AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT},100) +-- +-- ## 4 Zones +-- +-- For the setup, you need to set up a couple of zones: +-- +-- * An Orbit Zone, where your AWACS will orbit +-- * A Fighter Engagement Zone or FEZ +-- * A zone where your CAP flights will be stationed, waiting for assignments +-- * Optionally, an additional zone you wish to defend +-- * Optionally, a border of the opposing party +-- * Also, and move your BullsEye in the mission accordingly - this will be the key reference point for most AWACS callouts +-- +-- ### 4.1 Strategic considerations +-- +-- Your AWACS is an HVT or high-value-target. Thus it makes sense to position the Orbit Zone in a way that your FEZ and thus your CAP flights defend it. +-- It should hence be positioned behind the FEZ, away from the direction of enemy engagement. +-- The zone for CAP stations should be close to the FEZ, but not inside it. +-- The optional additional defense zone can be anywhere, but keep an eye on the location so your CAP flights don't take ages to get there. +-- The optional border is useful for e.g. "cold war" scenarios - planes across the border will not be considered as targets by AWACS. +-- +-- ## 5 Set up AWACS +-- +-- -- Set up AWACS called "AWACS North". It will use the AwacsAW Airwing set up above and be of the "blue" coalition. Homebase is Kutaisi. +-- -- The AWACS Orbit Zone is a round zone set in the mission editor named "Awacs Orbit", the FEZ is a Polygon-Zone called "Rock" we have also +-- -- set up in the mission editor with a late activated helo named "Rock#ZONE_POLYGON". Note this also sets the BullsEye to be referenced as "Rock". +-- -- The CAP station zone is called "Fremont". We will be on 255 AM. +-- local testawacs = AWACS:New("AWACS North",AwacsAW,"blue",AIRBASE.Caucasus.Kutaisi,"Awacs Orbit",ZONE:FindByName("Rock"),"Fremont",255,radio.modulation.AM ) +-- -- set one escort group; this example has two units in the template group, so they can fly a nice formation. +-- testawacs:SetEscort(1,ENUMS.Formation.FixedWing.FingerFour.Group,{x=-500,y=50,z=500},45) +-- -- Callsign will be "Focus". We'll be a Angels 30, doing 300 knots, orbit leg to 88deg with a length of 25nm. +-- testawacs:SetAwacsDetails(CALLSIGN.AWACS.Focus,1,30,300,88,25) +-- -- Set up SRS on port 5010 - change the below to your path and port +-- testawacs:SetSRS("C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio","female","en-GB",5010) +-- -- Add a "red" border we don't want to cross, set up in the mission editor with a late activated helo named "Red Border#ZONE_POLYGON" +-- testawacs:SetRejectionZone(ZONE:FindByName("Red Border")) +-- -- Our CAP flight will have the callsign "Ford", we want 4 AI planes, Time-On-Station is four hours, doing 300 kn IAS. +-- testawacs:SetAICAPDetails(CALLSIGN.Aircraft.Ford,4,4,300) +-- -- We're modern (default), e.g. we have EPLRS and get more fill-in information on detections +-- testawacs:SetModernEra() +-- -- And start +-- testawacs:__Start(5) +-- +-- ### 5.1 Alternative - Set up as GCI (no AWACS plane needed) Theater Air Control System (TACS) +-- +-- -- Set up as TACS called "GCI Senaki". It will use the AwacsAW Airwing set up above and be of the "blue" coalition. Homebase is Senaki. +-- -- No need to set the AWACS Orbit Zone; the FEZ is still a Polygon-Zone called "Rock" we have also +-- -- set up in the mission editor with a late activated helo named "Rock#ZONE_POLYGON". Note this also sets the BullsEye to be referenced as "Rock". +-- -- The CAP station zone is called "Fremont". We will be on 255 AM. Note the Orbit Zone is given as *nil* in the `New()`-Statement +-- local testawacs = AWACS:New("GCI Senaki",AwacsAW,"blue",AIRBASE.Caucasus.Senaki_Kolkhi,nil,ZONE:FindByName("Rock"),"Fremont",255,radio.modulation.AM ) +-- -- Set up SRS on port 5010 - change the below to your path and port +-- testawacs:SetSRS("C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio","female","en-GB",5010) +-- -- Add a "red" border we don't want to cross, set up in the mission editor with a late activated helo named "Red Border#ZONE_POLYGON" +-- testawacs:SetRejectionZone(ZONE:FindByName("Red Border")) +-- -- Our CAP flight will have the callsign "Ford", we want 4 AI planes, Time-On-Station is four hours, doing 300 kn IAS. +-- testawacs:SetAICAPDetails(CALLSIGN.Aircraft.Ford,4,4,300) +-- -- We're modern (default), e.g. we have EPLRS and get more fill-in information on detections +-- testawacs:SetModernEra() +-- -- Give it a fancy callsign +-- testawacs:SetAwacsDetails(CALLSIGN.AWACS.Wizard) +-- -- And start as GCI using a group name "Blue EWR" as main EWR station +-- testawacs:SetAsGCI(GROUP:FindByName("Blue EWR"),2) +-- -- Set Custom CAP Flight Callsigns for use with TTS +-- testawacs:SetCustomCallsigns({ +-- Devil = 'Bengal', +-- Snake = 'Winder', +-- Colt = 'Camelot', +-- Enfield = 'Victory', +-- Uzi = 'Evil Eye' +-- }) +-- testawacs:__Start(4) +-- +-- ## 6 Menu entries +-- +-- **Note on Radio Menu entries**: Due to a DCS limitation, these are on GROUP level and not individual (UNIT level). Hence, either put each player in his/her own group, +-- or ensure that only the flight lead will use the menu. Recommend the 1st option, unless you have a disciplined team. +-- +-- ### 6.1 Check-in +-- +-- In the base setup, you need to check in to the AWACS to get the full menu. This can be done once the AWACS is airborne. You will get an Alpha Check callout +-- and be assigned a CAP station. +-- +-- ### 6.2 Check-out +-- +-- You can check-out anytime, of course. +-- +-- ### 6.3 Picture +-- +-- Get a picture from the AWACS. It will call out the three most important groups. References are **always** to the (named) BullsEye position. +-- **Note** that AWACS will anyway do a regular picture call to all stations every five minutes. +-- +-- ### 6.4 Bogey Dope +-- +-- Get bogey dope from the AWACS. It will call out the closest bogey group, if any. Reference is BRAA to the Player position. +-- +-- ### 6.5 Declare +-- +-- AWACS will declare, if the bogey closest to the calling player in a 3nm circle is hostile, friendly or neutral. +-- +-- ### 6.6 Tasking +-- +-- Tasking will show you the current task with "Showtask". Updated directions are shown, also. +-- You can decline a **requested** task with "unable", and abort **any task but CAP station** with "abort". +-- You can "commit" to a requested task within 3 minutes. +-- "VID" - if AWACS is set to Visial ID or VID oncoming planes first, there will also be an "VID" entry. Similar to "Declare" you can declare the requested contact +-- to be hostile, friendly or neutral if you are close enough to it (3nm). If hostile, at the time of writing, an engagement task will be assigned to you (not: requested). +-- If neutral/friendly, contact will be excluded from further tasking. +-- +-- ## 7 Air-to-Air Timeline Support +-- +-- To support your engagement timeline, AWACS will make Tac-Range, Meld, Merge and Threat call-outs to the player/group (Figure 7-3, CNATRA P-877). Default settings in NM are +-- +-- Tac Distance = 45 +-- Meld Distance = 35 +-- Threat Distance = 25 +-- Merge Distance = 5 +-- +-- ## 8 Bespoke Player CallSigns +-- +-- Append the GROUP name of your client slots with "#CallSign" to use bespoke callsigns in AWACS callouts. E.g. "Player F14#Ghostrider" will be refered to +-- as "Ghostrider" plus group number, e.g. "Ghostrider 9". Alternatively, if you have set up your Player name in the "Logbook" in the mission editor main screen +-- as e.g. "Pikes | Goose", you will be addressed as "Goose" by the AWACS callouts. +-- +-- ## 9 Options +-- +-- There's a number of functions available, to set various options for the setup. +-- +-- * @{#AWACS.SetBullsEyeAlias}() : Set the alias name of the Bulls Eye. +-- * @{#AWACS.SetTOS}() : Set time on station for AWACS and CAP. +-- * @{#AWACS.SetReassignmentPause}() : Pause this number of seconds before re-assigning a Player to a task. +-- * @{#AWACS.SuppressScreenMessages}() : Suppress message output on screen. +-- * @{#AWACS.SetRadarBlur}() : Set the radar blur faktor in percent. +-- * @{#AWACS.SetColdWar}() : Set to cold war - no fill-ins, no EPLRS, VID as standard. +-- * @{#AWACS.SetModernEraDefensive}() : Set to modern, EPLRS, BVR/IFF engagement, fill-ins. +-- * @{#AWACS.SetModernEraAggressive}() : Set to modern, EPLRS, BVR/IFF engagement, fill-ins. +-- * @{#AWACS.SetPolicingModern}() : Set to modern, EPLRS, VID engagement, fill-ins. +-- * @{#AWACS.SetPolicingColdWar}() : Set to cold war, no EPLRS, VID engagement, no fill-ins. +-- * @{#AWACS.SetInterceptTimeline}() : Set distances for TAC, Meld and Threat range calls. +-- * @{#AWACS.SetAdditionalZone}() : Add one additional defense zone, e.g. own border. +-- * @{#AWACS.SetRejectionZone}() : Add one foreign border. Targets beyond will be ignored for tasking. +-- * @{#AWACS.DrawFEZ}() : Show the FEZ on the F10 map. +-- * @{#AWACS.SetAWACSDetails}() : Set AWACS details. +-- * @{#AWACS.AddGroupToDetection}() : Add a GROUP or SET_GROUP object to INTEL detection, e.g. EWR. +-- * @{#AWACS.SetSRS}() : Set SRS details. +-- * @{#AWACS.SetSRSVoiceCAP}() : Set voice details for AI CAP planes, using Windows dektop TTS. +-- * @{#AWACS.SetAICAPDetails}() : Set AI CAP details. +-- * @{#AWACS.SetEscort}() : Set number of escorting planes for AWACS. +-- * @{#AWACS.AddCAPAirWing}() : Add an additional @{Ops.Airwing#AIRWING} for CAP flights. +-- * @{#AWACS.ZipLip}() : Do not show messages on screen, no extra calls for player guidance, use short callsigns, no group tags. +-- * @{#AWACS.AddFrequencyAndModulation}() : Add additional frequencies with modulation which will receive AWACS SRS messages. +-- +-- ## 9.1 Single Options +-- +-- Further single options (set before starting your AWACS instance, but after `:New()`) +-- +-- testawacs.PlayerGuidance = true -- allow missile warning call-outs. +-- testawacs.NoGroupTags = false -- use group tags like Alpha, Bravo .. etc in call outs. +-- testawacs.callsignshort = true -- use short callsigns, e.g. "Moose 1", not "Moose 1-1". +-- testawacs.DeclareRadius = 5 -- you need to be this close to the lead unit for declare/VID to work, in NM. +-- testawacs.MenuStrict = true -- Players need to check-in to see the menu; check-in still require to use the menu. +-- testawacs.maxassigndistance = 100 -- Don't assign targets further out than this, in NM. +-- testawacs.debug = false -- set to true to produce more log output. +-- testawacs.NoMissileCalls = true -- suppress missile callouts +-- testawacs.PlayerCapAssignment = true -- no intercept task assignments for players +-- testawacs.invisible = false -- set AWACS to be invisible to hostiles +-- testawacs.immortal = false -- set AWACS to be immortal +-- -- By default, the radio queue is checked every 10 secs. This is altered by the calculated length of the sentence to speak +-- -- over the radio. Google and Windows speech speed is different. Use the below to fine-tune the setup in case of overlapping +-- -- messages or too long pauses +-- testawacs.GoogleTTSPadding = 1 -- seconds +-- testawacs.WindowsTTSPadding = 2.5 -- seconds +-- testawacs.PikesSpecialSwitch = false -- if set to true, AWACS will omit the "doing xy knots" on the station assignement callout +-- testawacs.IncludeHelicopters = false -- if set to true, Helicopter pilots will also get the AWACS Menu and options +-- +-- ## 9.2 Bespoke random voices for AI CAP (Google TTS only) +-- +-- Currently there are 10 voices defined which are randomly assigned to the AI CAP flights: +-- +-- Defaults are: +-- +-- testawacs.CapVoices = { +-- [1] = "de-DE-Wavenet-A", +-- [2] = "de-DE-Wavenet-B", +-- [3] = "fr-FR-Wavenet-A", +-- [4] = "fr-FR-Wavenet-B", +-- [5] = "en-GB-Wavenet-A", +-- [6] = "en-GB-Wavenet-B", +-- [7] = "en-GB-Wavenet-D", +-- [8] = "en-AU-Wavenet-B", +-- [9] = "en-US-Wavenet-J", +-- [10] = "en-US-Wavenet-H", +-- } +-- +-- ## 10 Using F10 map markers to create new player station points +-- +-- You can use F10 map markers to create new station points for human CAP flights. The latest created station will take priority for (new) station assignments for humans. +-- Enable this option with +-- +-- testawacs.AllowMarkers = true +-- +-- Set a marker on the map and add the following text to create a station: "AWACS Station London" - "AWACS Station" are the necessary keywords, "London" +-- in this example will be the name of the new station point. The user marker can then be deleted, an info marker point at the same place will remain. +-- You can delete a player station point the same way: "AWACS Delete London"; note this will only work if currently there are no assigned flights on this station. +-- Lastly, you can move the station around with keyword "Move": "AWACS Move London". +-- +-- ## 11 Localization +-- +-- Localization for English text is build-in. Default setting is English. Change with @{#AWACS.SetLocale}() +-- +-- ### 11.1 Adding Localization +-- +-- A list of fields to be defined follows below. **Note** that in some cases `string.format()` is used to format texts for screen and SRS. +-- Hence, the `%d`, `%s` and `%f` special characters need to appear in the exact same amount and order of appearance in the localized text or it will create errors. +-- To add a localization, the following texts need to be translated and set in your mission script **before** @{#AWACS.Start}(): +-- +-- AWACS.Messages = { +-- EN = +-- { +-- DEFEND = "%s, %s! %s! %s! Defend!", +-- VECTORTO = "%s, %s. Vector%s %s", +-- VECTORTOTTS = "%s, %s, Vector%s %s", +-- ANGELS = ". Angels ", +-- ZERO = "zero", +-- VANISHED = "%s, %s Group. Vanished.", +-- VANISHEDTTS = "%s, %s group vanished.", +-- SHIFTCHANGE = "%s shift change for %s control.", +-- GROUPCAP = "Group", +-- GROUP = "group", +-- MILES = "miles", +-- THOUSAND = "thousand", +-- BOGEY = "Bogey", +-- ALLSTATIONS = "All Stations", +-- PICCLEAN = "%s. %s. Picture Clean.", +-- PICTURE = "Picture", +-- ONE = "One", +-- GROUPMULTI = "groups", +-- NOTCHECKEDIN = "%s. %s. Negative. You are not checked in.", +-- CLEAN = "%s. %s. Clean.", +-- DOPE = "%s. %s. Bogey Dope. ", +-- VIDPOS = "%s. %s. Copy, target identified as %s.", +-- VIDNEG = "%s. %s. Negative, get closer to target.", +-- FFNEUTRAL = "Neutral", +-- FFFRIEND = "Friendly", +-- FFHOSTILE = "Hostile", +-- FFSPADES = "Spades", +-- FFCLEAN = "Clean", +-- COPY = "%s. %s. Copy.", +-- TARGETEDBY = "Targeted by %s.", +-- STATUS = "Status", +-- ALREADYCHECKEDIN = "%s. %s. Negative. You are already checked in.", +-- ALPHACHECK = "Alpha Check", +-- CHECKINAI = "%s. %s. Checking in as fragged. Expected playtime %d hours. Request Alpha Check %s.", +-- SAFEFLIGHT = "%s. %s. Copy. Have a safe flight home.", +-- VERYLOW = "very low", +-- AIONSTATION = "%s. %s. On station over anchor %d at angels %d. Ready for tasking.", +-- POPUP = "Pop-up", +-- NEWGROUP = "New group", +-- HIGH= " High.", +-- VERYFAST = " Very fast.", +-- FAST = " Fast.", +-- THREAT = "Threat", +-- MERGED = "Merged", +-- SCREENVID = "Intercept and VID %s group.", +-- SCREENINTER = "Intercept %s group.", +-- ENGAGETAG = "Targeted by %s.", +-- REQCOMMIT = "%s. %s group. %s. %s, request commit.", +-- AICOMMIT = "%s. %s group. %s. %s, commit.", +-- COMMIT = "Commit", +-- SUNRISE = "%s. All stations, SUNRISE SUNRISE SUNRISE, %s.", +-- AWONSTATION = "%s on station for %s control.", +-- STATIONAT = "%s. %s. Station at %s at angels %d.", +-- STATIONATLONG = "%s. %s. Station at %s at angels %d doing %d knots.", +-- STATIONSCREEN = "%s. %s.\nStation at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s.", +-- STATIONTASK = "Station at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s", +-- VECTORSTATION = " to Station", +-- TEXTOPTIONS1 = "Lost friendly flight", +-- TEXTOPTIONS2 = "Vanished friendly flight", +-- TEXTOPTIONS3 = "Faded friendly contact", +-- TEXTOPTIONS4 = "Lost contact with", +-- }, +-- } +-- +-- e.g. +-- +-- testawacs.Messages = { +-- DE = { +-- ... +-- FFNEUTRAL = "Neutral", +-- FFFRIEND = "Freund", +-- FFHOSTILE = "Feind", +-- FFSPADES = "Uneindeutig", +-- FFCLEAN = "Sauber", +-- ... +-- }, +-- +-- ## 12 Discussion +-- +-- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-awacs channel. +-- +-- +-- +-- +-- @field #AWACS +AWACS = { + ClassName = "AWACS", -- #string + version = "0.2.73", -- #string + lid = "", -- #string + coalition = coalition.side.BLUE, -- #number + coalitiontxt = "blue", -- #string + OpsZone = nil, + StationZone = nil, + AirWing = nil, + Frequency = 271, -- #number + Modulation = radio.modulation.AM, -- #number + Airbase = nil, + AwacsAngels = 25, -- orbit at 25'000 ft + OrbitZone = nil, + CallSign = CALLSIGN.AWACS.Magic, -- #number + CallSignNo = 1, -- #number + debug = false, + verbose = false, + ManagedGrps = {}, + ManagedGrpID = 0, -- #number + ManagedTaskID = 0, -- #number + AnchorStacks = {}, -- Utilities.FiFo#FIFO + CAPIdleAI = {}, + CAPIdleHuman = {}, + TaskedCAPAI = {}, + TaskedCAPHuman = {}, + OpenTasks = {}, -- Utilities.FiFo#FIFO + ManagedTasks = {}, -- Utilities.FiFo#FIFO + PictureAO = {}, -- Utilities.FiFo#FIFO + PictureEWR = {}, -- Utilities.FiFo#FIFO + Contacts = {}, -- Utilities.FiFo#FIFO + Countactcounter = 0, + ContactsAO = {}, -- Utilities.FiFo#FIFO + RadioQueue = {}, -- Utilities.FiFo#FIFO + PrioRadioQueue = {}, -- Utilities.FiFo#FIFO + TacticalQueue = {}, -- Utilities.FiFo#FIFO + AwacsTimeOnStation = 4, + AwacsTimeStamp = 0, + EscortsTimeOnStation = 4, + EscortsTimeStamp = 0, + CAPTimeOnStation = 4, + AwacsROE = "", + AwacsROT = "", + MenuStrict = true, + MaxAIonCAP = 3, + AIonCAP = 0, + AICAPMissions = {}, -- Utilities.FiFo#FIFO + ShiftChangeAwacsFlag = false, + ShiftChangeEscortsFlag = false, + ShiftChangeAwacsRequested = false, + ShiftChangeEscortsRequested = false, + CAPAirwings = {}, -- Utilities.FiFo#FIFO + MonitoringData = {}, + MonitoringOn = false, + FlightGroups = {}, + AwacsMission = nil, + AwacsInZone = false, -- not yet arrived or gone again + AwacsReady = false, + CatchAllMissions = {}, + CatchAllFGs = {}, + PictureInterval = 300, + ReassignTime = 120, + PictureTimeStamp = 0, + BorderZone = nil, + RejectZone = nil, + maxassigndistance = 100, + PlayerGuidance = true, + ModernEra = true, + callsignshort = true, + keepnumber = true, + callsignTranslations = nil, + TacDistance = 45, + MeldDistance = 35, + ThreatDistance = 25, + AOName = "Rock", + AOCoordinate = nil, + clientmenus = nil, + RadarBlur = 15, + ReassignmentPause = 180, + NoGroupTags = false, + SuppressScreenOutput = false, + NoMissileCalls = true, + GoogleTTSPadding = 1, + WindowsTTSPadding = 2.5, + PlayerCapAssignment = true, + AllowMarkers = false, + PlayerStationName = nil, + GCI = false, + GCIGroup = nil, + locale = "en", + IncludeHelicopters = false, + TacticalMenu = false, + TacticalFrequencies = {}, + TacticalSubscribers = {}, + TacticalBaseFreq = 130, + TacticalIncrFreq = 0.5, + TacticalModulation = radio.modulation.AM, + TacticalInterval = 120, + DetectionSet = nil, + MaxMissionRange = 125, +} + +--- +--@field CallSignClear +AWACS.CallSignClear = { + [1]="Overlord", + [2]="Magic", + [3]="Wizard", + [4]="Focus", + [5]="Darkstar", +} + +--- +-- @field AnchorNames +AWACS.AnchorNames = { + [1] = "One", + [2] = "Two", + [3] = "Three", + [4] = "Four", + [5] = "Five", + [6] = "Six", + [7] = "Seven", + [8] = "Eight", + [9] = "Nine", + [10] = "Ten", +} + +--- +-- @field IFF +AWACS.IFF = +{ + SPADES = "Spades", + NEUTRAL = "Neutral", + FRIENDLY = "Friendly", + ENEMY = "Hostile", + BOGEY = "Bogey", +} + +--- +-- @field Phonetic +AWACS.Phonetic = +{ + [1] = 'Alpha', + [2] = 'Bravo', + [3] = 'Charlie', + [4] = 'Delta', + [5] = 'Echo', + [6] = 'Foxtrot', + [7] = 'Golf', + [8] = 'Hotel', + [9] = 'India', + [10] = 'Juliett', + [11] = 'Kilo', + [12] = 'Lima', + [13] = 'Mike', + [14] = 'November', + [15] = 'Oscar', + [16] = 'Papa', + [17] = 'Quebec', + [18] = 'Romeo', + [19] = 'Sierra', + [20] = 'Tango', + [21] = 'Uniform', + [22] = 'Victor', + [23] = 'Whiskey', + [24] = 'Xray', + [25] = 'Yankee', + [26] = 'Zulu', +} + +--- +-- @field Shipsize +AWACS.Shipsize = +{ + [1] = "Singleton", + [2] = "Two-Ship", + [3] = "Heavy", + [4] = "Gorilla", +} + +--- +-- @field ROE +AWACS.ROE = { + POLICE = "Police", + VID = "Visual ID", + IFF = "IFF", + BVR = "Beyond Visual Range", +} + +--- +-- @field AWACS.ROT +AWACS.ROT = { + BYPASSESCAPE = "Bypass and Escape", + EVADE = "Evade Fire", + PASSIVE = "Passive Defense", + RETURNFIRE = "Return Fire", + OPENFIRE = "Open Fire", + } + +--- +--@field THREATLEVEL -- can be 1-10, thresholds +AWACS.THREATLEVEL = { + GREEN = 3, + AMBER = 7, + RED = 10, +} + +--- +--@field CapVoices -- Random CAP voices +AWACS.CapVoices = { + [1] = "de-DE-Wavenet-A", + [2] = "de-DE-Wavenet-B", + [3] = "fr-FR-Wavenet-A", + [4] = "fr-FR-Wavenet-B", + [5] = "en-GB-Wavenet-A", + [6] = "en-GB-Wavenet-B", + [7] = "en-GB-Wavenet-D", + [8] = "en-AU-Wavenet-B", + [9] = "en-US-Wavenet-J", + [10] = "en-US-Wavenet-H", +} + +--- +-- @field Messages +AWACS.Messages = { + EN = + { + DEFEND = "%s, %s! %s! %s! Defend!", + VECTORTO = "%s, %s. Vector%s %s", + VECTORTOTTS = "%s, %s, Vector%s %s", + ANGELS = ". Angels ", + ZERO = "zero", + VANISHED = "%s, %s Group. Vanished.", + VANISHEDTTS = "%s, %s group vanished.", + SHIFTCHANGE = "%s shift change for %s control.", + GROUPCAP = "Group", + GROUP = "group", + MILES = "miles", + THOUSAND = "thousand", + BOGEY = "Bogey", + ALLSTATIONS = "All Stations", + PICCLEAN = "%s. %s. Picture Clean.", + PICTURE = "Picture", + ONE = "One", + GROUPMULTI = "groups", + NOTCHECKEDIN = "%s. %s. Negative. You are not checked in.", + CLEAN = "%s. %s. Clean.", + DOPE = "%s. %s. Bogey Dope. ", + VIDPOS = "%s. %s. Copy, target identified as %s.", + VIDNEG = "%s. %s. Negative, get closer to target.", + FFNEUTRAL = "Neutral", + FFFRIEND = "Friendly", + FFHOSTILE = "Hostile", + FFSPADES = "Spades", + FFCLEAN = "Clean", + COPY = "%s. %s. Copy.", + TARGETEDBY = "Targeted by %s.", + STATUS = "Status", + ALREADYCHECKEDIN = "%s. %s. Negative. You are already checked in.", + ALPHACHECK = "Alpha Check", + CHECKINAI = "%s. %s. Checking in as fragged. Expected playtime %d hours. Request Alpha Check %s.", + SAFEFLIGHT = "%s. %s. Copy. Have a safe flight home.", + VERYLOW = "very low", + AIONSTATION = "%s. %s. On station over anchor %d at angels %d. Ready for tasking.", + POPUP = "Pop-up", + NEWGROUP = "New group", + HIGH= " High.", + VERYFAST = " Very fast.", + FAST = " Fast.", + THREAT = "Threat", + MERGED = "Merged", + SCREENVID = "Intercept and VID %s group.", + SCREENINTER = "Intercept %s group.", + ENGAGETAG = "Targeted by %s.", + REQCOMMIT = "%s. %s group. %s. %s, request commit.", + AICOMMIT = "%s. %s group. %s. %s, commit.", + COMMIT = "Commit", + SUNRISE = "%s. All stations, SUNRISE SUNRISE SUNRISE, %s.", + AWONSTATION = "%s on station for %s control.", + STATIONAT = "%s. %s. Station at %s at angels %d.", + STATIONATLONG = "%s. %s. Station at %s at angels %d doing %d knots.", + STATIONSCREEN = "%s. %s.\nStation at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s.", + STATIONTASK = "Station at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s", + VECTORSTATION = " to Station", + TEXTOPTIONS1 = "Lost friendly flight", + TEXTOPTIONS2 = "Vanished friendly flight", + TEXTOPTIONS3 = "Faded friendly contact", + TEXTOPTIONS4 = "Lost contact with", + }, +} + +--- +-- @type AWACS.MonitoringData +-- @field #string AwacsStateMission +-- @field #string AwacsStateFG +-- @field #boolean AwacsShiftChange +-- @field #table EscortsStateMission +-- @field #table EscortsStateFG +-- @field #boolean EscortsShiftChange +-- @field #number AICAPMax +-- @field #number AICAPCurrent +-- @field #number Airwings +-- @field #number Players +-- @field #number PlayersCheckedin + +--- +-- @type AWACS.MenuStructure +-- @field #boolean menuset +-- @field #string groupname +-- @field Core.Menu#MENU_GROUP basemenu +-- @field Core.Menu#MENU_GROUP_COMMAND checkin +-- @field Core.Menu#MENU_GROUP_COMMAND checkout +-- @field Core.Menu#MENU_GROUP_COMMAND picture +-- @field Core.Menu#MENU_GROUP_COMMAND bogeydope +-- @field Core.Menu#MENU_GROUP_COMMAND declare +-- @field Core.Menu#MENU_GROUP tasking +-- @field Core.Menu#MENU_GROUP_COMMAND showtask +-- @field Core.Menu#MENU_GROUP_COMMAND judy +-- @field Core.Menu#MENU_GROUP_COMMAND unable +-- @field Core.Menu#MENU_GROUP_COMMAND abort +-- @field Core.Menu#MENU_GROUP_COMMAND commit +-- @field Core.Menu#MENU_GROUP vid +-- @field Core.Menu#MENU_GROUP_COMMAND neutral +-- @field Core.Menu#MENU_GROUP_COMMAND hostile +-- @field Core.Menu#MENU_GROUP_COMMAND friendly + +--- Group Data +-- @type AWACS.ManagedGroup +-- @field Wrapper.Group#GROUP Group +-- @field #string GroupName +-- @field Ops.FlightGroup#FLIGHTGROUP FlightGroup for AI +-- @field #boolean IsPlayer +-- @field #boolean IsAI +-- @field #string CallSign +-- @field #number CurrentAuftrag -- Auftragsnummer for AI +-- @field #number CurrentTask -- ManagedTask ID +-- @field #boolean HasAssignedTask +-- @field #number GID +-- @field #number AnchorStackNo +-- @field #number AnchorStackAngels +-- @field #number ContactCID +-- @field Core.Point#COORDINATE LastKnownPosition +-- @field #number LastTasking TimeStamp + +--- Contact Data +-- @type AWACS.ManagedContact +-- @field #number CID +-- @field Ops.Intel#INTEL.Contact Contact +-- @field Ops.Intel#INTEL.Cluster Cluster +-- @field #string IFF -- ID'ed or not (yet) +-- @field Ops.Target#TARGET Target +-- @field #number LinkedTask --> TID +-- @field #number LinkedGroup --> GID +-- @field #string Status - #AWACS.TaskStatus +-- @field #string TargetGroupNaming -- Alpha, Charlie +-- @field #string ReportingName -- NATO platform name +-- @field #string EngagementTag +-- @field #boolean TACCallDone +-- @field #boolean MeldCallDone +-- @field #boolean MergeCallDone + +--- +-- @type AWACS.TaskDescription +AWACS.TaskDescription = { + ANCHOR = "Anchor", + REANCHOR = "Re-Anchor", + VID = "VID", + IFF = "IFF", + INTERCEPT = "Intercept", + SWEEP = "Sweep", + RTB = "RTB", +} + +--- +-- @type AWACS.TaskStatus +AWACS.TaskStatus = { + IDLE = "Idle", + UNASSIGNED = "Unassigned", + REQUESTED = "Requested", + ASSIGNED = "Assigned", + EXECUTING = "Executing", + SUCCESS = "Success", + FAILED = "Failed", + DEAD = "Dead", +} + +--- +-- @type AWACS.ManagedTask +-- @field #number TID +-- @field #number AssignedGroupID +-- @field #boolean IsPlayerTask +-- @field #boolean IsUnassigned +-- @field Ops.Target#TARGET Target +-- @field Ops.Auftrag#AUFTRAG Auftrag +-- @field #AWACS.TaskStatus Status +-- @field #AWACS.TaskDescription ToDo +-- @field #string ScreenText Long descrition +-- @field Ops.Intel#INTEL.Contact Contact +-- @field Ops.Intel#INTEL.Cluster Cluster +-- @field #number CurrentAuftrag +-- @field #number RequestedTimestamp + +--- +-- @type AWACS.AnchorAssignedEntry +-- @field #number ID +-- @field #number Angels + +--- +-- @type AWACS.AnchorData +-- @field #number AnchorBaseAngels +-- @field Core.Zone#ZONE_RADIUS StationZone +-- @field Core.Point#COORDINATE StationZoneCoordinate +-- @field #string StationZoneCoordinateText +-- @field #string StationName +-- @field Utilities.FiFo#FIFO AnchorAssignedID FiFo of #AWACS.AnchorAssignedEntry +-- @field Utilities.FiFo#FIFO Anchors FiFo of available stacks +-- @field Wrapper.Marker#MARKER AnchorMarker Tag for this station + +--- +--@type RadioEntry +--@field #string TextTTS +--@field #string TextScreen +--@field #boolean IsNew +--@field #boolean IsGroup +--@field #boolean GroupID +--@field #number Duration +--@field #boolean ToScreen +--@field #boolean FromAI + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO-List 0.2.54 +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- +-- DONE - WIP - Player tasking, VID +-- DONE - Localization (sensible?) +-- TODO - (LOW) LotATC +-- DONE - SW Optimization +-- WONTDO - Maybe check in AI only when airborne +-- DONE - remove SSML tag when not on google (currently sometimes spoken) +-- DONE - Maybe - Assign specific number of AI CAP to a station +-- DONE - Multiple AIRWING connection? Can't really get recruit to work, switched to random round robin +-- DONE - System for Players to VID contacts? +-- DONE - Task reassignment - if a player reject a task, don't choose him again for 3 minutes +-- DONE - added SSML tags to make google readouts nicer +-- DONE - 2nd audio queue for priority messages +-- DONE - (WIP) Missile launch callout +-- DONE - Event detection, Player joining, eject, crash, dead, leaving; AI shot -> DEFEND +-- DONE - AI Tasking +-- DONE - Shift Change, Change on asset RTB or dead or mission done (done for AWACS and Escorts) +-- DONE - TripWire - WIP - Threat (35nm), Meld (45nm, on mission), Merged (<3nm) +-- +-- DONE - Escorts via Airwing not staying on +-- DONE - Borders for INTEL. Optional, i.e. land based defense within borders +-- DONE - Use AO as Anchor of Bulls, AO as default +-- DONE - SRS TTS output +-- DONE - Check-In/Out Humans +-- DONE - Check-In/Out AI +-- DONE - Picture +-- DONE - Declare +-- DONE - Bogey Dope +-- DONE - Radio Menu +-- DONE - Intel Detection +-- DONE - ROE +-- DONE - Anchor Stack Management +-- DONE - Shift Length AWACS/AI +-- DONE - (WIP) Reporting +-- DONE - Do not report non-airborne groups +-- DONE - Added option for helos +-- DONE - Added setting a coordinate for SRS + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO Constructor + +--- Set up a new AI AWACS. +-- @param #AWACS self +-- @param #string Name Name of this AWACS for the radio menu. +-- @param #string AirWing The core Ops.Airwing#AIRWING managing the AWACS, Escort and (optionally) AI CAP planes for us. +-- @param #number Coalition Coalition, e.g. coalition.side.BLUE. Can also be passed as "blue", "red" or "neutral". +-- @param #string AirbaseName Name of the home airbase. +-- @param #string AwacsOrbit Name of the round, mission editor created zone where this AWACS orbits. +-- @param #string OpsZone Name of the round, mission editor created Fighter Engagement operations zone (FEZ) this AWACS controls. Can be passed as #ZONE_POLYGON. +-- The name of the zone will be used in reference calls as bulls eye name, so ensure a radio friendly name that does not collide with NATOPS keywords. +-- @param #string StationZone Name of the round, mission editor created anchor zone where CAP groups will be stationed. Usually a short city name. +-- @param #number Frequency Radio frequency, e.g. 271. +-- @param #number Modulation Radio modulation, e.g. radio.modulation.AM or radio.modulation.FM. +-- @return #AWACS self +-- @usage +-- You can set up the OpsZone/FEZ in a number of ways: +-- * As a string denominating a normal, round zone you have created and named in the mission editor, e.g. "Rock". +-- * As a polygon zone, defined e.g. like `ZONE_POLYGON:New("Rock",GROUP:FindByName("RockZone"))` where "RockZone" is the name of a late activated helo, and it\'s waypoints (not more than 10) describe a closed polygon zone in the mission editor. +-- * As a string denominating a polygon zone from the mission editor (same late activated helo, but named "Rock#ZONE_POLYGON" in the mission editor. Here, Moose will auto-create a polygon zone when loading, and name it "Rock". Pass as `ZONE:FindByName("Rock")`. +function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,StationZone,Frequency,Modulation) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) + + --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 AWACS!") + end + else + self.coalition = Coalition + self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition)) + end + + -- base setup + self.Name = Name -- #string + self.AirWing = AirWing -- Ops.Airwing#AIRWING object + + AirWing:SetUsingOpsAwacs(self) + + self.CAPAirwings = FIFO:New() -- Utilities.FiFo#FIFO + self.CAPAirwings:Push(AirWing,1) + + self.AwacsFG = nil + --self.AwacsPayload = PayLoad -- Ops.Airwing#AIRWING.Payload + --self.ModernEra = true -- use of EPLRS + self.RadarBlur = 15 -- +/-15% detection precision i.e. 85-115 reported group size + if type(OpsZone) == "string" then + self.OpsZone = ZONE:New(OpsZone) -- Core.Zone#ZONE + elseif type(OpsZone) == "table" and OpsZone.ClassName and string.find(OpsZone.ClassName,"ZONE") then + self.OpsZone = OpsZone + else + self:E("AWACS - Invalid Zone passed!") + return + end + + --self.AOCoordinate = self.OpsZone:GetCoordinate() + self.AOCoordinate = COORDINATE:NewFromVec3( coalition.getMainRefPoint( self.coalition ) ) -- bulls eye from ME + self.AOName = self.OpsZone:GetName() + self.UseBullsAO = true -- as per NATOPS + self.ControlZoneRadius = 100 -- nm + self.StationZone = ZONE:New(StationZone) -- Core.Zone#ZONE + self.StationZoneName = StationZone + + self.Frequency = Frequency or 271 -- #number + self.Modulation = Modulation or radio.modulation.AM + self.MultiFrequency = {self.Frequency} + self.MultiModulation = {self.Modulation} + + self.Airbase = AIRBASE:FindByName(AirbaseName) + self.AwacsAngels = 25 -- orbit at 25'000 ft + if AwacsOrbit then + self.OrbitZone = ZONE:New(AwacsOrbit) -- Core.Zone#ZONE + end + self.BorderZone = nil + self.CallSign = CALLSIGN.AWACS.Magic -- #number + self.CallSignNo = 1 -- #number + self.NoHelos = true + self.AIRequested = 0 + self.AIonCAP = 0 + self.AICAPMissions = FIFO:New() -- Utilities.FiFo#FIFO + self.FlightGroups = FIFO:New() -- Utilities.FiFo#FIFO + self.Countactcounter = 0 + + self.PictureInterval = 300 -- picture every 5s mins + self.PictureTimeStamp = 0 -- timestamp + self.ReassignTime = 120 -- time for player re-assignment + + self.intelstarted = false + self.sunrisedone = false + + local speed = 250 + self.SpeedBase = speed + --self.Speed = UTILS.KnotsToAltKIAS(speed,self.AwacsAngels*1000) + self.Speed = speed + + self.Heading = 0 -- north + self.Leg = 50 -- nm + self.invisible = false + self.immortal = false + self.callsigntxt = "AWACS" + + self.AwacsTimeOnStation = 4 + self.AwacsTimeStamp = 0 + self.EscortsTimeOnStation = 4 + self.EscortsTimeStamp = 0 + self.ShiftChangeTime = 0.25 -- 15mins + self.ShiftChangeAwacsFlag = false + self.ShiftChangeEscortsFlag = false + + self.CapSpeedBase = 270 + self.CAPTimeOnStation = 4 + self.MaxAIonCAP = 4 + self.AICAPCAllName = CALLSIGN.Aircraft.Colt + self.AICAPCAllNumber = 0 + self.CAPGender = "male" + self.CAPCulture = "en-US" + self.CAPVoice = nil + + self.AwacsMission = nil + self.AwacsInZone = false -- not yet arrived or gone again + self.AwacsReady = false + + self.AwacsROE = AWACS.ROE.IFF + self.AwacsROT = AWACS.ROT.BYPASSESCAPE + + -- Escorts + self.HasEscorts = false + self.EscortTemplate = "" + self.EscortMission = {} + self.EscortMissionReplacement = {} + + -- SRS + self.PathToSRS = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" + self.Gender = "female" + self.Culture = "en-GB" + self.Voice = nil + self.Port = 5002 + self.Volume = 1.0 + self.RadioQueue = FIFO:New() -- Utilities.FiFo#FIFO + self.PrioRadioQueue = FIFO:New() -- Utilities.FiFo#FIFO + self.TacticalQueue = FIFO:New() -- Utilities.FiFo#FIFO + self.maxspeakentries = 3 + self.GoogleTTSPadding = 1 + self.WindowsTTSPadding = 2.5 + + -- Client SET + self.clientset = SET_CLIENT:New():FilterActive(true):FilterCoalitions(self.coalitiontxt):FilterCategories("plane"):FilterStart() + + -- Player options + self.PlayerGuidance = true + self.ModernEra = true + self.NoGroupTags = false + self.SuppressScreenOutput = false + self.ReassignmentPause = 180 + self.callsignshort = true + self.DeclareRadius = 5 -- NM + self.MenuStrict = true + self.maxassigndistance = 100 --nm + self.NoMissileCalls = true + self.PlayerCapAssignment = true + + -- managed groups + self.ManagedGrps = {} -- #table of #AWACS.ManagedGroup entries + self.ManagedGrpID = 0 + self.callsignTranslations = nil + + -- Anchor stacks init + self.AnchorStacks = FIFO:New() -- Utilities.FiFo#FIFO + self.AnchorBaseAngels = 22 + self.AnchorStackDistance = 2 + self.AnchorMaxStacks = 4 + self.AnchorMaxAnchors = 2 + self.AnchorMaxZones = 6 + self.AnchorCurrZones = 1 + self.AnchorTurn = -(360/self.AnchorMaxZones) + + self:_CreateAnchorStack() + + -- Task lists + self.ManagedTasks = FIFO:New() -- Utilities.FiFo#FIFO + --self.OpenTasks = FIFO:New() -- Utilities.FiFo#FIFO + + -- Monitoring, init + local MonitoringData = {} -- #AWACS.MonitoringData + MonitoringData.AICAPCurrent = 0 + MonitoringData.AICAPMax = self.MaxAIonCAP + MonitoringData.Airwings = 1 + MonitoringData.PlayersCheckedin = 0 + MonitoringData.Players = 0 + MonitoringData.AwacsShiftChange = false + MonitoringData.AwacsStateFG = "unknown" + MonitoringData.AwacsStateMission = "unknown" + MonitoringData.EscortsShiftChange = false + MonitoringData.EscortsStateFG = {} + MonitoringData.EscortsStateMission = {} + self.MonitoringOn = false -- #boolean + self.MonitoringData = MonitoringData + + self.CatchAllMissions = {} + self.CatchAllFGs = {} + + -- Picture, Contacts, Bogeys + self.PictureAO = FIFO:New() -- Utilities.FiFo#FIFO + self.PictureEWR = FIFO:New() -- Utilities.FiFo#FIFO + self.Contacts = FIFO:New() -- Utilities.FiFo#FIFO + --self.ManagedContacts = FIFO:New() + self.CID = 0 + self.ContactsAO = FIFO:New() -- Utilities.FiFo#FIFO + + self.clientmenus = FIFO:New() -- Utilities.FiFo#FIFO + + -- Tactical Menu + self.TacticalMenu = false + self.TacticalBaseFreq = 130 + self.TacticalIncrFreq = 0.5 + self.TacticalModulation = radio.modulation.AM + self.acticalFrequencies = {} + self.TacticalSubscribers = {} + self.TacticalInterval = 120 + + -- SET for Intel Detection + self.DetectionSet=SET_GROUP:New() + + -- Set some string id for output to DCS.log file. + self.lid=string.format("%s (%s) | ", self.Name, 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", "StartUp") -- Start FSM. + self:AddTransition("StartUp", "Started", "Running") + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("*", "CheckedIn", "*") + self:AddTransition("*", "CheckedOut", "*") + self:AddTransition("*", "AssignAnchor", "*") + self:AddTransition("*", "AssignedAnchor", "*") + self:AddTransition("*", "ReAnchor", "*") + self:AddTransition("*", "NewCluster", "*") + self:AddTransition("*", "NewContact", "*") + self:AddTransition("*", "LostCluster", "*") + self:AddTransition("*", "LostContact", "*") + self:AddTransition("*", "CheckRadioQueue", "*") + self:AddTransition("*", "CheckTacticalQueue", "*") + self:AddTransition("*", "EscortShiftChange", "*") + self:AddTransition("*", "AwacsShiftChange", "*") + self:AddTransition("*", "FlightOnMission", "*") + self:AddTransition("*", "Intercept", "*") + self:AddTransition("*", "InterceptSuccess", "*") + self:AddTransition("*", "InterceptFailure", "*") + self:AddTransition("*", "VIDSuccess", "*") + self:AddTransition("*", "VIDFailure", "*") + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + + local text = string.format("%sAWACS Version %s Initiated",self.lid,self.version) + + self:I(text) + + -- Events + -- Player joins + self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler) + -- Player leaves + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler) + self:HandleEvent(EVENTS.Ejection, self._EventHandler) + self:HandleEvent(EVENTS.Crash, self._EventHandler) + self:HandleEvent(EVENTS.Dead, self._EventHandler) + self:HandleEvent(EVENTS.UnitLost, self._EventHandler) + self:HandleEvent(EVENTS.BDA, self._EventHandler) + self:HandleEvent(EVENTS.PilotDead, self._EventHandler) + -- Missile warning + self:HandleEvent(EVENTS.Shot, self._EventHandler) + + self:_InitLocalization() + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the AWACS. Initializes parameters and starts event handlers. + -- @function [parent=#AWACS] Start + -- @param #AWACS self + + --- Triggers the FSM event "Start" after a delay. Starts the AWACS. Initializes parameters and starts event handlers. + -- @function [parent=#AWACS] __Start + -- @param #AWACS self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the AWACS and all its event handlers. + -- @param #AWACS self + + --- Triggers the FSM event "Stop" after a delay. Stops the AWACS and all its event handlers. + -- @function [parent=#AWACS] __Stop + -- @param #AWACS self + -- @param #number delay Delay in seconds. + + --- On After "CheckedIn" event. AI or Player checked in. + -- @function [parent=#AWACS] OnAfterCheckedIn + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "CheckedOut" event. AI or Player checked out. + -- @function [parent=#AWACS] OnAfterCheckedOut + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "AssignedAnchor" event. AI or Player has been assigned a CAP station. + -- @function [parent=#AWACS] OnAfterAssignedAnchor + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "ReAnchor" event. AI or Player has been send back to station. + -- @function [parent=#AWACS] OnAfterReAnchor + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "NewCluster" event. AWACS detected a cluster. + -- @function [parent=#AWACS] OnAfterNewCluster + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "NewContact" event. AWACS detected a contact. + -- @function [parent=#AWACS] OnAfterNewContact + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "LostCluster" event. AWACS lost a radar cluster. + -- @function [parent=#AWACS] OnAfterLostCluster + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "LostContact" event. AWACS lost a radar contact. + -- @function [parent=#AWACS] OnAfterLostContact + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "EscortShiftChange" event. AWACS escorts shift change. + -- @function [parent=#AWACS] OnAfterEscortShiftChange + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "AwacsShiftChange" event. AWACS shift change. + -- @function [parent=#AWACS] OnAfterAwacsShiftChange + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "Intercept" event. CAP send on intercept. + -- @function [parent=#AWACS] OnAfterIntercept + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "InterceptSuccess" event. Intercept successful. + -- @function [parent=#AWACS] OnAfterInterceptSuccess + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "InterceptFailure" event. Intercept failure. + -- @function [parent=#AWACS] OnAfterInterceptFailure + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "VIDSuccess" event. Intercept successful. + -- @function [parent=#AWACS] OnAfterVIDSuccess + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number GID Managed group ID (Player) + -- @param Wrapper.Group#GROUP Group (Player) Group done the VID + -- @param #AWACS.ManagedContact Contact The contact that was VID'd + + --- On After "VIDFailure" event. Intercept failure. + -- @function [parent=#AWACS] OnAfterVIDFailure + -- @param #AWACS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number GID Managed group ID (Player) + -- @param Wrapper.Group#GROUP Group (Player) Group done the VID + -- @param #AWACS.ManagedContact Contact The contact that was VID'd + + return self +end + +-- TODO Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- [User] Set the tactical information option, create 10 radio channels groups can subscribe and get Bogey Dope on a specific frequency automatically. You **need** to set up SRS first before using this! +-- @param #AWACS self +-- @param #number BaseFreq Base Frequency to use, defaults to 130. +-- @param #number Increase Increase to use, defaults to 0.5, thus channels created are 130, 130.5, 131 .. etc. +-- @param #number Modulation Modulation to use, defaults to radio.modulation.AM. +-- @param #number Interval Seconds between each update call. +-- @param #number Number Number of Frequencies to create, can be 1..10. +-- @return #AWACS self +function AWACS:SetTacticalRadios(BaseFreq,Increase,Modulation,Interval,Number) + self:T(self.lid.."SetTacticalRadios") + if not self.AwacsSRS then + MESSAGE:New("AWACS: Setup SRS in your code BEFORE trying to add tac radios please!",30,"ERROR",true):ToLog():ToAll() + return self + end + self.TacticalMenu = true + self.TacticalBaseFreq = BaseFreq or 130 + self.TacticalIncrFreq = Increase or 0.5 + self.TacticalModulation = Modulation or radio.modulation.AM + self.TacticalInterval = Interval or 120 + local number = Number or 10 + if number < 1 then number = 1 end + if number > 10 then number = 10 end + for i=1,number do + local freq = self.TacticalBaseFreq + ((i-1)*self.TacticalIncrFreq) + self.TacticalFrequencies[freq] = freq + end + if self.AwacsSRS then + self.TacticalSRS = MSRS:New(self.PathToSRS,self.TacticalBaseFreq,self.TacticalModulation,self.Backend) + self.TacticalSRS:SetCoalition(self.coalition) + self.TacticalSRS:SetGender(self.Gender) + self.TacticalSRS:SetCulture(self.Culture) + self.TacticalSRS:SetVoice(self.Voice) + self.TacticalSRS:SetPort(self.Port) + self.TacticalSRS:SetLabel("AWACS") + self.TacticalSRS:SetVolume(self.Volume) + if self.PathToGoogleKey then + --self.TacticalSRS:SetGoogle(self.PathToGoogleKey) + self.TacticalSRS:SetProviderOptionsGoogle(self.PathToGoogleKey,self.AccessKey) + self.TacticalSRS:SetProvider(MSRS.Provider.GOOGLE) + end + self.TacticalSRSQ = MSRSQUEUE:New("Tactical AWACS") + end + return self +end + +--- TODO +-- [Internal] _RefreshMenuNonSubscribed +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_RefreshMenuNonSubscribed() + self:T(self.lid.."_RefreshMenuNonSubscribed") + local aliveset = self.clientset:GetAliveSet() + + for _,_group in pairs(aliveset) do + -- go through set and re-build the sub-menu + local grp = _group -- Wrapper.Client#CLIENT + local Group = grp:GetGroup() + local gname = nil + if Group and Group:IsAlive() then + gname = Group:GetName() + self:T(gname) + end + local menustr = self.clientmenus:ReadByID(gname) + local menu = menustr.tactical -- Core.Menu#MENU_GROUP + if not self.TacticalSubscribers[gname] and menu then + menu:RemoveSubMenus() + for _,_freq in UTILS.spairs(self.TacticalFrequencies) do + local modu = UTILS.GetModulationName(self.TacticalModulation) + local text = string.format("Subscribe to %.3f %s",_freq,modu) + local entry = MENU_GROUP_COMMAND:New(Group,text,menu,self._SubScribeTactRadio,self,Group,_freq) + end + end + end + return self +end + +--- [Internal] _UnsubScribeTactRadio +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group +-- @return #AWACS self +function AWACS:_UnsubScribeTactRadio(Group) + self:T(self.lid.."_UnsubScribeTactRadio") + local text = "" + local textScreen = "" + local GID, Outcome = self:_GetManagedGrpID(Group) + local gcallsign = self:_GetCallSign(Group,GID) or "Ghost 1" + local gname = Group:GetName() or "unknown" + + if Outcome and self.TacticalSubscribers[gname] then + -- Pilot is checked in + local Freq = self.TacticalSubscribers[gname] + self.TacticalFrequencies[Freq] = Freq + self.TacticalSubscribers[gname] = nil + local modu = self.TacticalModulation == 0 and "AM" or "FM" + text = string.format("%s, %s, switch back to AWACS main frequency!",gcallsign,self.callsigntxt) + self:_NewRadioEntry(text,text,GID,true,true,true,false,true) + self:_RefreshMenuNonSubscribed() + elseif self.AwacsFG then + -- no, unknown + local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale) + text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + return self +end + +--- [Internal] _SubScribeTactRadio +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group +-- @param #number Frequency +-- @return #AWACS self +function AWACS:_SubScribeTactRadio(Group,Frequency) + self:T(self.lid.."_SubScribeTactRadio") + local text = "" + local textScreen = "" + local GID, Outcome = self:_GetManagedGrpID(Group) + local gcallsign = self:_GetCallSign(Group,GID) or "Ghost 1" + local gname = Group:GetName() or "unknown" + + if Outcome then + -- Pilot is checked in + self.TacticalSubscribers[gname] = Frequency + self.TacticalFrequencies[Frequency] = nil + local modu = self.TacticalModulation == 0 and "AM" or "FM" + text = string.format("%s, %s, switch to %.3f %s for tactical information!",gcallsign,self.callsigntxt,Frequency,modu) + self:_NewRadioEntry(text,text,GID,true,true,true,false,true) + local menustr = self.clientmenus:ReadByID(gname) + local menu = menustr.tactical -- Core.Menu#MENU_GROUP + if menu then + menu:RemoveSubMenus() + local text = string.format("Unsubscribe %.3f %s",Frequency,modu) + local entry = MENU_GROUP_COMMAND:New(Group,text,menu,self._UnsubScribeTactRadio,self,Group) + end + elseif self.AwacsFG then + -- no, unknown + local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale) + text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + + return self +end + +--- [Internal] _CheckSubscribers +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_CheckSubscribers() + self:T(self.lid.."_InitLocalization") + + for _name,_freq in pairs(self.TacticalSubscribers or {}) do + local grp = GROUP:FindByName(_name) + if (not grp) or (not grp:IsAlive()) then + self.TacticalFrequencies[_freq] = _freq + self.TacticalSubscribers[_name] = nil + end + end + + return self +end + +--- [Internal] Init localization +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_InitLocalization() + self:T(self.lid.."_InitLocalization") + self.gettext = TEXTANDSOUND:New("AWACS","en") -- Core.TextAndSound#TEXTANDSOUND + self.locale = "en" + for locale,table in pairs(self.Messages) do + local Locale = string.lower(tostring(locale)) + self:T("**** Adding locale: "..Locale) + for ID,Text in pairs(table) do + self:T(string.format('Adding ID %s',tostring(ID))) + self.gettext:AddEntry(Locale,tostring(ID),Text) + end + end + return self +end + +--- [User] Set locale for localization. Defaults to "en" +-- @param #AWACS self +-- @param #string Locale The locale to use +-- @return #AWACS self +function AWACS:SetLocale(Locale) + self:T(self.lid.."SetLocale") + self.locale = Locale or "en" + return self +end + +--- [User] Set own coordinate for BullsEye. +-- @param #AWACS self +-- @param Core.Point#COORDINATE +-- @return #AWACS self +function AWACS:SetBullsCoordinate(Coordinate) + self:T(self.lid.."SetBullsCoordinate") + self.AOCoordinate = Coordinate + return self +end + +--- [User] Set the max mission range flights can be away from their home base. +-- @param #AWACS self +-- @param #number NM Distance in nautical miles +-- @return #AWACS self +function AWACS:SetMaxMissionRange(NM) + self.MaxMissionRange = NM or 125 + return self +end + +--- [User] Add additional frequency and modulation for AWACS SRS output. +-- @param #AWACS self +-- @param #number Frequency The frequency to add, e.g. 132.5 +-- @param #number Modulation The modulation to add for the frequency, e.g. radio.modulation.AM +-- @return #AWACS self +function AWACS:AddFrequencyAndModulation(Frequency,Modulation) + self:T(self.lid.."AddFrequencyAndModulation") + table.insert(self.MultiFrequency,Frequency) + table.insert(self.MultiModulation,Modulation) + if self.AwacsSRS then + self.AwacsSRS:SetFrequencies(self.MultiFrequency) + self.AwacsSRS:SetModulations(self.MultiModulation) + end + return self +end + +--- [User] Set this instance to act as GCI TACS Theater Air Control System +-- @param #AWACS self +-- @param Wrapper.Group#GROUP EWR The **main** Early Warning Radar (EWR) GROUP object for GCI. +-- @param #number Delay (option) Start after this many seconds (optional). +-- @return #AWACS self +function AWACS:SetAsGCI(EWR,Delay) + self:T(self.lid.."SetGCI") + local delay = Delay or -5 + if type(EWR) == "string" then + self.GCIGroup = GROUP:FindByName(EWR) + else + self.GCIGroup = EWR + end + self.GCI = true + self:SetEscort(0) + return self +end + +--- [Internal] Create a AIC-TTS message entry +-- @param #AWACS self +-- @param #string TextTTS Text to speak +-- @param #string TextScreen Text for screen +-- @param #number GID Group ID #AWACS.ManagedGroup GID +-- @param #boolean IsGroup Has a group +-- @param #boolean ToScreen Show on screen +-- @param #boolean IsNew New +-- @param #boolean FromAI From AI +-- @param #boolean IsPrio Priority entry +-- @param #boolean Tactical Is for tactical info +-- @return #AWACS self +function AWACS:_NewRadioEntry(TextTTS, TextScreen,GID,IsGroup,ToScreen,IsNew,FromAI,IsPrio,Tactical) + self:T(self.lid.."_NewRadioEntry") + local RadioEntry = {} -- #AWACS.RadioEntry + RadioEntry.IsNew = IsNew + RadioEntry.TextTTS = TextTTS + RadioEntry.TextScreen = TextScreen or TextTTS + RadioEntry.GroupID = GID + RadioEntry.ToScreen = ToScreen + RadioEntry.Duration = MSRS.getSpeechTime(TextTTS,0.95,false) or 8 + RadioEntry.FromAI = FromAI + RadioEntry.IsGroup = IsGroup + if Tactical then + self.TacticalQueue:Push(RadioEntry) + elseif IsPrio then + self.PrioRadioQueue:Push(RadioEntry) + else + self.RadioQueue:Push(RadioEntry) + end + return self +end + +--- [User] Change the bulls eye alias for AWACS callout. Defaults to "Rock" +-- @param #AWACS self +-- @param #string Name +-- @return #AWACS self +function AWACS:SetBullsEyeAlias(Name) + self:T(self.lid.."_SetBullsEyeAlias") + self.AOName = Name or "Rock" + return self +end + +--- [User] Set TOS Time-on-Station in Hours +-- @param #AWACS self +-- @param #number AICHours AWACS stays this number of hours on station before shift change, default is 4. +-- @param #number CapHours (optional) CAP stays this number of hours on station before shift change, default is 4. +-- @return #AWACS self +function AWACS:SetTOS(AICHours,CapHours) + self:T(self.lid.."SetTOS") + self.AwacsTimeOnStation = AICHours or 4 + self.CAPTimeOnStation = CapHours or 4 + return self +end + +--- [User] Change number of seconds AWACS waits until a Player is re-assigned a different task. Defaults to 180. +-- @param #AWACS self +-- @param #number Seconds +-- @return #AWACS self +function AWACS:SetReassignmentPause(Seconds) + self.ReassignmentPause = Seconds or 180 + return self +end + +--- [User] Do not show messages on screen +-- @param #AWACS self +-- @param #boolean Switch If true, no messages will be shown on screen. +-- @return #AWACS self +function AWACS:SuppressScreenMessages(Switch) + self:T(self.lid.."_SetBullsEyeAlias") + self.SuppressScreenOutput = Switch or false + return self +end + +--- [User] Do not show messages on screen, no extra calls for player guidance, use short callsigns etc. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:ZipLip() + self:T(self.lid.."ZipLip") + self:SuppressScreenMessages(true) + self.PlayerGuidance = false + self.callsignshort = true + --self.NoGroupTags = true + self.NoMissileCalls = true + return self +end + +--- [User] For CAP flights: Replace ME callsigns with user-defined callsigns for use with TTS and on-screen messaging +-- @param #AWACS self +-- @param #table translationTable with DCS callsigns as keys and replacements as values +-- @return #AWACS self +-- @usage +-- -- Set Custom CAP Flight Callsigns for use with TTS +-- testawacs:SetCustomCallsigns({ +-- Devil = 'Bengal', +-- Snake = 'Winder', +-- Colt = 'Camelot', +-- Enfield = 'Victory', +-- Uzi = 'Evil Eye' +-- }) +function AWACS:SetCustomCallsigns(translationTable) + self.callsignTranslations = translationTable +end + +--- [Internal] Event handler +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group, can also be passed as #string group name +-- @return #boolean found +-- @return #number GID +-- @return #string CallSign +function AWACS:_GetGIDFromGroupOrName(Group) + self:T(self.lid.."_GetGIDFromGroupOrName") + self:T({Group}) + local GID = 0 + local Outcome = false + local CallSign = "Ghost 1" + local nametocheck = CallSign + if Group and type(Group) == "string" then + nametocheck = Group + elseif Group and Group:IsInstanceOf("GROUP") then + nametocheck = Group:GetName() + else + return false, 0, CallSign + end + + local managedgrps = self.ManagedGrps or {} + for _,_managed in pairs (managedgrps) do + local managed = _managed -- #AWACS.ManagedGroup + if managed.GroupName == nametocheck then + GID = managed.GID + Outcome = true + CallSign = managed.CallSign + end + end + self:T({Outcome, GID, CallSign}) + return Outcome, GID, CallSign +end + +--- [Internal] Event handler +-- @param #AWACS self +-- @param Core.Event#EVENTDATA EventData +-- @return #AWACS self +function AWACS:_EventHandler(EventData) + self:T(self.lid.."_EventHandler") + self:T({Event = EventData.id}) + + local Event = EventData -- Core.Event#EVENTDATA + + if Event.id == EVENTS.PlayerEnterAircraft or Event.id == EVENTS.PlayerEnterUnit then --player entered unit + --self:T("Player enter unit: " .. Event.IniPlayerName) + --self:T("Coalition = " .. UTILS.GetCoalitionName(Event.IniCoalition)) + if Event.IniCoalition == self.coalition then + self:_SetClientMenus() + end + end + + if Event.id == EVENTS.PlayerLeaveUnit and Event.IniGroupName then --player left unit + -- check known player? + self:T("Player group left unit: " .. Event.IniGroupName) + self:T("Player name left: " .. Event.IniPlayerName) + self:T("Coalition = " .. UTILS.GetCoalitionName(Event.IniCoalition)) + if Event.IniCoalition == self.coalition then + local Outcome, GID, CallSign = self:_GetGIDFromGroupOrName(Event.IniGroupName) + if Outcome and GID > 0 then + self:T("Task Abort and Checkout Called") + self:_TaskAbort(Event.IniGroupName) + self:_CheckOut(nil,GID,true) + end + end + end + + if Event.id == EVENTS.Ejection or Event.id == EVENTS.Crash or Event.id == EVENTS.Dead or Event.id == EVENTS.PilotDead then --unit or player dead + -- check known group? + if Event.IniCoalition == self.coalition then + --self:T("Ejection/Crash/Dead/PilotDead Group: " .. Event.IniGroupName) + --self:T("Coalition = " .. UTILS.GetCoalitionName(Event.IniCoalition)) + local Outcome, GID, CallSign = self:_GetGIDFromGroupOrName(Event.IniGroupName) + if Outcome and GID > 0 then + self:_TaskAbort(Event.IniGroupName) + self:_CheckOut(nil,GID,true) + end + end + end + + if Event.id == EVENTS.Shot and self.PlayerGuidance and not self.NoMissileCalls then + if Event.IniCoalition ~= self.coalition then + self:T("Shot from: " .. Event.IniGroupName) + + local position = Event.IniGroup:GetCoordinate() + if not position then return self end + + -- Check missile type + local Category = Event.WeaponCategory + local WeaponDesc = EventData.Weapon:getDesc() -- https://wiki.hoggitworld.com/view/DCS_enum_weapon + self:T({WeaponDesc}) + + if WeaponDesc.category == 1 and (WeaponDesc.missileCategory == 1 or WeaponDesc.missileCategory == 2) then + self:T("AAM or SAM Missile fired") + -- Missile fired + -- WIP Missile Callouts + local warndist = 25 + local Type = "SAM" + if WeaponDesc.category == 1 then + Type = "Missile" + -- AAM + local guidance = WeaponDesc.guidance or 4 -- IR=2, Radar Active=3, Radar Semi Active=4, Radar Passive = 5 + if guidance == 2 then + warndist = 10 + elseif guidance == 3 then + warndist = 25 + elseif guidance == 4 then + warndist = 15 + elseif guidance == 5 then + warndist = 10 + end -- guidance + end -- cat 1 + self:_MissileWarning(position,Type,warndist) + end -- cat 1 or 2 + + end -- end coalition + end -- end shot + + return self +end + +--- [Internal] Missile Warning Callout +-- @param #AWACS self +-- @param Core.Point#COORDINATE Coordinate Where the shot happened +-- @param #string Type Type to call out, e.i. "SAM" or "Missile" +-- @param #number Warndist Distance in NM to find friendly planes +-- @return #AWACS self +function AWACS:_MissileWarning(Coordinate,Type,Warndist) + self:T(self.lid.."_MissileWarning Type="..Type.." WarnDist="..Warndist) + + if not Coordinate then return self end + local shotzone = ZONE_RADIUS:New("WarningZone",Coordinate:GetVec2(),UTILS.NMToMeters(Warndist)) + local targetgrpset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryAirplane():FilterActive():FilterZones({shotzone}):FilterOnce() + if targetgrpset:Count() > 0 then + local targets = targetgrpset:GetSetObjects() + for _,_grp in pairs (targets) do + -- DONE -- player callouts only + if _grp and _grp:IsAlive() then + local isPlayer = _grp:IsPlayer() + + if isPlayer then + local callsign = self:_GetCallSign(_grp) + local defend = self.gettext:GetEntry("DEFEND",self.locale) + --local text = string.format("%s, %s! %s! %s! Defend!",callsign,Type,Type,Type) + local text = string.format(defend,callsign,Type,Type,Type) + self:_NewRadioEntry(text, text,0,false,self.debug,true,false,true) + end + end + end + end + return self +end + +--- [User] Set AWACS Radar Blur - the radar contact count per group/cluster will be distored up or down by this number percent. Defaults to 15 in Modern Era and 25 in Cold War. +-- @param #AWACS self +-- @param #number Percent +-- @return #AWACS self +function AWACS:SetRadarBlur(Percent) + local percent = Percent or 15 + if percent < 0 then percent = 0 end + if percent > 100 then percent = 100 end + self.RadarBlur = Percent + return self +end + +--- [User] Set AWACS to Cold War standards - ROE to VID, ROT to Passive (bypass and escape). Radar blur 25%. +-- Sets TAC/Meld/Threat call distances to 35, 25 and 15 nm. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:SetColdWar() + self.ModernEra = false + self.AwacsROT = AWACS.ROT.PASSIVE + self.AwacsROE = AWACS.ROE.VID + self.RadarBlur = 25 + self:SetInterceptTimeline(35, 25, 15) + return self +end + +--- [User] Set AWACS to Modern Era standards - ROE to BVR, ROT to defensive (evade fire). Radar blur 15%. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:SetModernEra() + self.ModernEra = true + self.AwacsROT = AWACS.ROT.EVADE + self.AwacsROE = AWACS.ROE.BVR + self.RadarBlur = 15 + return self +end + +--- [User] Set AWACS to Modern Era standards - ROE to IFF, ROT to defensive (evade fire). Radar blur 15%. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:SetModernEraDefensive() + self.ModernEra = true + self.AwacsROT = AWACS.ROT.EVADE + self.AwacsROE = AWACS.ROE.IFF + self.RadarBlur = 15 + return self +end + +--- [User] Set AWACS to Modern Era standards - ROE to BVR, ROT to return fire. Radar blur 15%. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:SetModernEraAggressive() + self.ModernEra = true + self.AwacsROT = AWACS.ROT.RETURNFIRE + self.AwacsROE = AWACS.ROE.BVR + self.RadarBlur = 15 + return self +end + +--- [User] Set AWACS to Policing standards - ROE to VID, ROT to Lock (bypass and escape). Radar blur 15%. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:SetPolicingModern() + self.ModernEra = true + self.AwacsROT = AWACS.ROT.BYPASSESCAPE + self.AwacsROE = AWACS.ROE.VID + self.RadarBlur = 15 + return self +end + +--- [User] Set AWACS to Policing standards - ROE to VID, ROT to Lock (bypass and escape). Radar blur 25%. +-- Sets TAC/Meld/Threat call distances to 35, 25 and 15 nm. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:SetPolicingColdWar() + self.ModernEra = false + self.AwacsROT = AWACS.ROT.BYPASSESCAPE + self.AwacsROE = AWACS.ROE.VID + self.RadarBlur = 25 + self:SetInterceptTimeline(35, 25, 15) + return self +end + +--- [User] Set AWACS Player Guidance - influences missile callout and the "New" label in group callouts. +-- @param #AWACS self +-- @param #boolean Switch If true (default) it is on, if false, it is off. +-- @return #AWACS self +function AWACS:SetPlayerGuidance(Switch) + if (Switch == nil) or (Switch == true) then + self.PlayerGuidance = true + else + self.PlayerGuidance = false + end + return self +end + +--- [User] Get AWACS Name +-- @param #AWACS self +-- @return #string Name of this instance +function AWACS:GetName() + return self.Name or "not set" +end + +--- [User] Set AWACS intercept timeline support distance. +-- @param #AWACS self +-- @param #number TacDistance Distance for TAC call, default 45nm +-- @param #number MeldDistance Distance for Meld call, default 35nm +-- @param #number ThreatDistance Distance for Threat call, default 25nm +-- @return #AWACS self +function AWACS:SetInterceptTimeline(TacDistance, MeldDistance, ThreatDistance) + self.TacDistance = TacDistance or 45 + self.MeldDistance = MeldDistance or 35 + self.ThreatDistance = ThreatDistance or 25 + return self +end + +--- [User] Set additional defensive zone, e.g. the zone behind the FEZ to also be defended +-- @param #AWACS self +-- @param Core.Zone#ZONE Zone +-- @param #boolean Draw Draw lines around this zone if true +-- @return #AWACS self +function AWACS:SetAdditionalZone(Zone, Draw) + self:T(self.lid.."SetAdditionalZone") + self.BorderZone = Zone + if self.debug then + Zone:DrawZone(self.coalition,{1,0.64,0},1,{1,0.64,0},0.2,1,true) + if self.AllowMarkers then + MARKER:New(Zone:GetCoordinate(),"Defensive Zone"):ToCoalition(self.coalition) + end + elseif Draw then + Zone:DrawZone(self.coalition,{1,0.64,0},1,{1,0.64,0},0.2,1,true) + end + return self +end + +--- [User] Set rejection zone, e.g. a border of a foreign country. Detected bogeys in here won't be engaged. +-- @param #AWACS self +-- @param Core.Zone#ZONE Zone +-- @param #boolean Draw Draw lines around this zone if true +-- @return #AWACS self +function AWACS:SetRejectionZone(Zone,Draw) + self:T(self.lid.."SetRejectionZone") + self.RejectZone = Zone + if Draw then + Zone:DrawZone(self.coalition,{1,0.64,0},1,{1,0.64,0},0.2,1,true) + --MARKER:New(Zone:GetCoordinate(),"Rejection Zone"):ToAll() + elseif self.debug then + Zone:DrawZone(self.coalition,{1,0.64,0},1,{1,0.64,0},0.2,1,true) + if self.AllowMarkers then + MARKER:New(Zone:GetCoordinate(),"Rejection Zone"):ToCoalition(self.coalition) + end + end + return self +end + +--- [User] Draw a line around the FEZ on the F10 map. +-- @param #AWACS self +-- @return #AWACS self +function AWACS:DrawFEZ() + self.OpsZone:DrawZone(self.coalition,{1,0,0},1,{1,0,0},0.2,5,true) + return self +end + +--- [User] Set AWACS flight details +-- @param #AWACS self +-- @param #number CallSign Defaults to CALLSIGN.AWACS.Magic +-- @param #number CallSignNo Defaults to 1 +-- @param #number Angels Defaults to 25 (i.e. 25000 ft) +-- @param #number Speed Defaults to 250kn +-- @param #number Heading Defaults to 0 (North) +-- @param #number Leg Defaults to 25nm +-- @return #AWACS self +function AWACS:SetAwacsDetails(CallSign,CallSignNo,Angels,Speed,Heading,Leg) + self:T(self.lid.."SetAwacsDetails") + self.CallSign = CallSign or CALLSIGN.AWACS.Magic + self.CallSignNo = CallSignNo or 1 + self.AwacsAngels = Angels or 25 + local speed = Speed or 250 + self.SpeedBase = speed + --self.Speed = UTILS.KnotsToAltKIAS(speed,self.AwacsAngels*1000) + self.Speed = speed + self.Heading = Heading or 0 + self.Leg = Leg or 25 + return self +end + +--- [User] Set AWACS custom callsigns for TTS +-- @param #AWACS self +-- @param #table CallsignTable Table of custom callsigns to use with TTS +-- @return #AWACS self +-- @usage +-- You can overwrite the standard AWACS callsign for TTS usage with your own naming, e.g. like so: +-- testawacs:SetCustomAWACSCallSign({ +-- [1]="Overlord", -- Overlord +-- [2]="Bookshelf", -- Magic +-- [3]="Wizard", -- Wizard +-- [4]="Focus", -- Focus +-- [5]="Darkstar", -- Darkstar +-- }) +-- The default callsign used in AWACS is "Magic". With the above change, the AWACS will call itself "Bookshelf" over TTS instead. +function AWACS:SetCustomAWACSCallSign(CallsignTable) + self:T(self.lid.."SetCustomAWACSCallSign") + self.CallSignClear = CallsignTable + return self +end + +--- [User] Add a radar GROUP object to the INTEL detection SET_GROUP +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group The GROUP to be added. Can be passed as SET_GROUP. +-- @return #AWACS self +function AWACS:AddGroupToDetection(Group) + self:T(self.lid.."AddGroupToDetection") + if Group and Group.ClassName and Group.ClassName == "GROUP" then + self.DetectionSet:AddGroup(Group) + elseif Group and Group.ClassName and Group.ClassName == "SET_GROUP" then + self.DetectionSet:AddSet(Group) + end + return self +end + +--- [User] Set AWACS SRS TTS details - see @{Sound.SRS} for details. `SetSRS()` will try to use as many attributes configured with @{Sound.SRS#MSRS.LoadConfigFile}() as possible. +-- @param #AWACS self +-- @param #string PathToSRS Defaults to "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" +-- @param #string Gender Defaults to "male" +-- @param #string Culture Defaults to "en-US" +-- @param #number Port Defaults to 5002 +-- @param #string Voice (Optional) Use a specifc voice with the @{Sound.SRS#SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. +-- Note that this must be installed on your windows system. Can also be Google voice types, if you are using Google TTS. +-- @param #number Volume Volume - between 0.0 (silent) and 1.0 (loudest) +-- @param #string PathToGoogleKey (Optional) Path to your google key if you want to use google TTS; if you use a config file for MSRS, hand in nil here. +-- @param #string AccessKey (Optional) Your Google API access key. This is necessary if DCS-gRPC is used as backend; if you use a config file for MSRS, hand in nil here. +-- @param #string Backend (Optional) Your MSRS Backend if different from your config file settings, e.g. MSRS.Backend.SRSEXE or MSRS.Backend.GRPC +-- @return #AWACS self +function AWACS:SetSRS(PathToSRS,Gender,Culture,Port,Voice,Volume,PathToGoogleKey,AccessKey,Backend) + self:T(self.lid.."SetSRS") + self.PathToSRS = PathToSRS or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" + self.Gender = Gender or MSRS.gender or "male" + self.Culture = Culture or MSRS.culture or "en-US" + self.Port = Port or MSRS.port or 5002 + self.Voice = Voice or MSRS.voice + self.PathToGoogleKey = PathToGoogleKey + self.AccessKey = AccessKey + self.Volume = Volume or 1.0 + self.Backend = Backend or MSRS.backend + BASE:I({backend = self.Backend}) + self.AwacsSRS = MSRS:New(self.PathToSRS,self.MultiFrequency,self.MultiModulation,self.Backend) + self.AwacsSRS:SetCoalition(self.coalition) + self.AwacsSRS:SetGender(self.Gender) + self.AwacsSRS:SetCulture(self.Culture) + self.AwacsSRS:SetPort(self.Port) + self.AwacsSRS:SetLabel("AWACS") + self.AwacsSRS:SetVolume(Volume) + if self.PathToGoogleKey then + --self.AwacsSRS:SetGoogle(self.PathToGoogleKey) + self.AwacsSRS:SetProviderOptionsGoogle(self.PathToGoogleKey,self.AccessKey) + self.AwacsSRS:SetProvider(MSRS.Provider.GOOGLE) + end + -- Pre-configured Google? + if (not PathToGoogleKey) and self.AwacsSRS:GetProvider() == MSRS.Provider.GOOGLE then + self.PathToGoogleKey = MSRS.poptions.gcloud.credentials + self.Voice = Voice or MSRS.poptions.gcloud.voice + self.AccessKey = AccessKey or MSRS.poptions.gcloud.key + end + self.AwacsSRS:SetVoice(self.Voice) + return self +end + +--- [User] Set AWACS Voice Details for AI CAP Planes - SRS TTS - see @{Sound.SRS} for details +-- @param #AWACS self +-- @param #string Gender Defaults to "male" +-- @param #string Culture Defaults to "en-US" +-- @param #string Voice (Optional) 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. Can also be Google voice types, if you are using Google TTS. +-- @return #AWACS self +function AWACS:SetSRSVoiceCAP(Gender, Culture, Voice) + self:T(self.lid.."SetSRSVoiceCAP") + self.CAPGender = Gender or "male" + self.CAPCulture = Culture or "en-US" + self.CAPVoice = Voice or "en-GB-Standard-B" + return self +end + +--- [User] Set AI CAP Plane Details +-- @param #AWACS self +-- @param #number Callsign Callsign name of AI CAP, e.g. CALLSIGN.Aircraft.Dodge. Defaults to CALLSIGN.Aircraft.Colt. Note that not all available callsigns work for all plane types. +-- @param #number MaxAICap Maximum number of AI CAP planes on station that AWACS will set up automatically. Default to 4. +-- @param #number TOS Time on station, in hours. AI planes might go back to base earlier if they run out of fuel or missiles. +-- @param #number Speed Airspeed to be used in knots. Will be adjusted to flight height automatically. Defaults to 270. +-- @return #AWACS self +function AWACS:SetAICAPDetails(Callsign,MaxAICap,TOS,Speed) + self:T(self.lid.."SetAICAPDetails") + self.CapSpeedBase = Speed or 270 + self.CAPTimeOnStation = TOS or 4 + self.MaxAIonCAP = MaxAICap or 4 + self.AICAPCAllName = Callsign or CALLSIGN.Aircraft.Colt + return self +end + +--- [User] Set AWACS Escorts Template +-- @param #AWACS self +-- @param #number EscortNumber Number of fighther plane GROUPs to accompany this AWACS. 0 or nil means no escorts. If you want >1 plane in an escort group, you can either set the respective squadron grouping to the desired number, or use a template for escorts with >1 unit. +-- @param #number Formation Formation the escort should take (if more than one plane), e.g. `ENUMS.Formation.FixedWing.FingerFour.Group`. Formation is used on GROUP level, multiple groups of one unit will NOT conform to this formation. +-- @param #table OffsetVector Offset the escorts should fly behind the AWACS, given as table, distance in meters, e.g. `{x=-500,y=0,z=500}` - 500m behind (negative value) and to the right (negative for left), no vertical separation (positive over, negative under the AWACS flight). For multiple groups, the vectors will be slightly changed to avoid collisions. +-- @param #number EscortEngageMaxDistance Escorts engage air targets max this NM away, defaults to 45NM. +-- @return #AWACS self +function AWACS:SetEscort(EscortNumber,Formation,OffsetVector,EscortEngageMaxDistance) + self:T(self.lid.."SetEscort") + if EscortNumber and EscortNumber > 0 then + self.HasEscorts = true + self.EscortNumber = EscortNumber + else + self.HasEscorts = false + self.EscortNumber = 0 + end + self.EscortFormation = Formation + self.OffsetVec = OffsetVector or {x=500,y=100,z=500} + self.EscortEngageMaxDistance = EscortEngageMaxDistance or 45 + return self +end + +--- [Internal] Message a vector BR to a position +-- @param #AWACS self +-- @param #number GID Group GID +-- @param #string Tag (optional) Text to add after Vector, e.g. " to Anchor" - NOTE the leading space +-- @param Core.Point#COORDINATE Coordinate The Coordinate to use +-- @param #number Angels (Optional) Add Angels +-- @return #AWACS self +function AWACS:_MessageVector(GID,Tag,Coordinate,Angels) + self:T(self.lid.."_MessageVector") + + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local Tag = Tag or "" + + if managedgroup and Coordinate then + + local tocallsign = managedgroup.CallSign or "Ghost 1" + local group = managedgroup.Group + local groupposition = group:GetCoordinate() + + local BRtext,BRtextTTS = self:_ToStringBR(groupposition,Coordinate) + + local vector = self.gettext:GetEntry("VECTORTO",self.locale) + local vectortts = self.gettext:GetEntry("VECTORTOTTS",self.locale) + local angelstxt = self.gettext:GetEntry("ANGELS",self.locale) + + local text = string.format(vectortts,tocallsign, self.callsigntxt,Tag,BRtextTTS) + local textScreen = string.format(vector,tocallsign, self.callsigntxt,Tag,BRtext) + + if Angels then + text = text .. angelstxt ..tostring(Angels).."." + textScreen = textScreen ..angelstxt..tostring(Angels).."." + end + + self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false) + + end + + return self +end + +--- [Internal] Start AWACS Escorts FlightGroup +-- @param #AWACS self +-- @param #boolean Shiftchange This is a shift change call +-- @return #AWACS self +function AWACS:_StartEscorts(Shiftchange) + self:T(self.lid.."_StartEscorts") + + local AwacsFG = self.AwacsFG -- Ops.FlightGroup#FLIGHTGROUP + local group = AwacsFG:GetGroup() + + local timeonstation = (self.EscortsTimeOnStation + self.ShiftChangeTime) * 3600 -- hours to seconds + local OffsetX = 500 + local OffsetY = 500 + local OffsetZ = 500 + if self.OffsetVec then + OffsetX = self.OffsetVec.x or 500 + OffsetY = self.OffsetVec.y or 500 + OffsetZ = self.OffsetVec.z or 500 + end + + for i=1,self.EscortNumber do + -- every + local escort = AUFTRAG:NewESCORT(group, {x= OffsetX*((i + (i%2))/2), y=OffsetY*((i + (i%2))/2), z=(OffsetZ + OffsetZ*((i + (i%2))/2))*(-1)^i},self.EscortEngageMaxDistance,{"Air"}) + --local escort = AUFTRAG:NewESCORT(group,self.OffsetVec,self.EscortEngageMaxDistance,{"Air"}) + --escort:SetRequiredAssets(self.EscortNumber) + escort:SetTime(nil,timeonstation) + if self.Escortformation then + escort:SetFormation(self.Escortformation) + end + escort:SetMissionRange(self.MaxMissionRange) + + self.AirWing:AddMission(escort) + self.CatchAllMissions[#self.CatchAllMissions+1] = escort + + if Shiftchange then + self.EscortMissionReplacement[i] = escort + else + self.EscortMission[i] = escort + end + end + + return self +end + +--- [Internal] AWACS further Start Settings +-- @param #AWACS self +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup +-- @param Ops.Auftrag#AUFTRAG Mission +-- @return #AWACS self +function AWACS:_StartSettings(FlightGroup,Mission) + self:T(self.lid.."_StartSettings") + + local Mission = Mission -- Ops.Auftrag#AUFTRAG + local AwacsFG = FlightGroup -- Ops.FlightGroup#FLIGHTGROUP + + -- Is this our Awacs mission? + if self.AwacsMission:GetName() == Mission:GetName() then + self:T("Setting up Awacs") + AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation,false) + AwacsFG:SwitchRadio(self.Frequency,self.Modulation) + AwacsFG:SetDefaultAltitude(self.AwacsAngels*1000) + AwacsFG:SetHomebase(self.Airbase) + AwacsFG:SetDefaultCallsign(self.CallSign,self.CallSignNo) + AwacsFG:SetDefaultROE(ENUMS.ROE.WeaponHold) + AwacsFG:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN) + AwacsFG:SetDefaultEPLRS(self.ModernEra) + AwacsFG:SetDespawnAfterLanding() + AwacsFG:SetFuelLowRTB(true) + AwacsFG:SetFuelLowThreshold(20) + + local group = AwacsFG:GetGroup() -- Wrapper.Group#GROUP + + group:SetCommandInvisible(self.invisible) + group:SetCommandImmortal(self.immortal) + group:CommandSetCallsign(self.CallSign,self.CallSignNo,2) + group:CommandEPLRS(self.ModernEra,5) + -- Non AWACS does not seem take AWACS CS in DCS Group + + self.AwacsFG = AwacsFG + + --self.AwacsFG:SetSRS(self.PathToSRS,self.Gender,self.Culture,self.Voice,self.Port,self.PathToGoogleKey,"AWACS",self.Volume) + + self.callsigntxt = string.format("%s",self.CallSignClear[self.CallSign]) + + self:__CheckRadioQueue(10) + + if self.HasEscorts then + --mission:SetRequiredEscorts(self.EscortNumber) + self:_StartEscorts() + end + + self.AwacsTimeStamp = timer.getTime() + self.EscortsTimeStamp = timer.getTime() + + self.PictureTimeStamp = timer.getTime() + 10*60 + + self.AwacsReady = true + -- set FSM to started + self:Started() + + elseif self.ShiftChangeAwacsRequested and self.AwacsMissionReplacement and self.AwacsMissionReplacement:GetName() == Mission:GetName() then + self:T("Setting up Awacs Replacement") + -- manage AWACS Replacement + AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation,false) + AwacsFG:SwitchRadio(self.Frequency,self.Modulation) + AwacsFG:SetDefaultAltitude(self.AwacsAngels*1000) + AwacsFG:SetHomebase(self.Airbase) + self.CallSignNo = self.CallSignNo+1 + AwacsFG:SetDefaultCallsign(self.CallSign,self.CallSignNo) + AwacsFG:SetDefaultROE(ENUMS.ROE.WeaponHold) + AwacsFG:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN) + AwacsFG:SetDefaultEPLRS(self.ModernEra) + AwacsFG:SetDespawnAfterLanding() + AwacsFG:SetFuelLowRTB(true) + AwacsFG:SetFuelLowThreshold(20) + + local group = AwacsFG:GetGroup() -- Wrapper.Group#GROUP + + group:SetCommandInvisible(self.invisible) + group:SetCommandImmortal(self.immortal) + group:CommandSetCallsign(self.CallSign,self.CallSignNo,2) + + --AwacsFG:SetSRS(self.PathToSRS,self.Gender,self.Culture,self.Voice,self.Port,nil,"AWACS") + + self.callsigntxt = string.format("%s",self.CallSignClear[self.CallSign]) + + local shifting = self.gettext:GetEntry("SHIFTCHANGE",self.locale) + + local text = string.format(shifting,self.callsigntxt,self.AOName or "Rock") + + self:T(self.lid..text) + + AwacsFG:RadioTransmission(text,1,false) + + self.AwacsFG = AwacsFG + + if self.HasEscorts then + self:_StartEscorts(true) + end + + self.AwacsTimeStamp = timer.getTime() + self.EscortsTimeStamp = timer.getTime() + + self.AwacsReady = true + + end + return self +end + +--- [Internal] Return Bullseye BR for Alpha Check etc, returns e.g. "Rock 021, 16" ("Rock" being the set BE name) +-- @param #AWACS self +-- @param Core.Point#COORDINATE Coordinate +-- @param #boolean ssml Add SSML tag +-- @param #boolean TTS For non-Alpha checks, hand back in format "Rock 0 2 1, 16" +-- @return #string BullseyeBR +function AWACS:_ToStringBULLS( Coordinate, ssml, TTS ) + self:T(self.lid.."_ToStringBULLS") + local bullseyename = self.AOName or "Rock" + local BullsCoordinate = self.AOCoordinate + local DirectionVec3 = BullsCoordinate:GetDirectionVec3( Coordinate ) + local AngleRadians = Coordinate:GetAngleRadians( DirectionVec3 ) + local Distance = Coordinate:Get2DDistance( BullsCoordinate ) + local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) + local Bearing = string.format( '%03d', AngleDegrees ) + local Distance = UTILS.Round( UTILS.MetersToNM( Distance ), 0 ) + if ssml then + return string.format("%s %03d, %d",bullseyename,Bearing,Distance) + end + if TTS then + Bearing = self:_ToStringBullsTTS(Bearing) + local zero = self.gettext:GetEntry("ZERO",self.locale) + local BearingTTS = string.gsub(Bearing,"0",zero) + return string.format("%s %s, %d",bullseyename,BearingTTS,Distance) + else + return string.format("%s %s, %d",bullseyename,Bearing,Distance) + end +end + +--- [Internal] Change Bullseye string to be TTS friendly, "Bullseye 021, 16" returns e.g. "Bulls eye 0 2 1. 1 6" +-- @param #AWACS self +-- @param #string Text Input text +-- @return #string BullseyeBRTTS +function AWACS:_ToStringBullsTTS(Text) + local text = Text + text=string.gsub(text,"Bullseye","Bulls eye") + text=string.gsub(text,"%d","%1 ") + text=string.gsub(text," ," ,".") + text=string.gsub(text," $","") + return text +end + + +--- [Internal] Check if a group has checked in +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to check +-- @return #number ID +-- @return #boolean CheckedIn +-- @return #string CallSign +function AWACS:_GetManagedGrpID(Group) + if not Group or not Group:IsAlive() then + self:T(self.lid.."_GetManagedGrpID - Requested Group is not alive!") + return 0,false,"" + end + self:T(self.lid.."_GetManagedGrpID for "..Group:GetName()) + local GID = 0 + local Outcome = false + local CallSign = "Ghost 1" + local nametocheck = Group:GetName() + local managedgrps = self.ManagedGrps or {} + for _,_managed in pairs (managedgrps) do + local managed = _managed -- #AWACS.ManagedGroup + if managed.GroupName == nametocheck then + GID = managed.GID + Outcome = true + CallSign = managed.CallSign + end + end + return GID, Outcome, CallSign +end + +--- [Internal] AWACS Get TTS compatible callsign +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @param #number GID GID to use +-- @param #boolean IsPlayer Check in player if true +-- @return #string Callsign +function AWACS:_GetCallSign(Group,GID, IsPlayer) + self:T(self.lid.."_GetCallSign - GID "..tostring(GID)) + + if GID and type(GID) == "number" and GID > 0 then + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + self:T("Saved Callsign for TTS = " .. tostring(managedgroup.CallSign)) + return managedgroup.CallSign + end + + local callsign = "Ghost 1" + if Group and Group:IsAlive() then + callsign = Group:GetCustomCallSign(self.callsignshort,self.keepnumber,self.callsignTranslations,self.callsignCustomFunc,self.callsignCustomArgs) + end + return callsign +end + +--- [User] Set player callsign options for TTS output. See @{Wrapper.Group#GROUP.GetCustomCallSign}() on how to set customized callsigns. +-- @param #AWACS self +-- @param #boolean ShortCallsign If true, only call out the major flight number +-- @param #boolean Keepnumber If true, keep the **customized callsign** in the #GROUP name as-is, no amendments or numbers. +-- @param #table CallsignTranslations (Optional) Table to translate between DCS standard callsigns and bespoke ones. Does not apply if using customized. +-- callsigns from playername or group name. +-- @param #func CallsignCustomFunc (Optional) For player names only(!). If given, this function will return the callsign. Needs to take the groupname and the playername as first two arguments. +-- @param #arg ... (Optional) Comma separated arguments to add to the custom function call after groupname and playername. +-- @return #AWACS self +function AWACS:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations,CallsignCustomFunc,...) + if not ShortCallsign or ShortCallsign == false then + self.callsignshort = false + else + self.callsignshort = true + end + self.keepnumber = Keepnumber or false + self.callsignTranslations = CallsignTranslations + self.callsignCustomFunc = CallsignCustomFunc + self.callsignCustomArgs = arg or {} + return self +end + +--- [Internal] Update contact from cluster data +-- @param #AWACS self +-- @param #number CID Contact ID +-- @return #AWACS self +function AWACS:_UpdateContactFromCluster(CID) + self:T(self.lid.."_UpdateContactFromCluster CID="..CID) + + local existingcontact = self.Contacts:PullByID(CID) -- #AWACS.ManagedContact + local ContactTable = existingcontact.Cluster.Contacts or {} + + local function GetFirstAliveContact(table) + for _,_contact in pairs (table) do + local contact = _contact -- Ops.Intel#INTEL.Contact + if contact and contact.group and contact.group:IsAlive() then + return contact + end + end + return nil + end + + local NewContact = GetFirstAliveContact(ContactTable) + + if NewContact then + existingcontact.Contact = NewContact + self.Contacts:Push(existingcontact,existingcontact.CID) + end + + return self +end + +--- [Internal] Check merges for Players +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_CheckMerges() + self:T(self.lid.."_CheckMerges") + for _id,_pilot in pairs (self.ManagedGrps) do + local pilot = _pilot -- #AWACS.ManagedGroup + if pilot.Group and pilot.Group:IsAlive() then + local ppos = pilot.Group:GetCoordinate() + local pcallsign = pilot.CallSign + self:T(self.lid.."Checking for "..pcallsign) + if ppos then + self.Contacts:ForEach( + function (Contact) + local contact = Contact -- #AWACS.ManagedContact + local cpos = contact.Cluster.coordinate or contact.Contact.position or contact.Contact.group:GetCoordinate() + local dist = ppos:Get2DDistance(cpos) + local distnm = UTILS.Round(UTILS.MetersToNM(dist),0) + if (pilot.IsPlayer or self.debug) and distnm <= 5 then --and ((not contact.MergeCallDone) or (timer.getTime() - contact.MergeCallDone > 30)) then + --local label = contact.EngagementTag or "" + --if not contact.MergeCallDone or not string.find(label,pcallsign) then + self:T(self.lid.."Merged") + self:_MergedCall(_id) + --contact.MergeCallDone = true + --end + end + if (pilot.IsPlayer or self.debug) and distnm >5 and distnm <= self.ThreatDistance then + self:_ThreatRangeCall(_id,Contact) + end + if (pilot.IsPlayer or self.debug) and distnm > self.ThreatDistance and distnm <= self.MeldDistance then + self:_MeldRangeCall(_id,Contact) + end + if (pilot.IsPlayer or self.debug) and distnm > self.MeldDistance and distnm <= self.TacDistance then + self:_TACRangeCall(_id,Contact) + end + end + ) + end + end + end + return self +end + +--- [Internal] Clean up contacts list +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_CleanUpContacts() + self:T(self.lid.."_CleanUpContacts") + + if self.Contacts:Count() > 0 then + local deadcontacts = FIFO:New() + self.Contacts:ForEach( + function (Contact) + local contact = Contact -- #AWACS.ManagedContact + if not contact.Contact.group:IsAlive() or contact.Target:IsDead() or contact.Target:IsDestroyed() or contact.Target:CountTargets() == 0 then + deadcontacts:Push(contact,contact.CID) + self:T("DEAD contact CID="..contact.CID) + end + end + ) + + -- announce VANISHED + if deadcontacts:Count() > 0 and (not self.NoGroupTags) then + + self:T("DEAD count="..deadcontacts:Count()) + deadcontacts:ForEach( + function (Contact) + local contact = Contact -- #AWACS.ManagedContact + local vanished = self.gettext:GetEntry("VANISHED",self.locale) + local vanishedtts = self.gettext:GetEntry("VANISHEDTTS",self.locale) + local text = string.format(vanishedtts,self.callsigntxt, contact.TargetGroupNaming) + local textScreen = string.format(vanished, self.callsigntxt, contact.TargetGroupNaming) + self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true) + self.Contacts:PullByID(contact.CID) + -- end + end + ) + + end + + if self.Contacts:Count() > 0 then + self.Contacts:ForEach( + function (Contact) + local contact = Contact -- #AWACS.ManagedContact + self:_UpdateContactFromCluster(contact.CID) + end + ) + end + + -- cleanup + deadcontacts:Clear() + -- aliveclusters:Clear() + + end + return self +end + +--- [Internal] Select pilots available for tasking, return AI and Human +-- @param #AWACS self +-- @return #table AIPilots Table of #AWACS.ManagedGroup +-- @return #table HumanPilots Table of #AWACS.ManagedGroup +function AWACS:_GetIdlePilots() + self:T(self.lid.."_GetIdlePilots") + local AIPilots = {} + local HumanPilots = {} + + for _name,_entry in pairs (self.ManagedGrps) do + local entry = _entry -- #AWACS.ManagedGroup + self:T("Looking at entry "..entry.GID.." Name "..entry.GroupName) + local managedtask = self:_ReadAssignedTaskFromGID(entry.GID) -- #AWACS.ManagedTask + local overridetask = false + if managedtask then + self:T("Current task = "..(managedtask.ToDo or "Unknown")) + if managedtask.ToDo == AWACS.TaskDescription.ANCHOR then + overridetask = true + end + end + if entry.IsAI then + if entry.FlightGroup:IsAirborne() and ((not entry.HasAssignedTask) or overridetask) then -- must be idle, or? + self:T("Adding AI with Callsign: "..entry.CallSign) + AIPilots[#AIPilots+1] = _entry + end + elseif entry.IsPlayer and (not entry.Blocked) and (not entry.Group:IsHelicopter()) then + if (not entry.HasAssignedTask) or overridetask then -- must be idle, or? + -- check last assignment + local TNow = timer.getTime() + if entry.LastTasking and (TNow-entry.LastTasking > self.ReassignTime) then + self:T("Adding Human with Callsign: "..entry.CallSign) + HumanPilots[#HumanPilots+1] = _entry + end + end + end + end + + return AIPilots, HumanPilots + +end + +--- [Internal] Select max 3 targets for picture, bogey dope etc +-- @param #AWACS self +-- @param #boolean Untargeted Return not yet targeted contacts only +-- @return #boolean HaveTargets True if targets could be found, else false +-- @return Utilities.FiFo#FIFO Targetselection +function AWACS:_TargetSelectionProcess(Untargeted) + self:T(self.lid.."_TargetSelectionProcess") + + local maxtargets = 3 -- handleable number of callouts + local contactstable = self.Contacts:GetDataTable() + local targettable = FIFO:New() + local sortedtargets = FIFO:New() + local prefiltered = FIFO:New() + local HaveTargets = false + + self:T(self.lid.."Initial count: "..self.Contacts:Count()) + + -- Bucket sort + + if Untargeted then + -- pre-filter + self.Contacts:ForEach( + function (Contact) + local contact = Contact -- #AWACS.ManagedContact + if contact.Contact.group:IsAlive() and (contact.Status == AWACS.TaskStatus.IDLE or contact.Status == AWACS.TaskStatus.UNASSIGNED) then + if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then + -- filter out VID'd non-hostiles + if not (contact.IFF == AWACS.IFF.FRIENDLY or contact.IFF == AWACS.IFF.NEUTRAL) then + prefiltered:Push(contact,contact.CID) + end + else + prefiltered:Push(contact,contact.CID) + end + end + end + ) + contactstable = prefiltered:GetDataTable() + self:T(self.lid.."Untargeted: "..prefiltered:Count()) + end + + -- Loop through + for _,_contact in pairs(contactstable) do + local contact = _contact -- #AWACS.ManagedContact + local checked = false + local contactname = contact.TargetGroupNaming or "ZETA" + local typename = contact.ReportingName or "Unknown" + self:T(self.lid..string.format("Looking at group %s type %s",contactname,typename)) + local contactcoord = contact.Cluster.coordinate or contact.Contact.position or contact.Contact.group:GetCoordinate() + local contactvec2 = contactcoord:GetVec2() + + -- Bucket 0 - NOT in Rejection Zone :) + if self.RejectZone then + local isinrejzone = self.RejectZone:IsVec2InZone(contactvec2) + if isinrejzone then + self:T(self.lid.."Across Border = YES - ignore") + checked = true + end + end + -- Bucket 1 - close to AIC (HVT) ca ~45nm + if not self.GCI then + local HVTCoordinate = self.OrbitZone:GetCoordinate() + local distance = UTILS.NMToMeters(200) + if contactcoord then + distance = HVTCoordinate:Get2DDistance(contactcoord) + end + self:T(self.lid.."HVT Distance = "..UTILS.Round(UTILS.MetersToNM(distance),0)) + if UTILS.MetersToNM(distance) <= 45 and not checked then + self:T(self.lid.."In HVT Distance = YES") + targettable:Push(contact,distance) + checked = true + end + end + + -- Bucket 2 - in AO/FEZ + local isinopszone = self.OpsZone:IsVec2InZone(contactvec2) + local distance = self.OpsZone:Get2DDistance(contactcoord) + if isinopszone and not checked then + self:T(self.lid.."In FEZ = YES") + targettable:Push(contact,distance) + checked = true + end + + -- Bucket 3 - in Radar(Control)Zone, < 100nm to AO, Aspect HOT on AO + local isinopszone = self.ControlZone:IsVec2InZone(contactvec2) + if isinopszone and not checked then + self:T(self.lid.."In Radar Zone = YES") + -- Close to Bulls Eye? + local distance = self.AOCoordinate:Get2DDistance(contactcoord) -- m + local AOdist = UTILS.Round(UTILS.MetersToNM(distance),0) -- NM + if not contactcoord.Heading then + contactcoord.Heading = self.intel:CalcClusterDirection(contact.Cluster) + end -- end heading + local aspect = contactcoord:ToStringAspect(self.ControlZone:GetCoordinate()) + local sizing = contact.Cluster.size or self.intel:ClusterCountUnits(contact.Cluster) or 1 + -- prefer heavy groups + sizing = math.fmod((sizing * 0.1),1) + local AOdist2 = (AOdist / 2) * sizing + AOdist2 = UTILS.Round((AOdist/2)+((AOdist/2)-AOdist2), 0) + self:T(self.lid.."Aspect = "..aspect.." | Size = "..sizing ) + if (AOdist2 < 75) or (aspect == "Hot") then + local text = string.format("In AO(Adj) dist = %d(%d) NM",AOdist,AOdist2) + self:T(self.lid..text) + targettable:Push(contact,distance) + checked = true + end + end + + -- Bucket 4 (if set) within the border polyzone to be defended + if self.BorderZone then + local isinborderzone = self.BorderZone:IsVec2InZone(contactvec2) + if isinborderzone and not checked then + self:T(self.lid.."In BorderZone = YES") + targettable:Push(contact,distance) + checked = true + end + end + end + + self:T(self.lid.."Post filter count: "..targettable:Count()) + + if targettable:Count() > maxtargets then + local targets = targettable:GetSortedDataTable() + targettable:Clear() + for i=1,maxtargets do + targettable:Push(targets[i]) + end + end + + sortedtargets:Clear() + prefiltered:Clear() + + if targettable:Count() > 0 then + HaveTargets = true + end + + return HaveTargets, targettable +end + +--- [Internal] AWACS Speak Picture AO/EWR entries +-- @param #AWACS self +-- @param #boolean AO If true this is for AO, else EWR +-- @param #string Callsign Callsign to address +-- @param #number GID GroupID for comms +-- @param #number MaxEntries Max entries to show +-- @param #boolean IsGeneral Is a general picture, address all stations +-- @return #AWACS self +function AWACS:_CreatePicture(AO,Callsign,GID,MaxEntries,IsGeneral) + self:T(self.lid.."_CreatePicture AO="..tostring(AO).." for "..Callsign.." GID "..GID) + + local managedgroup = nil + local group = nil + local groupcoord = nil + + if not IsGeneral then + managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + group = managedgroup.Group -- Wrapper.Group#GROUP + groupcoord = group:GetCoordinate() + end + + local fifo = self.PictureAO -- Utilities.FiFo#FIFO + + local maxentries = self.maxspeakentries or 3 + + if MaxEntries and MaxEntries>0 and MaxEntries <= 3 then + maxentries = MaxEntries + end + + local counter = 0 + + if not AO then + -- fifo = self.PictureEWR + end + + local entries = fifo:GetSize() + + if entries < maxentries then maxentries = entries end + + local text = "" + local textScreen = "" + + -- " group, BRA for at angels , , " + while counter < maxentries do + counter = counter + 1 + local contact = fifo:Pull() -- #AWACS.ManagedContact + self:T({contact}) + if contact and contact.Contact.group and contact.Contact.group:IsAlive() then + + local coordinate = contact.Cluster.coordinate or contact.Contact.position or contact.Contact.group:GetCoordinate() -- Core.Point#COORDINATE + if not coordinate then + self:E(self.lid.."NO Coordinate for this cluster! CID="..contact.CID) + self:E({contact}) + break + end + if not coordinate.Heading then + coordinate.Heading = contact.Contact.heading or contact.Contact.group:GetHeading() + end + local refBRAA = "" + local refBRAATTS = "" + + if self.NoGroupTags then + local grouptxt = self.gettext:GetEntry("GROUPCAP",self.locale) + text = grouptxt .. "." -- Alpha Group. + textScreen = grouptxt .."," + else + local grouptxt = self.gettext:GetEntry("GROUP",self.locale) + text = contact.TargetGroupNaming.." "..grouptxt.."." -- Alpha Group. + textScreen = contact.TargetGroupNaming.." "..grouptxt.."," + end + + if IsGeneral or not self.PlayerGuidance then + local milestxt = self.gettext:GetEntry("MILES",self.locale) + local thsdtxt = self.gettext:GetEntry("THOUSAND",self.locale) + refBRAA=self:_ToStringBULLS(coordinate) + refBRAATTS = self:_ToStringBULLS(coordinate, false, true) + local alt = contact.Contact.group:GetAltitude() or 8000 + alt = UTILS.Round(UTILS.MetersToFeet(alt)/1000,0) + -- Alpha Group. Bulls eye 0 2 1, 16 miles, 25 thousand. + text = string.format("%s %s %s, %d %s.",text,refBRAATTS,milestxt,alt,thsdtxt) + textScreen = string.format("%s %s %s, %d %s.",textScreen,refBRAA,milestxt,alt,thsdtxt) + else + -- pilot reference + refBRAA = coordinate:ToStringBRAANATO(groupcoord,true,true) + refBRAATTS = string.gsub(refBRAA,"BRAA","brah") + refBRAATTS = string.gsub(refBRAATTS,"BRA","brah") + -- Charlie group, BRAA 045, 105 miles, Angels 41, Flanking, Track North-East, Bogey, Spades. + if self.PathToGoogleKey then + refBRAATTS = coordinate:ToStringBRAANATO(groupcoord,true,true,true,false,true) + end + if contact.IFF ~= AWACS.IFF.BOGEY then + local bogey = self.gettext:GetEntry("BOGEY",self.locale) + refBRAA = string.gsub(refBRAA,bogey, contact.IFF) + refBRAATTS = string.gsub(refBRAATTS,bogey, contact.IFF) + end + text = text .. " "..refBRAATTS + textScreen = textScreen .." "..refBRAA + end + + -- Aspect + local aspect = "" + + -- sizing + local size = contact.Contact.group:CountAliveUnits() + local threatsize, threatsizetext = self:_GetBlurredSize(size) + + if threatsize > 1 then + text = text.." "..threatsizetext.."." -- Alpha Group. Heavy. + textScreen = textScreen.." "..threatsizetext.."." + end + + -- engagement tag? + if contact.EngagementTag then + text = text .. " "..contact.EngagementTag -- Alpha Group. Bulls eye 0 2 1, 16. Heavy. Targeted by Jazz 1 1. + textScreen = textScreen .. " "..contact.EngagementTag -- Alpha Group, Bullseye 021, 16, Flanking. Targeted by Jazz 1 1. + end + + -- Transmit Radio + local RadioEntry_IsGroup = false + local RadioEntry_ToScreen = self.debug + if managedgroup and not IsGeneral then + RadioEntry_IsGroup = managedgroup.IsPlayer + RadioEntry_ToScreen = managedgroup.IsPlayer + end + + self:_NewRadioEntry(text,textScreen,GID,RadioEntry_IsGroup,RadioEntry_ToScreen,true,false) + + end + end + + -- empty queue from leftovers + fifo:Clear() + + return self +end + +--- [Internal] AWACS Speak Bogey Dope entries +-- @param #AWACS self +-- @param #string Callsign Callsign to address +-- @param #number GID GroupID for comms +-- @param #boolean Tactical Is for tactical info +-- @return #AWACS self +function AWACS:_CreateBogeyDope(Callsign,GID,Tactical) + self:T(self.lid.."_CreateBogeyDope for "..Callsign.." GID "..GID) + + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local group = managedgroup.Group -- Wrapper.Group#GROUP + local groupcoord = group:GetCoordinate() + + local fifo = self.ContactsAO -- Utilities.FiFo#FIFO + local maxentries = 1 + local counter = 0 + + local entries = fifo:GetSize() + + if entries < maxentries then maxentries = entries end + + local sortedIDs = fifo:GetIDStackSorted() -- sort by distance + + while counter < maxentries do + counter = counter + 1 + local contact = fifo:PullByID(sortedIDs[counter]) -- #AWACS.ManagedContact + self:T({contact}) + local position = contact.Cluster.coordinate or contact.Contact.position + if contact and position then + local tag = contact.TargetGroupNaming + local reportingname = contact.ReportingName + -- DONE - add tag + self:_AnnounceContact(contact,false,group,true,tag,false,reportingname,Tactical) + end + end + + -- empty queue from leftovers + fifo:Clear() + + return self +end + +--- [Internal] AWACS Menu for Picture +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @param #boolean IsGeneral General picture if true, address no-one specific +-- @return #AWACS self +function AWACS:_Picture(Group,IsGeneral) + self:T(self.lid.."_Picture") + local text = "" + local textScreen = text + local general = IsGeneral + local GID, Outcome, gcallsign = self:_GetManagedGrpID(Group) + + if general then + local allst = self.gettext:GetEntry("ALLSTATIONS",self.locale) + gcallsign = allst + end + + if Group and Outcome then + general = false + end + + if not self.intel then + -- no intel yet! + local picclean = self.gettext:GetEntry("PICCLEAN",self.locale) + text = string.format(picclean,gcallsign,self.callsigntxt) + textScreen = text + + self:_NewRadioEntry(text,text,GID,false,true,true,false) + + return self + end + + if Outcome or general then + -- Pilot is checked in + -- get clusters from Intel + local contactstable = self.Contacts:GetDataTable() + + -- sort into buckets + for _,_contact in pairs(contactstable) do + + local contact = _contact -- #AWACS.ManagedContact + + local coordVec2 = contact.Contact.position:GetVec2() + + if self.OpsZone:IsVec2InZone(coordVec2) then + self.PictureAO:Push(contact) + elseif self.OrbitZone and self.OrbitZone:IsVec2InZone(coordVec2) then + self.PictureAO:Push(contact) + elseif self.ControlZone:IsVec2InZone(coordVec2) then + local distance = math.floor((contact.Contact.position:Get2DDistance(self.ControlZone:GetCoordinate()) / 1000) + 1) -- km + self.PictureEWR:Push(contact,distance) + end + + end + + local clustersAO = self.PictureAO:GetSize() + local clustersEWR = self.PictureEWR:GetSize() + + if clustersAO < 3 and clustersEWR > 0 then + -- make sure we have 3, can only add 1, 2 or 3 + local IDstack = self.PictureEWR:GetSortedDataTable() + -- how many do we need? + local weneed = 3-clustersAO + -- do we have enough? + self:T(string.format("Picture - adding %d/%d contacts from EWR",weneed,clustersEWR)) + if weneed > clustersEWR then + weneed = clustersEWR + end + for i=1,weneed do + self.PictureAO:Push(IDstack[i]) + end + end + + clustersAO = self.PictureAO:GetSize() + + if clustersAO == 0 and clustersEWR == 0 then + -- clean + local picclean = self.gettext:GetEntry("PICCLEAN",self.locale) + text = string.format(picclean,gcallsign,self.callsigntxt) + textScreen = text + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + else + + if clustersAO > 0 then + local picture = self.gettext:GetEntry("PICTURE",self.locale) + text = string.format("%s, %s. %s. ",gcallsign, self.callsigntxt,picture) + textScreen = string.format("%s, %s. %s. ",gcallsign, self.callsigntxt,picture) + local onetxt = self.gettext:GetEntry("ONE",self.locale) + local grptxt = self.gettext:GetEntry("GROUP",self.locale) + local groupstxt = self.gettext:GetEntry("GROUPMULTI",self.locale) + if clustersAO == 1 then + text = string.format("%s%s %s. ",text,onetxt,grptxt) + textScreen = string.format("%s%s %s.\n",textScreen,onetxt,grptxt) + else + text = string.format("%s%d %s. ",text,clustersAO,groupstxt) + textScreen = string.format("%s%d %s.\n",textScreen,clustersAO,groupstxt) + end + self:_NewRadioEntry(text,textScreen,GID,Outcome,true,true,false) + + self:_CreatePicture(true,gcallsign,GID,3,general) + + self.PictureAO:Clear() + self.PictureEWR:Clear() + end + end + + elseif self.AwacsFG then + -- no, unknown + local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale) + text = string.format(nocheckin,gcallsign, self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + return self +end + +--- [Internal] AWACS Menu for Bogey Dope +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @param #boolean Tactical Check for tactical info +-- @return #AWACS self +function AWACS:_BogeyDope(Group,Tactical) + self:T(self.lid.."_BogeyDope") + local text = "" + local textScreen = "" + local GID, Outcome = self:_GetManagedGrpID(Group) + local gcallsign = self:_GetCallSign(Group,GID) or "Ghost 1" + + if not self.intel then + -- no intel yet! + local clean = self.gettext:GetEntry("CLEAN",self.locale) + text = string.format(clean,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,0,false,true,true,false,true,Tactical) + return self + end + + if Outcome then + -- Pilot is checked in + + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local pilotgroup = managedgroup.Group + local pilotcoord = managedgroup.Group:GetCoordinate() + + local contactstable = self.Contacts:GetDataTable() + + -- sort into buckets - AO only for bogey dope! + for _,_contact in pairs(contactstable) do + local managedcontact = _contact -- #AWACS.ManagedContact + local contactposition = managedcontact.Cluster.coordinate or managedcontact.Contact.position -- Core.Point#COORDINATE + local coordVec2 = contactposition:GetVec2() + -- Get distance for sorting + local dist = pilotcoord:Get2DDistance(contactposition) + + if self.ControlZone:IsVec2InZone(coordVec2) then + self.ContactsAO:Push(managedcontact,dist) + elseif self.BorderZone and self.BorderZone:IsVec2InZone(coordVec2) then + self.ContactsAO:Push(managedcontact,dist) + else + if self.OrbitZone then + local distance = contactposition:Get2DDistance(self.OrbitZone:GetCoordinate()) + if (distance <= UTILS.NMToMeters(45)) then + self.ContactsAO:Push(managedcontact,distance) + end + end + end + end + + local contactsAO = self.ContactsAO:GetSize() + + if contactsAO == 0 then + -- clean + local clean = self.gettext:GetEntry("CLEAN",self.locale) + text = string.format(clean,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + + self:_NewRadioEntry(text,text,GID,Outcome,Outcome,true,false,true,Tactical) + + else + + if contactsAO > 0 then + local dope = self.gettext:GetEntry("DOPE",self.locale) + text = string.format(dope,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + textScreen = string.format(dope,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + local onetxt = self.gettext:GetEntry("ONE",self.locale) + local grptxt = self.gettext:GetEntry("GROUP",self.locale) + local groupstxt = self.gettext:GetEntry("GROUPMULTI",self.locale) + if contactsAO == 1 then + text = string.format("%s%s %s. ",text,onetxt,grptxt) + textScreen = string.format("%s%s %s.\n",textScreen,onetxt,grptxt) + else + text = string.format("%s%d %s. ",text,contactsAO,groupstxt) + textScreen = string.format("%s%d %s.\n",textScreen,contactsAO,groupstxt) + end + + self:_NewRadioEntry(text,textScreen,GID,Outcome,true,true,false,true,Tactical) + + self:_CreateBogeyDope(self:_GetCallSign(Group,GID) or "Ghost 1",GID,Tactical) + end + end + + elseif self.AwacsFG then + -- no, unknown + local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale) + text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,Tactical) + + end + return self +end + +--- [Internal] AWACS Menu for Show Info +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_ShowAwacsInfo(Group) + self:T(self.lid.."_ShowAwacsInfo") + local report = REPORT:New("Info") + local STN = self.STN + report:Add("====================") + report:Add(string.format("AWACS %s",self.callsigntxt)) + report:Add(string.format("Radio: %.3f %s",self.Frequency,UTILS.GetModulationName(self.Modulation))) + if STN then + report:Add(string.format("Link-16 STN: %s",STN)) + end + report:Add(string.format("Bulls Alias: %s",self.AOName)) + report:Add(string.format("Coordinate: %s",self.AOCoordinate:ToStringLLDDM())) + report:Add("====================") + report:Add(string.format("Assignment Distance: %d NM",self.maxassigndistance)) + report:Add(string.format("TAC Distance: %d NM",self.TacDistance)) + report:Add(string.format("MELD Distance: %d NM",self.MeldDistance)) + report:Add(string.format("THREAT Distance: %d NM",self.ThreatDistance)) + report:Add("====================") + report:Add(string.format("ROE/ROT: %s, %s",self.AwacsROE,self.AwacsROT)) + MESSAGE:New(report:Text(),45,"AWACS"):ToGroup(Group) + return self +end + +--- [Internal] AWACS Menu for VID +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @param #string Declaration Text declaration the player used +-- @return #AWACS self +function AWACS:_VID(Group,Declaration) + self:T(self.lid.."_VID") + + local GID, Outcome, Callsign = self:_GetManagedGrpID(Group) + local text = "" + local TextTTS = "" + + if Outcome then + --yes, known + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local group = managedgroup.Group + local position = group:GetCoordinate() + local radius = UTILS.NMToMeters(self.DeclareRadius) or UTILS.NMToMeters(5) + + -- find tasked contact + local TID = managedgroup.CurrentTask or 0 + if TID > 0 then + local task = self.ManagedTasks:ReadByID(TID) -- #AWACS.ManagedTask + -- correct task? + if task.ToDo ~= AWACS.TaskDescription.VID then + return self + end + -- already done? + if task.Status ~= AWACS.TaskStatus.ASSIGNED then + return self + end + local CID = task.Cluster.CID + local cluster = self.Contacts:ReadByID(CID) -- #AWACS.ManagedContact + if cluster then + local gposition = cluster.Contact.group:GetCoordinate() + local cposition = gposition or cluster.Cluster.coordinate or cluster.Contact.position + local distance = cposition:Get2DDistance(position) + distance = UTILS.Round(distance,0) + 1 + if distance <= radius or self.debug then + -- we can VID + self:T("Contact VID as "..Declaration) + -- update + cluster.IFF = Declaration + task.Status = AWACS.TaskStatus.SUCCESS + self.ManagedTasks:PullByID(TID) + self.ManagedTasks:Push(task,TID) + self.Contacts:PullByID(CID) + self.Contacts:Push(cluster,CID) + local vidpos = self.gettext:GetEntry("VIDPOS",self.locale) + text = string.format(vidpos,Callsign,self.callsigntxt, Declaration) + self:T(text) + self:__VIDSuccess(3,GID,group,cluster) + else + -- too far away + self:T("Contact VID not close enough") + local vidneg = self.gettext:GetEntry("VIDNEG",self.locale) + text = string.format(vidneg,Callsign,self.callsigntxt) + self:T(text) + self:__VIDFailure(3,GID,group,cluster) + end + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) + end + end + -- + elseif self.AwacsFG then + -- no, unknown + local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale) + text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + return self +end + +--- [Internal] AWACS Menu for Declare +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_Declare(Group) + self:T(self.lid.."_Declare") + + local GID, Outcome, Callsign = self:_GetManagedGrpID(Group) + local text = "" + local TextTTS = "" + + if Outcome then + --yes, known + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local group = managedgroup.Group + local position = group:GetCoordinate() + local radius = UTILS.NMToMeters(self.DeclareRadius) or UTILS.NMToMeters(5) + -- find contacts nearby + local groupzone = ZONE_GROUP:New(group:GetName(),group, radius) + local Coalitions = {"red","neutral"} + if self.coalition == coalition.side.NEUTRAL then + Coalitions = {"red","blue"} + elseif self.coalition == coalition.side.RED then + Coalitions = {"blue","neutral"} + end + local contactset = SET_GROUP:New():FilterCategoryAirplane():FilterCoalitions(Coalitions):FilterZones({groupzone}):FilterOnce() + local numbercontacts = contactset:CountAlive() or 0 + local foundcontacts = {} + if numbercontacts > 0 then + -- we have some around + -- sort by distance + contactset:ForEach( + function (airpl) + local distance = position:Get2DDistance(airpl:GetCoordinate()) + distance = UTILS.Round(distance,0) + 1 + foundcontacts[distance] = airpl + end + ,{} + ) + for _dist,_contact in UTILS.spairs(foundcontacts) do + local distanz = _dist + local contact = _contact -- Wrapper.Group#GROUP + local ccoalition = contact:GetCoalition() + local ctypename = contact:GetTypeName() + + local ffneutral = self.gettext:GetEntry("FFNEUTRAL",self.locale) + local fffriend = self.gettext:GetEntry("FFFRIEND",self.locale) + local ffhostile = self.gettext:GetEntry("FFHOSTILE",self.locale) + local ffspades = self.gettext:GetEntry("FFSPADES",self.locale) + + local friendorfoe = ffneutral + if self.self.ModernEra then + if ccoalition == self.coalition then + friendorfoe = fffriend + elseif ccoalition == coalition.side.NEUTRAL then + friendorfoe = ffneutral + elseif ccoalition ~= self.coalition then + friendorfoe = ffhostile + end + else + friendorfoe = ffspades + end + -- see if that works + self:T(string.format("Distance %d ContactName %s Coalition %d (%s) TypeName %s",distanz,contact:GetName(),ccoalition,friendorfoe,ctypename)) + + text = string.format("%s. %s. %s.",Callsign,self.callsigntxt,friendorfoe) + TextTTS = text + if self.ModernEra then + text = string.format("%s %s.",text,ctypename) + end + break + end + else + -- clean + local ffclean = self.gettext:GetEntry("FFCLEAN",self.locale) + text = string.format("%s. %s. %s.",Callsign,self.callsigntxt,ffclean) + TextTTS = text + end + self:_NewRadioEntry(TextTTS,text,GID,Outcome,true,true,false,true) + -- + elseif self.AwacsFG then + -- no, unknown + local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale) + text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + return self +end + +--- [Internal] AWACS Menu for Commit +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_Commit(Group) + self:T(self.lid.."_Commit") + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "" + if Outcome then + local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + -- Get current task from the group + local currtaskid = Pilot.CurrentTask + local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask + self:T(string.format("TID %d(%d) | ToDo %s | Status %s",currtaskid,managedtask.TID,managedtask.ToDo,managedtask.Status)) + if managedtask then + -- got a task, status? + if managedtask.Status == AWACS.TaskStatus.REQUESTED then + -- ok let's commit this one + managedtask = self.ManagedTasks:PullByID(currtaskid) + managedtask.Status = AWACS.TaskStatus.ASSIGNED + self.ManagedTasks:Push(managedtask,currtaskid) + self:T(string.format("COMMITTED - TID %d(%d) for GID %d | ToDo %s | Status %s",currtaskid,GID,managedtask.TID,managedtask.ToDo,managedtask.Status)) + -- link to Pilot + Pilot.HasAssignedTask = true + Pilot.CurrentTask = currtaskid + self.ManagedGrps[GID] = Pilot + local copy = self.gettext:GetEntry("COPY",self.locale) + local targetedby = self.gettext:GetEntry("TARGETEDBY",self.locale) + text = string.format(copy,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + local EngagementTag = string.format(targetedby,Pilot.CallSign) + self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.ASSIGNED) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) + else + self:E(self.lid.."Cannot find REQUESTED managed task with TID="..currtaskid.." for GID="..GID) + end + else + self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) + end + elseif self.AwacsFG then + -- no, unknown + local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale) + text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + return self +end + +--- [Internal] AWACS Menu for Judy +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_Judy(Group) + self:T(self.lid.."_Judy") + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "" + if Outcome then + local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + -- Get current task from the group + local currtaskid = Pilot.CurrentTask + local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask + if managedtask then + -- got a task, status? + if managedtask.Status == AWACS.TaskStatus.REQUESTED or managedtask.Status == AWACS.TaskStatus.UNASSIGNED then + -- ok let's commit this one + managedtask = self.ManagedTasks:PullByID(currtaskid) + managedtask.Status = AWACS.TaskStatus.ASSIGNED + self.ManagedTasks:Push(managedtask,currtaskid) + local copy = self.gettext:GetEntry("COPY",self.locale) + local targetedby = self.gettext:GetEntry("TARGETEDBY",self.locale) + text = string.format(copy,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + local EngagementTag = string.format(targetedby,Pilot.CallSign) + self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.ASSIGNED) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) + else + self:E(self.lid.."Cannot find REQUESTED or UNASSIGNED managed task with TID="..currtaskid.." for GID="..GID) + end + else + self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) + end + elseif self.AwacsFG then + -- no, unknown + local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale) + text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + return self +end + +--- [Internal] AWACS Menu for Unable +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_Unable(Group) + self:T(self.lid.."_Unable") + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "" + if Outcome then + local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + -- Get current task from the group + local currtaskid = Pilot.CurrentTask + local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask + self:T(string.format("UNABLE for TID %d(%d) | ToDo %s | Status %s",currtaskid,managedtask.TID,managedtask.ToDo,managedtask.Status)) + if managedtask then + -- got a task, status? + if managedtask.Status == AWACS.TaskStatus.REQUESTED then + -- ok let's commit this one + managedtask = self.ManagedTasks:PullByID(currtaskid) + managedtask.IsUnassigned = true + managedtask.Status = AWACS.TaskStatus.FAILED + self.ManagedTasks:Push(managedtask,currtaskid) + self:T(string.format("REJECTED - TID %d(%d) for GID %d | ToDo %s | Status %s",currtaskid,GID,managedtask.TID,managedtask.ToDo,managedtask.Status)) + -- unlink group from task + Pilot.HasAssignedTask = false + Pilot.CurrentTask = 0 + Pilot.LastTasking = timer.getTime() + self.ManagedGrps[GID] = Pilot + local copy = self.gettext:GetEntry("COPY",self.locale) + text = string.format(copy,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + local EngagementTag = "" + self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.UNASSIGNED) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) + else + self:E(self.lid.."Cannot find REQUESTED managed task with TID="..currtaskid.." for GID="..GID) + end + else + self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) + end + elseif self.AwacsFG then + -- no, unknown + local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale) + text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + return self +end + +--- [Internal] AWACS Menu for Abort +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_TaskAbort(Group) + self:T(self.lid.."_TaskAbort") + local Outcome,GID = self:_GetGIDFromGroupOrName(Group) + local text = "" + if Outcome then + local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + self:T({Pilot}) + -- Get current task from the group + local currtaskid = Pilot.CurrentTask + local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask + if managedtask then + -- got a task, status? + self:T(string.format("ABORT for TID %d(%d) | ToDo %s | Status %s",currtaskid,managedtask.TID,managedtask.ToDo,managedtask.Status)) + if managedtask.Status == AWACS.TaskStatus.ASSIGNED then + -- ok let's un-commit this one + managedtask = self.ManagedTasks:PullByID(currtaskid) + managedtask.Status = AWACS.TaskStatus.FAILED + managedtask.IsUnassigned = true + self.ManagedTasks:Push(managedtask,currtaskid) + -- unlink group + self:T(string.format("ABORTED - TID %d(%d) for GID %d | ToDo %s | Status %s",currtaskid,GID,managedtask.TID,managedtask.ToDo,managedtask.Status)) + -- unlink group from task + Pilot.HasAssignedTask = false + Pilot.CurrentTask = 0 + Pilot.LastTasking = timer.getTime() + self.ManagedGrps[GID] = Pilot + local copy = self.gettext:GetEntry("COPY",self.locale) + text = string.format(copy,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + local EngagementTag = "" + self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.UNASSIGNED) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true) + else + self:E(self.lid.."Cannot find ASSIGNED managed task with TID="..currtaskid.." for GID="..GID) + end + else + self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID) + end + elseif self.AwacsFG then + -- no, unknown + local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale) + text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + + return self +end + +--- [Internal] AWACS Menu for Showtask +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_Showtask(Group) + self:T(self.lid.."_Showtask") + + local GID, Outcome, Callsign = self:_GetManagedGrpID(Group) + local text = "" + + if Outcome then + -- known group + + -- Do we have a task? + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + + if managedgroup.IsPlayer then + + if managedgroup.CurrentTask >0 and self.ManagedTasks:HasUniqueID(managedgroup.CurrentTask) then + -- get task structure + local currenttask = self.ManagedTasks:ReadByID(managedgroup.CurrentTask) -- #AWACS.ManagedTask + if currenttask then + local status = currenttask.Status + local targettype = currenttask.Target:GetCategory() + local targetstatus = currenttask.Target:GetState() + local ToDo = currenttask.ToDo + local description = currenttask.ScreenText + local descTTS = currenttask.ScreenText + local callsign = Callsign + + if self.debug then + local taskreport = REPORT:New("AWACS Tasking Display") + taskreport:Add("===============") + taskreport:Add(string.format("Task for Callsign: %s",Callsign)) + taskreport:Add(string.format("Task: %s with Status: %s",ToDo,status)) + taskreport:Add(string.format("Target of Type: %s",targettype)) + taskreport:Add(string.format("Target in State: %s",targetstatus)) + taskreport:Add("===============") + self:I(taskreport:Text()) + end + + local pposition = managedgroup.Group:GetCoordinate() or managedgroup.LastKnownPosition + if currenttask.ToDo == AWACS.TaskDescription.INTERCEPT or currenttask.ToDo == AWACS.TaskDescription.VID then + local targetpos = currenttask.Target:GetCoordinate() + if pposition and targetpos then + local alti = currenttask.Cluster.altitude or currenttask.Contact.altitude or currenttask.Contact.group:GetAltitude() + local direction, direcTTS = self:_ToStringBRA(pposition,targetpos,alti) + description = description .. "\nBRA "..direction + descTTS = descTTS ..";BRA "..direcTTS + end + elseif currenttask.ToDo == AWACS.TaskDescription.ANCHOR or currenttask.ToDo == AWACS.TaskDescription.REANCHOR then + local targetpos = currenttask.Target:GetCoordinate() + local direction, direcTTS = self:_ToStringBR(pposition,targetpos) + description = description .. "\nBR "..direction + descTTS = descTTS .. ";BR "..direcTTS + end + local statustxt = self.gettext:GetEntry("STATUS",self.locale) + --MESSAGE:New(string.format("%s\n%s %s",description,statustxt,status),30,"AWACS",true):ToGroup(Group) + local text = string.format("%s\n%s %s",description,statustxt,status) + local ttstext = string.format("%s. %s. %s",managedgroup.CallSign,self.callsigntxt,descTTS) + ttstext = string.gsub(ttstext,"\\n",";") + ttstext = string.gsub(ttstext,"VID","V I D") + self:_NewRadioEntry(ttstext,text,GID,true,true,false,false,true) + end + end + end + + elseif self.AwacsFG then + -- no, unknown + local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale) + text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:_NewRadioEntry(text,text,GID,Outcome,true,true,false) + end + return self +end + +--- [Internal] AWACS Menu for Check in +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @return #AWACS self +function AWACS:_CheckIn(Group) + self:T(self.lid.."_CheckIn "..Group:GetName()) + -- check if already known + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "" + local textTTS = "" + if not Outcome then + self.ManagedGrpID = self.ManagedGrpID + 1 + local managedgroup = {} -- #AWACS.ManagedGroup + managedgroup.Group = Group + managedgroup.GroupName = Group:GetName() + managedgroup.IsPlayer = true + managedgroup.IsAI = false + managedgroup.CallSign = self:_GetCallSign(Group,GID,true) or "Ghost 1" + managedgroup.CurrentAuftrag = 0 + managedgroup.CurrentTask = 0 + managedgroup.HasAssignedTask = true + managedgroup.Blocked = true + managedgroup.GID = self.ManagedGrpID + managedgroup.LastKnownPosition = Group:GetCoordinate() + managedgroup.LastTasking = timer.getTime() + + GID = managedgroup.GID + self.ManagedGrps[self.ManagedGrpID]=managedgroup + + local alphacheckbulls = self:_ToStringBULLS(Group:GetCoordinate()) + local alphacheckbullstts = self:_ToStringBULLS(Group:GetCoordinate(),false,true) + local alpha = self.gettext:GetEntry("ALPHACHECK",self.locale) + text = string.format("%s. %s. %s. %s",managedgroup.CallSign,self.callsigntxt,alpha,alphacheckbulls) + textTTS = string.format("%s. %s. %s. %s",managedgroup.CallSign,self.callsigntxt,alpha,alphacheckbullstts) + + self:__CheckedIn(1,managedgroup.GID) + + if self.PlayerStationName then + self:__AssignAnchor(5,managedgroup.GID,true,self.PlayerStationName) + else + self:__AssignAnchor(5,managedgroup.GID) + end + + elseif self.AwacsFG then + local nocheckin = self.gettext:GetEntry("ALREADYCHECKEDIN",self.locale) + text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + textTTS = text + end + + self:_NewRadioEntry(textTTS,text,GID,Outcome,true,true,false) + + return self +end + +--- [Internal] AWACS Menu for CheckInAI +-- @param #AWACS self +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup to use +-- @param Wrapper.Group#GROUP Group Group to use +-- @param #number AuftragsNr Ops.Auftrag#AUFTRAG.auftragsnummer +-- @return #AWACS self +function AWACS:_CheckInAI(FlightGroup,Group,AuftragsNr) + self:T(self.lid.."_CheckInAI "..Group:GetName() .. " to Auftrag Nr "..AuftragsNr) + -- check if already known + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "" + if not Outcome then + self.ManagedGrpID = self.ManagedGrpID + 1 + local managedgroup = {} -- #AWACS.ManagedGroup + managedgroup.Group = Group + managedgroup.GroupName = Group:GetName() + managedgroup.FlightGroup = FlightGroup + managedgroup.IsPlayer = false + managedgroup.IsAI = true + local callsignstring = UTILS.GetCallsignName(self.AICAPCAllName) + if self.callsignTranslations and self.callsignTranslations[callsignstring] then + callsignstring = self.callsignTranslations[callsignstring] + end + local callsignmajor = math.fmod(self.AICAPCAllNumber,9) + local callsign = string.format("%s %d 1",callsignstring,callsignmajor) + if self.callsignshort then + callsign = string.format("%s %d",callsignstring,callsignmajor) + end + self:T("Assigned Callsign: ".. callsign) + managedgroup.CallSign = callsign + managedgroup.CurrentAuftrag = AuftragsNr + managedgroup.HasAssignedTask = false + managedgroup.GID = self.ManagedGrpID + managedgroup.LastKnownPosition = Group:GetCoordinate() + + self.ManagedGrps[self.ManagedGrpID]=managedgroup + + -- SRS voice for CAP + FlightGroup:SetDefaultRadio(self.Frequency,self.Modulation,false) + FlightGroup:SwitchRadio(self.Frequency,self.Modulation) + + local CAPVoice = self.CAPVoice + + if self.PathToGoogleKey then + CAPVoice = self.CapVoices[math.floor(math.random(1,10))] + end + + FlightGroup:SetSRS(self.PathToSRS,self.CAPGender,self.CAPCulture,CAPVoice,self.Port,self.PathToGoogleKey,"FLIGHT",1) + + local checkai = self.gettext:GetEntry("CHECKINAI",self.locale) + text = string.format(checkai,self.callsigntxt, managedgroup.CallSign, self.CAPTimeOnStation, self.AOName) + + self:_NewRadioEntry(text,text,managedgroup.GID,Outcome,false,true,true) + + local alphacheckbulls = self:_ToStringBULLS(Group:GetCoordinate(),false,true) + + local alpha = self.gettext:GetEntry("ALPHACHECK",self.locale) + text = string.format("%s. %s. %s. %s",managedgroup.CallSign,self.callsigntxt,alpha,alphacheckbulls) + self:__CheckedIn(1,managedgroup.GID) + + local AW = FlightGroup.legion + if AW.HasOwnStation then + self:__AssignAnchor(5,managedgroup.GID,AW.HasOwnStation,AW.StationName) + else + self:__AssignAnchor(5,managedgroup.GID) + end + else + local nocheckin = self.gettext:GetEntry("ALREADYCHECKEDIN",self.locale) + text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + end + + self:_NewRadioEntry(text,text,GID,Outcome,false,true,false) + + return self +end + +--- [Internal] AWACS Menu for Check Out +-- @param #AWACS self +-- @param Wrapper.Group#GROUP Group Group to use +-- @param #number GID GroupID +-- @param #boolean dead If true, group is dead crashed or otherwise n/a +-- @return #AWACS self +function AWACS:_CheckOut(Group,GID,dead) + self:T(self.lid.."_CheckOut") + + -- check if already known + local GID, Outcome = self:_GetManagedGrpID(Group) + local text = "" + if Outcome then + -- yes, known + local safeflight = self.gettext:GetEntry("SAFEFLIGHT",self.locale) + text = string.format(safeflight,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + self:T(text) + -- grab some data before we nil the entry + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local Stack = managedgroup.AnchorStackNo + local Angels = managedgroup.AnchorStackAngels + local GroupName = managedgroup.GroupName + -- remove menus + if managedgroup.IsPlayer then + if self.clientmenus:HasUniqueID(GroupName) then + local menus = self.clientmenus:PullByID(GroupName) --#AWACS.MenuStructure + menus.basemenu:Remove() + if self.TacticalSubscribers[GroupName] then + local Freq = self.TacticalSubscribers[GroupName] + self.TacticalFrequencies[Freq] = Freq + self.TacticalSubscribers[GroupName] = nil + end + end + end + -- delete open tasks + if managedgroup.CurrentTask and managedgroup.CurrentTask > 0 then + self.ManagedTasks:PullByID(managedgroup.CurrentTask ) + self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false) + end + self.ManagedGrps[GID] = nil + self:__CheckedOut(1,GID,Stack,Angels) + else + -- no, unknown + if not dead then + local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale) + text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) + end + end + + if not dead then + self:_NewRadioEntry(text,text,GID,Outcome,false,true,false) + end + + return self +end + +--- [Internal] AWACS set client menus +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_SetClientMenus() + self:T(self.lid.."_SetClientMenus") + local clientset = self.clientset -- Core.Set#SET_CLIENT + local aliveset = clientset:GetSetObjects() or {}-- #table of #CLIENT objects + local clientcount = 0 + local clientcheckedin = 0 + for _,_group in pairs(aliveset) do + -- go through set and build the menu + local grp = _group -- Wrapper.Client#CLIENT + local cgrp = grp:GetGroup() + local cgrpname = nil + if cgrp and cgrp:IsAlive() then + cgrpname = cgrp:GetName() + self:T(cgrpname) + end + if self.MenuStrict then + -- check if pilot has checked in + if cgrp and cgrp:IsAlive() then + clientcount = clientcount + 1 + local GID, checkedin = self:_GetManagedGrpID(cgrp) + if checkedin then + -- full menu minus checkin + clientcheckedin = clientcheckedin + 1 + local hasclientmenu = self.clientmenus:ReadByID(cgrpname) -- #AWACS.MenuStructure + local basemenu = hasclientmenu.basemenu -- Core.Menu#MENU_GROUP + + if hasclientmenu and (not hasclientmenu.menuset) then + + self:T(self.lid.."Setting Menus for "..cgrpname) + + basemenu:RemoveSubMenus() + local bogeydope = MENU_GROUP_COMMAND:New(cgrp,"Bogey Dope",basemenu,self._BogeyDope,self,cgrp) + local picture = MENU_GROUP_COMMAND:New(cgrp,"Picture",basemenu,self._Picture,self,cgrp) + local declare = MENU_GROUP_COMMAND:New(cgrp,"Declare",basemenu,self._Declare,self,cgrp) + local tasking = MENU_GROUP:New(cgrp,"Tasking",basemenu) + local showtask = MENU_GROUP_COMMAND:New(cgrp,"Showtask",tasking,self._Showtask,self,cgrp) + + local commit + local unable + local abort + if self.PlayerCapAssignment then + commit = MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp) + unable = MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp) + abort = MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp) + --local judy = MENU_GROUP_COMMAND:New(cgrp,"Judy",tasking,self._Judy,self,cgrp) + end + + if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then + local vid = MENU_GROUP:New(cgrp,"VID as",tasking) + local hostile = MENU_GROUP_COMMAND:New(cgrp,"Hostile",vid,self._VID,self,cgrp,AWACS.IFF.ENEMY) + local neutral = MENU_GROUP_COMMAND:New(cgrp,"Neutral",vid,self._VID,self,cgrp,AWACS.IFF.NEUTRAL) + local friendly = MENU_GROUP_COMMAND:New(cgrp,"Friendly",vid,self._VID,self,cgrp,AWACS.IFF.FRIENDLY) + end + + local tactical + if self.TacticalMenu then + tactical = MENU_GROUP:New(cgrp,"Tactical Radio",basemenu) + if self.TacticalSubscribers[cgrpname] then + -- unsubscribe + local entry = MENU_GROUP_COMMAND:New(cgrp,"Unsubscribe",tactical,self._UnsubScribeTactRadio,self,cgrp) + else + -- subscribe + for _,_freq in UTILS.spairs(self.TacticalFrequencies) do + local modu = UTILS.GetModulationName(self.TacticalModulation) + local text = string.format("Subscribe to %.3f %s",_freq,modu) + local entry = MENU_GROUP_COMMAND:New(cgrp,text,tactical,self._SubScribeTactRadio,self,cgrp,_freq) + end + end + end + + local ainfo = MENU_GROUP_COMMAND:New(cgrp,"Awacs Info",basemenu,self._ShowAwacsInfo,self,cgrp) + local checkout = MENU_GROUP_COMMAND:New(cgrp,"Check Out",basemenu,self._CheckOut,self,cgrp) + + local menus = { -- #AWACS.MenuStructure + groupname = cgrpname, + menuset = true, + basemenu = basemenu, + checkout= checkout, + picture = picture, + bogeydope = bogeydope, + declare = declare, + tasking = tasking, + showtask = showtask, + --judy = judy, + unable = unable, + abort = abort, + commit=commit, + tactical=tactical, + } + self.clientmenus:PullByID(cgrpname) + self.clientmenus:Push(menus,cgrpname) + end + elseif not self.clientmenus:HasUniqueID(cgrpname) then + -- check in only + local basemenu = MENU_GROUP:New(cgrp,self.Name,nil) + local checkin = MENU_GROUP_COMMAND:New(cgrp,"Check In",basemenu,self._CheckIn,self,cgrp) + checkin:SetTag(cgrp:GetName()) + basemenu:Refresh() + local menus = { -- #AWACS.MenuStructure + groupname = cgrpname, + menuset = false, + basemenu = basemenu, + checkin = checkin, + } + self.clientmenus:Push(menus,cgrpname) + -- catch errors - when this entry is built we should NOT have a managed entry + local GID,hasentry = self:_GetManagedGrpID(cgrp) + if hasentry then + -- this user is checked in but has the check in entry ... not good. + self:_CheckOut(cgrp,GID,true) + end + end + end + else + if cgrp and cgrp:IsAlive() and not self.clientmenus:HasUniqueID(cgrpname) then + local basemenu = MENU_GROUP:New(cgrp,self.Name,nil) + local picture = MENU_GROUP_COMMAND:New(cgrp,"Picture",basemenu,self._Picture,self,cgrp) + local bogeydope = MENU_GROUP_COMMAND:New(cgrp,"Bogey Dope",basemenu,self._BogeyDope,self,cgrp) + local declare = MENU_GROUP_COMMAND:New(cgrp,"Declare",basemenu,self._Declare,self,cgrp) + + local tasking = MENU_GROUP:New(cgrp,"Tasking",basemenu) + local showtask = MENU_GROUP_COMMAND:New(cgrp,"Showtask",tasking,self._Showtask,self,cgrp) + local commit = MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp) + local unable = MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp) + local abort = MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp) + --local judy = MENU_GROUP_COMMAND:New(cgrp,"Judy",tasking,self._Judy,self,cgrp) + + if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then + local vid = MENU_GROUP:New(cgrp,"VID as",tasking) + local hostile = MENU_GROUP_COMMAND:New(cgrp,"Hostile",vid,self._VID,self,cgrp,AWACS.IFF.ENEMY) + local neutral = MENU_GROUP_COMMAND:New(cgrp,"Neutral",vid,self._VID,self,cgrp,AWACS.IFF.NEUTRAL) + local friendly = MENU_GROUP_COMMAND:New(cgrp,"Friendly",vid,self._VID,self,cgrp,AWACS.IFF.FRIENDLY) + end + + local ainfo = MENU_GROUP_COMMAND:New(cgrp,"Awacs Info",basemenu,self._ShowAwacsInfo,self,cgrp) + local checkin = MENU_GROUP_COMMAND:New(cgrp,"Check In",basemenu,self._CheckIn,self,cgrp) + local checkout = MENU_GROUP_COMMAND:New(cgrp,"Check Out",basemenu,self._CheckOut,self,cgrp) + + basemenu:Refresh() + + local menus = { -- #AWACS.MenuStructure + groupname = cgrpname, + menuset = true, + basemenu = basemenu, + checkin = checkin, + checkout= checkout, + picture = picture, + bogeydope = bogeydope, + declare = declare, + showtask = showtask, + tasking = tasking, + --judy = judy, + unable = unable, + abort = abort, + commit = commit, + } + self.clientmenus:Push(menus,cgrpname) + end + end + end + + self.MonitoringData.Players = clientcount or 0 + self.MonitoringData.PlayersCheckedin = clientcheckedin or 0 + + return self +end + +--- [Internal] AWACS Delete a new Anchor Stack from a Marker - only works if no assignments are on the station +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_DeleteAnchorStackFromMarker(Name,Coord) + self:T(self.lid.."_DeleteAnchorStackFromMarker") + if self.AnchorStacks:HasUniqueID(Name) and self.PlayerStationName == Name then + local stack = self.AnchorStacks:ReadByID(Name) -- #AWACS.AnchorData + local marker = stack.AnchorMarker + if stack.AnchorAssignedID:Count() == 0 then + marker:Remove() + if self.debug then + stack.StationZone:UndrawZone() + end + self.AnchorStacks:PullByID(Name) + self.PlayerStationName = nil + else + if self.debug then + self:I(self.lid.."**** Cannot delete station, there are CAPs assigned!") + local text = marker:GetText() + marker:TextUpdate(text.."\nMarked for deletion") + end + end + end + return self +end + +--- [Internal] AWACS Move a new Anchor Stack from a Marker +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_MoveAnchorStackFromMarker(Name,Coord) + self:T(self.lid.."_MoveAnchorStackFromMarker") + if self.AnchorStacks:HasUniqueID(Name) and self.PlayerStationName == Name then + local station = self.AnchorStacks:PullByID(Name) -- #AWACS.AnchorData + local stationtag = string.format("Station: %s\nCoordinate: %s",Name,Coord:ToStringLLDDM()) + local marker = station.AnchorMarker + local zone = station.StationZone + if self.debug then + zone:UndrawZone() + end + local radius = self.StationZone:GetRadius() + if radius < 10000 then radius = 10000 end + station.StationZone = ZONE_RADIUS:New(Name, Coord:GetVec2(), radius) + marker:UpdateCoordinate(Coord) + marker:UpdateText(stationtag) + station.AnchorMarker = marker + if self.debug then + station.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true) + end + self.AnchorStacks:Push(station,Name) + end + return self +end + +--- [Internal] AWACS Create a new Anchor Stack from a Marker - this then is the preferred station for players +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_CreateAnchorStackFromMarker(Name,Coord) + self:T(self.lid.."_CreateAnchorStackFromMarker") + local AnchorStackOne = {} -- #AWACS.AnchorData + AnchorStackOne.AnchorBaseAngels = self.AnchorBaseAngels + AnchorStackOne.Anchors = FIFO:New() -- Utilities.FiFo#FIFO + AnchorStackOne.AnchorAssignedID = FIFO:New() -- Utilities.FiFo#FIFO + + local newname = Name + + for i=1,self.AnchorMaxStacks do + AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels) + end + local radius = self.StationZone:GetRadius() + if radius < 10000 then radius = 10000 end + AnchorStackOne.StationZone = ZONE_RADIUS:New(newname, Coord:GetVec2(), radius) + AnchorStackOne.StationZoneCoordinate = Coord + AnchorStackOne.StationZoneCoordinateText = Coord:ToStringLLDDM() + AnchorStackOne.StationName = newname + + --push to AnchorStacks + if self.debug then + AnchorStackOne.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true) + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + if self.AllowMarkers then + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) + end + else + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + if self.AllowMarkers then + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) + end + end + + self.AnchorStacks:Push(AnchorStackOne,newname) + self.PlayerStationName = newname + + return self +end + +--- [Internal] AWACS Create a new Anchor Stack +-- @param #AWACS self +-- @return #boolean success +-- @return #number AnchorStackNo +function AWACS:_CreateAnchorStack() + self:T(self.lid.."_CreateAnchorStack") + local stackscreated = self.AnchorStacks:GetSize() + if stackscreated == self.AnchorMaxAnchors then + -- only create self.AnchorMaxAnchors Anchors + return false, 0 + end + local AnchorStackOne = {} -- #AWACS.AnchorData + AnchorStackOne.AnchorBaseAngels = self.AnchorBaseAngels + AnchorStackOne.Anchors = FIFO:New() -- Utilities.FiFo#FIFO + AnchorStackOne.AnchorAssignedID = FIFO:New() -- Utilities.FiFo#FIFO + + local newname = self.StationZone:GetName() + + for i=1,self.AnchorMaxStacks do + AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels) + end + + if stackscreated == 0 then + local newsubname = AWACS.AnchorNames[stackscreated+1] or tostring(stackscreated+1) + newname = self.StationZone:GetName() .. "-"..newsubname + AnchorStackOne.StationZone = self.StationZone + AnchorStackOne.StationZoneCoordinate = self.StationZone:GetCoordinate() + AnchorStackOne.StationZoneCoordinateText = self.StationZone:GetCoordinate():ToStringLLDDM() + AnchorStackOne.StationName = newname + --push to AnchorStacks + if self.debug then + --self.AnchorStacks:Flush() + AnchorStackOne.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true) + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + if self.AllowMarkers then + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) + end + else + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + if self.AllowMarkers then + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) + end + end + self.AnchorStacks:Push(AnchorStackOne,newname) + else + local newsubname = AWACS.AnchorNames[stackscreated+1] or tostring(stackscreated+1) + newname = self.StationZone:GetName() .. "-"..newsubname + local anchorbasecoord = self.OpsZone:GetCoordinate() -- Core.Point#COORDINATE + -- OpsZone can be Polygon, so use distance to StationZone as radius + local anchorradius = anchorbasecoord:Get2DDistance(self.StationZone:GetCoordinate()) + local angel = self.StationZone:GetCoordinate():GetAngleDegrees(self.OpsZone:GetVec3()) + self:T("Angel Radians= " .. angel) + local turn = math.fmod(self.AnchorTurn*stackscreated,360) -- #number + if self.AnchorTurn < 0 then turn = -turn end + local newanchorbasecoord = anchorbasecoord:Translate(anchorradius,turn+angel) -- Core.Point#COORDINATE + local radius = self.StationZone:GetRadius() + if radius < 10000 then radius = 10000 end + AnchorStackOne.StationZone = ZONE_RADIUS:New(newname, newanchorbasecoord:GetVec2(), radius) + AnchorStackOne.StationZoneCoordinate = newanchorbasecoord + AnchorStackOne.StationZoneCoordinateText = newanchorbasecoord:ToStringLLDDM() + AnchorStackOne.StationName = newname + --push to AnchorStacks + if self.debug then + AnchorStackOne.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true) + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + if self.AllowMarkers then + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) + end + else + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + if self.AllowMarkers then + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) + end + end + self.AnchorStacks:Push(AnchorStackOne,newname) + end + + return true,self.AnchorStacks:GetSize() + +end + +--- [Internal] AWACS get free anchor stack for managed groups +-- @param #AWACS self +-- @return #number AnchorStackNo +-- @return #boolean free +function AWACS:_GetFreeAnchorStack() + self:T(self.lid.."_GetFreeAnchorStack") + local AnchorStackNo, Free = 0, false + --return AnchorStackNo, Free + local availablestacks = self.AnchorStacks:GetPointerStack() or {} -- #table + for _id,_entry in pairs(availablestacks) do + local entry = _entry -- Utilities.FiFo#FIFO.IDEntry + local data = entry.data -- #AWACS.AnchorData + if data.Anchors:IsNotEmpty() then + AnchorStackNo = _id + Free = true + break + end + end + -- TODO - if extension of anchor stacks to max, send AI home + if not Free then + -- try to create another stack + local created, number = self:_CreateAnchorStack() + if created then + -- we could create a new one - phew! + self:_GetFreeAnchorStack() + end + end + return AnchorStackNo, Free +end + +--- [Internal] AWACS Assign Anchor Position to a Group +-- @param #AWACS self +-- @param #number GID Managed Group ID +-- @param #boolean HasOwnStation +-- @param #string StationName +-- @return #AWACS self +function AWACS:_AssignAnchorToID(GID, HasOwnStation, StationName) + self:T(self.lid.."_AssignAnchorToID") + if not HasOwnStation then + local AnchorStackNo, Free = self:_GetFreeAnchorStack() + if Free then + -- get the Anchor from the stack + local Anchor = self.AnchorStacks:PullByPointer(AnchorStackNo) -- #AWACS.AnchorData + -- pull one free angels + local freeangels = Anchor.Anchors:Pull() + -- push GID on anchor + Anchor.AnchorAssignedID:Push(GID) + -- push back to AnchorStacks + self.AnchorStacks:Push(Anchor,Anchor.StationName) + self:T({Anchor,freeangels}) + self:__AssignedAnchor(5,GID,Anchor,AnchorStackNo,freeangels) + else + self:E(self.lid .. "Cannot assign free anchor stack to GID ".. GID .. " Trying again in 10secs.") + -- try again ... + self:__AssignAnchor(10,GID) + end + else + local Anchor = self.AnchorStacks:PullByID(StationName) -- #AWACS.AnchorData + -- pull one free angels + local freeangels = Anchor.Anchors:Pull() or 25 + -- push GID on anchor + Anchor.AnchorAssignedID:Push(GID) + -- push back to AnchorStacks + self.AnchorStacks:Push(Anchor,StationName) + self:T({Anchor,freeangels}) + local StackNo = self.AnchorStacks.stackbyid[StationName].pointer + self:__AssignedAnchor(5,GID,Anchor,StackNo,freeangels) + end + return self +end + +--- [Internal] Remove GID (group) from Anchor Stack +-- @param #AWACS self +-- @param #AWACS.ManagedGroup.GID ID +-- @param #number AnchorStackNo +-- @param #number Angels +-- @return #AWACS self +function AWACS:_RemoveIDFromAnchor(GID,AnchorStackNo,Angels) + local gid = GID or 0 + local stack = AnchorStackNo or 0 + local angels = Angels or 0 + local debugstring = string.format("%s_RemoveIDFromAnchor for GID=%d Stack=%d Angels=%d",self.lid,gid,stack,angels) + self:T(debugstring) + -- pull correct anchor + if stack > 0 and angels > 0 then + local AnchorStackNo = AnchorStackNo or 1 + local Anchor = self.AnchorStacks:ReadByPointer(AnchorStackNo) -- #AWACS.AnchorData + -- pull GID from stack + local removedID = Anchor.AnchorAssignedID:PullByID(GID) + -- push free angels to stack + Anchor.Anchors:Push(Angels) + end + return self +end + +--- [Internal] Start INTEL detection when we reach the AWACS Orbit Zone +-- @param #AWACS self +-- @param Wrapper.Group#GROUP awacs +-- @return #AWACS self +function AWACS:_StartIntel(awacs) + self:T(self.lid.."_StartIntel") + + if self.intelstarted then return self end + + self.DetectionSet:AddGroup(awacs) + + local intel = INTEL:New(self.DetectionSet,self.coalition,self.callsigntxt) + + intel:SetClusterAnalysis(true,false,false) + + local acceptzoneset = SET_ZONE:New() + acceptzoneset:AddZone(self.ControlZone) + acceptzoneset:AddZone(self.OpsZone) + + if not self.GCI then + self.OrbitZone:SetRadius(UTILS.NMToMeters(55)) + acceptzoneset:AddZone(self.OrbitZone) + end + + if self.BorderZone then + acceptzoneset:AddZone(self.BorderZone) + end + + intel:SetAcceptZones(acceptzoneset) + + if self.NoHelos then + intel:SetFilterCategory({Unit.Category.AIRPLANE}) + else + intel:SetFilterCategory({Unit.Category.AIRPLANE,Unit.Category.HELICOPTER}) + end + + -- Callbacks + local function NewCluster(Cluster) + self:__NewCluster(5,Cluster) + end + function intel:OnAfterNewCluster(From,Event,To,Cluster) + NewCluster(Cluster) + end + + local function NewContact(Contact) + self:__NewContact(5,Contact) + end + function intel:OnAfterNewContact(From,Event,To,Contact) + NewContact(Contact) + end + + local function LostContact(Contact) + self:__LostContact(5,Contact) + end + function intel:OnAfterLostContact(From,Event,To,Contact) + LostContact(Contact) + end + + local function LostCluster(Cluster,Mission) + self:__LostCluster(5,Cluster,Mission) + end + function intel:OnAfterLostCluster(From,Event,To,Cluster,Mission) + LostCluster(Cluster,Mission) + end + + self.intelstarted = true + + intel.statusupdate = -30 + + intel:__Start(5) + + self.intel = intel -- Ops.Intel#INTEL + return self +end + +--- [Internal] Get blurred size of group or cluster +-- @param #AWACS self +-- @param #number size +-- @return #number adjusted size +-- @return #string AWACS.Shipsize entry for size 1..4 +function AWACS:_GetBlurredSize(size) + self:T(self.lid.."_GetBlurredSize") + local threatsize = 0 + local blur = self.RadarBlur + local blurmin = 100 - blur + local blurmax = 100 + blur + local actblur = math.random(blurmin,blurmax) / 100 + threatsize = math.floor(size * actblur) + if threatsize == 0 then threatsize = 1 end + if threatsize then end + local threatsizetext = AWACS.Shipsize[1] + if threatsize == 2 then + threatsizetext = AWACS.Shipsize[2] + elseif threatsize == 3 then + threatsizetext = AWACS.Shipsize[3] + elseif threatsize > 3 then + threatsizetext = AWACS.Shipsize[4] + end + return threatsize, threatsizetext +end + +--- [Internal] Get threat level as clear test +-- @param #AWACS self +-- @param #number threatlevel +-- @return #string threattext +function AWACS:_GetThreatLevelText(threatlevel) + self:T(self.lid.."_GetThreatLevelText") + local threattext = "GREEN" + if threatlevel <= AWACS.THREATLEVEL.GREEN then + threattext = "GREEN" + elseif threatlevel <= AWACS.THREATLEVEL.AMBER then + threattext = "AMBER" + else + threattext = "RED" + end + return threattext +end + + +--- [Internal] Get BR text for TTS +-- @param #AWACS self +-- @param Core.Point#COORDINATE FromCoordinate +-- @param Core.Point#COORDINATE ToCoordinate +-- @return #string BRText Desired Output (BR) "214, 35 miles" +-- @return #string BRTextTTS Desired Output (BR) "2 1 4, 35 miles" +function AWACS:_ToStringBR(FromCoordinate,ToCoordinate) + self:T(self.lid.."_ToStringBR") + local BRText = "" + local BRTextTTS = "" + local DirectionVec3 = FromCoordinate:GetDirectionVec3( ToCoordinate ) + local AngleRadians = FromCoordinate:GetAngleRadians( DirectionVec3 ) + local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) -- degrees + + local AngleDegText = string.format("%03d",AngleDegrees) -- 051 + local AngleDegTextTTS = "" + + local zero = self.gettext:GetEntry("ZERO",self.locale) + local miles = self.gettext:GetEntry("MILES",self.locale) + + AngleDegText = string.gsub(AngleDegText,"%d","%1 ") -- "0 5 1 " + AngleDegText = string.gsub(AngleDegText," $","") -- "0 5 1" + + AngleDegTextTTS = string.gsub(AngleDegText,"0",zero) + + local Distance = ToCoordinate:Get2DDistance( FromCoordinate ) --meters + local distancenm = UTILS.Round(UTILS.MetersToNM(Distance),0) + + BRText = string.format("%03d, %d %s",AngleDegrees,distancenm,miles) + BRTextTTS = string.format("%s, %d %s",AngleDegText,distancenm,miles) + + if self.PathToGoogleKey then + BRTextTTS = string.format("%s, %d %s",AngleDegTextTTS,distancenm,miles) + end + + self:T(BRText,BRTextTTS) + return BRText,BRTextTTS +end + +--- [Internal] Get BRA text for TTS +-- @param #AWACS self +-- @param Core.Point#COORDINATE FromCoordinate +-- @param Core.Point#COORDINATE ToCoordinate +-- @param #number Altitude Altitude in meters +-- @return #string BRText Desired Output (BRA) "214, 35 miles, 20 thousand" +-- @return #string BRTextTTS Desired Output (BRA) "2 1 4, 35 miles, 20 thousand" +function AWACS:_ToStringBRA(FromCoordinate,ToCoordinate,Altitude) + self:T(self.lid.."_ToStringBRA") + local BRText = "" + local BRTextTTS = "" + local altitude = UTILS.Round(UTILS.MetersToFeet(Altitude)/1000,0) + local DirectionVec3 = FromCoordinate:GetDirectionVec3( ToCoordinate ) + local AngleRadians = FromCoordinate:GetAngleRadians( DirectionVec3 ) + local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) -- degrees + + local AngleDegText = string.format("%03d",AngleDegrees) -- 051 + + AngleDegText = string.gsub(AngleDegText,"%d","%1 ") -- "0 5 1 " + AngleDegText = string.gsub(AngleDegText," $","") -- "0 5 1" + local AngleDegTextTTS = string.gsub(AngleDegText,"0","zero") + local Distance = ToCoordinate:Get2DDistance( FromCoordinate ) --meters + local distancenm = UTILS.Round(UTILS.MetersToNM(Distance),0) + + local zero = self.gettext:GetEntry("ZERO",self.locale) + local miles = self.gettext:GetEntry("MILES",self.locale) + local thsd = self.gettext:GetEntry("THOUSAND",self.locale) + local vlow = self.gettext:GetEntry("VERYLOW",self.locale) + + if altitude >= 1 then + BRText = string.format("%03d, %d %s, %d %s",AngleDegrees,distancenm,miles,altitude,thsd) + BRTextTTS = string.format("%s, %d %s, %d %s",AngleDegText,distancenm,miles,altitude,thsd) + if self.PathToGoogleKey then + BRTextTTS = string.format("%s, %d %s, %d %s",AngleDegTextTTS,distancenm,miles,altitude,thsd) + end + else + BRText = string.format("%03d, %d %s, %s",AngleDegrees,distancenm,miles,vlow) + BRTextTTS = string.format("%s, %d %s, %s",AngleDegText,distancenm,miles,vlow) + if self.PathToGoogleKey then + BRTextTTS = string.format("%s, %d %s, %s",AngleDegTextTTS,distancenm,miles,vlow) + end + end + self:T(BRText,BRTextTTS) + return BRText,BRTextTTS +end + +--- [Internal] Get BR text for TTS - ie "Rock 214, 24 miles" and TTS "Rock 2 1 4, 24 miles" +-- @param #AWACS self +-- @param Core.Point#COORDINATE clustercoordinate +-- @return #string BRAText +-- @return #string BRATextTTS +function AWACS:_GetBRAfromBullsOrAO(clustercoordinate) + self:T(self.lid.."_GetBRAfromBullsOrAO") + local refcoord = self.AOCoordinate -- Core.Point#COORDINATE + local BRAText = "" + local BRATextTTS = "" + -- get BR from AO + local bullsname = self.AOName or "Rock" + local stringbr, stringbrtts = self:_ToStringBR(refcoord,clustercoordinate) + BRAText = string.format("%s %s",bullsname,stringbr) + BRATextTTS = string.format("%s %s",bullsname,stringbrtts) + self:T(BRAText,BRATextTTS) + return BRAText,BRATextTTS +end + +--- [Internal] Register Task for Group by GID +-- @param #AWACS self +-- @param #number GroupID ManagedGroup ID +-- @param #AWACS.TaskDescription Description Short Description Task Type +-- @param #string ScreenText Long task description for screen output +-- @param #table Object Object for Ops.Target#TARGET assignment +-- @param #AWACS.TaskStatus TaskStatus Status of this task +-- @param Ops.Auftrag#AUFTRAG Auftrag The Auftrag for this task if any +-- @param Ops.Intel#INTEL.Cluster Cluster Intel Cluster for this task +-- @param Ops.Intel#INTEL.Contact Contact Intel Contact for this task +-- @return #number TID Task ID created +function AWACS:_CreateTaskForGroup(GroupID,Description,ScreenText,Object,TaskStatus,Auftrag,Cluster,Contact) + self:T(self.lid.."_CreateTaskForGroup "..GroupID .." Description: "..Description) + + local managedgroup = self.ManagedGrps[GroupID] -- #AWACS.ManagedGroup + local task = {} -- #AWACS.ManagedTask + self.ManagedTaskID = self.ManagedTaskID + 1 + task.TID = self.ManagedTaskID + task.AssignedGroupID = GroupID + task.Status = TaskStatus or AWACS.TaskStatus.ASSIGNED + task.ToDo = Description + task.Auftrag = Auftrag + task.Cluster = Cluster + task.Contact = Contact + task.IsPlayerTask = managedgroup.IsPlayer + task.IsUnassigned = TaskStatus == AWACS.TaskStatus.UNASSIGNED and false or true + -- task. + if Object and Object:IsInstanceOf("TARGET") then + task.Target = Object + else + task.Target = TARGET:New(Object) + end + task.ScreenText = ScreenText + if Description == AWACS.TaskDescription.ANCHOR or Description == AWACS.TaskDescription.REANCHOR then + task.Target.Type = TARGET.ObjectType.ZONE + end + task.RequestedTimestamp = timer.getTime() + + self.ManagedTasks:Push(task,task.TID) + + managedgroup.HasAssignedTask = true + managedgroup.CurrentTask = task.TID + --managedgroup.TaskQueue:Push(task.TID) + + self:T({managedgroup}) + self.ManagedGrps[GroupID] = managedgroup + + return task.TID +end + +--- [Internal] Read registered Task for Group by its ID +-- @param #AWACS self +-- @param #number GroupID ManagedGroup ID +-- @return #AWACS.ManagedTask Task or nil if n/e +function AWACS:_ReadAssignedTaskFromGID(GroupID) + self:T(self.lid.."_GetAssignedTaskFromGID "..GroupID) + local managedgroup = self.ManagedGrps[GroupID] -- #AWACS.ManagedGroup + if managedgroup and managedgroup.HasAssignedTask and managedgroup.CurrentTask ~= 0 then + local TaskID = managedgroup.CurrentTask + if self.ManagedTasks:HasUniqueID(TaskID) then + return self.ManagedTasks:ReadByID(TaskID) + end + end + return nil +end + +--- [Internal] Read assigned Group from a TaskID +-- @param #AWACS self +-- @param #number TaskID ManagedTask ID +-- @return #AWACS.ManagedGroup Group structure or nil if n/e +function AWACS:_ReadAssignedGroupFromTID(TaskID) + self:T(self.lid.."_ReadAssignedGroupFromTID "..TaskID) + if self.ManagedTasks:HasUniqueID(TaskID) then + local task = self.ManagedTasks:ReadByID(TaskID) -- #AWACS.ManagedTask + if task and task.AssignedGroupID and task.AssignedGroupID > 0 then + return self.ManagedGrps[task.AssignedGroupID] + end + end + return nil +end + +--- [Internal] Create radio entry to tell players that CAP is on station in Anchor +-- @param #AWACS self +-- @param #number GID Group ID +-- @return #AWACS self +function AWACS:_MessageAIReadyForTasking(GID) + self:T(self.lid.."_MessageAIReadyForTasking") + -- obtain group details + if GID >0 and self.ManagedGrps[GID] then + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local GFCallsign = self:_GetCallSign(managedgroup.Group) + local aionst = self.gettext:GetEntry("AIONSTATION",self.locale) + local TextTTS = string.format(aionst,GFCallsign,self.callsigntxt,managedgroup.AnchorStackNo or 1,managedgroup.AnchorStackAngels or 25) + self:_NewRadioEntry(TextTTS,TextTTS,GID,false,false,true,true) + end + return self +end + +--- [Internal] Update Contact Tag +-- @param #AWACS self +-- @param #number CID Contact ID +-- @param #string Text Text to be used +-- @param #boolean TAC TAC Call done +-- @param #boolean MELD MELD Call done +-- @param #string TaskStatus Overwrite status with #AWACS.TaskStatus Status +-- @return #AWACS self +function AWACS:_UpdateContactEngagementTag(CID,Text,TAC,MELD,TaskStatus) + self:T(self.lid.."_UpdateContactEngagementTag") + local text = Text or "" + -- get contact + local contact = self.Contacts:PullByID(CID) -- #AWACS.ManagedContact + if contact then + contact.EngagementTag = text + contact.TACCallDone = TAC or false + contact.MeldCallDone = MELD or false + contact.Status = TaskStatus or AWACS.TaskStatus.UNASSIGNED + self.Contacts:Push(contact,CID) + end + return self +end + +--- [Internal] Check available tasks and status +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_CheckTaskQueue() + self:T(self.lid.."_CheckTaskQueue") + local opentasks = 0 + local assignedtasks = 0 + + -- update last known positions + for _id,_managedgroup in pairs(self.ManagedGrps) do + local group = _managedgroup -- #AWACS.ManagedGroup + if group.Group and group.Group:IsAlive() then + local coordinate = group.Group:GetCoordinate() + if coordinate then + local NewCoordinate = COORDINATE:New(0,0,0) + group.LastKnownPosition = group.LastKnownPosition:UpdateFromCoordinate(coordinate) + self.ManagedGrps[_id] = group + end + end + end + + ---------------------------------------- + -- ANCHOR + ---------------------------------------- + + if self.ManagedTasks:IsNotEmpty() then + opentasks = self.ManagedTasks:GetSize() + self:T("Assigned Tasks: " .. opentasks) + local taskstack = self.ManagedTasks:GetPointerStack() + for _id,_entry in pairs(taskstack) do + local data = _entry -- Utilities.FiFo#FIFO.IDEntry + local entry = data.data -- #AWACS.ManagedTask + local target = entry.Target -- Ops.Target#TARGET + local description = entry.ToDo + if description == AWACS.TaskDescription.ANCHOR or description == AWACS.TaskDescription.REANCHOR then + self:T("Open Task ANCHOR/REANCHOR") + -- see if we have reached the anchor zone + local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup + if managedgroup then + local group = managedgroup.Group + if group and group:IsAlive() then + local groupcoord = group:GetCoordinate() + local zone = target:GetObject() -- Core.Zone#ZONE + self:T({zone}) + if group:IsInZone(zone) then + self:T("Open Task ANCHOR/REANCHOR success for GroupID "..entry.AssignedGroupID) + -- made it + target:Stop() + -- add group to idle stack + if managedgroup.IsAI then + -- message AI on station + self:_MessageAIReadyForTasking(managedgroup.GID) + end -- end isAI + managedgroup.HasAssignedTask = false + self.ManagedGrps[entry.AssignedGroupID] = managedgroup + -- pull task from OpenTasks + self.ManagedTasks:PullByID(entry.TID) + else --inzone + -- not there yet + self:T("Open Task ANCHOR/REANCHOR executing for GroupID "..entry.AssignedGroupID) + end + else + -- group dead, pull task + self.ManagedTasks:PullByID(entry.TID) + end + end + + ---------------------------------------- + -- INTERCEPT + ---------------------------------------- + + elseif description == AWACS.TaskDescription.INTERCEPT then + -- DONE + self:T("Open Tasks INTERCEPT") + local taskstatus = entry.Status + local targetstatus = entry.Target:GetState() + + if taskstatus == AWACS.TaskStatus.UNASSIGNED then + -- thou shallst not be in this list! + self.ManagedTasks:PullByID(entry.TID) + break + end + + local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup + + -- Check ranges for TAC and MELD + -- postions relative to CAP position + --[[ + local targetgrp = entry.Contact.group + local position = entry.Contact.position or entry.Cluster.coordinate + if targetgrp and targetgrp:IsAlive() and managedgroup then + if position and managedgroup.Group and managedgroup.Group:IsAlive() then + local grouposition = managedgroup.Group:GetCoordinate() or managedgroup.Group:GetCoordinate() + local distance = 1000 + if grouposition then + distance = grouposition:Get2DDistance(position) + distance = UTILS.Round(UTILS.MetersToNM(distance),0) + end + self:T("TAC/MELD distance check: "..distance.."NM!") + if distance <= self.TacDistance and distance >= self.MeldDistance then + -- TAC distance + self:T("TAC distance: "..distance.."NM!") + local Contact = self.Contacts:ReadByID(entry.Contact.CID) + self:_TACRangeCall(entry.AssignedGroupID,Contact) + elseif distance <= self.MeldDistance and distance >= self.ThreatDistance then + -- MELD distance + self:T("MELD distance: "..distance.."NM!") + local Contact = self.Contacts:ReadByID(entry.Contact.CID) + self:_MeldRangeCall(entry.AssignedGroupID,Contact) + end + end + end + --]] + + local auftrag = entry.Auftrag -- Ops.Auftrag#AUFTRAG + local auftragstatus = "Not Known" + if auftrag then + auftragstatus = auftrag:GetState() + end + local text = string.format("ID=%d | Status=%s | TargetState=%s | AuftragState=%s",entry.TID,taskstatus,targetstatus,auftragstatus) + self:T(text) + if auftrag then + if auftrag:IsExecuting() then + entry.Status = AWACS.TaskStatus.EXECUTING + elseif auftrag:IsSuccess() then + entry.Status = AWACS.TaskStatus.SUCCESS + elseif auftrag:GetState() == AUFTRAG.Status.FAILED then + entry.Status = AWACS.TaskStatus.FAILED + end + if targetstatus == "Dead" then + entry.Status = AWACS.TaskStatus.SUCCESS + elseif targetstatus == "Alive" and auftrag:IsOver() then + entry.Status = AWACS.TaskStatus.FAILED + end + elseif entry.IsPlayerTask then + -- Player task + -- DONE + if entry.Target:IsDead() or entry.Target:IsDestroyed() or entry.Target:CountTargets() == 0 then + -- success! + entry.Status = AWACS.TaskStatus.SUCCESS + elseif entry.Target:IsAlive() then + -- still alive + -- out of zones? + local targetpos = entry.Target:GetCoordinate() + -- success == out of our controlled zones + local outofzones = false + self.RejectZoneSet:ForEachZone( + function(Zone,Position) + local zone = Zone -- Core.Zone#ZONE + local pos = Position -- Core.Point#VEC2 + if pos and zone:IsVec2InZone(pos) then + -- crossed the border + outofzones = true + end + end, + targetpos:GetVec2() + ) + if not outofzones then + outofzones = true + self.ZoneSet:ForEachZone( + function(Zone,Position) + local zone = Zone -- Core.Zone#ZONE + local pos = Position -- Core.Point#VEC2 + if pos and zone:IsVec2InZone(pos) then + -- in any zone + outofzones = false + end + end, + targetpos:GetVec2() + ) + end + if outofzones then + entry.Status = AWACS.TaskStatus.SUCCESS + end + end + end + + if entry.Status == AWACS.TaskStatus.SUCCESS then + self:T("Open Tasks INTERCEPT success for GroupID "..entry.AssignedGroupID) + if managedgroup then + + self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",true,true,AWACS.TaskStatus.SUCCESS) + + managedgroup.HasAssignedTask = false + managedgroup.ContactCID = 0 + managedgroup.LastTasking = timer.getTime() + + if managedgroup.IsAI then + managedgroup.CurrentAuftrag = 0 + else + managedgroup.CurrentTask = 0 + end + + self.ManagedGrps[entry.AssignedGroupID] = managedgroup + self.ManagedTasks:PullByID(entry.TID) + + self:__InterceptSuccess(1) + self:__ReAnchor(5,managedgroup.GID) + end + + elseif entry.Status == AWACS.TaskStatus.FAILED then + self:T("Open Tasks INTERCEPT failed for GroupID "..entry.AssignedGroupID) + if managedgroup then + managedgroup.HasAssignedTask = false + self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) + managedgroup.ContactCID = 0 + managedgroup.LastTasking = timer.getTime() + if managedgroup.IsAI then + managedgroup.CurrentAuftrag = 0 + else + managedgroup.CurrentTask = 0 + end + if managedgroup.IsPlayer then + entry.IsPlayerTask = false + end + self.ManagedGrps[entry.AssignedGroupID] = managedgroup + if managedgroup.Group:IsAlive() or (managedgroup.FlightGroup and managedgroup.FlightGroup:IsAlive()) then + self:__ReAnchor(5,managedgroup.GID) + end + end + -- remove + self.ManagedTasks:PullByID(entry.TID) + self:__InterceptFailure(1) + + elseif entry.Status == AWACS.TaskStatus.REQUESTED then + -- requested - player tasks only! + self:T("Open Tasks INTERCEPT REQUESTED for GroupID "..entry.AssignedGroupID) + local created = entry.RequestedTimestamp or timer.getTime() - 120 + local Tnow = timer.getTime() + local Trunning = (Tnow-created) / 60 -- mins + local text = string.format("Task TID %s Requested %d minutes ago.",entry.TID,Trunning) + if Trunning > self.ReassignmentPause then + -- reassign if player didn't react within 3 mins + entry.Status = AWACS.TaskStatus.UNASSIGNED + self.ManagedTasks:PullByID(entry.TID) + end + self:T(text) + end + + ---------------------------------------- + -- VID/POLICE + ---------------------------------------- + + elseif description == AWACS.TaskDescription.VID then + -- TODO - how to do this with AI? + -- humans only ATM + local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup + -- check we're alive + if (not managedgroup) or (not managedgroup.Group:IsAlive()) then + self.ManagedTasks:PullByID(entry.TID) + return self + end + + -- target dead or out of bounds? + if entry.Target:IsDead() or entry.Target:IsDestroyed() or entry.Target:CountTargets() == 0 then + -- success! + entry.Status = AWACS.TaskStatus.SUCCESS + elseif entry.Target:IsAlive() then + -- still alive + -- out of zones? + self:T("Checking VID target out of bounds") + local targetpos = entry.Target:GetCoordinate() + -- success == out of our controlled zones + local outofzones = false + self.RejectZoneSet:ForEachZone( + function(Zone,Position) + local zone = Zone -- Core.Zone#ZONE + local pos = Position -- Core.Point#VEC2 + if pos and zone:IsVec2InZone(pos) then + -- crossed the border + outofzones = true + end + end, + targetpos:GetVec2() + ) + if not outofzones then + outofzones = true + self.ZoneSet:ForEachZone( + function(Zone,Position) + local zone = Zone -- Core.Zone#ZONE + local pos = Position -- Core.Point#VEC2 + if pos and zone:IsVec2InZone(pos) then + -- in any zone + outofzones = false + end + end, + targetpos:GetVec2() + ) + end + if outofzones then + entry.Status = AWACS.TaskStatus.SUCCESS + self:T("Out of bounds - SUCCESS") + end + end + + if entry.Status == AWACS.TaskStatus.REQUESTED then + -- requested - player tasks only! + self:T("Open Tasks VID REQUESTED for GroupID "..entry.AssignedGroupID) + local created = entry.RequestedTimestamp or timer.getTime() - 120 + local Tnow = timer.getTime() + local Trunning = (Tnow-created) / 60 -- mins + local text = string.format("Task TID %s Requested %d minutes ago.",entry.TID,Trunning) + if Trunning > self.ReassignmentPause then + -- reassign if player didn't react within 3 mins + entry.Status = AWACS.TaskStatus.UNASSIGNED + self.ManagedTasks:PullByID(entry.TID) + end + self:T(text) + elseif entry.Status == AWACS.TaskStatus.ASSIGNED then + self:T("Open Tasks VID ASSIGNED for GroupID "..entry.AssignedGroupID) + -- check TAC/MELD ranges + --[[ + local targetgrp = entry.Contact.group + local position = entry.Contact.position or entry.Cluster.coordinate + if targetgrp and targetgrp:IsAlive() and managedgroup then + if position and managedgroup.Group and managedgroup.Group:IsAlive() then + local grouposition = managedgroup.Group:GetCoordinate() or managedgroup.Group:GetCoordinate() + local distance = 1000 + if grouposition then + distance = grouposition:Get2DDistance(position) + distance = UTILS.Round(UTILS.MetersToNM(distance),0) + end + self:T("TAC/MELD distance check: "..distance.."NM!") + if distance <= self.TacDistance and distance >= self.MeldDistance then + -- TAC distance + self:T("TAC distance: "..distance.."NM!") + local Contact = self.Contacts:ReadByID(entry.Contact.CID) + self:_TACRangeCall(entry.AssignedGroupID,Contact) + elseif distance <= self.MeldDistance and distance >= self.ThreatDistance then + -- MELD distance + self:T("MELD distance: "..distance.."NM!") + local Contact = self.Contacts:ReadByID(entry.Contact.CID) + self:_MeldRangeCall(entry.AssignedGroupID,Contact) + end + end + end + --]] + elseif entry.Status == AWACS.TaskStatus.SUCCESS then + self:T("Open Tasks VID success for GroupID "..entry.AssignedGroupID) + -- outcomes - player ID'd + -- target dead or left zones handled above + -- target ID'd --> if hostile, assign INTERCEPT TASK + self.ManagedTasks:PullByID(entry.TID) + local Contact = self.Contacts:ReadByID(entry.Contact.CID) -- #AWACS.ManagedContact + if Contact and (Contact.IFF == AWACS.IFF.FRIENDLY or Contact.IFF == AWACS.IFF.NEUTRAL) then + self:T("IFF outcome friendly/neutral for GroupID "..entry.AssignedGroupID) + -- nothing todo, re-anchor + if managedgroup then + managedgroup.HasAssignedTask = false + self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) + managedgroup.ContactCID = 0 + managedgroup.LastTasking = timer.getTime() + if managedgroup.IsAI then + managedgroup.CurrentAuftrag = 0 + else + managedgroup.CurrentTask = 0 + end + if managedgroup.IsPlayer then + entry.IsPlayerTask = false + end + self.ManagedGrps[entry.AssignedGroupID] = managedgroup + self:__ReAnchor(5,managedgroup.GID) + end + elseif Contact and Contact.IFF == AWACS.IFF.ENEMY then + self:T("IFF outcome hostile for GroupID "..entry.AssignedGroupID) + -- change to intercept + entry.ToDo = AWACS.TaskDescription.INTERCEPT + entry.Status = AWACS.TaskStatus.ASSIGNED + local cname = Contact.TargetGroupNaming + entry.ScreenText = string.format("Engage hostile %s group.",cname) + self.ManagedTasks:Push(entry,entry.TID) + local TextTTS = string.format("%s, %s. Engage hostile target!",managedgroup.CallSign,self.callsigntxt) + self:_NewRadioEntry(TextTTS,TextTTS,managedgroup.GID,true,self.debug,true,false,true) + elseif not Contact then + self:T("IFF outcome target DEAD for GroupID "..entry.AssignedGroupID) + -- nothing todo, re-anchor + if managedgroup then + managedgroup.HasAssignedTask = false + self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) + managedgroup.ContactCID = 0 + managedgroup.LastTasking = timer.getTime() + if managedgroup.IsAI then + managedgroup.CurrentAuftrag = 0 + else + managedgroup.CurrentTask = 0 + end + if managedgroup.IsPlayer then + entry.IsPlayerTask = false + end + self.ManagedGrps[entry.AssignedGroupID] = managedgroup + if managedgroup.Group:IsAlive() or managedgroup.FlightGroup:IsAlive() then + self:__ReAnchor(5,managedgroup.GID) + end + end + end + elseif entry.Status == AWACS.TaskStatus.FAILED then + -- outcomes - player unable/abort + -- Player dead managed above + -- Remove task + self:T("Open Tasks VID failed for GroupID "..entry.AssignedGroupID) + if managedgroup then + managedgroup.HasAssignedTask = false + self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED) + managedgroup.ContactCID = 0 + managedgroup.LastTasking = timer.getTime() + if managedgroup.IsAI then + managedgroup.CurrentAuftrag = 0 + else + managedgroup.CurrentTask = 0 + end + if managedgroup.IsPlayer then + entry.IsPlayerTask = false + end + self.ManagedGrps[entry.AssignedGroupID] = managedgroup + if managedgroup.Group:IsAlive() or managedgroup.FlightGroup:IsAlive() then + self:__ReAnchor(5,managedgroup.GID) + end + end + -- remove + self.ManagedTasks:PullByID(entry.TID) + self:__InterceptFailure(1) + end + end + + end + end + + return self +end + +--- [Internal] Write stats to log +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_LogStatistics() + self:T(self.lid.."_LogStatistics") + local text = string.gsub(UTILS.OneLineSerialize(self.MonitoringData),",","\n") + local text = string.gsub(text,"{","\n") + local text = string.gsub(text,"}","") + local text = string.gsub(text,"="," = ") + self:T(text) + if self.MonitoringOn then + MESSAGE:New(text,20,"AWACS",false):ToAll() + end + return self +end + +--- [User] Add another AirWing for AI CAP Flights under management +-- @param #AWACS self +-- @param Ops.Airwing#AIRWING AirWing The AirWing to (also) obtain CAP flights from +-- @param Core.Zone#ZONE_RADIUS Zone (optional) This AirWing has it's own station zone, AI CAP will be send there +-- @return #AWACS self +function AWACS:AddCAPAirWing(AirWing,Zone) + self:T(self.lid.."AddCAPAirWing") + if AirWing then + AirWing:SetUsingOpsAwacs(self) + local distance = self.AOCoordinate:Get2DDistance(AirWing:GetCoordinate()) + if Zone then + -- create AnchorStack + local stackscreated = self.AnchorStacks:GetSize() + if stackscreated == self.AnchorMaxAnchors then + -- only create self.AnchorMaxAnchors Anchors + self:E(self.lid.."Max number of stacks already created!") + else + local AnchorStackOne = {} -- #AWACS.AnchorData + AnchorStackOne.AnchorBaseAngels = self.AnchorBaseAngels + AnchorStackOne.Anchors = FIFO:New() -- Utilities.FiFo#FIFO + AnchorStackOne.AnchorAssignedID = FIFO:New() -- Utilities.FiFo#FIFO + + local newname = Zone:GetName() + + for i=1,self.AnchorMaxStacks do + AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels) + end + + local newsubname = AWACS.AnchorNames[stackscreated+1] or tostring(stackscreated+1) + newname = Zone:GetName() .. "-"..newsubname + AnchorStackOne.StationZone = Zone + AnchorStackOne.StationZoneCoordinate = Zone:GetCoordinate() + AnchorStackOne.StationZoneCoordinateText = Zone:GetCoordinate():ToStringLLDDM() + AnchorStackOne.StationName = newname + --push to AnchorStacks + if self.debug then + AnchorStackOne.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true) + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + if self.AllowMarkers then + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) + end + else + local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM()) + if self.AllowMarkers then + AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) + end + end + self.AnchorStacks:Push(AnchorStackOne,newname) + AirWing.HasOwnStation = true + AirWing.StationName = newname + end + end + self.CAPAirwings:Push(AirWing,distance) + end + return self +end + +--- [Internal] Announce a new contact +-- @param #AWACS self +-- @param #AWACS.ManagedContact Contact +-- @param #boolean IsNew Is a new contact +-- @param Wrapper.Group#GROUP Group Announce to Group if not nil +-- @param #boolean IsBogeyDope If true, this is a bogey dope announcement +-- @param #string Tag Tag name for this contact. Alpha, Brave, Charlie ... +-- @param #boolean IsPopup This is a pop-up group +-- @param #string ReportingName The NATO code reporting name for the contact, e.g. "Foxbat". "Bogey" if unknown. +-- @param #boolean Tactical +-- @return #AWACS self +function AWACS:_AnnounceContact(Contact,IsNew,Group,IsBogeyDope,Tag,IsPopup,ReportingName,Tactical) + self:T(self.lid.."_AnnounceContact") + -- do we have a group to talk to? + local tag = "" + local Tag = Tag + local CID = 0 + if not Tag then + -- injected data available? + CID = Contact.CID or 0 + Tag = Contact.TargetGroupNaming or "" + end + if self.NoGroupTags then + Tag = nil + end + local isGroup = false + local GID = 0 + local grpcallsign = "Ghost 1" + if Group and Group:IsAlive() then + GID, isGroup,grpcallsign = self:_GetManagedGrpID(Group) + self:T("GID="..GID.." CheckedIn = "..tostring(isGroup)) + end + + local cluster = Contact.Cluster + local intel = self.intel -- Ops.Intel#INTEL + + local size = self.intel:ClusterCountUnits(cluster) + local threatsize, threatsizetext = self:_GetBlurredSize(size) + + local clustercoordinate = Contact.Cluster.coordinate or Contact.Contact.position + + local heading = Contact.Contact.group:GetHeading() or self.intel:CalcClusterDirection(cluster) + + clustercoordinate:SetHeading(Contact.Contact.group:GetHeading()) + + local BRAfromBulls, BRAfromBullsTTS = self:_GetBRAfromBullsOrAO(clustercoordinate) + + self:T(BRAfromBulls) + self:T(BRAfromBullsTTS) + BRAfromBulls=BRAfromBulls.."." + BRAfromBullsTTS=BRAfromBullsTTS.."." + + if isGroup then + BRAfromBulls = clustercoordinate:ToStringBRAANATO(Group:GetCoordinate(),true,true) + BRAfromBullsTTS = string.gsub(BRAfromBulls,"BRAA","brah") + BRAfromBullsTTS = string.gsub(BRAfromBullsTTS,"BRA","brah") + if self.PathToGoogleKey then + BRAfromBullsTTS = clustercoordinate:ToStringBRAANATO(Group:GetCoordinate(),true,true,true,false,true) + end + end + + local BRAText = "" + local TextScreen = "" + + if isGroup then + BRAText = string.format("%s, %s.",grpcallsign,self.callsigntxt) + TextScreen = string.format("%s, %s.",grpcallsign,self.callsigntxt) + else + BRAText = string.format("%s.",self.callsigntxt) + TextScreen = string.format("%s.",self.callsigntxt) + end + + local newgrp = self.gettext:GetEntry("NEWGROUP",self.locale) + local grptxt = self.gettext:GetEntry("GROUP",self.locale) + local GRPtxt = self.gettext:GetEntry("GROUPCAP",self.locale) + local popup = self.gettext:GetEntry("POPUP",self.locale) + + if IsNew and self.PlayerGuidance then + BRAText = string.format("%s %s.",BRAText,newgrp) + TextScreen = string.format("%s %s.",TextScreen,newgrp) + elseif IsPopup then + BRAText = string.format("%s %s %s.",BRAText,popup,grptxt) + TextScreen = string.format("%s %s %s.",TextScreen,popup,grptxt) + elseif IsBogeyDope and Tag and Tag ~= "" then + BRAText = string.format("%s %s %s.",BRAText,Tag,grptxt) + TextScreen = string.format("%s %s %s.",TextScreen,Tag,grptxt) + else + BRAText = string.format("%s %s.",BRAText,GRPtxt) + TextScreen = string.format("%s %s.",TextScreen,GRPtxt) + end + + if not IsBogeyDope then + if Tag and Tag ~= "" then + BRAText = BRAText .. " "..Tag.."." + TextScreen = TextScreen .. " "..Tag.."." + end + end + + if threatsize > 1 then + BRAText = BRAText .. " "..BRAfromBullsTTS.." "..threatsizetext.."." + TextScreen = TextScreen .. " "..BRAfromBulls.." "..threatsizetext.."." + else + BRAText = BRAText .. " "..BRAfromBullsTTS + TextScreen = TextScreen .. " "..BRAfromBulls + end + + if self.ModernEra then + local high = self.gettext:GetEntry("HIGH",self.locale) + local vfast = self.gettext:GetEntry("VERYFAST",self.locale) + local fast = self.gettext:GetEntry("FAST",self.locale) + -- Platform + if ReportingName and ReportingName ~= "Bogey" then + ReportingName = string.gsub(ReportingName,"_"," ") + BRAText = BRAText .. " "..ReportingName.."." + TextScreen = TextScreen .. " "..ReportingName.."." + end + -- High - > 40k feet + local height = Contact.Contact.group:GetHeight() + local height = UTILS.Round(UTILS.MetersToFeet(height)/1000,0) -- e.g, 25 + if height >= 40 then + BRAText = BRAText .. high + TextScreen = TextScreen .. high + end + -- Fast (>600kn) or Very fast (>900kn) + local speed = Contact.Contact.group:GetVelocityKNOTS() + if speed > 900 then + BRAText = BRAText .. vfast + TextScreen = TextScreen .. vfast + elseif speed >= 600 and speed <= 900 then + BRAText = BRAText .. fast + TextScreen = TextScreen .. fast + end + end + + BRAText = string.gsub(BRAText,"BRAA","brah") + BRAText = string.gsub(BRAText,"BRA","brah") + + local prio = IsNew or IsBogeyDope + self:_NewRadioEntry(BRAText,TextScreen,GID,isGroup,true,IsNew,false,prio,Tactical) + + return self +end + +--- [Internal] Check for alive OpsGroup from Mission OpsGroups table +-- @param #AWACS self +-- @param #table OpsGroups +-- @return Ops.OpsGroup#OPSGROUP or nil +function AWACS:_GetAliveOpsGroupFromTable(OpsGroups) + self:T(self.lid.."_GetAliveOpsGroupFromTable") + local handback = nil + for _,_OG in pairs(OpsGroups or {}) do + local OG = _OG -- Ops.OpsGroup#OPSGROUP + if OG and OG:IsAlive() then + handback = OG + break + end + end + return handback +end + +--- [Internal] Clean up mission stack +-- @param #AWACS self +-- @return #number CAPMissions +-- @return #number Alert5Missions +-- @return #number InterceptMissions +function AWACS:_CleanUpAIMissionStack() + self:T(self.lid.."_CleanUpAIMissionStack") + + local CAPMissions = 0 + local Alert5Missions = 0 + local InterceptMissions = 0 + + local MissionStack = FIFO:New() + + self:T("Checking MissionStack") + for _,_mission in pairs(self.CatchAllMissions) do + -- looking for missions of type CAP and ALERT5 + local mission = _mission -- Ops.Auftrag#AUFTRAG + local type = mission:GetType() + if type == AUFTRAG.Type.ALERT5 and mission:IsNotOver() then + MissionStack:Push(mission,mission.auftragsnummer) + Alert5Missions = Alert5Missions + 1 + elseif type == AUFTRAG.Type.CAP and mission:IsNotOver() then + MissionStack:Push(mission,mission.auftragsnummer) + CAPMissions = CAPMissions + 1 + elseif type == AUFTRAG.Type.INTERCEPT and mission:IsNotOver() then + MissionStack:Push(mission,mission.auftragsnummer) + InterceptMissions = InterceptMissions + 1 + end + end + + self.AICAPMissions = nil + self.AICAPMissions = MissionStack + + return CAPMissions, Alert5Missions, InterceptMissions + +end + +function AWACS:_ConsistencyCheck() + self:T(self.lid.."_ConsistencyCheck") + if self.debug then + self:T("CatchAllMissions") + local catchallm = {} + local report1 = REPORT:New("CatchAll") + report1:Add("====================") + report1:Add("CatchAllMissions") + report1:Add("====================") + for _,_mission in pairs(self.CatchAllMissions) do + local mission = _mission -- Ops.Auftrag#AUFTRAG + local nummer = mission.auftragsnummer or 0 + local type = mission:GetType() + local state = mission:GetState() + local FG = mission:GetOpsGroups() + local OG = self:_GetAliveOpsGroupFromTable(FG) + local OGName = "UnknownFromMission" + if OG then + OGName=OG:GetName() + end + report1:Add(string.format("Auftrag Nr %d Type %s State %s FlightGroup %s",nummer,type,state,OGName)) + if mission:IsNotOver() then + catchallm[#catchallm+1] = mission + end + end + + self.CatchAllMissions = nil + self.CatchAllMissions = catchallm + + local catchallfg = {} + + self:T("CatchAllFGs") + report1:Add("====================") + report1:Add("CatchAllFGs") + report1:Add("====================") + for _,_fg in pairs(self.CatchAllFGs) do + local FG = _fg -- Ops.FlightGroup#FLIGHTGROUP + local mission = FG:GetMissionCurrent() + local OGName = FG:GetName() or "UnknownFromFG" + local nummer = 0 + local type = "No Type" + local state = "None" + if mission then + type = mission:GetType() + nummer = mission.auftragsnummer or 0 + state = mission:GetState() + end + report1:Add(string.format("Auftrag Nr %d Type %s State %s FlightGroup %s",nummer,type,state,OGName)) + if FG:IsAlive() then + catchallfg[#catchallfg+1] = FG + end + end + report1:Add("====================") + self:T(report1:Text()) + + self.CatchAllFGs = nil + self.CatchAllFGs = catchallfg + + end + return self +end + +--- [Internal] Check Enough AI CAP on Station +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_CheckAICAPOnStation() + self:T(self.lid.."_CheckAICAPOnStation") + + self:_ConsistencyCheck() + + local capmissions, alert5missions, interceptmissions = self:_CleanUpAIMissionStack() + self:T("CAP="..capmissions.." ALERT5="..alert5missions.." Requested="..self.AIRequested) + + if self.MaxAIonCAP > 0 then + + local onstation = capmissions + alert5missions + + if capmissions > self.MaxAIonCAP then + -- too many, send one home + self:T(string.format("*** Onstation %d > MaxAIOnCAP %d",onstation,self.MaxAIonCAP)) + local mission = self.AICAPMissions:Pull() -- Ops.Auftrag#AUFTRAG + local Groups = mission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(Groups) + local GID,checkedin = self:_GetManagedGrpID(OpsGroup) + mission:__Cancel(30) + self.AIRequested = self.AIRequested - 1 + if checkedin then + self:_CheckOut(OpsGroup,GID) + end + end + + -- control number of AI CAP Flights + if capmissions < self.MaxAIonCAP and alert5missions < self.MaxAIonCAP+2 then + -- not enough + local AnchorStackNo,free = self:_GetFreeAnchorStack() + if free then + -- create Alert5 and assign to ONE of our AWs + -- TODO better selection due to resource shortage? + local mission = AUFTRAG:NewALERT5(AUFTRAG.Type.CAP) + self.CatchAllMissions[#self.CatchAllMissions+1] = mission + local availableAWS = self.CAPAirwings:Count() + local AWS = self.CAPAirwings:GetDataTable() + -- round robin + self.AIRequested = self.AIRequested + 1 + local selectedAW = AWS[(((self.AIRequested-1) % availableAWS)+1)] + selectedAW:AddMission(mission) + self:T("CAP="..capmissions.." ALERT5="..alert5missions.." Requested="..self.AIRequested) + end + end + + -- Check CAP Mission states + if onstation > 0 and capmissions < self.MaxAIonCAP then + local missions = self.AICAPMissions:GetDataTable() + -- get mission type and state + for _,_Mission in pairs(missions) do + + local mission = _Mission -- Ops.Auftrag#AUFTRAG + self:T("Looking at AuftragsNr " .. mission.auftragsnummer) + local type = mission:GetType() + local state = mission:GetState() + + if type == AUFTRAG.Type.ALERT5 then + -- parked up for CAP + local OpsGroups = mission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) + local FGstate = mission:GetGroupStatus(OpsGroup) + if OpsGroup then + FGstate = OpsGroup:GetState() + self:T("FG Object in state: " .. FGstate) + end + -- FG ready? + + if OpsGroup and (FGstate == "Parking" or FGstate == "Cruising") then + -- has this group checked in already? Avoid double tasking + local GID, CheckedInAlready = self:_GetManagedGrpID(OpsGroup:GetGroup()) + if not CheckedInAlready then + self:_SetAIROE(OpsGroup,OpsGroup:GetGroup()) + self:_CheckInAI(OpsGroup,OpsGroup:GetGroup(),mission.auftragsnummer) + end + end + end + end + end + + -- cycle mission status + if onstation > 0 then + local report = REPORT:New("CAP Mission Status") + report:Add("===============") + --local missionIDs = self.AICAPMissions:GetIDStackSorted() + local missions = self.AICAPMissions:GetDataTable() + local i = 1 + for _,_Mission in pairs(missions) do + local mission = _Mission -- Ops.Auftrag#AUFTRAG + if mission then + i = i + 1 + report:Add(string.format("Entry %d",i)) + report:Add(string.format("Mission No %d",mission.auftragsnummer)) + report:Add(string.format("Mission Type %s",mission:GetType())) + report:Add(string.format("Mission State %s",mission:GetState())) + local OpsGroups = mission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP + if OpsGroup then + local OpsName = OpsGroup:GetName() or "Unknown" + local found,GID,OpsCallSign = self:_GetGIDFromGroupOrName(OpsGroup) + report:Add(string.format("Mission FG %s",OpsName)) + report:Add(string.format("Callsign %s",OpsCallSign)) + report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) + else + report:Add("***** Cannot obtain (yet) this missions OpsGroup!") + end + report:Add(string.format("Target Type %s",mission:GetTargetType())) + end + report:Add("===============") + end + if self.debug then + self:I(report:Text()) + end + end + end + return self +end + +--- [Internal] Set ROE for AI CAP +-- @param #AWACS self +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup +-- @param Wrapper.Group#GROUP Group +-- @return #AWACS self +function AWACS:_SetAIROE(FlightGroup,Group) + self:T(self.lid.."_SetAIROE") + local ROE = self.AwacsROE or AWACS.ROE.POLICE + local ROT = self.AwacsROT or AWACS.ROT.PASSIVE + + -- TODO adjust to AWACS set ROE + -- for the time being set to be defensive + Group:OptionAlarmStateGreen() + Group:OptionECM_OnlyLockByRadar() + Group:OptionROEHoldFire() + Group:OptionROTEvadeFire() + Group:OptionRTBBingoFuel(true) + Group:OptionKeepWeaponsOnThreat() + local callname = self.AICAPCAllName or CALLSIGN.Aircraft.Colt + self.AICAPCAllNumber = self.AICAPCAllNumber + 1 + Group:CommandSetCallsign(callname,math.fmod(self.AICAPCAllNumber,9)) + -- FG level + FlightGroup:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN) + FlightGroup:SetDefaultCallsign(callname,math.fmod(self.AICAPCAllNumber,9)) + if ROE == AWACS.ROE.POLICE or ROE == AWACS.ROE.VID then + FlightGroup:SetDefaultROE(ENUMS.ROE.WeaponHold) + elseif ROE == AWACS.ROE.IFF then + FlightGroup:SetDefaultROE(ENUMS.ROE.ReturnFire) + elseif ROE == AWACS.ROE.BVR then + FlightGroup:SetDefaultROE(ENUMS.ROE.OpenFire) + end + if ROT == AWACS.ROT.BYPASSESCAPE or ROT == AWACS.ROT.PASSIVE then + FlightGroup:SetDefaultROT(ENUMS.ROT.PassiveDefense) + elseif ROT == AWACS.ROT.OPENFIRE or ROT == AWACS.ROT.RETURNFIRE then + FlightGroup:SetDefaultROT(ENUMS.ROT.BypassAndEscape) + elseif ROT == AWACS.ROT.EVADE then + FlightGroup:SetDefaultROT(ENUMS.ROT.EvadeFire) + end + FlightGroup:SetFuelLowRTB(true) + FlightGroup:SetFuelLowThreshold(0.2) + FlightGroup:SetEngageDetectedOff() + FlightGroup:SetOutOfAAMRTB(true) + return self +end + +--- [Internal] TAC Range Call to Pilot +-- @param #AWACS self +-- @param #number GID GID +-- @param #AWACS.ManagedContact Contact +-- @return #AWACS self +function AWACS:_TACRangeCall(GID,Contact) + self:T(self.lid.."_TACRangeCall") + -- AIC: “Enforcer 11, single group, 30 miles.” + if not Contact then return self end + local pilotcallsign = self:_GetCallSign(nil,GID) + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local contact = Contact.Contact -- Ops.Intel#INTEL.Contact + local contacttag = Contact.TargetGroupNaming + local name = managedgroup.GroupName + if contact then --and not Contact.TACCallDone then + local position = contact.position -- Core.Point#COORDINATE + if position then + local distance = position:Get2DDistance(managedgroup.Group:GetCoordinate()) + distance = UTILS.Round(UTILS.MetersToNM(distance)) -- 30nm - hopefully + local grptxt = self.gettext:GetEntry("GROUP",self.locale) + local miles = self.gettext:GetEntry("MILES",self.locale) + local text = string.format("%s. %s. %s %s, %d %s.",self.callsigntxt,pilotcallsign,contacttag,grptxt,distance,miles) + if not self.TacticalSubscribers[name] then + self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) + end + self:_UpdateContactEngagementTag(Contact.CID,Contact.EngagementTag,true,false,AWACS.TaskStatus.EXECUTING) + if GID and GID ~= 0 then + --local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then + if self.TacticalSubscribers[name] then + self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true,true) + end + end + end + end + end + return self +end + +--- [Internal] Meld Range Call to Pilot +-- @param #AWACS self +-- @param #number GID GID +-- @param #AWACS.ManagedContact Contact +-- @return #AWACS self +function AWACS:_MeldRangeCall(GID,Contact) + self:T(self.lid.."_MeldRangeCall") + if not Contact then return self end + -- AIC: “Heat 11, single group, BRAA 089/28, 32 thousand, hot, hostile, crow.” + local pilotcallsign = self:_GetCallSign(nil,GID) + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local flightpos = managedgroup.Group:GetCoordinate() + local contact = Contact.Contact -- Ops.Intel#INTEL.Contact + local contacttag = Contact.TargetGroupNaming or "Bogey" + local name = managedgroup.GroupName + if contact then --and not Contact.MeldCallDone then + local position = contact.position -- Core.Point#COORDINATE + if position then + local BRATExt = "" + if self.PathToGoogleKey then + BRATExt = position:ToStringBRAANATO(flightpos,false,false,true,false,true) + else + BRATExt = position:ToStringBRAANATO(flightpos,false,false) + end + local grptxt = self.gettext:GetEntry("GROUP",self.locale) + local text = string.format("%s. %s. %s %s, %s",self.callsigntxt,pilotcallsign,contacttag,grptxt,BRATExt) + if not self.TacticalSubscribers[name] then + self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) + end + self:_UpdateContactEngagementTag(Contact.CID,Contact.EngagementTag,true,true,AWACS.TaskStatus.EXECUTING) + if GID and GID ~= 0 then + --local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then + local name = managedgroup.GroupName + if self.TacticalSubscribers[name] then + self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true,true) + end + end + end + end + end + return self +end + +--- [Internal] Threat Range Call to Pilot +-- @param #AWACS self +-- @return #AWACS self +function AWACS:_ThreatRangeCall(GID,Contact) + self:T(self.lid.."_ThreatRangeCall") + if not Contact then return self end + -- AIC: “Enforcer 11 12, east group, THREAT, BRAA 260/15, 29 thousand, hot, hostile, robin.” + local pilotcallsign = self:_GetCallSign(nil,GID) + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local flightpos = managedgroup.Group:GetCoordinate() or managedgroup.LastKnownPosition + local contact = Contact.Contact -- Ops.Intel#INTEL.Contact + local contacttag = Contact.TargetGroupNaming or "Bogey" + local name = managedgroup.GroupName + local IsSub = self.TacticalSubscribers[name] and true or false + if contact then + local position = contact.position or contact.group:GetCoordinate() -- Core.Point#COORDINATE + if position then + local BRATExt = "" + if self.PathToGoogleKey then + BRATExt = position:ToStringBRAANATO(flightpos,false,false,true,false,true) + else + BRATExt = position:ToStringBRAANATO(flightpos,false,false) + end + local grptxt = self.gettext:GetEntry("GROUP",self.locale) + local thrt = self.gettext:GetEntry("THREAT",self.locale) + local text = string.format("%s. %s. %s %s, %s. %s",self.callsigntxt,pilotcallsign,contacttag,grptxt, thrt, BRATExt) + -- DONE MS TTS - fix spelling out B-R-A in this case + if string.find(text,"BRAA",1,true) then + text = string.gsub(text,"BRAA","brah") + elseif string.find(text,"BRA",1,true) then + text = string.gsub(text,"BRA","brah") + end + if IsSub == false then + self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) + end + if GID and GID ~= 0 then + --local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then + local name = managedgroup.GroupName + if self.TacticalSubscribers[name] then + self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true,true) + end + end + end + end + end + return self +end + +--- [Internal] Merged Call to Pilot +-- @param #AWACS self +-- @param #number GID +-- @return #AWACS self +function AWACS:_MergedCall(GID) + self:T(self.lid.."_MergedCall") + -- AIC: “Enforcer, mergedb” + local pilotcallsign = self:_GetCallSign(nil,GID) + local merge = self.gettext:GetEntry("MERGED",self.locale) + local text = string.format("%s. %s. %s.",self.callsigntxt,pilotcallsign,merge) + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + local name + if managedgroup then + name = managedgroup.GroupName or "none" + end + if not self.TacticalSubscribers[name] then + self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true) + end + if GID and GID ~= 0 then + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then + if self.TacticalSubscribers[name] then + self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true,true) + end + end + end + return self +end + +--- [Internal] Assign a Pilot to a target +-- @param #AWACS self +-- @param #table Pilots Table of #AWACS.ManagedGroup Pilot +-- @param Utilities.FiFo#FIFO Targets FiFo of #AWACS.ManagedContact Targets +-- @return #AWACS self +function AWACS:_AssignPilotToTarget(Pilots,Targets) + self:T(self.lid.."_AssignPilotToTarget") + + local inreach = false + local Pilot = nil -- #AWACS.ManagedGroup + + + local closest = UTILS.NMToMeters(self.maxassigndistance+1) + local targets = Targets:GetDataTable() + local Target = nil + + for _,_target in pairs(targets) do + -- Check Distance + local targetgroupcoord = _target.Contact.position + -- get closest pilot from target + for _,_Pilot in pairs(Pilots) do + local pilotcoord = _Pilot.Group:GetCoordinate() + local targetdist = targetgroupcoord:Get2DDistance(pilotcoord) + if UTILS.MetersToNM(targetdist) < self.maxassigndistance and targetdist < closest then + self:T(string.format("%sTarget distance %d! Assignment %s!",self.lid,UTILS.Round(UTILS.MetersToNM(targetdist),0),_Pilot.CallSign)) + inreach = true + closest = targetdist + Pilot = _Pilot + Target = _target + Targets:PullByID(_target.CID) + break + else + self:T(self.lid .. "Target distance > "..self.maxassigndistance.."NM! No Assignment!") + end + end + end + + -- DONE Check Human assignment working + if inreach and Pilot and Pilot.IsPlayer then + local callsign = Pilot.CallSign + -- update pilot TaskSheet + self.ManagedTasks:PullByID(Pilot.CurrentTask) + + Pilot.HasAssignedTask = true + local TargetPosition = Target.Target:GetCoordinate() + local PlayerPositon = Pilot.LastKnownPosition + local TargetAlt = Target.Contact.altitude or Target.Cluster.altitude or Target.Contact.group:GetAltitude() + local TargetDirections, TargetDirectionsTTS = self:_ToStringBRA(PlayerPositon,TargetPosition,TargetAlt) + local ScreenText = "" + local TaskType = AWACS.TaskDescription.INTERCEPT + if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then + local interc = self.gettext:GetEntry("SCREENVID",self.locale) + ScreenText = string.format(interc,Target.TargetGroupNaming) + TaskType = AWACS.TaskDescription.VID + else + local interc = self.gettext:GetEntry("SCREENINTER",self.locale) + ScreenText = string.format(interc,Target.TargetGroupNaming) + end + Pilot.CurrentTask = self:_CreateTaskForGroup(Pilot.GID,TaskType,ScreenText,Target.Target,AWACS.TaskStatus.REQUESTED,nil,Target.Cluster,Target.Contact) + + Pilot.ContactCID = Target.CID + + -- update managed group + self.ManagedGrps[Pilot.GID] = Pilot + + -- Update Contact Status + Target.LinkedTask = Pilot.CurrentTask + Target.LinkedGroup = Pilot.GID + Target.Status = AWACS.TaskStatus.REQUESTED + local targeted = self.gettext:GetEntry("ENGAGETAG",self.locale) + Target.EngagementTag = string.format(targeted,Pilot.CallSign) + + self.Contacts:PullByID(Target.CID) + self.Contacts:Push(Target,Target.CID) + + local reqcomm = self.gettext:GetEntry("REQCOMMIT",self.locale) + local text = string.format(reqcomm, self.callsigntxt,Target.TargetGroupNaming,TargetDirectionsTTS,Pilot.CallSign) + local textScreen = string.format(reqcomm, self.callsigntxt,Target.TargetGroupNaming,TargetDirections,Pilot.CallSign) + + self:_NewRadioEntry(text,textScreen,Pilot.GID,true,self.debug,true,false,true) + + elseif inreach and Pilot and Pilot.IsAI then + -- Target information + local callsign = Pilot.CallSign + local FGStatus = Pilot.FlightGroup:GetState() + self:T("Pilot AI Callsign: " .. callsign) + self:T("Pilot FG State: " .. FGStatus) + local targetstatus = Target.Target:GetState() + self:T("Target State: " .. targetstatus) + + -- + local currmission = Pilot.FlightGroup:GetMissionCurrent() + if currmission then + self:T("Current Mission: " .. currmission:GetType()) + end + -- create one intercept Auftrag and one to return to CAP post this one + local ZoneSet = self.ZoneSet + local RejectZoneSet = self.RejectZoneSet + + local intercept = AUFTRAG:NewINTERCEPT(Target.Target) + intercept:SetWeaponExpend(AI.Task.WeaponExpend.ALL) + intercept:SetWeaponType(ENUMS.WeaponFlag.Auto) + intercept:SetMissionRange(self.MaxMissionRange) + -- TODO + -- now this is going to be interesting... + -- Check if the target left the "hot" area or is dead already + intercept:AddConditionSuccess( + function(target,zoneset,rzoneset) + -- BASE:I("AUFTRAG Condition Succes Eval Running") + local success = true + local target = target -- Ops.Target#TARGET + if target:IsDestroyed() or target:IsDead() or target:CountTargets() == 0 then return true end + local tgtcoord = target:GetCoordinate() + local tgtvec2 = nil + if tgtcoord then + tgtvec2 = tgtcoord:GetVec2() + end + local zones = zoneset -- Core.Set#SET_ZONE + local rzones = rzoneset -- Core.Set#SET_ZONE + if tgtvec2 then + zones:ForEachZone( + function(zone) + if zone:IsVec2InZone(tgtvec2) then + success = false + end + end + ) + rzones:ForEachZone( + function(zone) + if zone:IsVec2InZone(tgtvec2) then + success = true + end + end + ) + end + return success + end, + Target.Target, + ZoneSet, + RejectZoneSet + ) + + Pilot.FlightGroup:AddMission(intercept) + + local Angels = Pilot.AnchorStackAngels or 25 + Angels = Angels * 1000 + local AnchorSpeed = self.CapSpeedBase or 270 + AnchorSpeed = UTILS.KnotsToAltKIAS(AnchorSpeed,Angels) + local Anchor = self.AnchorStacks:ReadByPointer(Pilot.AnchorStackNo) -- #AWACS.AnchorData + local capauftrag = AUFTRAG:NewCAP(Anchor.StationZone,Angels,AnchorSpeed,Anchor.StationZoneCoordinate,0,15,{}) + capauftrag:SetMissionRange(self.MaxMissionRange) + capauftrag:SetTime(nil,((self.CAPTimeOnStation*3600)+(15*60))) + Pilot.FlightGroup:AddMission(capauftrag) + + -- cancel current mission + if currmission then + currmission:__Cancel(3) + end + + -- update known mission list + self.CatchAllMissions[#self.CatchAllMissions+1] = intercept + self.CatchAllMissions[#self.CatchAllMissions+1] = capauftrag + + -- update pilot TaskSheet + self.ManagedTasks:PullByID(Pilot.CurrentTask) + + Pilot.HasAssignedTask = true + Pilot.CurrentTask = self:_CreateTaskForGroup(Pilot.GID,AWACS.TaskDescription.INTERCEPT,"Intercept Task",Target.Target,AWACS.TaskStatus.ASSIGNED,intercept,Target.Cluster,Target.Contact) + Pilot.CurrentAuftrag = intercept.auftragsnummer + Pilot.ContactCID = Target.CID + + -- update managed group + self.ManagedGrps[Pilot.GID] = Pilot + + -- Update Contact Status + Target.LinkedTask = Pilot.CurrentTask + Target.LinkedGroup = Pilot.GID + Target.Status = AWACS.TaskStatus.ASSIGNED + local targeted = self.gettext:GetEntry("ENGAGETAG",self.locale) + Target.EngagementTag = string.format(targeted,Pilot.CallSign) + + self.Contacts:PullByID(Target.CID) + self.Contacts:Push(Target,Target.CID) + + local altitude = Target.Contact.altitude or Target.Contact.group:GetAltitude() + local position = Target.Cluster.coordinate or Target.Contact.position + if not position then + self.intel:GetClusterCoordinate(Target.Cluster,true) + end + local bratext, bratexttts = self:_ToStringBRA(Pilot.Group:GetCoordinate(),position,altitude or 8000) + + local aicomm = self.gettext:GetEntry("AICOMMIT",self.locale) + local text = string.format(aicomm, self.callsigntxt,Target.TargetGroupNaming,bratexttts,Pilot.CallSign) + local textScreen = string.format(aicomm, self.callsigntxt,Target.TargetGroupNaming,bratext,Pilot.CallSign) + + self:_NewRadioEntry(text,textScreen,Pilot.GID,true,self.debug,true,false,true) + + local comm = self.gettext:GetEntry("COMMIT",self.locale) + local text = string.format("%s. %s.",Pilot.CallSign,comm) + + self:_NewRadioEntry(text,text,Pilot.GID,true,self.debug,true,true,true) + + self:__Intercept(2) + + end + + return self +end + +-- TODO FSMs +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- [Internal] onbeforeStart +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onbeforeStart(From,Event,To) + self:T({From, Event, To}) + if self.IncludeHelicopters then + self.clientset:FilterCategories("helicopter") + end + return self +end + +--- [Internal] onafterStart +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterStart(From, Event, To) + self:T({From, Event, To}) + + -- Set up control zone + local controlzonename = "FEZ-"..self.AOName + self.ControlZone = ZONE_RADIUS:New(controlzonename,self.OpsZone:GetVec2(),UTILS.NMToMeters(self.ControlZoneRadius)) + if self.debug then + self.ControlZone:DrawZone(self.coalition,{0,1,0},1,{1,0,0},0.05,3,true) + self.OpsZone:DrawZone(self.coalition,{1,0,0},1,{1,0,0},0.2,5,true) + local AOCoordString = self.AOCoordinate:ToStringLLDDM() + local Rocktag = string.format("FEZ: %s\nBulls Coordinate: %s",self.AOName,AOCoordString) + if self.AllowMarkers then + MARKER:New(self.AOCoordinate,Rocktag):ToCoalition(self.coalition) + end + self.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true) + local stationtag = string.format("Station: %s\nCoordinate: %s",self.StationZoneName,self.StationZone:GetCoordinate():ToStringLLDDM()) + if not self.GCI then + if self.AllowMarkers then + MARKER:New(self.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) + end + self.OrbitZone:DrawZone(self.coalition,{0,1,0},1,{0,1,0},0.2,5,true) + if self.AllowMarkers then + MARKER:New(self.OrbitZone:GetCoordinate(),"AIC Orbit Zone"):ToCoalition(self.coalition) + end + end + else + local AOCoordString = self.AOCoordinate:ToStringLLDDM() + local Rocktag = string.format("FEZ: %s\nBulls Coordinate: %s",self.AOName,AOCoordString) + if self.AllowMarkers then + MARKER:New(self.AOCoordinate,Rocktag):ToCoalition(self.coalition) + end + if not self.GCI then + if self.AllowMarkers then + MARKER:New(self.OrbitZone:GetCoordinate(),"AIC Orbit Zone"):ToCoalition(self.coalition) + end + end + local stationtag = string.format("Station: %s\nCoordinate: %s",self.StationZoneName,self.StationZone:GetCoordinate():ToStringLLDDM()) + if self.AllowMarkers then + MARKER:New(self.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition) + end + end + + if not self.GCI then + -- set up the AWACS and let it orbit + local AwacsAW = self.AirWing -- Ops.Airwing#AIRWING + local mission = AUFTRAG:NewORBIT_RACETRACK(self.OrbitZone:GetCoordinate(),self.AwacsAngels*1000,self.Speed,self.Heading,self.Leg) + mission:SetMissionRange(self.MaxMissionRange) + mission:SetRequiredAttribute({ GROUP.Attribute.AIR_AWACS }) -- prefered plane type, thanks to Heart8reaker + local timeonstation = (self.AwacsTimeOnStation + self.ShiftChangeTime) * 3600 + mission:SetTime(nil,timeonstation) + self.CatchAllMissions[#self.CatchAllMissions+1] = mission + + AwacsAW:AddMission(mission) + + self.AwacsMission = mission + self.AwacsInZone = false -- not yet arrived or gone again + self.AwacsReady = false + else + self.AwacsInZone = true -- for GCI - arrived + self.AwacsReady = true + self:_StartIntel(self.GCIGroup) + + if self.GCIGroup:IsGround() then + self.AwacsFG = ARMYGROUP:New(self.GCIGroup) + self.AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation) + self.AwacsFG:SwitchRadio(self.Frequency,self.Modulation) + elseif self.GCIGroup:IsShip() then + self.AwacsFG = NAVYGROUP:New(self.GCIGroup) + self.AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation) + self.AwacsFG:SwitchRadio(self.Frequency,self.Modulation) + else + self:E(self.lid.."**** Group unsuitable for GCI ops! Needs to be a GROUND or SHIP type group!") + self:Stop() + return self + end + + self.callsigntxt = string.format("%s",self.CallSignClear[self.CallSign]) + self:__CheckRadioQueue(-10) + + local sunrise = self.gettext:GetEntry("SUNRISE",self.locale) + local text = string.format(sunrise,self.callsigntxt,self.callsigntxt) + self:_NewRadioEntry(text,text,0,false,false,false,false,true) + self:T(self.lid..text) + self.sunrisedone = true + end + + local ZoneSet = SET_ZONE:New() + ZoneSet:AddZone(self.ControlZone) + + if not self.GCI then + ZoneSet:AddZone(self.OrbitZone) + end + + if self.BorderZone then + ZoneSet:AddZone(self.BorderZone) + end + + local RejectZoneSet = SET_ZONE:New() + if self.RejectZone then + RejectZoneSet:AddZone(self.RejectZone) + end + + self.ZoneSet = ZoneSet + self.RejectZoneSet = RejectZoneSet + + if self.AllowMarkers then + -- Add MarkerOps + + local MarkerOps = MARKEROPS_BASE:New("AWACS",{"Station","Delete","Move"}) + + local function Handler(Keywords,Coord,Text) + self:T(Text) + for _,_word in pairs (Keywords) do + if string.lower(_word) == "station" then + -- get the station name from the text field + local Name = string.match(Text," ([%a]+)$") + self:_CreateAnchorStackFromMarker(Name,Coord) + break + elseif string.lower(_word) == "delete" then + -- get the station name from the text field + local Name = string.match(Text," ([%a]+)$") + self:_DeleteAnchorStackFromMarker(Name,Coord) + break + elseif string.lower(_word) == "move" then + -- get the station name from the text field + local Name = string.match(Text," ([%a]+)$") + self:_MoveAnchorStackFromMarker(Name,Coord) + break + end + end + end + + -- Event functions + function MarkerOps:OnAfterMarkAdded(From,Event,To,Text,Keywords,Coord) + Handler(Keywords,Coord,Text) + end + + function MarkerOps:OnAfterMarkChanged(From,Event,To,Text,Keywords,Coord) + Handler(Keywords,Coord,Text) + end + + function MarkerOps:OnAfterMarkDeleted(From,Event,To) + end + + self.MarkerOps = MarkerOps + + end + + if self.GCI then + -- set FSM to started + self:__Started(-5) + end + + if self.TacticalMenu then + self:__CheckTacticalQueue(55) + end + + self:__Status(-30) + return self +end + +function AWACS:_CheckAwacsStatus() + self:T(self.lid.."_CheckAwacsStatus") + + local awacs = nil -- Wrapper.Group#GROUP + if self.AwacsFG then + awacs = self.AwacsFG:GetGroup() -- Wrapper.Group#GROUP + local unit = awacs:GetUnit(1) + if unit then + self.STN = tostring(unit:GetSTN()) + end + end + + local monitoringdata = self.MonitoringData -- #AWACS.MonitoringData + + if not self.GCI then + if awacs and awacs:IsAlive() and not self.AwacsInZone then + -- check if we arrived + local orbitzone = self.OrbitZone -- Core.Zone#ZONE + if awacs:IsInZone(orbitzone) then + -- arrived + self.AwacsInZone = true + self:T(self.lid.."Arrived in Orbit Zone: " .. orbitzone:GetName()) + local onstationtxt = self.gettext:GetEntry("AWONSTATION",self.locale) + local text = string.format(onstationtxt,self.callsigntxt,self.AOName or "Rock") + local textScreen = text + self:_NewRadioEntry(text,textScreen,0,false,true,true,false,true) + end + end + end + + -------------------------------- + -- AWACS + -------------------------------- + + if (awacs and awacs:IsAlive()) then + + if not self.intelstarted then + local alt = UTILS.Round(UTILS.MetersToFeet(awacs:GetAltitude())/1000,0) + if alt >= 10 then + self:_StartIntel(awacs) + end + end + + if self.intelstarted and not self.sunrisedone then + -- TODO Sunrise call on after airborne at ca 10k feet + local alt = UTILS.Round(UTILS.MetersToFeet(awacs:GetAltitude())/1000,0) + if alt >= 10 then + local sunrise = self.gettext:GetEntry("SUNRISE",self.locale) + local text = string.format(sunrise,self.callsigntxt,self.callsigntxt) + self:_NewRadioEntry(text,text,0,false,false,false,false,true) + self:T(self.lid..text) + self.sunrisedone = true + end + end + + -- Check on Awacs Mission Status + local AWmission = self.AwacsMission -- Ops.Auftrag#AUFTRAG + local awstatus = AWmission:GetState() + local AWmissiontime = (timer.getTime() - self.AwacsTimeStamp) + + local AWTOSLeft = UTILS.Round((((self.AwacsTimeOnStation+self.ShiftChangeTime)*3600) - AWmissiontime),0) -- seconds + + AWTOSLeft = UTILS.Round(AWTOSLeft/60,0) -- minutes + + local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0) + + local Changedue = "No" + + if not self.ShiftChangeAwacsFlag and (AWTOSLeft <= ChangeTime or AWmission:IsOver()) then + Changedue = "Yes" + self.ShiftChangeAwacsFlag = true + self:__AwacsShiftChange(2) + end + + local report = REPORT:New("AWACS:") + report:Add("====================") + report:Add("AWACS:") + report:Add(string.format("Auftrag Status: %s",awstatus)) + report:Add(string.format("TOS Left: %d min",AWTOSLeft)) + report:Add(string.format("Needs ShiftChange: %s",Changedue)) + + local OpsGroups = AWmission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP + if OpsGroup then + local OpsName = OpsGroup:GetName() or "Unknown" + local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown" + report:Add(string.format("Mission FG %s",OpsName)) + report:Add(string.format("Callsign %s",OpsCallSign)) + report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) + else + report:Add("***** Cannot obtain (yet) this missions OpsGroup!") + end + + -- Check for replacement mission - if any + if self.ShiftChangeAwacsFlag and self.ShiftChangeAwacsRequested then -- Ops.Auftrag#AUFTRAG + AWmission = self.AwacsMissionReplacement + local esstatus = AWmission:GetState() + local ESmissiontime = (timer.getTime() - self.AwacsTimeStamp) + local ESTOSLeft = UTILS.Round((((self.AwacsTimeOnStation+self.ShiftChangeTime)*3600) - ESmissiontime),0) -- seconds + ESTOSLeft = UTILS.Round(ESTOSLeft/60,0) -- minutes + local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0) + + report:Add("AWACS REPLACEMENT:") + report:Add(string.format("Auftrag Status: %s",esstatus)) + report:Add(string.format("TOS Left: %d min",ESTOSLeft)) + + local OpsGroups = AWmission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP + if OpsGroup then + local OpsName = OpsGroup:GetName() or "Unknown" + local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown" + report:Add(string.format("Mission FG %s",OpsName)) + report:Add(string.format("Callsign %s",OpsCallSign)) + report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) + else + report:Add("***** Cannot obtain (yet) this missions OpsGroup!") + end + + if AWmission:IsExecuting() then + -- make the actual change in the queue + self.ShiftChangeAwacsFlag = false + self.ShiftChangeAwacsRequested = false + self.sunrisedone = false + -- cancel old mission + if self.AwacsMission and self.AwacsMission:IsNotOver() then + self.AwacsMission:Cancel() + end + self.AwacsMission = self.AwacsMissionReplacement + self.AwacsMissionReplacement = nil + self.AwacsTimeStamp = timer.getTime() + report:Add("*** Replacement DONE ***") + end + report:Add("====================") + end + + -------------------------------- + -- ESCORTS + -------------------------------- + + if self.HasEscorts then + for i=1, self.EscortNumber do + local ESmission = self.EscortMission[i] -- Ops.Auftrag#AUFTRAG + if not ESmission then break end + local esstatus = ESmission:GetState() + local ESmissiontime = (timer.getTime() - self.EscortsTimeStamp) + local ESTOSLeft = UTILS.Round((((self.EscortsTimeOnStation+self.ShiftChangeTime)*3600) - ESmissiontime),0) -- seconds + ESTOSLeft = UTILS.Round(ESTOSLeft/60,0) -- minutes + local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0) + local Changedue = "No" + + if (ESTOSLeft <= ChangeTime and not self.ShiftChangeEscortsFlag) or (ESmission:IsOver() and not self.ShiftChangeEscortsFlag) then + Changedue = "Yes" + self.ShiftChangeEscortsFlag = true -- set this back when new Escorts arrived + self:__EscortShiftChange(2) + end + + report:Add("====================") + report:Add("ESCORTS:") + report:Add(string.format("Auftrag Status: %s",esstatus)) + report:Add(string.format("TOS Left: %d min",ESTOSLeft)) + report:Add(string.format("Needs ShiftChange: %s",Changedue)) + + local OpsGroups = ESmission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP + if OpsGroup then + local OpsName = OpsGroup:GetName() or "Unknown" + local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown" + report:Add(string.format("Mission FG %s",OpsName)) + report:Add(string.format("Callsign %s",OpsCallSign)) + report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) + monitoringdata.EscortsStateMission[i] = esstatus + monitoringdata.EscortsStateFG[i] = OpsGroup:GetState() + else + report:Add("***** Cannot obtain (yet) this missions OpsGroup!") + end + + report:Add("====================") + + local RESMission + -- Check for replacement mission - if any + if self.ShiftChangeEscortsFlag and self.ShiftChangeEscortsRequested then -- Ops.Auftrag#AUFTRAG + RESMission = self.EscortMissionReplacement[i] + local esstatus = RESMission:GetState() + local RESMissiontime = (timer.getTime() - self.EscortsTimeStamp) + local ESTOSLeft = UTILS.Round((((self.EscortsTimeOnStation+self.ShiftChangeTime)*3600) - RESMissiontime),0) -- seconds + ESTOSLeft = UTILS.Round(ESTOSLeft/60,0) -- minutes + local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0) + + report:Add("ESCORTS REPLACEMENT:") + report:Add(string.format("Auftrag Status: %s",esstatus)) + report:Add(string.format("TOS Left: %d min",ESTOSLeft)) + + local OpsGroups = RESMission:GetOpsGroups() + local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP + if OpsGroup then + local OpsName = OpsGroup:GetName() or "Unknown" + local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown" + report:Add(string.format("Mission FG %s",OpsName)) + report:Add(string.format("Callsign %s",OpsCallSign)) + report:Add(string.format("Mission FG State %s",OpsGroup:GetState())) + else + report:Add("***** Cannot obtain (yet) this missions OpsGroup!") + end + + if RESMission and RESMission:IsExecuting() then + -- make the actual change in the queue + self.ShiftChangeEscortsFlag = false + self.ShiftChangeEscortsRequested = false + -- cancel old mission + if ESmission and ESmission:IsNotOver() then + ESmission:__Cancel(1) + end + self.EscortMission[i] = self.EscortMissionReplacement[i] + self.EscortMissionReplacement[i] = nil + self.EscortsTimeStamp = timer.getTime() + report:Add("*** Replacement DONE ***") + end + report:Add("====================") + end + end + end + + if self.debug then + self:T(report:Text()) + end + + else + -- Check on Awacs Mission Status + local AWmission = self.AwacsMission -- Ops.Auftrag#AUFTRAG + local awstatus = AWmission:GetState() + if AWmission:IsOver() then + -- yup we're dead + self:I(self.lid.."*****AWACS is dead!*****") + self.ShiftChangeAwacsFlag = true + self:__AwacsShiftChange(2) + end + end + + return monitoringdata +end + +--- [Internal] onafterStatus +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterStatus(From, Event, To) + self:T({From, Event, To}) + + self:_SetClientMenus() + + local monitoringdata = self.MonitoringData -- #AWACS.MonitoringData + + if not self.GCI then + monitoringdata = self:_CheckAwacsStatus() + end + + local awacsalive = false + if self.AwacsFG then + local awacs = self.AwacsFG:GetGroup() -- Wrapper.Group#GROUP + if awacs and awacs:IsAlive() then + awacsalive= true + end + end + + -- Check on AUFTRAG status for CAP AI + if self:Is("Running") and (awacsalive or self.AwacsInZone) then + + + -- update coord for SRS + + if self.AwacsSRS then + self.AwacsSRS:SetCoordinate(self.AwacsFG:GetCoordinate()) + if self.TacticalSRS then + self.TacticalSRS:SetCoordinate(self.AwacsFG:GetCoordinate()) + end + end + + self:_CheckAICAPOnStation() + + self:_CleanUpContacts() + + self:_CheckMerges() + + self:_CheckSubscribers() + + local outcome, targets = self:_TargetSelectionProcess(true) + + self:_CheckTaskQueue() + + local AI, Humans = self:_GetIdlePilots() + -- assign Pilot if there are targets and available Pilots, prefer Humans to AI + -- DONE - Implemented AI First, Humans laters - need to work out how to loop the targets to assign a pilot + if outcome and #Humans > 0 and self.PlayerCapAssignment then + -- add a task for AI + self:_AssignPilotToTarget(Humans,targets) + end + if outcome and #AI > 0 then + -- add a task for AI + self:_AssignPilotToTarget(AI,targets) + end + end + + if not self.GCI then + monitoringdata.AwacsShiftChange = self.ShiftChangeAwacsFlag + + if self.AwacsFG then + monitoringdata.AwacsStateFG = self.AwacsFG:GetState() + end + + monitoringdata.AwacsStateMission = self.AwacsMission:GetState() + monitoringdata.EscortsShiftChange = self.ShiftChangeEscortsFlag + end + + monitoringdata.AICAPCurrent = self.AICAPMissions:Count() + monitoringdata.AICAPMax = self.MaxAIonCAP + monitoringdata.Airwings = self.CAPAirwings:Count() + + self.MonitoringData = monitoringdata + + if self.debug then + self:_LogStatistics() + end + + local picturetime = timer.getTime() - self.PictureTimeStamp + + if self.AwacsInZone and picturetime > self.PictureInterval then + -- reset timer + self.PictureTimeStamp = timer.getTime() + self:_Picture(nil,true) + end + + self:__Status(30) + + return self +end + +--- [Internal] onafterStop +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterStop(From, Event, To) + self:T({From, Event, To}) + -- unhandle stuff, exit intel + + self.intel:Stop() + + local AWFiFo = self.CAPAirwings -- Utilities.FiFo#FIFO + local AWStack = AWFiFo:GetPointerStack() + for _ID,_AWID in pairs(AWStack) do + local SubAW = self.CAPAirwings:ReadByPointer(_ID) + if SubAW then + SubAW:RemoveUsingOpsAwacs() + end + end + -- Events + -- Player joins + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + self:UnHandleEvent(EVENTS.PlayerEnterUnit) + -- Player leaves + self:UnHandleEvent(EVENTS.PlayerLeaveUnit) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.UnitLost) + self:UnHandleEvent(EVENTS.BDA) + self:UnHandleEvent(EVENTS.PilotDead) + -- Missile warning + self:UnHandleEvent(EVENTS.Shot) + + return self +end + +--- [Internal] onafterAssignAnchor +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number GID Group ID +-- @param #boolean HasOwnStation +-- @param #string HasOwnStation +-- @return #AWACS self +function AWACS:onafterAssignAnchor(From, Event, To, GID, HasOwnStation, StationName) + self:T({From, Event, To, "GID = " .. GID}) + self:_AssignAnchorToID(GID, HasOwnStation, StationName) + return self +end + +--- [Internal] onafterCheckedOut +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #AWACS.ManagedGroup.GID Group ID +-- @param #number AnchorStackNo +-- @param #number Angels +-- @return #AWACS self +function AWACS:onafterCheckedOut(From, Event, To, GID, AnchorStackNo, Angels) + self:T({From, Event, To, "GID = " .. GID}) + self:_RemoveIDFromAnchor(GID,AnchorStackNo,Angels) + return self +end + +--- [Internal] onafterAssignedAnchor +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number GID Managed Group ID +-- @param #AWACS.AnchorData Anchor +-- @param #number AnchorStackNo +-- @return #AWACS self +function AWACS:onafterAssignedAnchor(From, Event, To, GID, Anchor, AnchorStackNo, AnchorAngels) + self:T({From, Event, To, "GID=" .. GID, "Stack=" .. AnchorStackNo}) + -- TODO + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + if not managedgroup then + self:E(self.lid .. "**** GID "..GID.." Not Registered!") + return self + end + managedgroup.AnchorStackNo = AnchorStackNo + managedgroup.AnchorStackAngels = AnchorAngels + managedgroup.Blocked = false + local isPlayer = managedgroup.IsPlayer + local isAI = managedgroup.IsAI + local Group = managedgroup.Group + local CallSign = managedgroup.CallSign or "Ghost 1" + local AnchorName = Anchor.StationName or "unknown" + local AnchorCoordTxt = Anchor.StationZoneCoordinateText or "unknown" + local Angels = AnchorAngels or 25 + local AnchorSpeed = self.CapSpeedBase or 270 + local AuftragsNr = managedgroup.CurrentAuftrag + + local textTTS = "" + if self.PikesSpecialSwitch then + local stationtxt = self.gettext:GetEntry("STATIONAT",self.locale) + textTTS = string.format(stationtxt,CallSign,self.callsigntxt,AnchorName,Angels) + else + local stationtxt = self.gettext:GetEntry("STATIONATLONG",self.locale) + textTTS = string.format(stationtxt,CallSign,self.callsigntxt,AnchorName,Angels,AnchorSpeed) + end + local ROEROT = self.AwacsROE..", "..self.AwacsROT + local stationtxtsc = self.gettext:GetEntry("STATIONSCREEN",self.locale) + local stationtxtta = self.gettext:GetEntry("STATIONTASK",self.locale) + local textScreen = string.format(stationtxtsc,CallSign,self.callsigntxt,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) + local TextTasking = string.format(stationtxtta,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) + + self:_NewRadioEntry(textTTS,textScreen,GID,isPlayer,isPlayer,true,false) + + managedgroup.CurrentTask = self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,TextTasking,Anchor.StationZone) + + -- if it's a Alert5, we want to push CAP instead + if isAI then + local auftrag = managedgroup.FlightGroup:GetMissionCurrent() -- Ops.Auftrag#AUFTRAG + if auftrag then + local auftragtype = auftrag:GetType() + if auftragtype == AUFTRAG.Type.ALERT5 then + -- all correct + local capauftrag = AUFTRAG:NewCAP(Anchor.StationZone,Angels*1000,AnchorSpeed,Anchor.StationZone:GetCoordinate(),0,15,{}) + capauftrag:SetMissionRange(self.MaxMissionRange) + capauftrag:SetTime(nil,((self.CAPTimeOnStation*3600)+(15*60))) + capauftrag:AddAsset(managedgroup.FlightGroup) + self.CatchAllMissions[#self.CatchAllMissions+1] = capauftrag + managedgroup.FlightGroup:AddMission(capauftrag) + auftrag:Cancel() + else + self:E("**** AssignedAnchor but Auftrag NOT ALERT5!") + end + else + self:E("**** AssignedAnchor but NO Auftrag!") + end + end + + self.ManagedGrps[GID] = managedgroup + + return self +end + +--- [Internal] onafterNewCluster +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.Intel#INTEL.Cluster Cluster +-- @return #AWACS self +function AWACS:onafterNewCluster(From,Event,To,Cluster) + self:T({From, Event, To, Cluster.index}) + + self.CID = self.CID + 1 + self.Countactcounter = self.Countactcounter + 1 + + local ContactTable = Cluster.Contacts or {} + + local function GetFirstAliveContact(table) + for _,_contact in pairs (table) do + local contact = _contact -- Ops.Intel#INTEL.Contact + if contact and contact.group and contact.group:IsAlive() then + return contact, contact.group + end + end + return nil + end + + local Contact, Group = GetFirstAliveContact(ContactTable) -- Ops.Intel#INTEL.Contact + + if not Contact then return self end + + if Group and not Group:IsAirborne() then + return self + end + + local targetset = SET_GROUP:New() + -- SET for TARGET + for _,_grp in pairs(ContactTable) do + local grp = _grp -- Ops.Intel#INTEL.Contact + targetset:AddGroup(grp.group, true) + end + local managedcontact = {} -- #AWACS.ManagedContact + managedcontact.CID = self.CID + managedcontact.Contact = Contact + managedcontact.Cluster = Cluster + -- TODO set as per tech / engagement / alarm level age... + managedcontact.IFF = AWACS.IFF.BOGEY -- no IFF yet + managedcontact.Target = TARGET:New(targetset) + managedcontact.LinkedGroup = 0 + managedcontact.LinkedTask = 0 + managedcontact.Status = AWACS.TaskStatus.IDLE + local phoneid = math.fmod(self.Countactcounter,27) + if phoneid == 0 then phoneid = 1 end + managedcontact.TargetGroupNaming = AWACS.Phonetic[phoneid] + managedcontact.ReportingName = Contact.group:GetNatoReportingName() -- e.g. Foxbat. Bogey if unknown + managedcontact.TACCallDone = false + managedcontact.MeldCallDone = false + managedcontact.EngagementTag = "" + + local IsPopup = false + -- is this a pop-up group? i.e. appeared inside AO + if self.OpsZone:IsVec2InZone(Contact.position:GetVec2()) then + IsPopup = true + end + + -- let's see if we can inject some info into Contact + Contact.CID = managedcontact.CID + Contact.TargetGroupNaming = managedcontact.TargetGroupNaming + Cluster.CID = managedcontact.CID + Cluster.TargetGroupNaming = managedcontact.TargetGroupNaming + + self.Contacts:Push(managedcontact,self.CID) + + -- only announce if in right distance to HVT/AIC or in ControlZone or in BorderZone + local ContactCoordinate = Contact.position:GetVec2() + local incontrolzone = self.ControlZone:IsVec2InZone(ContactCoordinate) + + -- distance check to HVT + local distance = 1000000 + if not self.GCI then + distance = Contact.position:Get2DDistance(self.OrbitZone:GetCoordinate()) + end + + local inborderzone = false + if self.BorderZone then + inborderzone = self.BorderZone:IsVec2InZone(ContactCoordinate) + end + + if incontrolzone or inborderzone or (distance <= UTILS.NMToMeters(55)) or IsPopup then + self:_AnnounceContact(managedcontact,true,nil,false,managedcontact.TargetGroupNaming,IsPopup,managedcontact.ReportingName) + end + + return self +end + +--- [Internal] onafterNewContact +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.Intel#INTEL.Contact Contact +-- @return #AWACS self +function AWACS:onafterNewContact(From,Event,To,Contact) + self:T({From, Event, To, Contact}) + local tdist = self.ThreatDistance -- NM + -- is any plane near-by? + for _gid,_mgroup in pairs(self.ManagedGrps) do + local managedgroup = _mgroup -- #AWACS.ManagedGroup + local group = managedgroup.Group + if group and group:IsAlive() and group:IsAirborne() then + -- contact distance + local cpos = Contact.position or Contact.group:GetCoordinate() -- Core.Point#COORDINATE + local mpos = group:GetCoordinate() + local dist = cpos:Get2DDistance(mpos) -- meter + dist = UTILS.Round(UTILS.MetersToNM(dist),0) + if dist <= tdist then + -- threat call + self:_ThreatRangeCall(_gid,Contact) + end + end + end + return self +end + +--- [Internal] onafterLostContact +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.Intel#INTEL.Contact Contact +-- @return #AWACS self +function AWACS:onafterLostContact(From,Event,To,Contact) + self:T({From, Event, To, Contact}) + return self +end + +--- [Internal] onafterLostCluster +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.Intel#INTEL.Cluster Cluster +-- @param Ops.Auftrag#AUFTRAG Mission +-- @return #AWACS self +function AWACS:onafterLostCluster(From,Event,To,Cluster,Mission) + self:T({From, Event, To}) + return self +end + +--- [Internal] onafterCheckTacticalQueue +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterCheckTacticalQueue(From,Event,To) + self:T({From, Event, To}) + -- do we have messages queued? + + if self.clientset:CountAlive() == 0 then + self:T(self.lid.."No player connected.") + self:__CheckTacticalQueue(-5) + return self + end + + for _name,_freq in pairs(self.TacticalSubscribers) do + local Group = nil + if _name then + Group = GROUP:FindByName(_name) + end + if Group and Group:IsAlive() then + self:_BogeyDope(Group,true) + end + end + + if (self.TacticalQueue:IsNotEmpty()) then + + while self.TacticalQueue:Count() > 0 do + + local RadioEntry = self.TacticalQueue:Pull() -- #AWACS.RadioEntry + self:T({RadioEntry}) + local frequency = self.TacticalBaseFreq + if RadioEntry.GroupID and RadioEntry.GroupID ~= 0 then + local managedgroup = self.ManagedGrps[RadioEntry.GroupID] -- #AWACS.ManagedGroup + if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then + local name = managedgroup.GroupName + frequency = self.TacticalSubscribers[name] + end + end + -- AI AWACS Speaking + local gtext = RadioEntry.TextTTS + if self.PathToGoogleKey then + gtext = string.format("%s",gtext) + end + self.TacticalSRSQ:NewTransmission(gtext,nil,self.TacticalSRS,nil,0.5,nil,nil,nil,frequency,self.TacticalModulation) + + self:T(RadioEntry.TextTTS) + + if RadioEntry.ToScreen and RadioEntry.TextScreen and (not self.SuppressScreenOutput) then + if RadioEntry.GroupID and RadioEntry.GroupID ~= 0 then + local managedgroup = self.ManagedGrps[RadioEntry.GroupID] -- #AWACS.ManagedGroup + if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then + MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToGroup(managedgroup.Group) + self:T(RadioEntry.TextScreen) + end + else + MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToCoalition(self.coalition) + end + end + end + + end -- end while + + if not self:Is("Stopped") then + self:__CheckTacticalQueue(-self.TacticalInterval) + end + return self +end + + +--- [Internal] onafterCheckRadioQueue +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterCheckRadioQueue(From,Event,To) + self:T({From, Event, To}) + -- do we have messages queued? + + local nextcall = 10 + if (self.RadioQueue:IsNotEmpty() or self.PrioRadioQueue:IsNotEmpty()) then + + local RadioEntry = nil + + if self.PrioRadioQueue:IsNotEmpty() then + RadioEntry = self.PrioRadioQueue:Pull() -- #AWACS.RadioEntry + else + RadioEntry = self.RadioQueue:Pull() -- #AWACS.RadioEntry + end + self:T({RadioEntry}) + + if self.clientset:CountAlive() == 0 then + self:T(self.lid.."No player connected.") + self:__CheckRadioQueue(-5) + return self + end + + if not RadioEntry.FromAI then + -- AI AWACS Speaking + if self.PathToGoogleKey then + local gtext = RadioEntry.TextTTS + gtext = string.format("%s",gtext) + self.AwacsSRS:PlayTextExt(gtext,nil,self.MultiFrequency,self.MultiModulation,self.Gender,self.Culture,self.Voice,self.Volume,"AWACS") + else + self.AwacsSRS:PlayTextExt(RadioEntry.TextTTS,nil,self.MultiFrequency,self.MultiModulation,self.Gender,self.Culture,self.Voice,self.Volume,"AWACS") + end + self:T(RadioEntry.TextTTS) + else + -- CAP AI speaking + if RadioEntry.GroupID and RadioEntry.GroupID ~= 0 then + local managedgroup = self.ManagedGrps[RadioEntry.GroupID] -- #AWACS.ManagedGroup + if managedgroup and managedgroup.FlightGroup and managedgroup.FlightGroup:IsAlive() then + if self.PathToGoogleKey then + local gtext = RadioEntry.TextTTS + gtext = string.format("%s",gtext) + managedgroup.FlightGroup:RadioTransmission(gtext,1,false) + else + managedgroup.FlightGroup:RadioTransmission(RadioEntry.TextTTS,1,false) + end + self:T(RadioEntry.TextTTS) + end + end + end + + if RadioEntry.Duration then nextcall = RadioEntry.Duration end + + if RadioEntry.ToScreen and RadioEntry.TextScreen and (not self.SuppressScreenOutput) then + if RadioEntry.GroupID and RadioEntry.GroupID ~= 0 then + local managedgroup = self.ManagedGrps[RadioEntry.GroupID] -- #AWACS.ManagedGroup + if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then + MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToGroup(managedgroup.Group) + self:T(RadioEntry.TextScreen) + end + else + MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToCoalition(self.coalition) + end + end + end + + if self:Is("Running") then + -- exit if stopped + if self.PathToGoogleKey then + nextcall = nextcall + self.GoogleTTSPadding + else + nextcall = nextcall + self.WindowsTTSPadding + end + self:__CheckRadioQueue(-nextcall) + end + return self +end + +--- [Internal] onafterEscortShiftChange +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterEscortShiftChange(From,Event,To) + self:T({From, Event, To}) + -- request new Escorts, check if AWACS-FG still alive first! + if self.AwacsFG and self.ShiftChangeEscortsFlag and not self.ShiftChangeEscortsRequested then + local awacs = self.AwacsFG:GetGroup() -- Wrapper.Group#GROUP + if awacs and awacs:IsAlive() then + -- ok we're good to re-request + self.ShiftChangeEscortsRequested = true + self.EscortsTimeStamp = timer.getTime() + self:_StartEscorts(true) + else + -- should not happen + self:E("**** AWACS group dead at onafterEscortShiftChange!") + end + end + return self +end + +--- [Internal] onafterAwacsShiftChange +-- @param #AWACS self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #AWACS self +function AWACS:onafterAwacsShiftChange(From,Event,To) + self:T({From, Event, To}) + -- request new AWACS + if self.AwacsFG and self.ShiftChangeAwacsFlag and not self.ShiftChangeAwacsRequested then + + -- ok we're good to re-request + self.ShiftChangeAwacsRequested = true + self.AwacsTimeStamp = timer.getTime() + + -- set up the AWACS and let it orbit + local AwacsAW = self.AirWing -- Ops.Airwing#AIRWING + local mission = AUFTRAG:NewORBIT_RACETRACK(self.OrbitZone:GetCoordinate(),self.AwacsAngels*1000,self.Speed,self.Heading,self.Leg) + self.CatchAllMissions[#self.CatchAllMissions+1] = mission + local timeonstation = (self.AwacsTimeOnStation + self.ShiftChangeTime) * 3600 + mission:SetTime(nil,timeonstation) + mission:SetMissionRange(self.MaxMissionRange) + + AwacsAW:AddMission(mission) + + self.AwacsMissionReplacement = mission + + end + return self +end + +--- On after "FlightOnMission". +-- @param #AWACS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup on mission. +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +-- @return #AWACS self +function AWACS:onafterFlightOnMission(From, Event, To, FlightGroup, Mission) + self:T({From, Event, To}) + -- coming back from AW, set up the flight + self:T("FlightGroup " .. FlightGroup:GetName() .. " Mission " .. Mission:GetName() .. " Type "..Mission:GetType()) + self.CatchAllFGs[#self.CatchAllFGs+1] = FlightGroup + if not self:Is("Stopped") then + if not self.AwacsReady or self.ShiftChangeAwacsFlag or self.ShiftChangeEscortsFlag then + self:_StartSettings(FlightGroup,Mission) + elseif Mission and (Mission:GetType() == AUFTRAG.Type.CAP or Mission:GetType() == AUFTRAG.Type.ALERT5 or Mission:GetType() == AUFTRAG.Type.ORBIT) then + if not self.FlightGroups:HasUniqueID(FlightGroup:GetName()) then + self:T("Pushing FG " .. FlightGroup:GetName() .. " to stack!") + self.FlightGroups:Push(FlightGroup,FlightGroup:GetName()) + end + end + end + return self +end + +--- On after "ReAnchor". +-- @param #AWACS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number GID Group ID to check and re-anchor if possible +-- @return #AWACS self +function AWACS:onafterReAnchor(From, Event, To, GID) + self:T({From, Event, To, GID}) + -- get managedgroup, heck AI FG state, heck weapon state, check fuel state, vector back to anchor or RTB + local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup + if managedgroup then + if managedgroup.IsAI then + -- AI will now have a new CAP AUFTRAG and head back to the stack anyway + local AIFG = managedgroup.FlightGroup -- Ops.FlightGroup#FLIGHTGROUP + if AIFG and AIFG:IsAlive() then + -- check state + if AIFG:IsFuelLow() or AIFG:IsOutOfMissiles() or AIFG:IsOutOfAmmo() then + local destbase = AIFG.homebase + if not destbase then destbase = self.Airbase end + -- RTB call needs an AIRBASE + AIFG:RTB(destbase) + -- Check out + self:_CheckOut(AIFG:GetGroup(),GID) + self.AIRequested = self.AIRequested - 1 + else + -- re-establish anchor task, get anchor zone data + local Anchor = self.AnchorStacks:ReadByPointer(managedgroup.AnchorStackNo) -- #AWACS.AnchorData + local StationZone = Anchor.StationZone -- Core.Zone#ZONE + managedgroup.CurrentTask = self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,"Re-Station AI",StationZone) + managedgroup.HasAssignedTask = true + local mission = AIFG:GetMissionCurrent() -- Ops.Auftrag#AUFTRAG + if mission then + managedgroup.CurrentAuftrag = mission.auftragsnummer or 0 + else + managedgroup.CurrentAuftrag = 0 + end + managedgroup.ContactCID = 0 + self.ManagedGrps[GID] = managedgroup + local tostation = self.gettext:GetEntry("VECTORSTATION",self.locale) + self:_MessageVector(GID,tostation,Anchor.StationZoneCoordinate,managedgroup.AnchorStackAngels) + end + else + -- lost group, remove from known groups, declare vanished + -- AI - remove from known FGs! -- done in status loop + -- ALL remove from managedgrps + + -- message loss + local savedcallsign = managedgroup.CallSign + --vanished/friendly flight faded/lost contact with C/S/CSAR Scramble + -- Magic, RIGHTGUARD, RIGHTGUARD, Dodge 41, Bullseye X/Y + local textoptions = {} + textoptions[1] = self.gettext:GetEntry("TEXTOPTIONS1",self.locale) + textoptions[2] = self.gettext:GetEntry("TEXTOPTIONS2",self.locale) + textoptions[3] = self.gettext:GetEntry("TEXTOPTIONS3",self.locale) + textoptions[4] = self.gettext:GetEntry("TEXTOPTIONS4",self.locale) + local allstations = self.gettext:GetEntry("ALLSTATIONS",self.locale) + local milestxt = self.gettext:GetEntry("MILES",self.locale) + -- DONE - need to save last known coordinate + + if managedgroup.LastKnownPosition then + local lastknown = UTILS.DeepCopy(managedgroup.LastKnownPosition) + local faded = textoptions[math.random(1,4)] + local text = string.format("%s. %s. %s %s.",allstations,self.callsigntxt, faded, savedcallsign) + local textScreen = string.format("%s, %s. %s %s.",allstations, self.callsigntxt, faded, savedcallsign) + + local brtext = self:_ToStringBULLS(lastknown) + local brtexttts = self:_ToStringBULLS(lastknown,false,true) + + text = text .. " "..brtexttts.." "..milestxt.."." + textScreen = textScreen .. " "..brtext.." "..milestxt.."." + + self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true) + end + self.ManagedGrps[GID] = nil + end + elseif managedgroup.IsPlayer then + -- TODO + local PLFG = managedgroup.Group -- Wrapper.Group#GROUP + if PLFG and PLFG:IsAlive() then + -- re-establish anchor task + -- get anchor zone data + local Anchor = self.AnchorStacks:ReadByPointer(managedgroup.AnchorStackNo) -- #AWACS.AnchorData + local AnchorName = Anchor.StationName or "unknown" + local AnchorCoordTxt = Anchor.StationZoneCoordinateText or "unknown" + local Angels = managedgroup.AnchorStackAngels or 25 + local AnchorSpeed = self.CapSpeedBase or 270 + local StationZone = Anchor.StationZone -- Core.Zone#ZONE + local ROEROT = self.AwacsROE.." "..self.AwacsROT + local stationtxt = self.gettext:GetEntry("STATIONTASK",self.locale) + local TextTasking = string.format(stationtxt,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT) + managedgroup.CurrentTask = self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,TextTasking,StationZone) + managedgroup.HasAssignedTask = true + managedgroup.ContactCID = 0 + self.ManagedGrps[GID] = managedgroup + local vectortxt = self.gettext:GetEntry("VECTORSTATION",self.locale) + self:_MessageVector(GID,vectortxt,Anchor.StationZoneCoordinate,managedgroup.AnchorStackAngels) + else + -- lost group, remove from known groups, declare vanished + -- ALL remove from managedgrps + -- message loss + local savedcallsign = managedgroup.CallSign + --vanished/friendly flight faded/lost contact with C/S/CSAR Scramble + -- Magic, RIGHTGUARD, RIGHTGUARD, Dodge 41, Bullseye X/Y + local textoptions = {} + textoptions[1] = self.gettext:GetEntry("TEXTOPTIONS1",self.locale) + textoptions[2] = self.gettext:GetEntry("TEXTOPTIONS2",self.locale) + textoptions[3] = self.gettext:GetEntry("TEXTOPTIONS3",self.locale) + textoptions[4] = self.gettext:GetEntry("TEXTOPTIONS4",self.locale) + local allstations = self.gettext:GetEntry("ALLSTATIONS",self.locale) + local milestxt = self.gettext:GetEntry("MILES",self.locale) + + -- DONE - need to save last known coordinate + local faded = textoptions[math.random(1,4)] + local text = string.format("%s. %s. %s %s.",allstations, self.callsigntxt, faded, savedcallsign) + local textScreen = string.format("%s, %s. %s %s.", allstations,self.callsigntxt, faded, savedcallsign) + if managedgroup.LastKnownPosition then + local lastknown = UTILS.DeepCopy(managedgroup.LastKnownPosition) + local brtext = self:_ToStringBULLS(lastknown) + local brtexttts = self:_ToStringBULLS(lastknown,false,true) + text = text .. " "..brtexttts.." "..milestxt.."." + textScreen = textScreen .. " "..brtext.." "..milestxt.."." + + self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true) + end + self.ManagedGrps[GID] = nil + end + end + end +end + +end -- end do +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- END AWACS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Brigade.lua b/Moose Development/Moose/Ops/Brigade.lua new file mode 100644 index 000000000..5734e0f9c --- /dev/null +++ b/Moose Development/Moose/Ops/Brigade.lua @@ -0,0 +1,697 @@ +--- **Ops** - Brigade Warehouse. +-- +-- **Main Features:** +-- +-- * Manage platoons +-- * Carry out ARTY and PATROLZONE missions (AUFTRAG) +-- * Define rearming zones +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/Brigade). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.Brigade +-- @image OPS_Brigade_.png + + +--- BRIGADE class. +-- @type BRIGADE +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field #table rearmingZones Rearming zones. Each element is of type `#BRIGADE.SupplyZone`. +-- @field #table refuellingZones Refuelling zones. Each element is of type `#BRIGADE.SupplyZone`. +-- @field Core.Set#SET_ZONE retreatZones Retreat zone set. +-- @extends Ops.Legion#LEGION + +--- *I am not afraid of an Army of lions lead by a sheep; I am afraid of sheep lead by a lion* -- Alexander the Great +-- +-- === +-- +-- # The BRIGADE Concept +-- +-- A BRIGADE consists of one or multiple PLATOONs. These platoons "live" in a WAREHOUSE that has a phyiscal struction (STATIC or UNIT) and can be captured or destroyed. +-- +-- +-- @field #BRIGADE +BRIGADE = { + ClassName = "BRIGADE", + verbose = 0, + rearmingZones = {}, + refuellingZones = {}, +} + +--- Supply Zone. +-- @type BRIGADE.SupplyZone +-- @field Core.Zone#ZONE zone The zone. +-- @field Ops.Auftrag#AUFTRAG mission Mission assigned to supply ammo or fuel. +-- @field #boolean markerOn If `true`, marker is on. +-- @field Wrapper.Marker#MARKER marker F10 marker. + +--- BRIGADE class version. +-- @field #string version +BRIGADE.version="0.1.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Spawn when hosting warehouse is a ship or oil rig or gas platform. +-- TODO: Rearming zones. +-- TODO: Retreat zones. +-- DONE: Add weapon range. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new BRIGADE class object. +-- @param #BRIGADE self +-- @param #string WarehouseName Name of the warehouse STATIC or UNIT object representing the warehouse. +-- @param #string BrigadeName Name of the brigade. +-- @return #BRIGADE self +function BRIGADE:New(WarehouseName, BrigadeName) + + -- Inherit everything from LEGION class. + local self=BASE:Inherit(self, LEGION:New(WarehouseName, BrigadeName)) -- #BRIGADE + + -- 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("BRIGADE %s | ", self.alias) + + -- Defaults + self:SetRetreatZones() + + -- Turn ship into NAVYGROUP. + if self:IsShip() then + local wh=self.warehouse --Wrapper.Unit#UNIT + local group=wh:GetGroup() + self.warehouseOpsGroup=NAVYGROUP:New(group) --Ops.NavyGroup#NAVYGROUP + self.warehouseOpsElement=self.warehouseOpsGroup:GetElementByName(wh:GetName()) + end + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "ArmyOnMission", "*") -- An ARMYGROUP was send on a Mission (AUFTRAG). + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the BRIGADE. Initializes parameters and starts event handlers. + -- @function [parent=#BRIGADE] Start + -- @param #BRIGADE self + + --- Triggers the FSM event "Start" after a delay. Starts the BRIGADE. Initializes parameters and starts event handlers. + -- @function [parent=#BRIGADE] __Start + -- @param #BRIGADE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". Stops the BRIGADE and all its event handlers. + -- @param #BRIGADE self + + --- Triggers the FSM event "Stop" after a delay. Stops the BRIGADE and all its event handlers. + -- @function [parent=#BRIGADE] __Stop + -- @param #BRIGADE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "ArmyOnMission". + -- @function [parent=#BRIGADE] ArmyOnMission + -- @param #BRIGADE self + -- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup The ARMYGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "ArmyOnMission" after a delay. + -- @function [parent=#BRIGADE] __ArmyOnMission + -- @param #BRIGADE self + -- @param #number delay Delay in seconds. + -- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup The ARMYGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "ArmyOnMission" event. + -- @function [parent=#BRIGADE] OnAfterArmyOnMission + -- @param #BRIGADE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup The ARMYGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add a platoon to the brigade. +-- @param #BRIGADE self +-- @param Ops.Platoon#PLATOON Platoon The platoon object. +-- @return #BRIGADE self +function BRIGADE:AddPlatoon(Platoon) + + -- Add platoon to brigade. + table.insert(self.cohorts, Platoon) + + -- Add assets to platoon. + self:AddAssetToPlatoon(Platoon, Platoon.Ngroups) + + -- Set brigade of platoon. + Platoon:SetBrigade(self) + + -- Start platoon. + if Platoon:IsStopped() then + Platoon:Start() + end + + return self +end + +--- Add asset group(s) to platoon. +-- @param #BRIGADE self +-- @param Ops.Platoon#PLATOON Platoon The platoon object. +-- @param #number Nassets Number of asset groups to add. +-- @return #BRIGADE self +function BRIGADE:AddAssetToPlatoon(Platoon, Nassets) + + if Platoon then + + -- Get the template group of the platoon. + local Group=GROUP:FindByName(Platoon.templatename) + + if Group then + + -- Debug text. + local text=string.format("Adding asset %s to platoon %s", Group:GetName(), Platoon.name) + self:T(self.lid..text) + + -- Add assets to airwing warehouse. + self:AddAsset(Group, Nassets, nil, nil, nil, nil, Platoon.skill, Platoon.livery, Platoon.name) + + else + self:E(self.lid.."ERROR: Group does not exist!") + end + + else + self:E(self.lid.."ERROR: Platoon does not exit!") + end + + return self +end + +--- Define a set of retreat zones. +-- @param #BRIGADE self +-- @param Core.Set#SET_ZONE RetreatZoneSet Set of retreat zones. +-- @return #BRIGADE self +function BRIGADE:SetRetreatZones(RetreatZoneSet) + self.retreatZones=RetreatZoneSet or SET_ZONE:New() + return self +end + +--- Add a retreat zone. +-- @param #BRIGADE self +-- @param Core.Zone#ZONE RetreatZone Retreat zone. +-- @return #BRIGADE self +function BRIGADE:AddRetreatZone(RetreatZone) + self.retreatZones:AddZone(RetreatZone) + return self +end + +--- Get retreat zones. +-- @param #BRIGADE self +-- @return Core.Set#SET_ZONE Set of retreat zones. +function BRIGADE:GetRetreatZones() + return self.retreatZones +end + +--- Add a rearming zone. +-- @param #BRIGADE self +-- @param Core.Zone#ZONE RearmingZone Rearming zone. +-- @return #BRIGADE.SupplyZone The rearming zone data. +function BRIGADE:AddRearmingZone(RearmingZone) + + local rearmingzone={} --#BRIGADE.SupplyZone + + rearmingzone.zone=RearmingZone + rearmingzone.mission=nil + rearmingzone.marker=MARKER:New(rearmingzone.zone:GetCoordinate(), "Rearming Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.rearmingZones, rearmingzone) + + return rearmingzone +end + + +--- Add a refuelling zone. +-- @param #BRIGADE self +-- @param Core.Zone#ZONE RefuellingZone Refuelling zone. +-- @return #BRIGADE.SupplyZone The refuelling zone data. +function BRIGADE:AddRefuellingZone(RefuellingZone) + + local supplyzone={} --#BRIGADE.SupplyZone + + supplyzone.zone=RefuellingZone + supplyzone.mission=nil + supplyzone.marker=MARKER:New(supplyzone.zone:GetCoordinate(), "Refuelling Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.refuellingZones, supplyzone) + + return supplyzone +end + + +--- Get platoon by name. +-- @param #BRIGADE self +-- @param #string PlatoonName Name of the platoon. +-- @return Ops.Platoon#PLATOON The Platoon object. +function BRIGADE:GetPlatoon(PlatoonName) + local platoon=self:_GetCohort(PlatoonName) + return platoon +end + +--- Get platoon of an asset. +-- @param #BRIGADE self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The platoon asset. +-- @return Ops.Platoon#PLATOON The platoon object. +function BRIGADE:GetPlatoonOfAsset(Asset) + local platoon=self:GetPlatoon(Asset.squadname) + return platoon +end + +--- Remove asset from platoon. +-- @param #BRIGADE self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The platoon asset. +function BRIGADE:RemoveAssetFromPlatoon(Asset) + local platoon=self:GetPlatoonOfAsset(Asset) + if platoon then + platoon:DelAsset(Asset) + end +end + + +--- [ GROUND ] Function to load back an asset in the field that has been filed before. +-- @param #BRIGADE self +-- @param #string Templatename e.g."1 PzDv LogRg I\_AID-976" - that's the alias (name) of an platoon spawned as `"platoon - alias"_AID-"asset-ID"` +-- @param Core.Point#COORDINATE Position where to spawn the platoon +-- @return #BRIGADE self +-- @usage +-- Prerequisites: +-- Save the assets spawned by BRIGADE/CHIEF regularly (~every 5 mins) into a file, e.g. like this: +-- +-- local Path = FilePath or "C:\\Users\\\\Saved Games\\DCS\\Missions\\" -- example path +-- local BlueOpsFilename = BlueFileName or "ExamplePlatoonSave.csv" -- example filename +-- local BlueSaveOps = SET_OPSGROUP:New():FilterCoalitions("blue"):FilterCategoryGround():FilterOnce() +-- UTILS.SaveSetOfOpsGroups(BlueSaveOps,Path,BlueOpsFilename) +-- +-- where Path and Filename are strings, as chosen by you. +-- You can then load back the assets at the start of your next mission run. Be aware that it takes a couple of seconds for the +-- platoon data to arrive in brigade, so make this an action after ~20 seconds, e.g. like so: +-- +-- function LoadBackAssets() +-- local Path = FilePath or "C:\\Users\\\\Saved Games\\DCS\\Missions\\" -- example path +-- local BlueOpsFilename = BlueFileName or "ExamplePlatoonSave.csv" -- example filename +-- if UTILS.CheckFileExists(Path,BlueOpsFilename) then +-- local loadback = UTILS.LoadSetOfOpsGroups(Path,BlueOpsFilename,false) +-- for _,_platoondata in pairs (loadback) do +-- local groupname = _platoondata.groupname -- #string +-- local coordinate = _platoondata.coordinate -- Core.Point#COORDINATE +-- Your_Brigade:LoadBackAssetInPosition(groupname,coordinate) +-- end +-- end +-- end +-- +-- local AssetLoader = TIMER:New(LoadBackAssets) +-- AssetLoader:Start(20) +-- +-- The assets loaded back into the mission will be considered for AUFTRAG type missions from CHIEF and BRIGADE. +function BRIGADE:LoadBackAssetInPosition(Templatename,Position) + self:T(self.lid .. "LoadBackAssetInPosition: " .. tostring(Templatename)) + + -- get Platoon alias from Templatename + local nametbl = UTILS.Split(Templatename,"_") + + local name = nametbl[1] + + self:T(string.format("*** Target Platoon = %s ***",name)) + + -- find a matching asset table from BRIGADE + local cohorts = self.cohorts or {} + local thisasset = nil --Functional.Warehouse#WAREHOUSE.Assetitem + local found = false + + for _,_cohort in pairs(cohorts) do + local asset = _cohort:GetName() + self:T(string.format("*** Looking at Platoon = %s ***",asset)) + if asset == name then + self:T("**** Found Platoon ****") + local cohassets = _cohort.assets or {} + for _,_zug in pairs (cohassets) do + local zug = _zug -- Functional.Warehouse#WAREHOUSE.Assetitem + if zug.assignment == name and zug.requested == false then + self:T("**** Found Asset ****") + found = true + thisasset = zug --Functional.Warehouse#WAREHOUSE.Assetitem + break + end + end + end + end + + if found then + + -- prep asset + thisasset.rid = thisasset.uid + thisasset.requested = false + thisasset.score=100 + thisasset.missionTask="CAS" + thisasset.spawned = true + local template = thisasset.templatename + local alias = thisasset.spawngroupname + + -- Spawn group + local spawnasset = SPAWN:NewWithAlias(template,alias) + :InitDelayOff() + :SpawnFromCoordinate(Position) + + -- build a new self request + local request = {} --Functional.Warehouse#WAREHOUSE.Pendingitem + request.assignment = name + request.warehouse = self + request.assets = {thisasset} + request.ntransporthome = 0 + request.ndelivered = 0 + request.ntransport = 0 + request.cargoattribute = thisasset.attribute + request.category = thisasset.category + request.cargoassets = {thisasset} + request.assetdesc = WAREHOUSE.Descriptor.ASSETLIST + request.cargocategory = thisasset.category + request.toself = true + request.transporttype = WAREHOUSE.TransportType.SELFPROPELLED + request.assetproblem = {} + request.born = true + request.prio = 50 + request.uid = thisasset.uid + request.airbase = nil + request.timestamp = timer.getAbsTime() + request.assetdescval = {thisasset} + request.nasset = 1 + request.cargogroupset = SET_GROUP:New() + request.cargogroupset:AddGroup(spawnasset) + request.iscargo = true + + -- Call Brigade self + self:__AssetSpawned(2, spawnasset, thisasset, request) + + end + return self +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start BRIGADE FSM. +-- @param #BRIGADE self +function BRIGADE:onafterStart(From, Event, To) + + -- Start parent Warehouse. + self:GetParent(self, BRIGADE).onafterStart(self, From, Event, To) + + -- Info. + self:I(self.lid..string.format("Starting BRIGADE v%s", BRIGADE.version)) + +end + +--- Update status. +-- @param #BRIGADE self +function BRIGADE:onafterStatus(From, Event, To) + + -- Status of parent Warehouse. + self:GetParent(self).onafterStatus(self, From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + ---------------- + -- Transport --- + ---------------- + + self:CheckTransportQueue() + + -------------- + -- Mission --- + -------------- + + -- Check if any missions should be cancelled. + self:CheckMissionQueue() + + --------------------- + -- Rearming Zones --- + --------------------- + + for _,_rearmingzone in pairs(self.rearmingZones) do + local rearmingzone=_rearmingzone --#BRIGADE.SupplyZone + if (not rearmingzone.mission) or rearmingzone.mission:IsOver() then + rearmingzone.mission=AUFTRAG:NewAMMOSUPPLY(rearmingzone.zone) + self:AddMission(rearmingzone.mission) + end + end + + ----------------------- + -- Refuelling Zones --- + ----------------------- + + -- Check refuelling zones. + for _,_supplyzone in pairs(self.refuellingZones) do + local supplyzone=_supplyzone --#BRIGADE.SupplyZone + -- Check if mission is nil or over. + if (not supplyzone.mission) or supplyzone.mission:IsOver() then + supplyzone.mission=AUFTRAG:NewFUELSUPPLY(supplyzone.zone) + self:AddMission(supplyzone.mission) + end + end + + + ----------- + -- Info --- + ----------- + + -- Display tactival overview. + self:_TacticalOverview() + + -- General info: + if self.verbose>=1 then + + -- Count missions not over yet. + local Nmissions=self:CountMissionsInQueue() + + -- Asset count. + local Npq, Np, Nq=self:CountAssetsOnMission() + + -- Asset string. + 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, Platoons=%d, Assets=%s", fsmstate, Nmissions, #self.cohorts, 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 or 0) + 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 + + -------------------- + -- Transport Info -- + -------------------- + if self.verbose>=2 then + local text=string.format("Transports Total=%d:", #self.transportqueue) + for i,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + local prio=string.format("%d/%s", transport.prio, tostring(transport.importance)) ; if transport.urgent then prio=prio.." (!)" end + local carriers=string.format("Ncargo=%d/%d, Ncarriers=%d", transport.Ncargo, transport.Ndelivered, transport.Ncarrier) + + text=text..string.format("\n[%d] UID=%d: Status=%s, Prio=%s, Cargo: %s", i, transport.uid, transport:GetState(), prio, carriers) + end + self:I(self.lid..text) + end + + ------------------- + -- Platoon Info -- + ------------------- + if self.verbose>=3 then + local text="Platoons:" + for i,_platoon in pairs(self.cohorts) do + local platoon=_platoon --Ops.Platoon#PLATOON + + local callsign=platoon.callsignName and UTILS.GetCallsignName(platoon.callsignName) or "N/A" + local modex=platoon.modex and platoon.modex or -1 + local skill=platoon.skill and tostring(platoon.skill) or "N/A" + + -- Platoon text. + text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", platoon.name, platoon:GetState(), platoon.aircrafttype, platoon:CountAssets(true), #platoon.assets, callsign, modex, skill) + end + self:I(self.lid..text) + end + + ------------------- + -- Rearming Info -- + ------------------- + if self.verbose>=4 then + local text="Rearming Zones:" + for i,_rearmingzone in pairs(self.rearmingZones) do + local rearmingzone=_rearmingzone --#BRIGADE.SupplyZone + -- Info text. + text=text..string.format("\n* %s: Mission status=%s, suppliers=%d", rearmingzone.zone:GetName(), rearmingzone.mission:GetState(), rearmingzone.mission:CountOpsGroups()) + end + self:I(self.lid..text) + end + + --------------------- + -- Refuelling Info -- + --------------------- + if self.verbose>=4 then + local text="Refuelling Zones:" + for i,_refuellingzone in pairs(self.refuellingZones) do + local refuellingzone=_refuellingzone --#BRIGADE.SupplyZone + -- Info text. + text=text..string.format("\n* %s: Mission status=%s, suppliers=%d", refuellingzone.zone:GetName(), refuellingzone.mission:GetState(), refuellingzone.mission:CountOpsGroups()) + end + self:I(self.lid..text) + end + + ---------------- + -- Asset Info -- + ---------------- + if self.verbose>=5 then + local text="Assets in stock:" + for i,_asset in pairs(self.stock) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + -- Info text. + text=text..string.format("\n* %s: spawned=%s", asset.spawngroupname, tostring(asset.spawned)) + end + self:I(self.lid..text) + end + + if self.verbose>=3 then + + -- Count numbers + local Ntotal=0 + local Nspawned=0 + local Nrequested=0 + local Nreserved=0 + local Nstock=0 + + local text="\n===========================================\n" + text=text.."Assets:" + local legion=self --Ops.Legion#LEGION + + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + for _,_asset in pairs(cohort.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + local state="In Stock" + if asset.flightgroup then + state=asset.flightgroup:GetState() + local mission=legion:GetAssetCurrentMission(asset) + if mission then + state=state..string.format(", Mission \"%s\" [%s]", mission:GetName(), mission:GetType()) + end + else + if asset.spawned then + env.info("FF ERROR: asset has opsgroup but is NOT spawned!") + end + if asset.requested and asset.isReserved then + env.info("FF ERROR: asset is requested and reserved. Should not be both!") + state="Reserved+Requested!" + elseif asset.isReserved then + state="Reserved" + elseif asset.requested then + state="Requested" + end + end + + -- Text. + text=text..string.format("\n[UID=%03d] %s Legion=%s [%s]: State=%s [RID=%s]", + asset.uid, asset.spawngroupname, legion.alias, cohort.name, state, tostring(asset.rid)) + + + if asset.spawned then + Nspawned=Nspawned+1 + end + if asset.requested then + Nrequested=Nrequested+1 + end + if asset.isReserved then + Nreserved=Nreserved+1 + end + if not (asset.spawned or asset.requested or asset.isReserved) then + Nstock=Nstock+1 + end + + Ntotal=Ntotal+1 + + end + + end + text=text.."\n-------------------------------------------" + text=text..string.format("\nNstock = %d", Nstock) + text=text..string.format("\nNreserved = %d", Nreserved) + text=text..string.format("\nNrequested = %d", Nrequested) + text=text..string.format("\nNspawned = %d", Nspawned) + text=text..string.format("\nNtotal = %d (=%d)", Ntotal, Nstock+Nspawned+Nrequested+Nreserved) + text=text.."\n===========================================" + self:I(self.lid..text) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "ArmyOnMission". +-- @param #BRIGADE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup Ops army group on mission. +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function BRIGADE:onafterArmyOnMission(From, Event, To, ArmyGroup, Mission) + -- Debug info. + self:T(self.lid..string.format("Group %s on %s mission %s", ArmyGroup:GetName(), Mission:GetType(), Mission:GetName())) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Chief.lua b/Moose Development/Moose/Ops/Chief.lua new file mode 100644 index 000000000..9500ae313 --- /dev/null +++ b/Moose Development/Moose/Ops/Chief.lua @@ -0,0 +1,3362 @@ +--- **Ops** - Chief of Staff. +-- +-- **Main Features:** +-- +-- * Automatic target engagement based on detection network +-- * Define multiple border, conflict and attack zones +-- * Define strategic "capture" zones +-- * Set strategy of chief from passive to agressive +-- * Manual target engagement via AUFTRAG and TARGET classes +-- * Add AIRWINGS, BRIGADES and FLEETS as resources +-- * Seamless air-to-air, air-to-ground, ground-to-ground dispatching +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Chief +-- @image OPS_Chief.png + + +--- CHIEF class. +-- @type CHIEF +-- @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 targetqueue Target queue. +-- @field #table zonequeue Strategic zone queue. +-- @field Core.Set#SET_ZONE borderzoneset Set of zones defining the border of our territory. +-- @field Core.Set#SET_ZONE yellowzoneset Set of zones defining the extended border. Defcon is set to YELLOW if enemy activity is detected. +-- @field Core.Set#SET_ZONE engagezoneset Set of zones where enemies are actively engaged. +-- @field #number threatLevelMin Lowest threat level of targets to attack. +-- @field #number threatLevelMax Highest threat level of targets to attack. +-- @field #string Defcon Defence condition. +-- @field #string strategy Strategy of the CHIEF. +-- @field Ops.Commander#COMMANDER commander Commander of assigned legions. +-- @field #number Nsuccess Number of successful missions. +-- @field #number Nfailure Number of failed mission. +-- @field #table assetNumbers Asset numbers. Each entry is a table of data type `#CHIEF.AssetNumber`. +-- @extends Ops.Intelligence#INTEL + +--- *In preparing for battle I have always found that plans are useless, but planning is indispensable* -- Dwight D Eisenhower +-- +-- === +-- +-- # The CHIEF Concept +-- +-- The Chief of staff gathers INTEL and assigns missions (AUFTRAG) to the airforce, army and/or navy. The distinguished feature here is that this class combines all three +-- forces under one hood. Therefore, this class be used as an air-to-air, air-to-ground, ground-to-ground, air-to-sea, sea-to-ground, etc. dispachter. +-- +-- # Territory +-- +-- The chief class allows you to define border zones, conflict zones and attack zones. +-- +-- ## Border Zones +-- +-- Border zones define your own territory. +-- They can be set via the @{#CHIEF.SetBorderZones}() function as a set or added zone by zone via the @{#CHIEF.AddBorderZone}() function. +-- +-- ## Conflict Zones +-- +-- Conflict zones define areas, which usually are under dispute of different coalitions. +-- They can be set via the @{#CHIEF.SetConflictZones}() function as a set or added zone by zone via the @{#CHIEF.AddConflictZone}() function. +-- +-- ## Attack Zones +-- +-- Attack zones are zones that usually lie within the enemy territory. They are only enganged with an agressive strategy. +-- They can be set via the @{#CHIEF.SetAttackZones}() function as a set or added zone by zone via the @{#CHIEF.AddAttackZone}() function. +-- +-- # Defense Condition +-- +-- The defence condition (DEFCON) depends on enemy activity detected in the different zone types and is set automatically. +-- +-- * `CHIEF.Defcon.GREEN`: No enemy activities detected. +-- * `CHIEF.Defcon.YELLOW`: Enemy activity detected in conflict zones. +-- * `CHIEF.Defcon.RED`: Enemy activity detected in border zones. +-- +-- The current DEFCON can be retrieved with the @(#CHIEF.GetDefcon)() function. +-- +-- When the DEFCON changed, an FSM event @{#CHIEF.DefconChange} is triggered. Mission designers can hook into this event via the @{#CHIEF.OnAfterDefconChange}() function: +-- +-- --- Function called when the DEFCON changes. +-- function myChief:OnAfterDefconChange(From, Event, To, Defcon) +-- local text=string.format("Changed DEFCON to %s", Defcon) +-- MESSAGE:New(text, 120):ToAll() +-- end +-- +-- # Strategy +-- +-- The strategy of the chief determines, in which areas targets are engaged automatically. +-- +-- * `CHIEF.Strategy.PASSIVE`: Chief is completely passive. No targets at all are engaged automatically. +-- * `CHIEF.Strategy.DEFENSIVE`: Chief acts defensively. Only targets in his own territory are engaged. +-- * `CHIEF.Strategy.OFFENSIVE`: Chief behaves offensively. Targets in his own territory and in conflict zones are enganged. +-- * `CHIEF.Strategy.AGGRESSIVE`: Chief is aggressive. Targets in his own territory, in conflict zones and in attack zones are enganged. +-- * `CHIEF.Strategy.TOTALWAR`: Anything anywhere is enganged. +-- +-- The strategy can be set by the @(#CHIEF.SetStrategy)() and retrieved with the @(#CHIEF.GetStrategy)() function. +-- +-- When the strategy is changed, the FSM event @{#CHIEF.StrategyChange} is triggered and customized code can be added to the @{#CHIEF.OnAfterStrategyChange}() function: +-- +-- --- Function called when the STRATEGY changes. +-- function myChief:OnAfterStrategyChange(From, Event, To, Strategy) +-- local text=string.format("Strategy changd to %s", Strategy) +-- MESSAGE:New(text, 120):ToAll() +-- end +-- +-- # Resources +-- +-- A chief needs resources such as air, ground and naval assets. These can be added in form of AIRWINGs, BRIGADEs and FLEETs. +-- +-- Whenever the chief detects a target or receives a mission, he will select the best available assets and assign them to the mission. +-- The best assets are determined by their mission performance, payload performance (in case of air), distance to the target, skill level, etc. +-- +-- ## Adding Airwings +-- +-- Airwings can be added via the @{#CHIEF.AddAirwing}() function. +-- +-- ## Adding Brigades +-- +-- Brigades can be added via the @{#CHIEF.AddBrigade}() function. +-- +-- ## Adding Fleets +-- +-- Fleets can be added via the @{#CHIEF.AddFleet}() function. +-- +-- ## Response on Target +-- +-- When the chief detects a valid target, he will launch a certain number of selected assets. Only whole groups from SQUADRONs, PLATOONs or FLOTILLAs can be selected. +-- In other words, it is not possible to specify the abount of individual *units*. +-- +-- By default, one group is selected for any detected target. This can, however, be customized with the @{#CHIEF.SetResponseOnTarget}() function. The number of min and max +-- asset groups can be specified depending on threatlevel, category, mission type, number of units, defcon and strategy. +-- +-- For example: +-- +-- -- One group for aircraft targets of threat level 0 or higher. +-- myChief:SetResponseOnTarget(1, 1, 0, TARGET.Category.AIRCRAFT) +-- -- At least one and up to two groups for aircraft targets of threat level 8 or higher. This will overrule the previous response! +-- myChief:SetResponseOnTarget(1, 2, 8, TARGET.Category.AIRCRAFT) +-- +-- -- At least one and up to three groups for ground targets of threat level 0 or higher if current strategy is aggressive. +-- myChief:SetResponseOnTarget(1, 1, 0, TARGET.Category.GROUND, nil ,nil, nil, CHIEF.Strategy.DEFENSIVE) +-- +-- -- One group for BAI missions if current defcon is green. +-- myChief:SetResponseOnTarget(1, 1, 0, nil, AUFTRAG.Type.BAI, nil, CHIEF.DEFCON.GREEN) +-- +-- -- At least one and up to four groups for BAI missions if current defcon is red. +-- myChief:SetResponseOnTarget(1, 2, 0, nil, AUFTRAG.Type.BAI, nil, CHIEF.DEFCON.YELLOW) +-- +-- -- At least one and up to four groups for BAI missions if current defcon is red. +-- myChief:SetResponseOnTarget(1, 3, 0, nil, AUFTRAG.Type.BAI, nil, CHIEF.DEFCON.RED) +-- +-- +-- # Strategic (Capture) Zones +-- +-- Strategically important zones, which should be captured can be added via the @{#CHIEF.AddStrategicZone}(*OpsZone, Prio, Importance*) function. +-- The first parameter *OpsZone* is an @{Ops.OpsZone#OPSZONE} specifying the zone. This has to be a **circular zone** due to DCS API restrictions. +-- The second parameter *Prio* is the priority. The zone queue is sorted wrt to lower prio values. By default this is set to 50. +-- The third parameter *Importance* is the importance of the zone. By default this is `nil`. If you specify one zone with importance 2 and a second zone with +-- importance 3, then the zone of importance 2 is attacked first and only if that zone has been captured, zones that have importances with higher values are attacked. +-- +-- For example: +-- +-- local myStratZone=myChief:AddStrategicZone(myOpsZone, nil , 2) +-- +-- Will at a strategic zone with importance 2. +-- +-- If the zone is currently owned by another coalition and enemy ground troops are present in the zone, a CAS and an ARTY mission are launched: +-- +-- * A mission of type `AUFTRAG.Type.CASENHANCED` is started if assets are available that can carry out this mission type. +-- * A mission of type `AUFTRAG.Type.ARTY` is started provided assets are available. +-- +-- The CAS flight(s) will patrol the zone randomly and take out enemy ground units they detect. It can always be possible that the enemies cannot be detected however. +-- The assets will shell the zone. However, it is unlikely that they hit anything as they do not have any information about the location of the enemies. +-- +-- Once the zone is cleaned of enemy forces, ground troops are send there. By default, two missions are launched: +-- +-- * First mission is of type `AUFTRAG.Type.ONGUARD` and will send infantry groups. These are transported by helicopters. Therefore, helo assets with `AUFTRAG.Type.OPSTRANSPORT` need to be available. +-- * The second mission is also of type `AUFTRAG.Type.ONGUARD` but will send tanks if these are available. +-- +-- ## Customized Reaction +-- +-- The default mission types and number of assets can be customized for the two scenarious (zone empty or zone occupied by the enemy). +-- +-- In order to do this, you need to create resource lists (one for each scenario) via the @{#CHIEF.CreateResource}() function. +-- These lists can than passed as additional parameters to the @{#CHIEF.AddStrategicZone} function. +-- +-- For example: +-- +-- --- Create a resource list of mission types and required assets for the case that the zone is OCCUPIED. +-- -- +-- -- Here, we create an enhanced CAS mission and employ at least on and at most two asset groups. +-- -- NOTE that two objects are returned, the resource list (ResourceOccupied) and the first resource of that list (resourceCAS). +-- local ResourceOccupied, resourceCAS=myChief:CreateResource(AUFTRAG.Type.CASENHANCED, 1, 2) +-- -- We also add ARTY missions with at least one and at most two assets. We additionally require these to be MLRS groups (and not howitzers). +-- myChief:AddToResource(ResourceOccupied, AUFTRAG.Type.ARTY, 1, 2, nil, "MLRS") +-- -- Add at least one RECON mission that uses UAV type assets. +-- myChief:AddToResource(ResourceOccupied, AUFTRAG.Type.RECON, 1, nil, GROUP.Attribute.AIR_UAV) +-- -- Add at least one but at most two BOMBCARPET missions. +-- myChief:AddToResource(ResourceOccupied, AUFTRAG.Type.BOMBCARPET, 1, 2) +-- +-- --- Create a resource list of mission types and required assets for the case that the zone is EMPTY. +-- -- NOTE that two objects are returned, the resource list (ResourceEmpty) and the first resource of that list (resourceInf). +-- -- Here, we create an ONGUARD mission and employ at least on and at most five infantry assets. +-- local ResourceEmpty, resourceInf=myChief:CreateResource(AUFTRAG.Type.ONGUARD, 1, 5, GROUP.Attribute.GROUND_INFANTRY) +-- -- Additionally, we send up to three tank groups. +-- myChief:AddToResource(ResourceEmpty, AUFTRAG.Type.ONGUARD, 1, 3, GROUP.Attribute.GROUND_TANK) +-- -- Finally, we send two groups that patrol the zone. +-- myChief:AddToResource(ResourceEmpty, AUFTRAG.Type.PATROLZONE, 2) +-- +-- -- Add a transport to the infantry resource. We want at least one and up to two transport helicopters. +-- myChief:AddTransportToResource(resourceInf, 1, 2, GROUP.Attribute.AIR_TRANSPORTHELO) +-- +-- -- Add stratetic zone with customized reaction. +-- myChief:AddStrategicZone(myOpsZone, nil , 2, ResourceOccupied, ResourceEmpty) +-- +-- As the location of the enemies is not known, only mission types that don't require an explicit target group are possible. These are +-- +-- * `AUFTRAG.Type.CASENHANCED` +-- * `AUFTRAG.Type.ARTY` +-- * `AUFTRAG.Type.PATROLZONE` +-- * `AUFTRAG.Type.ONGUARD` +-- * `AUFTRAG.Type.CAPTUREZONE` +-- * `AUFTRAG.Type.RECON` +-- * `AUFTRAG.Type.AMMOSUPPLY` +-- * `AUFTRAG.Type.BOMBING` +-- * `AUFTRAG.Type.BOMBCARPET` +-- * `AUFTRAG.Type.BARRAGE` +-- +-- ## Events +-- +-- Whenever a strategic zone is captured by us the FSM event @{#CHIEF.ZoneCaptured} is triggered and customized further actions can be executed +-- with the @{#CHIEF.OnAfterZoneCaptured}() function. +-- +-- Whenever a strategic zone is lost (captured by the enemy), the FSM event @{#CHIEF.ZoneLost} is triggered and customized further actions can be executed +-- with the @{#CHIEF.OnAfterZoneLost}() function. +-- +-- Further events are +-- +-- * @{#CHIEF.ZoneEmpty}, once the zone is completely empty of ground troops. Code can be added to the @{#CHIEF.OnAfterZoneEmpty}() function. +-- * @{#CHIEF.ZoneAttacked}, once the zone is under attack. Code can be added to the @{#CHIEF.OnAfterZoneAttacked}() function. +-- +-- Note that the ownership of a zone is determined via zone scans, i.e. not via the detection network. In other words, there is an all knowing eye. +-- Think of it as the local population providing the intel. It's not totally realistic but the best compromise within the limits of DCS. +-- +-- +-- +-- @field #CHIEF +CHIEF = { + ClassName = "CHIEF", + verbose = 0, + lid = nil, + targetqueue = {}, + zonequeue = {}, + borderzoneset = nil, + yellowzoneset = nil, + engagezoneset = nil, + tacview = false, + Nsuccess = 0, + Nfailure = 0, +} + +--- Defence condition. +-- @type CHIEF.DEFCON +-- @field #string GREEN No enemy activities detected in our terretory or conflict zones. +-- @field #string YELLOW Enemy in conflict zones. +-- @field #string RED Enemy within our border. +CHIEF.DEFCON = { + GREEN="Green", + YELLOW="Yellow", + RED="Red", +} + +--- Strategy. +-- @type CHIEF.Strategy +-- @field #string PASSIVE No targets at all are engaged. +-- @field #string DEFENSIVE Only target in our own terretory are engaged. +-- @field #string OFFENSIVE Targets in own terretory and yellow zones are engaged. +-- @field #string AGGRESSIVE Targets in own terretory, conflict zones and attack zones are engaged. +-- @field #string TOTALWAR Anything is engaged anywhere. +CHIEF.Strategy = { + PASSIVE="Passive", + DEFENSIVE="Defensive", + OFFENSIVE="Offensive", + AGGRESSIVE="Aggressive", + TOTALWAR="Total War" +} + +--- Mission performance. +-- @type CHIEF.MissionPerformance +-- @field #string MissionType Mission Type. +-- @field #number Performance Performance: a number between 0 and 100, where 100 is best performance. + +--- Asset numbers for detected targets. +-- @type CHIEF.AssetNumber +-- @field #number nAssetMin Min number of assets. +-- @field #number nAssetMax Max number of assets. +-- @field #number threatlevel Threat level. +-- @field #string targetCategory Target category. +-- @field #string missionType Mission type. +-- @field #number nUnits Number of enemy units. +-- @field #string defcon Defense condition. +-- @field #string strategy Strategy. + +--- Strategic zone. +-- @type CHIEF.StrategicZone +-- @field Ops.OpsZone#OPSZONE opszone OPS zone. +-- @field #number prio Priority. +-- @field #number importance Importance. +-- @field #CHIEF.Resources resourceEmpty List of resources employed when the zone is empty. +-- @field #CHIEF.Resources resourceOccup List of resources employed when the zone is occupied by an enemy. +-- @field #table missions Mission. + +--- Resource list. +-- @type CHIEF.Resources +-- @field #CHIEF.Resource List of resources. + +--- Resource. +-- @type CHIEF.Resource +-- @field #string MissionType Mission type, e.g. `AUFTRAG.Type.BAI`. +-- @field #number Nmin Min number of assets. +-- @field #number Nmax Max number of assets. +-- @field #table Attributes Generalized attribute, e.g. `{GROUP.Attribute.GROUND_INFANTRY}`. +-- @field #table Properties Properties ([DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes)), e.g. `"Attack helicopters"` or `"Mobile AAA"`. +-- @field #table Categories Categories Group categories. +-- @field Ops.Auftrag#AUFTRAG mission Attached mission. +-- @field #number carrierNmin Min number of assets. +-- @field #number carrierNmax Max number of assets. +-- @field #table carrierCategories Group categories. +-- @field #table carrierAttributes Generalized attribute, e.g. `{GROUP.Attribute.GROUND_INFANTRY}`. +-- @field #table carrierProperties Properties ([DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes)), e.g. `"Attack helicopters"` or `"Mobile AAA"`. + + +--- CHIEF class version. +-- @field #string version +CHIEF.version="0.7.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Event when asset groups die. +-- TODO: PLAYERTASK integration. +-- DONE: Let user specify amount of resources. +-- DONE: Tactical overview. +-- DONE: Add event for opsgroups on mission. +-- DONE: Add event for zone captured. +-- DONE: Limits of missions? +-- DONE: Create a good mission, which can be passed on to the COMMANDER. +-- DONE: Capture OPSZONEs. +-- DONE: Get list of own assets and capabilities. +-- DONE: Get list/overview of enemy assets etc. +-- DONE: Put all contacts into target list. Then make missions from them. +-- DONE: Set of interesting zones. +-- DONE: Add/remove spawned flightgroups to detection set. +-- DONE: Borderzones. +-- NOGO: Maybe it's possible to preselect the assets for the mission. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new CHIEF object and start the FSM. +-- @param #CHIEF self +-- @param #number Coalition Coalition side, e.g. `coaliton.side.BLUE`. Can also be passed as a string "red", "blue" or "neutral". +-- @param Core.Set#SET_GROUP AgentSet Set of agents (groups) providing intel. Default is an empty set. +-- @param #string Alias An *optional* alias how this object is called in the logs etc. +-- @return #CHIEF self +function CHIEF:New(Coalition, AgentSet, Alias) + + -- Set alias. + Alias=Alias or "CHIEF" + + -- coalition + if type(Coalition) == "string" then + if string.lower(Coalition) == "blue" then + Coalition = coalition.side.BLUE + elseif string.lower(Coalition) == "red" then + Coalition = coalition.side.RED + else + Coalition = coalition.side.NEUTRAL + end + end + + -- Inherit everything from INTEL class. + local self=BASE:Inherit(self, INTEL:New(AgentSet, Coalition, Alias)) --#CHIEF + + -- Defaults. + self:SetBorderZones() + self:SetConflictZones() + self:SetAttackZones() + self:SetThreatLevelRange() + + -- Init stuff. + self.Defcon=CHIEF.DEFCON.GREEN + self.strategy=CHIEF.Strategy.DEFENSIVE + self.TransportCategories = {Group.Category.HELICOPTER} + + -- Create a new COMMANDER. + self.commander=COMMANDER:New(Coalition) + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "MissionAssign", "*") -- Assign mission to a COMMANDER. + self:AddTransition("*", "MissionCancel", "*") -- Cancel mission. + + self:AddTransition("*", "TransportCancel", "*") -- Cancel transport. + + self:AddTransition("*", "OpsOnMission", "*") -- An OPSGROUP was send on a Mission (AUFTRAG). + + self:AddTransition("*", "ZoneCaptured", "*") -- + self:AddTransition("*", "ZoneLost", "*") -- + self:AddTransition("*", "ZoneEmpty", "*") -- + self:AddTransition("*", "ZoneAttacked", "*") -- + + self:AddTransition("*", "DefconChange", "*") -- Change defence condition. + self:AddTransition("*", "StrategyChange", "*") -- Change strategy condition. + + self:AddTransition("*", "LegionLost", "*") -- Out of our legions was lost to the enemy. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". + -- @function [parent=#CHIEF] Start + -- @param #CHIEF self + + --- Triggers the FSM event "Start" after a delay. + -- @function [parent=#CHIEF] __Start + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". + -- @param #CHIEF self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#CHIEF] __Stop + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status". + -- @function [parent=#CHIEF] Status + -- @param #CHIEF self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#CHIEF] __Status + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "DefconChange". + -- @function [parent=#CHIEF] DefconChange + -- @param #CHIEF self + -- @param #string Defcon New Defence Condition. + + --- Triggers the FSM event "DefconChange" after a delay. + -- @function [parent=#CHIEF] __DefconChange + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param #string Defcon New Defence Condition. + + --- On after "DefconChange" event. + -- @function [parent=#CHIEF] OnAfterDefconChange + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Defcon New Defence Condition. + + + --- Triggers the FSM event "StrategyChange". + -- @function [parent=#CHIEF] StrategyChange + -- @param #CHIEF self + -- @param #string Strategy New strategy. + + --- Triggers the FSM event "StrategyChange" after a delay. + -- @function [parent=#CHIEF] __StrategyChange + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param #string Strategy New strategy. + + --- On after "StrategyChange" event. + -- @function [parent=#CHIEF] OnAfterStrategyChange + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Strategy New strategy. + + + --- Triggers the FSM event "MissionAssign". + -- @function [parent=#CHIEF] MissionAssign + -- @param #CHIEF self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + --- Triggers the FSM event "MissionAssign" after a delay. + -- @function [parent=#CHIEF] __MissionAssign + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + --- On after "MissionAssign" event. + -- @function [parent=#CHIEF] OnAfterMissionAssign + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + --- Triggers the FSM event "MissionCancel". + -- @function [parent=#CHIEF] MissionCancel + -- @param #CHIEF self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionCancel" after a delay. + -- @function [parent=#CHIEF] __MissionCancel + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionCancel" event. + -- @function [parent=#CHIEF] OnAfterMissionCancel + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "TransportCancel". + -- @function [parent=#CHIEF] TransportCancel + -- @param #CHIEF self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportCancel" after a delay. + -- @function [parent=#CHIEF] __TransportCancel + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportCancel" event. + -- @function [parent=#CHIEF] OnAfterTransportCancel + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + + --- Triggers the FSM event "OpsOnMission". + -- @function [parent=#CHIEF] OpsOnMission + -- @param #CHIEF self + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "OpsOnMission" after a delay. + -- @function [parent=#CHIEF] __OpsOnMission + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "OpsOnMission" event. + -- @function [parent=#CHIEF] OnAfterOpsOnMission + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "ZoneCaptured". + -- @function [parent=#CHIEF] ZoneCaptured + -- @param #CHIEF self + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was captured. + + --- Triggers the FSM event "ZoneCaptured" after a delay. + -- @function [parent=#CHIEF] __ZoneCaptured + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was captured. + + --- On after "ZoneCaptured" event. + -- @function [parent=#CHIEF] OnAfterZoneCaptured + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was captured. + + --- Triggers the FSM event "ZoneLost". + -- @function [parent=#CHIEF] ZoneLost + -- @param #CHIEF self + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was lost. + + --- Triggers the FSM event "ZoneLost" after a delay. + -- @function [parent=#CHIEF] __ZoneLost + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was lost. + + --- On after "ZoneLost" event. + -- @function [parent=#CHIEF] OnAfterZoneLost + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was lost. + + --- Triggers the FSM event "ZoneEmpty". + -- @function [parent=#CHIEF] ZoneEmpty + -- @param #CHIEF self + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is empty now. + + --- Triggers the FSM event "ZoneEmpty" after a delay. + -- @function [parent=#CHIEF] __ZoneEmpty + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is empty now. + + --- On after "ZoneEmpty" event. + -- @function [parent=#CHIEF] OnAfterZoneEmpty + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is empty now. + + --- Triggers the FSM event "ZoneAttacked". + -- @function [parent=#CHIEF] ZoneAttacked + -- @param #CHIEF self + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is being attacked. + + --- Triggers the FSM event "ZoneAttacked" after a delay. + -- @function [parent=#CHIEF] __ZoneAttacked + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is being attacked. + + --- On after "ZoneAttacked" event. + -- @function [parent=#CHIEF] OnAfterZoneAttacked + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is being attacked. + + + --- Triggers the FSM event "LegionLost". + -- @function [parent=#CHIEF] LegionLost + -- @param #CHIEF self + -- @param Ops.Legion#LEGION Legion The legion that was lost. + -- @param DCS#coalition.side Coalition which captured the warehouse. + -- @param DCS#country.id Country which has captured the warehouse. + + --- Triggers the FSM event "LegionLost". + -- @function [parent=#CHIEF] __LegionLost + -- @param #CHIEF self + -- @param #number delay Delay in seconds. + -- @param Ops.Legion#LEGION Legion The legion that was lost. + -- @param DCS#coalition.side Coalition which captured the warehouse. + -- @param DCS#country.id Country which has captured the warehouse. + + --- On after "LegionLost" event. + -- @function [parent=#CHIEF] OnAfterLegionLost + -- @param #CHIEF self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Legion#LEGION Legion The legion that was lost. + -- @param DCS#coalition.side Coalition which captured the warehouse. + -- @param DCS#country.id Country which has captured the warehouse. + + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set this to be an air-to-any dispatcher, i.e. engaging air, ground and naval targets. This is the default anyway. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToAny() + + self:SetFilterCategory({}) + + return self +end + +--- Set this to be an air-to-air dispatcher. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToAir() + + self:SetFilterCategory({Unit.Category.AIRPLANE, Unit.Category.HELICOPTER}) + + return self +end + +--- Set this to be an air-to-ground dispatcher, i.e. engage only ground units +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToGround() + + self:SetFilterCategory({Unit.Category.GROUND_UNIT}) + + return self +end + +--- Set this to be an air-to-sea dispatcher, i.e. engage only naval units. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToSea() + + self:SetFilterCategory({Unit.Category.SHIP}) + + return self +end + +--- Set this to be an air-to-surface dispatcher, i.e. engaging ground and naval groups. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetAirToSurface() + + self:SetFilterCategory({Unit.Category.GROUND_UNIT, Unit.Category.SHIP}) + + return self +end + +--- Set a threat level range that will be engaged. Threat level is a number between 0 and 10, where 10 is a very dangerous threat. +-- Targets with threat level 0 are usually harmless. +-- @param #CHIEF self +-- @param #number ThreatLevelMin Min threat level. Default 1. +-- @param #number ThreatLevelMax Max threat level. Default 10. +-- @return #CHIEF self +function CHIEF:SetThreatLevelRange(ThreatLevelMin, ThreatLevelMax) + + self.threatLevelMin=ThreatLevelMin or 1 + self.threatLevelMax=ThreatLevelMax or 10 + + return self +end + +--- Set defence condition. +-- @param #CHIEF self +-- @param #string Defcon Defence condition. See @{#CHIEF.DEFCON}, e.g. `CHIEF.DEFCON.RED`. +-- @return #CHIEF self +function CHIEF:SetDefcon(Defcon) + + -- Check if valid string was passed. + local gotit=false + for _,defcon in pairs(CHIEF.DEFCON) do + if defcon==Defcon then + gotit=true + end + end + if not gotit then + self:E(self.lid..string.format("ERROR: Unknown DEFCON specified! Dont know defcon=%s", tostring(Defcon))) + return self + end + + -- Trigger event if defcon changed. + if Defcon~=self.Defcon then + self:DefconChange(Defcon) + end + + -- Set new DEFCON. + self.Defcon=Defcon + + return self +end + +--- Create a new resource list of required assets. +-- @param #CHIEF self +-- @param #string MissionType The mission type. +-- @param #number Nmin Min number of required assets. Default 1. +-- @param #number Nmax Max number of requried assets. Default 1. +-- @param #table Attributes Generalized attribute(s). Default `nil`. +-- @param #table Properties DCS attribute(s). Default `nil`. +-- @param #table Categories Group categories. +-- @return #CHIEF.Resources The newly created resource list table. +-- @return #CHIEF.Resource The resource object that was added. +function CHIEF:CreateResource(MissionType, Nmin, Nmax, Attributes, Properties, Categories) + + local resources={} + + local resource=self:AddToResource(resources, MissionType, Nmin, Nmax, Attributes, Properties, Categories) + + return resources, resource +end + +--- Add mission type and number of required assets to resource list. +-- @param #CHIEF self +-- @param #CHIEF.Resources Resource List of resources. +-- @param #string MissionType Mission Type. +-- @param #number Nmin Min number of required assets. Default 1. +-- @param #number Nmax Max number of requried assets. Default equal `Nmin`. +-- @param #table Attributes Generalized attribute(s). +-- @param #table Properties DCS attribute(s). Default `nil`. +-- @param #table Categories Group categories. +-- @return #CHIEF.Resource Resource table. +function CHIEF:AddToResource(Resource, MissionType, Nmin, Nmax, Attributes, Properties, Categories) + + -- Create new resource table. + local resource={} --#CHIEF.Resource + resource.MissionType=MissionType + resource.Nmin=Nmin or 1 + resource.Nmax=Nmax or Nmin + resource.Attributes=UTILS.EnsureTable(Attributes, true) + resource.Properties=UTILS.EnsureTable(Properties, true) + resource.Categories=UTILS.EnsureTable(Categories, true) + + -- Transport carrier parameters. + resource.carrierNmin=nil + resource.carrierNmax=nil + resource.carrierAttributes=nil + resource.carrierProperties=nil + resource.carrierCategories=nil + + -- Add to table. + table.insert(Resource, resource) + + -- Debug output. + if self.verbose>10 then + local text="Resource:" + for _,_r in pairs(Resource) do + local r=_r --#CHIEF.Resource + text=text..string.format("\nmission=%s, Nmin=%d, Nmax=%d, attribute=%s, properties=%s", r.MissionType, r.Nmin, r.Nmax, tostring(r.Attributes[1]), tostring(r.Properties[1])) + end + self:I(self.lid..text) + end + + return resource +end + +--- Define which assets will be transported and define the number and attributes/properties of the cargo carrier assets. +-- @param #CHIEF self +-- @param #CHIEF.Resource Resource Resource table. +-- @param #number Nmin Min number of required assets. Default 1. +-- @param #number Nmax Max number of requried assets. Default is equal to `Nmin`. +-- @param #table CarrierAttributes Generalized attribute(s) of the carrier assets. +-- @param #table CarrierProperties DCS attribute(s) of the carrier assets. +-- @param #table CarrierCategories Group categories of the carrier assets. +-- @return #CHIEF self +function CHIEF:AddTransportToResource(Resource, Nmin, Nmax, CarrierAttributes, CarrierProperties, CarrierCategories) + + Resource.carrierNmin=Nmin or 1 + Resource.carrierNmax=Nmax or Nmin + Resource.carrierCategories=UTILS.EnsureTable(CarrierCategories, true) + Resource.carrierAttributes=UTILS.EnsureTable(CarrierAttributes, true) + Resource.carrierProperties=UTILS.EnsureTable(CarrierProperties, true) + + return self +end + +--- Delete mission type from resource list. All running missions are cancelled. +-- @param #CHIEF self +-- @param #table Resource Resource table. +-- @param #string MissionType Mission Type. +-- @return #CHIEF self +function CHIEF:DeleteFromResource(Resource, MissionType) + + for i=#Resource,1,-1 do + local resource=Resource[i] --#CHIEF.Resource + if resource.MissionType==MissionType then + if resource.mission and resource.mission:IsNotOver() then + resource.mission:Cancel() + end + table.remove(Resource, i) + end + end + + return self +end + +--- Set number of assets requested for detected targets. +-- @param #CHIEF self +-- @param #number NassetsMin Min number of assets. Should be at least 1. Default 1. +-- @param #number NassetsMax Max number of assets. Default is same as `NassetsMin`. +-- @param #number ThreatLevel Only apply this setting if the target threat level is greater or equal this number. Default 0. +-- @param #string TargetCategory Only apply this setting if the target is of this category, e.g. `TARGET.Category.AIRCRAFT`. +-- @param #string MissionType Only apply this setting for this mission type, e.g. `AUFTRAG.Type.INTERCEPT`. +-- @param #string Nunits Only apply this setting if the number of enemy units is greater or equal this number. +-- @param #string Defcon Only apply this setting if this defense condition is in place. +-- @param #string Strategy Only apply this setting if this strategy is in currently. place. +-- @return #CHIEF self +function CHIEF:SetResponseOnTarget(NassetsMin, NassetsMax, ThreatLevel, TargetCategory, MissionType, Nunits, Defcon, Strategy) + + local bla={} --#CHIEF.AssetNumber + + bla.nAssetMin=NassetsMin or 1 + bla.nAssetMax=NassetsMax or bla.nAssetMin + bla.threatlevel=ThreatLevel or 0 + bla.targetCategory=TargetCategory + bla.missionType=MissionType + bla.nUnits=Nunits or 1 + bla.defcon=Defcon + bla.strategy=Strategy + + self.assetNumbers=self.assetNumbers or {} + + -- Add to table. + table.insert(self.assetNumbers, bla) + +end + +--- Add mission type and number of required assets to resource. +-- @param #CHIEF self +-- @param Ops.Target#TARGET Target The target. +-- @param #string MissionType Mission type. +-- @return #number Number of min assets. +-- @return #number Number of max assets. +function CHIEF:_GetAssetsForTarget(Target, MissionType) + + -- Threat level. + local threatlevel=Target:GetThreatLevelMax() + + -- Number of units. + local nUnits=Target.N0 + + -- Target category. + local targetcategory=Target:GetCategory() + + -- Debug info. + self:T(self.lid..string.format("Getting number of assets for target with TL=%d, Category=%s, nUnits=%s, MissionType=%s", threatlevel, targetcategory, nUnits, tostring(MissionType))) + + -- Candidates. + local candidates={} + + local threatlevelMatch=nil + for _,_assetnumber in pairs(self.assetNumbers or {}) do + local assetnumber=_assetnumber --#CHIEF.AssetNumber + + if (threatlevelMatch==nil and threatlevel>=assetnumber.threatlevel) or (threatlevelMatch~=nil and threatlevelMatch==threatlevel) then + + if threatlevelMatch==nil then + threatlevelMatch=threatlevel + end + + -- Number of other parameters matching. + local nMatch=0 + + -- Assume cand. + local cand=true + + if assetnumber.targetCategory~=nil then + if assetnumber.targetCategory==targetcategory then + nMatch=nMatch+1 + else + cand=false + end + end + + if MissionType and assetnumber.missionType~=nil then + if assetnumber.missionType==MissionType then + nMatch=nMatch+1 + else + cand=false + end + end + + if assetnumber.nUnits~=nil then + if assetnumber.nUnits>=nUnits then + nMatch=nMatch+1 + else + cand=false + end + end + + if assetnumber.defcon~=nil then + if assetnumber.defcon==self.Defcon then + nMatch=nMatch+1 + else + cand=false + end + end + + if assetnumber.strategy~=nil then + if assetnumber.strategy==self.strategy then + nMatch=nMatch+1 + else + cand=false + end + end + + -- Add to candidates. + if cand then + table.insert(candidates, {assetnumber=assetnumber, nMatch=nMatch}) + end + + end + + end + + if #candidates>0 then + + -- Return greater match. + local function _sort(a,b) + return a.nMatch>b.nMatch + end + + -- Sort table by matches. + table.sort(candidates, _sort) + + -- Pick the candidate with most matches. + local candidate=candidates[1] + + -- Asset number. + local an=candidate.assetnumber --#CHIEF.AssetNumber + + -- Debug message. + self:T(self.lid..string.format("Picking candidate with %d matches: NassetsMin=%d, NassetsMax=%d, ThreatLevel=%d, TargetCategory=%s, MissionType=%s, Defcon=%s, Strategy=%s", + candidate.nMatch, an.nAssetMin, an.nAssetMax, an.threatlevel, tostring(an.targetCategory), tostring(an.missionType), tostring(an.defcon), tostring(an.strategy))) + + -- Return number of assetes. + return an.nAssetMin, an.nAssetMax + else + return 1, 1 + end + +end + +--- Get defence condition. +-- @param #CHIEF self +-- @param #string Current Defence condition. See @{#CHIEF.DEFCON}, e.g. `CHIEF.DEFCON.RED`. +function CHIEF:GetDefcon(Defcon) + return self.Defcon +end + +--- Set limit for number of total or specific missions to be executed simultaniously. +-- @param #CHIEF self +-- @param #number Limit Number of max. mission of this type. Default 10. +-- @param #string MissionType Type of mission, e.g. `AUFTRAG.Type.BAI`. Default `"Total"` for total number of missions. +-- @return #CHIEF self +function CHIEF:SetLimitMission(Limit, MissionType) + self.commander:SetLimitMission(Limit, MissionType) + return self +end + +--- Set tactical overview on. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetTacticalOverviewOn() + self.tacview=true + return self +end + +--- Set tactical overview off. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:SetTacticalOverviewOff() + self.tacview=false + return self +end + + +--- Set strategy. +-- @param #CHIEF self +-- @param #string Strategy Strategy. See @{#CHIEF.strategy}, e.g. `CHIEF.Strategy.DEFENSIVE` (default). +-- @return #CHIEF self +function CHIEF:SetStrategy(Strategy) + + -- Trigger event if Strategy changed. + if Strategy~=self.strategy then + self:StrategyChange(Strategy) + end + + -- Set new Strategy. + self.strategy=Strategy + + return self +end + +--- Get current strategy. +-- @param #CHIEF self +-- @return #string Strategy. +function CHIEF:GetStrategy() + return self.strategy +end + +--- Get defence condition. +-- @param #CHIEF self +-- @param #string Current Defence condition. See @{#CHIEF.DEFCON}, e.g. `CHIEF.DEFCON.RED`. +function CHIEF:GetDefcon(Defcon) + return self.Defcon +end + + +--- Get the commander. +-- @param #CHIEF self +-- @return Ops.Commander#COMMANDER The commander. +function CHIEF:GetCommander() + return self.commander +end + + +--- Add an AIRWING to the chief's commander. +-- @param #CHIEF self +-- @param Ops.Airwing#AIRWING Airwing The airwing to add. +-- @return #CHIEF self +function CHIEF:AddAirwing(Airwing) + + -- Add airwing to the commander. + self:AddLegion(Airwing) + + return self +end + +--- Add a BRIGADE to the chief's commander. +-- @param #CHIEF self +-- @param Ops.Brigade#BRIGADE Brigade The brigade to add. +-- @return #CHIEF self +function CHIEF:AddBrigade(Brigade) + + -- Add brigade to the commander. + self:AddLegion(Brigade) + + return self +end + +--- Add a FLEET to the chief's commander. +-- @param #CHIEF self +-- @param Ops.Fleet#FLEET Fleet The fleet to add. +-- @return #CHIEF self +function CHIEF:AddFleet(Fleet) + + -- Add fleet to the commander. + self:AddLegion(Fleet) + + return self +end + +--- Add a LEGION to the chief's commander. +-- @param #CHIEF self +-- @param Ops.Legion#LEGION Legion The legion to add. +-- @return #CHIEF self +function CHIEF:AddLegion(Legion) + + -- Set chief of the legion. + Legion.chief=self + + -- Add legion to the commander. + self.commander:AddLegion(Legion) + + return self +end + +--- Remove a LEGION to the chief's commander. +-- @param #CHIEF self +-- @param Ops.Legion#LEGION Legion The legion to add. +-- @return #CHIEF self +function CHIEF:RemoveLegion(Legion) + + -- Set chief of the legion. + Legion.chief=nil + + -- Add legion to the commander. + self.commander:RemoveLegion(Legion) + + return self +end + + +--- Add mission to mission queue of the COMMANDER. +-- @param #CHIEF self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be added. +-- @return #CHIEF self +function CHIEF:AddMission(Mission) + + Mission.chief=self + + Mission.statusChief=AUFTRAG.Status.PLANNED + + self:I(self.lid..string.format("Adding mission #%d", Mission.auftragsnummer)) + + self.commander:AddMission(Mission) + + return self +end + +--- Remove mission from queue. +-- @param #CHIEF self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #CHIEF self +function CHIEF:RemoveMission(Mission) + + Mission.chief=nil + + self.commander:RemoveMission(Mission) + + return self +end + +--- Add transport to transport queue of the COMMANDER. +-- @param #CHIEF self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport Transport to be added. +-- @return #CHIEF self +function CHIEF:AddOpsTransport(Transport) + + Transport.chief=self + + self.commander:AddOpsTransport(Transport) + + return self +end + +--- Remove transport from queue. +-- @param #CHIEF self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport Transport to be removed. +-- @return #CHIEF self +function CHIEF:RemoveTransport(Transport) + + Transport.chief=nil + + self.commander:RemoveTransport(Transport) + + return self +end + +--- Add target. +-- @param #CHIEF self +-- @param Ops.Target#TARGET Target Target object to be added. +-- @return #CHIEF self +function CHIEF:AddTarget(Target) + + if not self:IsTarget(Target) then + Target.chief=self + table.insert(self.targetqueue, Target) + end + + return self +end + +--- Check if a TARGET is already in the queue. +-- @param #CHIEF self +-- @param Ops.Target#TARGET Target Target object to be added. +-- @return #boolean If `true`, target exists in the target queue. +function CHIEF:IsTarget(Target) + + for _,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + if target.uid==Target.uid or target:GetName()==Target:GetName() then + return true + end + end + + return false +end + +--- Remove target from queue. +-- @param #CHIEF self +-- @param Ops.Target#TARGET Target The target. +-- @return #CHIEF self +function CHIEF:RemoveTarget(Target) + + for i,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + + if target.uid==Target.uid then + self:T(self.lid..string.format("Removing target %s from queue", Target.name)) + table.remove(self.targetqueue, i) + break + end + + end + + return self +end + +--- Add strategically important zone. +-- By default two resource lists are created. One for the case that the zone is empty and the other for the case that the zone is occupied. +-- +-- Occupied: +-- +-- * `AUFTRAG.Type.ARTY` with Nmin=1, Nmax=2 +-- * `AUFTRAG.Type.CASENHANCED` with Nmin=1, Nmax=2 +-- +-- Empty: +-- +-- * `AUFTRAG.Type.ONGURAD` with Nmin=0 and Nmax=1 assets, Attribute=`GROUP.Attribute.GROUND_TANK`. +-- * `AUFTRAG.Type.ONGURAD` with Nmin=0 and Nmax=1 assets, Attribute=`GROUP.Attribute.GROUND_IFV`. +-- * `AUFTRAG.Type.ONGUARD` with Nmin=1 and Nmax=3 assets, Attribute=`GROUP.Attribute.GROUND_INFANTRY`. +-- * `AUFTRAG.Type.OPSTRANSPORT` with Nmin=0 and Nmax=1 assets, Attribute=`GROUP.Attribute.AIR_TRANSPORTHELO` or `GROUP.Attribute.GROUND_APC`. This asset is used to transport the infantry groups. +-- +-- Resources can be created with the @{#CHIEF.CreateResource} and @{#CHIEF.AddToResource} functions. +-- +-- @param #CHIEF self +-- @param Ops.OpsZone#OPSZONE OpsZone OPS zone object. +-- @param #number Priority Priority. Default 50. +-- @param #number Importance Importance. Default `#nil`. +-- @param #CHIEF.Resources ResourceOccupied (Optional) Resources used then zone is occupied by the enemy. +-- @param #CHIEF.Resources ResourceEmpty (Optional) Resources used then zone is empty. +-- @return #CHIEF.StrategicZone The strategic zone. +function CHIEF:AddStrategicZone(OpsZone, Priority, Importance, ResourceOccupied, ResourceEmpty) + + local stratzone={} --#CHIEF.StrategicZone + + stratzone.opszone=OpsZone + stratzone.prio=Priority or 50 + stratzone.importance=Importance + + stratzone.missions={} + + -- Start ops zone. + if OpsZone:IsStopped() then + OpsZone:Start() + end + + -- Add resources if zone is occupied. + if ResourceOccupied then + stratzone.resourceOccup=UTILS.DeepCopy(ResourceOccupied) + else + stratzone.resourceOccup=self:CreateResource(AUFTRAG.Type.ARTY, 1, 2) + self:AddToResource(stratzone.resourceOccup, AUFTRAG.Type.CASENHANCED, 1, 2) + end + + -- Add resources if zone is empty + if ResourceEmpty then + stratzone.resourceEmpty=UTILS.DeepCopy(ResourceEmpty) + else + local resourceEmpty, resourceInfantry=self:CreateResource(AUFTRAG.Type.ONGUARD, 1, 3, GROUP.Attribute.GROUND_INFANTRY) + self:AddToResource(resourceEmpty, AUFTRAG.Type.ONGUARD, 0, 1, GROUP.Attribute.GROUND_TANK) + self:AddToResource(resourceEmpty, AUFTRAG.Type.ONGUARD, 0, 1, GROUP.Attribute.GROUND_IFV) + self:AddTransportToResource(resourceInfantry, 0, 1, {GROUP.Attribute.AIR_TRANSPORTHELO, GROUP.Attribute.GROUND_APC}) + stratzone.resourceEmpty=resourceEmpty + end + + -- Add to table. + table.insert(self.zonequeue, stratzone) + + -- Add chief so we get informed when something happens. + OpsZone:_AddChief(self) + + return stratzone +end + +--- Set the resource list of missions and assets employed when the zone is empty. +-- @param #CHIEF self +-- @param #CHIEF.StrategicZone StrategicZone The strategic zone. +-- @param #CHIEF.Resource Resource Resource list of missions and assets. +-- @param #boolean NoCopy If `true`, do **not** create a deep copy of the resource. +-- @return #CHIEF self +function CHIEF:SetStrategicZoneResourceEmpty(StrategicZone, Resource, NoCopy) + if NoCopy then + StrategicZone.resourceEmpty=Resource + else + StrategicZone.resourceEmpty=UTILS.DeepCopy(Resource) + end + return self +end + +--- Set the resource list of missions and assets employed when the zone is occupied by the enemy. +-- @param #CHIEF self +-- @param #CHIEF.StrategicZone StrategicZone The strategic zone. +-- @param #CHIEF.Resource Resource Resource list of missions and assets. +-- @param #boolean NoCopy If `true`, do **not** create a deep copy of the resource. +-- @return #CHIEF self +function CHIEF:SetStrategicZoneResourceOccupied(StrategicZone, Resource, NoCopy) + if NoCopy then + StrategicZone.resourceOccup=Resource + else + StrategicZone.resourceOccup=UTILS.DeepCopy(Resource) + end + return self +end + +--- Get the resource list of missions and assets employed when the zone is empty. +-- @param #CHIEF self +-- @param #CHIEF.StrategicZone StrategicZone The strategic zone. +-- @return #CHIEF.Resource Resource list of missions and assets. +function CHIEF:GetStrategicZoneResourceEmpty(StrategicZone) + return StrategicZone.resourceEmpty +end + +--- Get the resource list of missions and assets employed when the zone is occupied by the enemy. +-- @param #CHIEF self +-- @param #CHIEF.StrategicZone StrategicZone The strategic zone. +-- @return #CHIEF.Resource Resource list of missions and assets. +function CHIEF:GetStrategicZoneResourceOccupied(StrategicZone) + return StrategicZone.resourceOccup +end + + +--- Remove strategically important zone. All runing missions are cancelled. +-- @param #CHIEF self +-- @param Ops.OpsZone#OPSZONE OpsZone OPS zone object. +-- @param #number Delay Delay in seconds before the zone is removed. Default immidiately. +-- @return #CHIEF self +function CHIEF:RemoveStrategicZone(OpsZone, Delay) + + if Delay and Delay>0 then + -- Delayed call. + self:ScheduleOnce(Delay, CHIEF.RemoveStrategicZone, self, OpsZone) + else + + -- Loop over all zones in the queue. + for i=#self.zonequeue,1,-1 do + local stratzone=self.zonequeue[i] --#CHIEF.StrategicZone + + if OpsZone.zoneName==stratzone.opszone.zoneName then + + -- Debug info. + self:T(self.lid..string.format("Removing OPS zone \"%s\" from queue! All running missions will be cancelled", OpsZone.zoneName)) + + -- Cancel running missions. + for _,_resource in pairs(stratzone.resourceEmpty) do + local resource=_resource --#CHIEF.Resource + if resource.mission and resource.mission:IsNotOver() then + resource.mission:Cancel() + end + end + + -- Cancel running missions. + for _,_resource in pairs(stratzone.resourceOccup) do + local resource=_resource --#CHIEF.Resource + if resource.mission and resource.mission:IsNotOver() then + resource.mission:Cancel() + end + end + + -- Remove from table. + table.remove(self.zonequeue, i) + + -- Done! + return self + end + end + + end + + return self +end + +--- Add a rearming zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE RearmingZone Rearming zone. +-- @return Ops.Brigade#BRIGADE.SupplyZone The rearming zone data. +function CHIEF:AddRearmingZone(RearmingZone) + + -- Hand over to commander. + local supplyzone=self.commander:AddRearmingZone(RearmingZone) + + return supplyzone +end + +--- Add a refuelling zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE RefuellingZone Refuelling zone. +-- @return Ops.Brigade#BRIGADE.SupplyZone The refuelling zone data. +function CHIEF:AddRefuellingZone(RefuellingZone) + + -- Hand over to commander. + local supplyzone=self.commander:AddRefuellingZone(RefuellingZone) + + return supplyzone +end + +--- Add a CAP zone. Flights will engage detected targets inside this zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone CAP Zone. Has to be a circular zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet. +-- @param #number Speed Orbit speed in KIAS. 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 30 NM. +-- @return Ops.Airwing#AIRWING.PatrolZone The CAP zone data. +function CHIEF:AddCapZone(Zone, Altitude, Speed, Heading, Leg) + + -- Hand over to commander. + local zone=self.commander:AddCapZone(Zone, Altitude, Speed, Heading, Leg) + + return zone +end + +--- Add a GCI CAP. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone Zone, where the flight orbits. +-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet. +-- @param #number Speed Orbit speed in KIAS. 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 30 NM. +-- @return Ops.Airwing#AIRWING.PatrolZone The CAP zone data. +function CHIEF:AddGciCapZone(Zone, Altitude, Speed, Heading, Leg) + + -- Hand over to commander. + local zone=self.commander:AddGciCapZone(Zone, Altitude, Speed, Heading, Leg) + + return zone +end + +--- Remove a GCI CAP +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone Zone, where the flight orbits. +function CHIEF:RemoveGciCapZone(Zone) + + -- Hand over to commander. + local zone=self.commander:RemoveGciCapZone(Zone) + + return zone +end + +--- Add an AWACS zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet. +-- @param #number Speed Orbit speed in KIAS. 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 30 NM. +-- @return Ops.Airwing#AIRWING.PatrolZone The AWACS zone data. +function CHIEF:AddAwacsZone(Zone, Altitude, Speed, Heading, Leg) + + -- Hand over to commander. + local zone=self.commander:AddAwacsZone(Zone, Altitude, Speed, Heading, Leg) + + return zone +end + +--- Remove a AWACS zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone Zone, where the flight orbits. +function CHIEF:RemoveAwacsZone(Zone) + + -- Hand over to commander. + local zone=self.commander:RemoveAwacsZone(Zone) + + return zone +end + +--- Add a refuelling tanker zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet. +-- @param #number Speed Orbit speed in KIAS. 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 30 NM. +-- @param #number RefuelSystem Refuelling system. +-- @return Ops.Airwing#AIRWING.TankerZone The tanker zone data. +function CHIEF:AddTankerZone(Zone, Altitude, Speed, Heading, Leg, RefuelSystem) + + -- Hand over to commander. + local zone=self.commander:AddTankerZone(Zone, Altitude, Speed, Heading, Leg, RefuelSystem) + + return zone +end + +--- Remove a refuelling tanker zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone Zone, where the flight orbits. +function CHIEF:RemoveTankerZone(Zone) + + -- Hand over to commander. + local zone=self.commander:RemoveTankerZone(Zone) + + return zone +end + +--- Set border zone set, defining your territory. +-- +-- * Detected enemy troops in these zones will trigger defence condition `RED`. +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.DEFENSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Set#SET_ZONE BorderZoneSet Set of zones defining our borders. +-- @return #CHIEF self +function CHIEF:SetBorderZones(BorderZoneSet) + + -- Border zones. + self.borderzoneset=BorderZoneSet or SET_ZONE:New() + + return self +end + +--- Add a zone defining your territory. +-- +-- * Detected enemy troops in these zones will trigger defence condition `RED`. +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.DEFENSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone The zone to be added. +-- @return #CHIEF self +function CHIEF:AddBorderZone(Zone) + + -- Add a border zone. + self.borderzoneset:AddZone(Zone) + + return self +end + +--- Remove a border zone defining your territory. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone The zone to be removed. +-- @return #CHIEF self +function CHIEF:RemoveBorderZone(Zone) + + -- Add a border zone. + self.borderzoneset:Remove(Zone:GetName()) + + return self +end + +--- Set conflict zone set. +-- +-- * Detected enemy troops in these zones will trigger defence condition `YELLOW`. +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.OFFENSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Set#SET_ZONE ZoneSet Set of zones. +-- @return #CHIEF self +function CHIEF:SetConflictZones(ZoneSet) + + -- Conflict zones. + self.yellowzoneset=ZoneSet or SET_ZONE:New() + + return self +end + +--- Add a conflict zone. +-- +-- * Detected enemy troops in these zones will trigger defence condition `YELLOW`. +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.OFFENSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone The zone to be added. +-- @return #CHIEF self +function CHIEF:AddConflictZone(Zone) + + -- Add a conflict zone. + self.yellowzoneset:AddZone(Zone) + + return self +end + +--- Remove a conflict zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone The zone to be removed. +-- @return #CHIEF self +function CHIEF:RemoveConflictZone(Zone) + + -- Add a conflict zone. + self.yellowzoneset:Remove(Zone:GetName()) + + return self +end + + +--- Set attack zone set. +-- +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.AGGRESSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Set#SET_ZONE ZoneSet Set of zones. +-- @return #CHIEF self +function CHIEF:SetAttackZones(ZoneSet) + + -- Attacak zones. + self.engagezoneset=ZoneSet or SET_ZONE:New() + + return self +end + +--- Add an attack zone. +-- +-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.AGGRESSIVE`. +-- +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone The zone to add. +-- @return #CHIEF self +function CHIEF:AddAttackZone(Zone) + + -- Add an attack zone. + self.engagezoneset:AddZone(Zone) + + return self +end + +--- Remove an attack zone. +-- @param #CHIEF self +-- @param Core.Zone#ZONE Zone The zone to be removed. +-- @return #CHIEF self +function CHIEF:RemoveAttackZone(Zone) + + -- Add an attack zone. + self.engagezoneset:Remove(Zone:GetName()) + + return self +end + +--- Allow chief to use GROUND units for transport tasks. Helicopters are still preferred, and be aware there's no check as of now +-- if a destination can be reached on land. +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:AllowGroundTransport() + env.warning("WARNING: CHIEF:AllowGroundTransport() is deprecated and will be removed in the future!") + self.TransportCategories = {Group.Category.GROUND, Group.Category.HELICOPTER} + return self +end + +--- Forbid chief to use GROUND units for transport tasks. Restrict to Helicopters. This is the default +-- @param #CHIEF self +-- @return #CHIEF self +function CHIEF:ForbidGroundTransport() + env.warning("WARNING: CHIEF:ForbidGroundTransport() is deprecated and will be removed in the future!") + self.TransportCategories = {Group.Category.HELICOPTER} + return self +end + +--- Check if current strategy is passive. +-- @param #CHIEF self +-- @return #boolean If `true`, strategy is passive. +function CHIEF:IsPassive() + return self.strategy==CHIEF.Strategy.PASSIVE +end + +--- Check if current strategy is defensive. +-- @param #CHIEF self +-- @return #boolean If `true`, strategy is defensive. +function CHIEF:IsDefensive() + return self.strategy==CHIEF.Strategy.DEFENSIVE +end + +--- Check if current strategy is offensive. +-- @param #CHIEF self +-- @return #boolean If `true`, strategy is offensive. +function CHIEF:IsOffensive() + return self.strategy==CHIEF.Strategy.OFFENSIVE +end + +--- Check if current strategy is aggressive. +-- @param #CHIEF self +-- @return #boolean If `true`, strategy is agressive. +function CHIEF:IsAgressive() + return self.strategy==CHIEF.Strategy.AGGRESSIVE +end + +--- Check if current strategy is total war. +-- @param #CHIEF self +-- @return #boolean If `true`, strategy is total war. +function CHIEF:IsTotalWar() + return self.strategy==CHIEF.Strategy.TOTALWAR +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function CHIEF:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting Chief of Staff") + self:I(self.lid..text) + + -- Start parent INTEL. + self:GetParent(self).onafterStart(self, From, Event, To) + + -- Start commander. + if self.commander then + if self.commander:GetState()=="NotReadyYet" then + self.commander:Start() + end + end + +end + +--- On after "Status" event. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function CHIEF:onafterStatus(From, Event, To) + + -- Start parent INTEL. + self:GetParent(self).onafterStatus(self, From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + --- + -- CONTACTS: Mission Cleanup + --- + + -- Clean up missions where the contact was lost. + for _,_contact in pairs(self.ContactsLost) do + local contact=_contact --Ops.Intel#INTEL.Contact + + if contact.mission and contact.mission:IsNotOver() then + + -- Debug info. + local text=string.format("Lost contact to target %s! %s mission %s will be cancelled.", contact.groupname, contact.mission.type:upper(), contact.mission.name) + MESSAGE:New(text, 120, "CHIEF"):ToAll() + self:T(self.lid..text) + + -- Cancel this mission. + contact.mission:Cancel() + + end + + -- Remove a target from the queue. + if contact.target then + self:RemoveTarget(contact.target) + end + + end + + --- + -- CONTACTS: Create new TARGETS + --- + + -- Create TARGETs for all new contacts. + self.Nborder=0 ; self.Nconflict=0 ; self.Nattack=0 + for _,_contact in pairs(self.Contacts) do + local contact=_contact --Ops.Intel#INTEL.Contact + local group=contact.group --Wrapper.Group#GROUP + + -- Check if contact inside of our borders. + local inred=self:CheckGroupInBorder(group) + if inred then + self.Nborder=self.Nborder+1 + end + + -- Check if contact is in the conflict zones. + local inyellow=self:CheckGroupInConflict(group) + if inyellow then + self.Nconflict=self.Nconflict+1 + end + + -- Check if contact is in the attack zones. + local inattack=self:CheckGroupInAttack(group) + if inattack then + self.Nattack=self.Nattack+1 + end + + + -- Check if this is not already a target. + if not contact.target then + + -- Create a new TARGET of the contact group. + local Target=TARGET:New(contact.group) + + -- Set to contact. + contact.target=Target + + -- Set contact to target. Might be handy. + Target.contact=contact + + -- Add target to queue. + self:AddTarget(Target) + + end + + end + + + + --- + -- Defcon + --- + + -- TODO: Need to introduce time check to avoid fast oscillation between different defcon states in case groups move in and out of the zones. + if self.Nborder>0 then + self:SetDefcon(CHIEF.DEFCON.RED) + elseif self.Nconflict>0 then + self:SetDefcon(CHIEF.DEFCON.YELLOW) + else + self:SetDefcon(CHIEF.DEFCON.GREEN) + end + + --- + -- Check Target Queue + --- + + -- Check target queue and assign missions to new targets. + self:CheckTargetQueue() + + -- Loop over targets. + for _,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + + if target and target:IsAlive() and target.chief and target.mission and target.mission:IsNotOver() then + + local inborder=self:CheckTargetInZones(target, self.borderzoneset) + + local inyellow=self:CheckTargetInZones(target, self.yellowzoneset) + + local inattack=self:CheckTargetInZones(target, self.engagezoneset) + + if self.strategy==CHIEF.Strategy.PASSIVE then + + -- Passive: No targets are engaged at all. + self:T(self.lid..string.format("Cancelling mission for target %s as strategy is PASSIVE", target:GetName())) + target.mission:Cancel() + + elseif self.strategy==CHIEF.Strategy.DEFENSIVE then + + -- Defensive: Cancel if not in border. + if not inborder then + self:T(self.lid..string.format("Cancelling mission for target %s as strategy is DEFENSIVE and not inside border", target:GetName())) + target.mission:Cancel() + end + + elseif self.strategy==CHIEF.Strategy.OFFENSIVE then + + -- Offensive: Cancel if not in border or conflict. + if not (inborder or inyellow) then + self:T(self.lid..string.format("Cancelling mission for target %s as strategy is OFFENSIVE and not inside border or conflict", target:GetName())) + target.mission:Cancel() + end + + elseif self.strategy==CHIEF.Strategy.AGGRESSIVE then + + -- Aggessive: Cancel if not in border, conflict or attack. + if not (inborder or inyellow or inattack) then + self:T(self.lid..string.format("Cancelling mission for target %s as strategy is AGGRESSIVE and not inside border, conflict or attack", target:GetName())) + target.mission:Cancel() + end + + elseif self.strategy==CHIEF.Strategy.TOTALWAR then + + -- Total War: No missions are cancelled. + + end + + end + + end + + --- + -- Check Strategic Zone Queue + --- + + -- Check target queue and assign missions to new targets. + self:CheckOpsZoneQueue() + + + -- Display tactival overview. + self:_TacticalOverview() + + --- + -- Info General + --- + + if self.verbose>=1 then + local Nassets=self.commander:CountAssets() + local Ncontacts=#self.Contacts + local Nmissions=#self.commander.missionqueue + local Ntargets=#self.targetqueue + + -- Info message + local text=string.format("Defcon=%s Strategy=%s: Assets=%d, Contacts=%d [Border=%d, Conflict=%d, Attack=%d], Targets=%d, Missions=%d", + self.Defcon, self.strategy, Nassets, Ncontacts, self.Nborder, self.Nconflict, self.Nattack, Ntargets, Nmissions) + self:I(self.lid..text) + + end + + --- + -- Info Contacts + --- + + -- Info about contacts. + if self.verbose>=2 and #self.Contacts>0 then + local text="Contacts:" + for i,_contact in pairs(self.Contacts) do + local contact=_contact --Ops.Intel#INTEL.Contact + + local mtext="N/A" + if contact.mission then + mtext=string.format("\"%s\" [%s] %s", contact.mission:GetName(), contact.mission:GetType(), contact.mission.status:upper()) + end + text=text..string.format("\n[%d] %s Type=%s (%s): Threat=%d Mission=%s", i, contact.groupname, contact.categoryname, contact.typename, contact.threatlevel, mtext) + end + self:I(self.lid..text) + end + + --- + -- Info Targets + --- + + if self.verbose>=3 and #self.targetqueue>0 then + local text="Targets:" + for i,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + + local mtext="N/A" + if target.mission then + mtext=string.format("\"%s\" [%s] %s", target.mission:GetName(), target.mission:GetType(), target.mission.status:upper()) + end + text=text..string.format("\n[%d] %s: Category=%s, prio=%d, importance=%d, alive=%s [%.1f/%.1f], Mission=%s", + i, target:GetName(), target.category, target.prio, target.importance or -1, tostring(target:IsAlive()), target:GetLife(), target:GetLife0(), mtext) + end + self:I(self.lid..text) + end + + --- + -- Info Missions + --- + + -- Mission queue. + if self.verbose>=4 and #self.commander.missionqueue>0 then + local text="Mission queue:" + for i,_mission in pairs(self.commander.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + local target=mission:GetTargetName() or "unknown" + + text=text..string.format("\n[%d] %s (%s): status=%s, target=%s", i, mission.name, mission.type, mission.status, target) + end + self:I(self.lid..text) + end + + --- + -- Info Strategic Zones + --- + + -- Loop over targets. + if self.verbose>=4 and #self.zonequeue>0 then + local text="Zone queue:" + for i,_stratzone in pairs(self.zonequeue) do + local stratzone=_stratzone --#CHIEF.StrategicZone + + -- OPS zone object. + local opszone=stratzone.opszone + + local owner=UTILS.GetCoalitionName(opszone.ownerCurrent) + local prevowner=UTILS.GetCoalitionName(opszone.ownerPrevious) + + text=text..string.format("\n[%d] %s [%s]: owner=%s [%s] (prio=%d, importance=%s): Blue=%d, Red=%d, Neutral=%d", + i, opszone.zone:GetName(), opszone:GetState(), owner, prevowner, stratzone.prio, tostring(stratzone.importance), opszone.Nblu, opszone.Nred, opszone.Nnut) + + end + self:I(self.lid..text) + end + + + --- + -- Info Assets + --- + + if self.verbose>=5 then + local text="Assets:" + for _,missiontype in pairs(AUFTRAG.Type) do + local N=self.commander:CountAssets(nil, missiontype) + if N>0 then + text=text..string.format("\n- %s: %d", missiontype, N) + end + end + self:I(self.lid..text) + + local text="Assets:" + for _,attribute in pairs(WAREHOUSE.Attribute) do + local N=self.commander:CountAssets(nil, nil, attribute) + if N>0 or self.verbose>=10 then + text=text..string.format("\n- %s: %d", attribute, N) + end + end + self:T(self.lid..text) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "MissionAssignToAny" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. +function CHIEF:onafterMissionAssign(From, Event, To, Mission, Legions) + + if self.commander then + self:T(self.lid..string.format("Assigning mission %s (%s) to COMMANDER", Mission.name, Mission.type)) + Mission.chief=self + Mission.statusChief=AUFTRAG.Status.QUEUED + self.commander:MissionAssign(Mission, Legions) + else + self:E(self.lid..string.format("Mission cannot be assigned as no COMMANDER is defined!")) + end + +end + +--- On after "MissionCancel" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +function CHIEF:onafterMissionCancel(From, Event, To, Mission) + + -- Debug info. + self:T(self.lid..string.format("Cancelling mission %s (%s) in status %s", Mission.name, Mission.type, Mission.status)) + + -- Set status to CANCELLED. + Mission.statusChief=AUFTRAG.Status.CANCELLED + + if Mission:IsPlanned() then + + -- Mission is still in planning stage. Should not have any LEGIONS assigned ==> Just remove it form the COMMANDER queue. + self:RemoveMission(Mission) + + else + + -- COMMANDER will cancel mission. + if Mission.commander then + Mission.commander:MissionCancel(Mission) + end + + end + +end + +--- On after "TransportCancel" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. +function CHIEF:onafterTransportCancel(From, Event, To, Transport) + + -- Debug info. + self:T(self.lid..string.format("Cancelling transport UID=%d in status %s", Transport.uid, Transport:GetState())) + + if Transport:IsPlanned() then + + -- Mission is still in planning stage. Should not have any LEGIONS assigned ==> Just remove it form the COMMANDER queue. + self:RemoveTransport(Transport) + + else + + -- COMMANDER will cancel mission. + if Transport.commander then + Transport.commander:TransportCancel(Transport) + end + + end + +end + +--- On after "DefconChange" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Defcon New defence condition. +function CHIEF:onafterDefconChange(From, Event, To, Defcon) + self:T(self.lid..string.format("Changing Defcon from %s --> %s", self.Defcon, Defcon)) +end + +--- On after "StrategyChange" event. +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Strategy +function CHIEF:onafterStrategyChange(From, Event, To, Strategy) + self:T(self.lid..string.format("Changing Strategy from %s --> %s", self.strategy, Strategy)) +end + +--- On after "OpsOnMission". +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup Ops group on mission +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function CHIEF:onafterOpsOnMission(From, Event, To, OpsGroup, Mission) + -- Debug info. + self:T(self.lid..string.format("Group %s on mission %s [%s]", OpsGroup:GetName(), Mission:GetName(), Mission:GetType())) +end + + +--- On after "ZoneCaptured". +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsZone#OPSZONE OpsZone The zone that was captured by us. +function CHIEF:onafterZoneCaptured(From, Event, To, OpsZone) + -- Debug info. + self:T(self.lid..string.format("Zone %s captured!", OpsZone:GetName())) +end + + +--- On after "ZoneLost". +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsZone#OPSZONE OpsZone The zone that was lost. +function CHIEF:onafterZoneLost(From, Event, To, OpsZone) + -- Debug info. + self:T(self.lid..string.format("Zone %s lost!", OpsZone:GetName())) +end + +--- On after "ZoneEmpty". +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsZone#OPSZONE OpsZone The zone that is empty now. +function CHIEF:onafterZoneEmpty(From, Event, To, OpsZone) + -- Debug info. + self:T(self.lid..string.format("Zone %s empty!", OpsZone:GetName())) +end + +--- On after "ZoneAttacked". +-- @param #CHIEF self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsZone#OPSZONE OpsZone The zone that being attacked. +function CHIEF:onafterZoneAttacked(From, Event, To, OpsZone) + -- Debug info. + self:T(self.lid..string.format("Zone %s attacked!", OpsZone:GetName())) +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Target Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Display tactical overview. +-- @param #CHIEF self +function CHIEF:_TacticalOverview() + + if self.tacview then + + local NassetsTotal=self.commander:CountAssets() + local NassetsStock=self.commander:CountAssets(true) + local Ncontacts=#self.Contacts + local NmissionsTotal=#self.commander.missionqueue + local NmissionsRunni=self.commander:CountMissions(AUFTRAG.Type, true) + local Ntargets=#self.targetqueue + local Nzones=#self.zonequeue + local Nagents=self.detectionset:CountAlive() + + -- Info message + local text=string.format("Tactical Overview\n") + text=text..string.format("=================\n") + + -- Strategy and defcon info. + text=text..string.format("Strategy: %s - Defcon: %s - Agents=%s\n", self.strategy, self.Defcon, Nagents) + + -- Contact info. + text=text..string.format("Contacts: %d [Border=%d, Conflict=%d, Attack=%d]\n", Ncontacts, self.Nborder, self.Nconflict, self.Nattack) + + -- Asset info. + text=text..string.format("Assets: %d [Active=%d, Stock=%d]\n", NassetsTotal, NassetsTotal-NassetsStock, NassetsStock) + + -- Target info. + text=text..string.format("Targets: %d\n", Ntargets) + + -- Mission info. + text=text..string.format("Missions: %d [Running=%d/%d - Success=%d, Failure=%d]\n", NmissionsTotal, NmissionsRunni, self:GetMissionLimit("Total"), self.Nsuccess, self.Nfailure) + for _,mtype in pairs(AUFTRAG.Type) do + local n=self.commander:CountMissions(mtype) + if n>0 then + local N=self.commander:CountMissions(mtype, true) + local limit=self:GetMissionLimit(mtype) + text=text..string.format(" - %s: %d [Running=%d/%d]\n", mtype, n, N, limit) + end + end + + -- Strategic zone info. + text=text..string.format("Strategic Zones: %d\n", Nzones) + for _,_stratzone in pairs(self.zonequeue) do + local stratzone=_stratzone --#CHIEF.StrategicZone + local owner=stratzone.opszone:GetOwnerName() + text=text..string.format(" - %s: %s - %s [I=%d, P=%d]\n", stratzone.opszone:GetName(), owner, stratzone.opszone:GetState(), stratzone.importance or 0, stratzone.prio or 0) + end + + local Ntransports=#self.commander.transportqueue + if Ntransports>0 then + text=text..string.format("Transports: %d\n", Ntransports) + for _,_transport in pairs(self.commander.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + text=text..string.format(" - %s", transport:GetState()) + end + end + + -- Message to coalition. + MESSAGE:New(text, 60, nil, true):ToCoalition(self.coalition) + + -- Output to log. + if self.verbose>=4 then + self:I(self.lid..text) + end + + end + +end + + +--- Check target queue and assign ONE valid target by adding it to the mission queue of the COMMANDER. +-- @param #CHIEF self +function CHIEF:CheckTargetQueue() + + -- Number of missions. + local Ntargets=#self.targetqueue + + -- Treat special cases. + if Ntargets==0 then + return nil + end + + -- Check if total number of missions is reached. + local NoLimit=self:_CheckMissionLimit("Total") + --env.info("FF chief total nolimit="..tostring(NoLimit)) + if NoLimit==false then + return nil + end + + -- Sort results table wrt prio and threatlevel. + local function _sort(a, b) + local taskA=a --Ops.Target#TARGET + local taskB=b --Ops.Target#TARGET + return (taskA.priotaskB.threatlevel0) + end + table.sort(self.targetqueue, _sort) + + -- Get the lowest importance value (lower means more important). + -- If a target with importance 1 exists, targets with importance 2 will not be assigned. Targets with no importance (nil) can still be selected. + local vip=math.huge + for _,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + if target:IsAlive() and target.importance and target.importance=self.threatLevelMin and threatlevel<=self.threatLevelMax + + -- Airbases, Zones and Coordinates have threat level 0. We consider them threads independent of min/max threat level set. + if target.category==TARGET.Category.AIRBASE or target.category==TARGET.Category.ZONE or target.Category==TARGET.Category.COORDINATE then + isThreat=true + end + + -- Debug message. + local text=string.format("Target %s: Alive=%s, Threat=%s, Important=%s", target:GetName(), tostring(isAlive), tostring(isThreat), tostring(isImportant)) + + -- Check if mission is done. + if target.mission then + text=text..string.format(", Mission \"%s\" (%s) [%s]", target.mission:GetName(), target.mission:GetState(), target.mission:GetType()) + if target.mission:IsOver() then + text=text..string.format(" - DONE ==> removing mission") + target.mission=nil + end + else + text=text..string.format(", NO mission yet") + end + self:T2(self.lid..text) + + -- Check that target is alive and not already a mission has been assigned. + if isAlive and isThreat and isImportant and not target.mission then + + -- Check if this target is "valid", i.e. fits with the current strategy. + local valid=false + if self.strategy==CHIEF.Strategy.PASSIVE then + + --- + -- PASSIVE: No targets at all are attacked. + --- + + valid=false + + elseif self.strategy==CHIEF.Strategy.DEFENSIVE then + + --- + -- DEFENSIVE: Attack inside borders only. + --- + + if self:CheckTargetInZones(target, self.borderzoneset) then + valid=true + end + + elseif self.strategy==CHIEF.Strategy.OFFENSIVE then + + --- + -- OFFENSIVE: Attack inside borders and in yellow zones. + --- + + if self:CheckTargetInZones(target, self.borderzoneset) or self:CheckTargetInZones(target, self.yellowzoneset) then + valid=true + end + + elseif self.strategy==CHIEF.Strategy.AGGRESSIVE then + + --- + -- AGGRESSIVE: Attack in all zone sets. + --- + + if self:CheckTargetInZones(target, self.borderzoneset) or self:CheckTargetInZones(target, self.yellowzoneset) or self:CheckTargetInZones(target, self.engagezoneset) then + valid=true + end + + elseif self.strategy==CHIEF.Strategy.TOTALWAR then + + --- + -- TOTAL WAR: We attack anything we find. + --- + + valid=true + end + + -- Valid target? + if valid then + + -- Debug info. + self:T(self.lid..string.format("Got valid target %s: category=%s, threatlevel=%d", target:GetName(), target.category, threatlevel)) + + -- Get mission performances for the given target. + local MissionPerformances=self:_GetMissionPerformanceFromTarget(target) + + -- Mission. + local mission=nil --Ops.Auftrag#AUFTRAG + local Legions=nil + + if #MissionPerformances>0 then + + for _,_mp in pairs(MissionPerformances) do + local mp=_mp --#CHIEF.MissionPerformance + + -- Check mission type limit. + local notlimited=self:_CheckMissionLimit(mp.MissionType) + + --env.info(string.format("FF chief %s nolimit=%s", mp.MissionType, tostring(NoLimit))) + + if notlimited then + + -- Get min/max number of assets. + local NassetsMin, NassetsMax=self:_GetAssetsForTarget(target, mp.MissionType) + + -- Debug info. + self:T2(self.lid..string.format("Recruiting assets for mission type %s [performance=%d] of target %s", mp.MissionType, mp.Performance, target:GetName())) + + -- Recruit assets. + local recruited, assets, legions=self.commander:RecruitAssetsForTarget(target, mp.MissionType, NassetsMin, NassetsMax) + + if recruited then + + self:T(self.lid..string.format("Recruited %d assets for mission type %s [performance=%d] of target %s", #assets, mp.MissionType, mp.Performance, target:GetName())) + + -- Create a mission. + mission=AUFTRAG:NewFromTarget(target, mp.MissionType) + + -- Add asset to mission. + if mission then + + mission:_AddAssets(assets) + Legions=legions + + -- We got what we wanted ==> leave loop. + break + end + else + self:T(self.lid..string.format("Could NOT recruit assets for mission type %s [performance=%d] of target %s", mp.MissionType, mp.Performance, target:GetName())) + end + end + end + end + + -- Check if mission could be defined. + if mission and Legions then + + -- Set target mission entry. + target.mission=mission + + -- Mission parameters. + mission.prio=target.prio + mission.importance=target.importance + + -- Assign mission to legions. + self:MissionAssign(mission, Legions) + + -- Only ONE target is assigned per check. + return + end + + end + + end + end + +end + +--- Check if limit of missions has been reached. +-- @param #CHIEF self +-- @param #string MissionType Type of mission. +-- @return #boolean If `true`, mission limit has **not** been reached. If `false`, limit has been reached. +function CHIEF:_CheckMissionLimit(MissionType) + return self.commander:_CheckMissionLimit(MissionType) +end + +--- Get mission limit. +-- @param #CHIEF self +-- @param #string MissionType Type of mission. +-- @return #number Limit. Unlimited mission types are returned as 999. +function CHIEF:GetMissionLimit(MissionType) + local l=self.commander.limitMission[MissionType] + if not l then + l=999 + end + return l +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Strategic Zone Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check strategic zone queue. +-- @param #CHIEF self +function CHIEF:CheckOpsZoneQueue() + + -- Number of zones. + local Nzones=#self.zonequeue + + -- Treat special cases. + if Nzones==0 then + return nil + end + + -- Loop over strategic zone and remove stopped zones. + for i=Nzones, 1, -1 do + local stratzone=self.zonequeue[i] --#CHIEF.StrategicZone + if stratzone.opszone:IsStopped() then + self:RemoveStrategicZone(stratzone.opszone) + end + end + + -- Loop over strategic zones and cancel missions for occupied zones if zone is not occupied any more. + for _,_startzone in pairs(self.zonequeue) do + local stratzone=_startzone --#CHIEF.StrategicZone + + -- Current owner of the zone. + local ownercoalition=stratzone.opszone:GetOwner() + + -- Check if we own the zone or it is empty. + if ownercoalition==self.coalition or stratzone.opszone:IsEmpty() then + + -- Loop over resources. + for _,_resource in pairs(stratzone.resourceOccup or {}) do + local resource=_resource --#CHIEF.Resource + + -- Cancel running missions. + if resource.mission then + resource.mission:Cancel() + end + + end + end + end + + -- Passive strategy ==> Do not act. + if self:IsPassive() then + return + end + + -- Check if total number of missions is reached. + local NoLimit=self:_CheckMissionLimit("Total") + if NoLimit==false then + return nil + end + + -- Sort results table wrt prio. + local function _sort(a, b) + local taskA=a --#CHIEF.StrategicZone + local taskB=b --#CHIEF.StrategicZone + return (taskA.prio Recruiting for mission type %s: Nmin=%d, Nmax=%d", zoneName, missionType, resource.Nmin, resource.Nmax)) + + -- Recruit assets. + local recruited=self:RecruitAssetsForZone(stratzone, resource) + + if recruited then + self:T(self.lid..string.format("Successfully recruited assets for empty zone \"%s\" [mission type=%s]", zoneName, missionType)) + else + self:T(self.lid..string.format("Could not recruited assets for empty zone \"%s\" [mission type=%s]", zoneName, missionType)) + end + + end + + end + + else + + --- + -- Zone is NOT EMPTY + -- + -- We first send a CAS flight to eliminate enemy activity. + --- + + for _,_resource in pairs(stratzone.resourceOccup or {}) do + local resource=_resource --#CHIEF.Resource + + -- Mission type. + local missionType=resource.MissionType + + if (not resource.mission) or resource.mission:IsOver() then + + -- Debug info. + self:T2(self.lid..string.format("Zone %s is NOT empty ==> Recruiting for mission type %s: Nmin=%d, Nmax=%d", zoneName, missionType, resource.Nmin, resource.Nmax)) + + -- Recruit assets. + local recruited=self:RecruitAssetsForZone(stratzone, resource) + + if recruited then + self:T(self.lid..string.format("Successfully recruited assets for occupied zone %s, mission type=%s", zoneName, missionType)) + else + self:T(self.lid..string.format("Could not recruited assets for occupied zone %s, mission type=%s", zoneName, missionType)) + end + + end + + end + + end + + end + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Zone Check Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if group is inside our border. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP group The group. +-- @return #boolean If true, group is in any border zone. +function CHIEF:CheckGroupInBorder(group) + + local inside=self:CheckGroupInZones(group, self.borderzoneset) + + return inside +end + +--- Check if group is in a conflict zone. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP group The group. +-- @return #boolean If true, group is in any conflict zone. +function CHIEF:CheckGroupInConflict(group) + + -- Check inside yellow but not inside our border. + local inside=self:CheckGroupInZones(group, self.yellowzoneset) --and not self:CheckGroupInZones(group, self.borderzoneset) + + return inside +end + +--- Check if group is in a attack zone. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP group The group. +-- @return #boolean If true, group is in any attack zone. +function CHIEF:CheckGroupInAttack(group) + + -- Check inside yellow but not inside our border. + local inside=self:CheckGroupInZones(group, self.engagezoneset) --and not self:CheckGroupInZones(group, self.borderzoneset) + + return inside +end + +--- Check if group is inside a zone. +-- @param #CHIEF self +-- @param Wrapper.Group#GROUP group The group. +-- @param Core.Set#SET_ZONE zoneset Set of zones. +-- @return #boolean If true, group is in any zone. +function CHIEF:CheckGroupInZones(group, zoneset) + + for _,_zone in pairs(zoneset.Set or {}) do + local zone=_zone --Core.Zone#ZONE + + if group:IsInZone(zone) then + return true + end + end + + return false +end + +--- Check if group is inside a zone. +-- @param #CHIEF self +-- @param Ops.Target#TARGET target The target. +-- @param Core.Set#SET_ZONE zoneset Set of zones. +-- @return #boolean If true, group is in any zone. +function CHIEF:CheckTargetInZones(target, zoneset) + + for _,_zone in pairs(zoneset.Set or {}) do + local zone=_zone --Core.Zone#ZONE + + if zone:IsCoordinateInZone(target:GetCoordinate()) then + return true + end + end + + return false +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Resources +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a mission performance table. +-- @param #CHIEF self +-- @param #string MissionType Mission type. +-- @param #number Performance Performance. +-- @return #CHIEF.MissionPerformance Mission performance. +function CHIEF:_CreateMissionPerformance(MissionType, Performance) + local mp={} --#CHIEF.MissionPerformance + mp.MissionType=MissionType + mp.Performance=Performance + return mp +end + +--- Get mission performance for a given TARGET. +-- @param #CHIEF self +-- @param Ops.Target#TARGET Target The target. +-- @return #table Mission performances of type `#CHIEF.MissionPerformance`. +function CHIEF:_GetMissionPerformanceFromTarget(Target) + + -- Possible target objects. + local group=nil --Wrapper.Group#GROUP + local airbase=nil --Wrapper.Airbase#AIRBASE + local scenery=nil --Wrapper.Scenery#SCENERY + local static=nil --Wrapper.Static#STATIC + local coordinate=nil --Core.Point#COORDINATE + + -- Get target objective. + local target=Target:GetObject() + + 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("STATIC") then + static=target + elseif target:IsInstanceOf("SCENERY") then + scenery=target + end + + -- Target category. + local TargetCategory=Target:GetCategory() + + -- Mission performances. + local missionperf={} --#CHIEF.MissionPerformance + + if group then + + local category=group:GetCategory() + local attribute=group:GetAttribute() + + if category==Group.Category.AIRPLANE or category==Group.Category.HELICOPTER then + + --- + -- A2A: Intercept + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.INTERCEPT, 100)) + + elseif category==Group.Category.GROUND or category==Group.Category.TRAIN then + + --- + -- GROUND + --- + + if attribute==GROUP.Attribute.GROUND_SAM then + + -- SEAD/DEAD + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.SEAD, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + elseif attribute==GROUP.Attribute.GROUND_EWR then + + -- EWR + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + elseif attribute==GROUP.Attribute.GROUND_AAA then + + -- AAA + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK, 40)) + + elseif attribute==GROUP.Attribute.GROUND_ARTILLERY then + + -- ARTY + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 75)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK, 70)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING, 70)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + elseif attribute==GROUP.Attribute.GROUND_INFANTRY then + + -- Infantry + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK, 40)) + + elseif attribute==GROUP.Attribute.GROUND_TANK then + + -- Tanks + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.CAS, 90)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.CASENHANCED, 90)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK, 40)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + else + + -- Everything else + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + end + + + elseif category==Group.Category.SHIP then + + --- + -- NAVAL + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ANTISHIP, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.NAVALENGAGEMENT, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + else + self:E(self.lid.."ERROR: Unknown Group category!") + end + + elseif airbase then + + --- + -- AIRBASE + --- + + -- Bomb runway. + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBRUNWAY, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + elseif static then + + --- + -- STATIC + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING, 70)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + elseif scenery then + + --- + -- SCENERY + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.STRIKE, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING, 70)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + elseif coordinate then + + --- + -- COORDINATE + --- + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING, 100)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET, 50)) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30)) + + end + + return missionperf +end + +--- Get mission performances for a given Group Attribute. +-- @param #CHIEF self +-- @param #string Attribute Group attibute. +-- @return #table Mission performances of type `#CHIEF.MissionPerformance`. +function CHIEF:_GetMissionTypeForGroupAttribute(Attribute) + + local missionperf={} --#CHIEF.MissionPerformance + + if Attribute==GROUP.Attribute.AIR_ATTACKHELO then + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.INTERCEPT), 100) + + elseif Attribute==GROUP.Attribute.GROUND_AAA then + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI), 100) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING), 80) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET), 70) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY), 30) + + elseif Attribute==GROUP.Attribute.GROUND_SAM then + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.SEAD), 100) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI), 90) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY), 50) + + elseif Attribute==GROUP.Attribute.GROUND_EWR then + + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.SEAD), 100) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI), 100) + table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY), 50) + + end + + return missionperf +end + + +--- Recruit assets for a given OPS zone. +-- @param #CHIEF self +-- @param #CHIEF.StrategicZone StratZone The strategic zone. +-- @param #CHIEF.Resource Resource The required resources. +-- @return #boolean If `true` enough assets could be recruited. +function CHIEF:RecruitAssetsForZone(StratZone, Resource) + + -- Cohorts. + local Cohorts=self.commander:_GetCohorts() + + -- Shortcuts. + local MissionType=Resource.MissionType + local NassetsMin=Resource.Nmin + local NassetsMax=Resource.Nmax + local Categories=Resource.Categories + local Attributes=Resource.Attributes + local Properties=Resource.Properties + + -- Target position. + local TargetVec2=StratZone.opszone.zone:GetVec2() + + -- Max range in meters. + local RangeMax=nil + + -- Set max range to 250 NM because we use helos as transport for the infantry. + if MissionType==AUFTRAG.Type.PATROLZONE or MissionType==AUFTRAG.Type.ONGUARD then + RangeMax=UTILS.NMToMeters(250) + end + + -- Set max range to 50 NM because we use armor. + if MissionType==AUFTRAG.Type.ARMOREDGUARD then + RangeMax=UTILS.NMToMeters(50) + end + + -- Recruite infantry assets. + self:T(self.lid..string.format("Recruiting assets for zone %s", StratZone.opszone:GetName())) + self:T(self.lid.."Missiontype="..MissionType) + self:T({categories=Categories}) + self:T({attributes=Attributes}) + self:T({properties=Properties}) + + + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, MissionType, nil, NassetsMin, NassetsMax, TargetVec2, nil, RangeMax, nil, nil, nil, nil, Categories, Attributes, Properties) + + if recruited then + + -- Mission for zone. + local mission=nil --Ops.Auftrag#AUFTRAG + + -- Debug messgage. + self:T2(self.lid..string.format("Recruited %d assets for %s mission STRATEGIC zone %s", #assets, MissionType, tostring(StratZone.opszone.zoneName))) + + -- Short cuts. + local TargetZone = StratZone.opszone.zone + local TargetCoord = TargetZone:GetCoordinate() + + -- First check if we need a transportation. + local transport=nil --Ops.OpsTransport#OPSTRANSPORT + local Ntransports=0 + if Resource.carrierNmin and Resource.carrierNmax and Resource.carrierNmax>0 then + + self:T(self.lid..string.format("Recruiting carrier assets: Nmin=%s, Nmax=%s", tostring(Resource.carrierNmin), tostring(Resource.carrierNmax))) + + -- Filter only those assets that shall be transported. + local cargoassets=CHIEF._FilterAssets(assets, Resource.Categories, Resource.Attributes, Resource.Properties) + + if #cargoassets>0 then + + -- Recruit transport carrier assets. + recruited, transport=LEGION.AssignAssetsForTransport(self.commander, self.commander.legions, cargoassets, + Resource.carrierNmin, Resource.carrierNmax, TargetZone, nil, Resource.carrierCategories, Resource.carrierAttributes, Resource.carrierProperties) + + Ntransports=transport~=nil and #transport.assets or 0 + + self:T(self.lid..string.format("Recruited %d transport carrier assets success=%s", Ntransports, tostring(recruited))) + + end + + end + + -- Check if everything was recruited. + if not recruited then + -- No (transport) assets ==> no mission! + self:T(self.lid..string.format("Could not allocate assets or transport of OPSZONE!")) + LEGION.UnRecruitAssets(assets) + return false + end + + -- Debug message + self:T2(self.lid..string.format("Recruited %d assets for mission %s", #assets, MissionType)) + + + if MissionType==AUFTRAG.Type.PATROLZONE or MissionType==AUFTRAG.Type.ONGUARD then + + --- + -- PATROLZONE or ONGUARD + --- + + if MissionType==AUFTRAG.Type.PATROLZONE then + mission=AUFTRAG:NewPATROLZONE(TargetZone) + + elseif MissionType==AUFTRAG.Type.ONGUARD then + mission=AUFTRAG:NewONGUARD(TargetZone:GetRandomCoordinate(nil, nil, {land.SurfaceType.LAND})) + end + + -- Engage detected targets. + mission:SetEngageDetected(25, {"Ground Units", "Light armed ships", "Helicopters"}) + + elseif MissionType==AUFTRAG.Type.CAPTUREZONE then + + --- + -- CAPTUREZONE + --- + + mission=AUFTRAG:NewCAPTUREZONE(StratZone.opszone, self.coalition) + + elseif MissionType==AUFTRAG.Type.CASENHANCED then + + --- + -- CAS ENHANCED + --- + + -- Create Patrol zone mission. + local height = UTILS.MetersToFeet(TargetCoord:GetLandHeight())+2500 + + local Speed=200 + if assets[1] then + if assets[1].speedmax then + Speed = UTILS.KmphToKnots(assets[1].speedmax * 0.7) or 200 + end + end + + -- CAS mission. + mission=AUFTRAG:NewCASENHANCED(TargetZone, height, Speed) + + elseif MissionType==AUFTRAG.Type.CAS then + + --- + -- CAS + --- + + -- Create Patrol zone mission. + local height = UTILS.MetersToFeet(TargetCoord:GetLandHeight())+2500 + + local Speed = 200 + if assets[1] then + if assets[1].speedmax then + Speed = UTILS.KmphToKnots(assets[1].speedmax * 0.7) or 200 + end + end + + -- Here we need a circular zone. + TargetZone=StratZone.opszone.zoneCircular + + -- Leg length. + local Leg = TargetZone:GetRadius() <= 10000 and 5 or UTILS.MetersToNM(TargetZone:GetRadius()) + + -- CAS mission. + mission=AUFTRAG:NewCAS(TargetZone, height, Speed, TargetCoord, math.random(0,359), Leg) + + elseif MissionType==AUFTRAG.Type.ARTY then + + --- + -- ARTY + --- + + -- Create ARTY zone mission. + local Radius = TargetZone:GetRadius() + + mission=AUFTRAG:NewARTY(TargetCoord, 120, Radius) + + elseif MissionType==AUFTRAG.Type.ARMOREDGUARD then + + --- + -- ARMORGUARD + --- + + -- Create Armored on guard mission + mission=AUFTRAG:NewARMOREDGUARD(TargetCoord) + + elseif MissionType==AUFTRAG.Type.BOMBCARPET then + + --- + -- BOMB CARPET + --- + + -- Create ARTY zone mission. + mission=AUFTRAG:NewBOMBCARPET(TargetCoord, nil, 1000) + + elseif MissionType==AUFTRAG.Type.BOMBING then + + --- + -- BOMBING + --- + + local coord=TargetZone:GetRandomCoordinate() + + mission=AUFTRAG:NewBOMBING(TargetCoord) + + elseif MissionType==AUFTRAG.Type.RECON then + + --- + -- RECON + --- + + mission=AUFTRAG:NewRECON(TargetZone, nil, 5000) + + elseif MissionType==AUFTRAG.Type.BARRAGE then + + --- + -- BARRAGE + --- + + mission=AUFTRAG:NewBARRAGE(TargetZone) + + elseif MissionType==AUFTRAG.Type.AMMOSUPPLY then + + --- + -- AMMO SUPPLY + --- + + mission=AUFTRAG:NewAMMOSUPPLY(TargetZone) + + end + + if mission then + + -- Add assets to mission. + mission:_AddAssets(assets) + + -- Assign mission to legions. + self:MissionAssign(mission, legions) + + -- Attach mission to ops zone. + StratZone.opszone:_AddMission(self.coalition, MissionType, mission) + + mission:SetName(string.format("Stratzone %s-%d", StratZone.opszone:GetName(), mission.auftragsnummer)) + + -- Attach mission to resource. + Resource.mission=mission + + -- Check if transport assets could be allocated. If carrier Nmin=0 and 0 assets could be allocated, transport would still be created but not usefull obviously + if transport and Ntransports>0 then + -- Attach OPS transport to mission. + mission.opstransport=transport + -- Set ops zone to transport. + transport.opszone=StratZone.opszone + transport.chief=self + transport.commander=self.commander + end + + return true + else + + -- Mission not supported. + self:E(self.lid..string.format("ERROR: Mission type %s not supported for OPSZONE! Unrecruiting assets...", tostring(MissionType))) + LEGION.UnRecruitAssets(assets) + + return false + end + + end + + -- Debug messgage. + self:T2(self.lid..string.format("Could NOT recruit assets for %s mission of STRATEGIC zone %s", MissionType, tostring(StratZone.opszone.zoneName))) + + return false +end + +--- Filter assets, which have certain categories, attributes and/or properties. +-- @param #table Assets The assets to be filtered. +-- @param #table Categories Group categories. +-- @param #table Attributes Generalized attributes. +-- @param #table Properties DCS attributes +-- @return #table Table of filtered assets. +function CHIEF._FilterAssets(Assets, Categories, Attributes, Properties) + + local filtered={} + + for _,_asset in pairs(Assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + local hasCat=CHIEF._CheckAssetCategories(asset, Categories) + local hasAtt=CHIEF._CheckAssetAttributes(asset, Attributes) + local hasPro=CHIEF._CheckAssetProperties(asset, Properties) + + if hasAtt and hasCat and hasPro then + table.insert(filtered, asset) + end + + end + + return filtered +end + +--- Check if a given asset has certain attribute(s). +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset item. +-- @param #table Attributes The required attributes. See `WAREHOUSE.Attribute` enum. Can also be passed as a single attribute `#string`. +-- @return #boolean Returns `true`, the asset has at least one requested attribute. +function CHIEF._CheckAssetAttributes(Asset, Attributes) + + if not Attributes then + return true + end + + for _,attribute in pairs(UTILS.EnsureTable(Attributes)) do + if attribute==Asset.attribute then + return true + end + end + + return false +end + +--- Check if a given asset has certain categories. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset item. +-- @param #table Categories DCS group categories. +-- @return #boolean Returns `true`, the asset has at least one requested category. +function CHIEF._CheckAssetCategories(Asset, Categories) + + if not Categories then + return true + end + + for _,attribute in pairs(UTILS.EnsureTable(Categories)) do + if attribute==Asset.category then + return true + end + end + + return false +end + +--- Check if a given asset has certain properties. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset item. +-- @param #table Categories DCS group categories. +-- @return #boolean Returns `true`, the asset has at least one requested property. +function CHIEF._CheckAssetProperties(Asset, Properties) + + if not Properties then + return true + end + + for _,attribute in pairs(UTILS.EnsureTable(Properties)) do + if attribute==Asset.DCSdesc then + return true + end + end + + return false +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Cohort.lua b/Moose Development/Moose/Ops/Cohort.lua new file mode 100644 index 000000000..48f63112c --- /dev/null +++ b/Moose Development/Moose/Ops/Cohort.lua @@ -0,0 +1,1675 @@ +--- **Ops** - Cohort encompassed all characteristics of SQUADRONs, PLATOONs and FLOTILLAs. +-- +-- **Main Features:** +-- +-- * Set parameters like livery, skill valid for all cohort members. +-- * Define modex and callsigns. +-- * Define mission types, this cohort can perform (see Ops.Auftrag#AUFTRAG). +-- * Pause/unpause cohort operations. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.Cohort +-- @image OPS_Cohort.png + + +--- COHORT class. +-- @type COHORT +-- @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 cohort. +-- @field #string templatename Name of the template group. +-- @field #string aircrafttype Type of the units the cohort is using. +-- @field #number category Group category of the assets: `Group.Category.AIRPLANE`, `Group.Category.HELICOPTER`, `Group.Category.GROUND`, `Group.Category.SHIP`, `Group.Category.TRAIN`. +-- @field Wrapper.Group#GROUP templategroup Template group. +-- @field #boolean isAir +-- @field #boolean isGround Is ground. +-- @field #boolean isNaval Is naval. +-- @field #table assets Cohort assets. +-- @field #table missiontypes Capabilities (mission types and performances) of the cohort. +-- @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 cohort. +-- @field #number skill Skill of cohort members. +-- @field Ops.Legion#LEGION legion The LEGION object the cohort belongs to. +-- @field #number Ngroups Number of asset OPS groups this cohort has. +-- @field #number Nkilled Number of destroyed asset groups. +-- @field #number engageRange Mission range in meters. +-- @field #string attribute Generalized attribute of the cohort template group. +-- @field #table descriptors DCS descriptors. +-- @field #table properties DCS attributes. +-- @field #table tacanChannel List of TACAN channels available to the cohort. +-- @field #number radioFreq Radio frequency in MHz the cohort uses. +-- @field #number radioModu Radio modulation the cohort uses. +-- @field #table tacanChannel List of TACAN channels available to the cohort. +-- @field #number weightAsset Weight of one assets group in kg. +-- @field #number cargobayLimit Cargo bay capacity in kg. +-- @field #table operations Operations this cohort is part of. +-- @extends Core.Fsm#FSM + +--- *I came, I saw, I conquered.* -- Julius Caesar +-- +-- === +-- +-- # The COHORT Concept +-- +-- A COHORT is essential part of a LEGION and consists of **one** unit type. +-- +-- +-- +-- @field #COHORT +COHORT = { + ClassName = "COHORT", + verbose = 0, + lid = nil, + name = nil, + templatename = nil, + assets = {}, + missiontypes = {}, + repairtime = 0, + maintenancetime= 0, + livery = nil, + skill = nil, + legion = nil, + --Ngroups = nil, + Ngroups = 0, + engageRange = nil, + tacanChannel = {}, + weightAsset = 99999, + cargobayLimit = 0, + descriptors = {}, + properties = {}, + operations = {}, +} + +--- COHORT class version. +-- @field #string version +COHORT.version="0.3.7" + +--- Global variable to store the unique(!) cohort names +_COHORTNAMES={} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Create FLOTILLA class. +-- DONE: Added check for properties. +-- DONE: Make general so that PLATOON and SQUADRON can inherit this class. +-- DONE: Better setting of call signs. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new COHORT object and start the FSM. +-- @param #COHORT self +-- @param #string TemplateGroupName Name of the template group. +-- @param #number Ngroups Number of asset groups of this Cohort. Default 3. +-- @param #string CohortName Name of the cohort. +-- @return #COHORT self +function COHORT:New(TemplateGroupName, Ngroups, CohortName) + + -- Name of the cohort. + local name=tostring(CohortName or TemplateGroupName) + + -- Cohort name has to be unique or we will get serious problems! + if UTILS.IsAnyInTable(_COHORTNAMES, name) then + env.error(string.format('ERROR: cannot create cohort "%s" because another cohort with that name already exists. Names must be unique!', name)) + return nil + else + table.insert(_COHORTNAMES, name) + end + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #COHORT + + -- Name of the template group. + self.templatename=TemplateGroupName + + -- Cohort name. + self.name=name + + -- Set some string id for output to DCS.log file. + self.lid=string.format("COHORT %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 + + -- Generalized attribute. + self.attribute=self.templategroup:GetAttribute() + + -- Group category. + self.category=self.templategroup:GetCategory() + + -- Aircraft type. + self.aircrafttype=self.templategroup:GetTypeName() + + -- Get descriptors. + self.descriptors=self.templategroup:GetUnit(1):GetDesc() + + -- Properties (DCS attributes). + self.properties=self.descriptors.attributes + + -- Print properties. + --self:I(self.properties) + + -- Defaults. + self.Ngroups=Ngroups or 3 + self:SetSkill(AI.Skill.GOOD) + + -- Mission range depends on + if self.category==Group.Category.AIRPLANE then + self:SetMissionRange(200) + elseif self.category==Group.Category.HELICOPTER then + self:SetMissionRange(150) + elseif self.category==Group.Category.GROUND then + self:SetMissionRange(75) + elseif self.category==Group.Category.SHIP then + self:SetMissionRange(100) + elseif self.category==Group.Category.TRAIN then + self:SetMissionRange(100) + else + self:SetMissionRange(150) + end + + -- Units. + local units=self.templategroup:GetUnits() + + -- Weight of the whole group. + self.weightAsset=0 + for i,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + local desc=unit:GetDesc() + local mass=666 + if desc then + mass=desc.massMax or desc.massEmpty + end + self.weightAsset=self.weightAsset + (mass or 666) + if i==1 then + self.cargobayLimit=unit:GetCargoBayFreeWeight() + end + end + + -- 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 cohort. + self:AddTransition("Paused", "Unpause", "OnDuty") -- Unpause cohort. + + self:AddTransition("OnDuty", "Relocate", "Relocating") -- Relocate. + self:AddTransition("Relocating", "Relocated", "OnDuty") -- Relocated. + + self:AddTransition("*", "Stop", "Stopped") -- Stop cohort. + + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the COHORT. + -- @function [parent=#COHORT] Start + -- @param #COHORT self + + --- Triggers the FSM event "Start" after a delay. Starts the COHORT. + -- @function [parent=#COHORT] __Start + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". + -- @param #COHORT self + + --- Triggers the FSM event "Stop" after a delay. Stops the COHORT and all its event handlers. + -- @function [parent=#COHORT] __Stop + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status". + -- @function [parent=#COHORT] Status + -- @param #COHORT self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#COHORT] __Status + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Pause". + -- @function [parent=#COHORT] Pause + -- @param #COHORT self + + --- Triggers the FSM event "Pause" after a delay. + -- @function [parent=#COHORT] __Pause + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + --- On after "Pause" event. + -- @function [parent=#COHORT] OnAfterPause + -- @param #COHORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Unpause". + -- @function [parent=#COHORT] Unpause + -- @param #COHORT self + + --- Triggers the FSM event "Unpause" after a delay. + -- @function [parent=#COHORT] __Unpause + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + --- On after "Unpause" event. + -- @function [parent=#COHORT] OnAfterUnpause + -- @param #COHORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Relocate". + -- @function [parent=#COHORT] Relocate + -- @param #COHORT self + + --- Triggers the FSM event "Relocate" after a delay. + -- @function [parent=#COHORT] __Relocate + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + --- On after "Relocate" event. + -- @function [parent=#COHORT] OnAfterRelocate + -- @param #COHORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Relocated". + -- @function [parent=#COHORT] Relocated + -- @param #COHORT self + + --- Triggers the FSM event "Relocated" after a delay. + -- @function [parent=#COHORT] __Relocated + -- @param #COHORT self + -- @param #number delay Delay in seconds. + + --- On after "Relocated" event. + -- @function [parent=#COHORT] OnAfterRelocated + -- @param #COHORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set livery painted on all cohort units. +-- 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 #COHORT self +-- @param #string LiveryName Name of the livery. +-- @return #COHORT self +function COHORT:SetLivery(LiveryName) + self.livery=LiveryName + return self +end + +--- Set skill level of all cohort team members. +-- @param #COHORT self +-- @param #string Skill Skill of all flights. +-- @usage mycohort:SetSkill(AI.Skill.EXCELLENT) +-- @return #COHORT self +function COHORT:SetSkill(Skill) + self.skill=Skill + return self +end + +--- Set verbosity level. +-- @param #COHORT self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #COHORT self +function COHORT:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set turnover and repair time. If an asset returns from a mission, it will need some time until the asset is available for further missions. +-- @param #COHORT 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 #COHORT self +function COHORT: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 cohort uses. +-- @param #COHORT self +-- @param #number Frequency Radio frequency in MHz. Default 251 MHz. +-- @param #number Modulation Radio modulation. Default 0=AM. +-- @return #COHORT self +function COHORT: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 #COHORT self +-- @param #number nunits Number of units. Default 2. +-- @return #COHORT self +function COHORT:SetGrouping(nunits) + self.ngrouping=nunits or 2 + return self +end + +--- Set mission types this cohort is able to perform. +-- @param #COHORT 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 #COHORT self +function COHORT: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 + + local Capability=self:GetMissionCapability(missiontype) + + -- Check not to add the same twice. + if Capability then + self:E(self.lid.."WARNING: Mission capability already present! No need to add it twice. Will update the performance though!") + Capability.Performance=Performance or 50 + else + + local capability={} --Ops.Auftrag#AUFTRAG.Capability + capability.MissionType=missiontype + capability.Performance=Performance or 50 + table.insert(self.missiontypes, capability) + self:T(self.lid..string.format("Adding mission capability %s, performance=%d", tostring(capability.MissionType), capability.Performance)) + end + end + + -- Debug info. + self:T2(self.missiontypes) + + return self +end + +--- Get missin capability for a given mission type. +-- @param #COHORT self +-- @param #string MissionType Mission type, e.g. `AUFTRAG.Type.BAI`. +-- @return Ops.Auftrag#AUFTRAG.Capability Capability table or `nil` if the capability does not exist. +function COHORT:GetMissionCapability(MissionType) + + for _,_capability in pairs(self.missiontypes) do + local capability=_capability --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return capability + end + end + + return nil +end + +--- Check if cohort assets have a given property (DCS attribute). +-- @param #COHORT self +-- @param #string Property The property. +-- @return #boolean If `true`, cohort assets have the attribute. +function COHORT:HasProperty(Property) + + for _,property in pairs(self.properties) do + if Property==property then + return true + end + end + + return false +end + +--- Get mission types this cohort is able to perform. +-- @param #COHORT self +-- @return #table Table of mission types. Could be empty {}. +function COHORT: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 cohort. +-- @param #COHORT self +-- @return #table Table of mission capabilities. +function COHORT:GetMissionCapabilities() + return self.missiontypes +end + +--- Get mission performance for a given type of misson. +-- @param #COHORT self +-- @param #string MissionType Type of mission. +-- @return #number Performance or -1. +function COHORT: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 cohort base are executed. +-- @param #COHORT self +-- @param #number Range Range in NM. Default 150 NM. +-- @return #COHORT self +function COHORT:SetMissionRange(Range) + self.engageRange=UTILS.NMToMeters(Range or 150) + return self +end + +--- Set call sign. +-- @param #COHORT self +-- @param #number Callsign Callsign from CALLSIGN.Aircraft, e.g. "Chevy" for CALLSIGN.Aircraft.CHEVY. +-- @param #number Index Callsign index, Chevy-**1**. +-- @param #string CallsignString (optional) Set this for tasks like TANKER, AWACS or KIOWA and the like, which have special names. E.g. "Darkstar" or "Roughneck". +-- @return #COHORT self +function COHORT:SetCallsign(Callsign, Index, CallsignString) + self.callsignName=Callsign + self.callsignIndex=Index + self.callsignClearName=CallsignString + self.callsign={} + self.callsign.NumberSquad=Callsign + self.callsign.NumberGroup=Index + return self +end + +--- Set generalized attribute. +-- @param #COHORT self +-- @param #string Attribute Generalized attribute, e.g. `GROUP.Attribute.Ground_Infantry`. +-- @return #COHORT self +function COHORT:SetAttribute(Attribute) + self.attribute=Attribute + return self +end + +--- Get generalized attribute. +-- @param #COHORT self +-- @return #string Generalized attribute, e.g. `GROUP.Attribute.Ground_Infantry`. +function COHORT:GetAttribute() + return self.attribute +end + +--- Get group category. +-- @param #COHORT self +-- @return #string Group category +function COHORT:GetCategory() + return self.category +end + +--- Get properties, *i.e.* DCS attributes. +-- @param #COHORT self +-- @return #table Properties table. +function COHORT:GetProperties() + return self.properties +end + + +--- Set modex. +-- @param #COHORT 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 #COHORT self +function COHORT:SetModex(Modex, Prefix, Suffix) + self.modex=Modex + self.modexPrefix=Prefix + self.modexSuffix=Suffix + return self +end + +--- Set Legion. +-- @param #COHORT self +-- @param Ops.Legion#LEGION Legion The Legion. +-- @return #COHORT self +function COHORT:SetLegion(Legion) + self.legion=Legion + return self +end + +--- Add asset to cohort. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The warehouse asset. +-- @return #COHORT self +function COHORT:AddAsset(Asset) + self:T(self.lid..string.format("Adding asset %s of type %s", Asset.spawngroupname, Asset.unittype)) + Asset.squadname=self.name + Asset.legion=self.legion + Asset.cohort=self + table.insert(self.assets, Asset) + return self +end + +--- Remove specific asset from chort. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. +-- @return #COHORT self +function COHORT:DelAsset(Asset) + for i,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + 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 asset group from cohort. +-- @param #COHORT self +-- @param #string GroupName Name of the asset group. +-- @return #COHORT self +function COHORT:DelGroup(GroupName) + for i,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + 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 + +--- Remove assets from pool. Not that assets must not be spawned or already reserved or requested. +-- @param #COHORT self +-- @param #number N Number of assets to be removed. Default 1. +-- @param #number Delay Delay in seconds before assets are removed. +-- @return #COHORT self +function COHORT:RemoveAssets(N, Delay) + self:T2(self.lid..string.format("Remove %d assets of Cohort", N)) + + if Delay and Delay>0 then + -- Delayed call + self:ScheduleOnce(Delay, COHORT.RemoveAssets, self, N, 0) + else + + N=N or 1 + + local n=0 + for i=#self.assets,1,-1 do + local asset=self.assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + + self:T2(self.lid..string.format("Checking removing asset %s", asset.spawngroupname)) + if not (asset.requested or asset.spawned or asset.isReserved) then + self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname)) + -- Remove from warehouse and warehouse DB + asset.legion:_DeleteStockItem(asset) + table.remove(self.assets, i) + n=n+1 + else + self:T2(self.lid..string.format("Could NOT Remove asset %s", asset.spawngroupname)) + end + + if n>=N then + break + end + + end + + self:T(self.lid..string.format("Removed %d/%d assets. New asset count=%d", n, N, #self.assets)) + + end + + return self +end + + +--- Get name of the cohort. +-- @param #COHORT self +-- @return #string Name of the cohort. +function COHORT:GetName() + return self.name +end + +--- Get radio frequency and modulation. +-- @param #COHORT self +-- @return #number Radio frequency in MHz. +-- @return #number Radio Modulation (0=AM, 1=FM). +function COHORT:GetRadio() + return self.radioFreq, self.radioModu +end + +--- Create a callsign for the asset. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The warehouse asset. +-- @return #COHORT self +function COHORT:GetCallsign(Asset) + + if self.callsignName then + --[[ + ["callsign"] = + { + [2] = 1, + ["name"] = "Darkstar11", + [3] = 1, + [1] = 5, + [4] = "Darkstar11", + }, -- end of ["callsign"] + ]] + 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 + callsign["name"] = self.callsignClearName or UTILS.GetCallsignName(self.callsignName) or "None" + callsign["name"] = string.format("%s%d%d",callsign["name"],callsign[2],callsign[3]) + callsign[4] = callsign["name"] + + Asset.callsign[i]=callsign + + self:T3({callsign=callsign}) + + --DONE: there is also a table entry .name, which is a string. + --UTILS.PrintTableToLog(callsign) + end + + + end + +end + +--- Create a modex for the asset. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The warehouse asset. +-- @return #COHORT self +function COHORT: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 cohort. Note that channels can only range from 1 to 126. +-- @param #COHORT self +-- @param #number ChannelMin Channel. +-- @param #number ChannelMax Channel. +-- @return #COHORT self +-- @usage mysquad:AddTacanChannel(64,69) -- adds channels 64, 65, 66, 67, 68, 69 +function COHORT: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 #COHORT self +-- @return #number TACAN channel or *nil* if no channel is free. +function COHORT:FetchTacan() + + -- Get the smallest free channel if there is one. + local freechannel=nil + for channel,free in pairs(self.tacanChannel) do + if free then + if freechannel==nil or channel=2 then + local text="Weapon data:" + for _,_weapondata in pairs(self.weaponData) do + local weapondata=_weapondata + text=text..string.format("\n- Bit=%s, Rmin=%d m, Rmax=%d m", tostring(weapondata.BitType), weapondata.RangeMin, weapondata.RangeMax) + end + self:I(self.lid..text) + end + + return self +end + +--- Get weapon range for given bit type. +-- @param #COHORT self +-- @param #number BitType Bit mask of weapon type. +-- @return Ops.OpsGroup#OPSGROUP.WeaponData Weapon data. +function COHORT:GetWeaponData(BitType) + return self.weaponData[tostring(BitType)] +end + +--- Check if cohort is "OnDuty". +-- @param #COHORT self +-- @return #boolean If true, cohort is in state "OnDuty". +function COHORT:IsOnDuty() + return self:Is("OnDuty") +end + +--- Check if cohort is "Stopped". +-- @param #COHORT self +-- @return #boolean If true, cohort is in state "Stopped". +function COHORT:IsStopped() + return self:Is("Stopped") +end + +--- Check if cohort is "Paused". +-- @param #COHORT self +-- @return #boolean If true, cohort is in state "Paused". +function COHORT:IsPaused() + return self:Is("Paused") +end + +--- Check if cohort is "Relocating". +-- @param #COHORT self +-- @return #boolean If true, cohort is relocating. +function COHORT:IsRelocating() + return self:Is("Relocating") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #COHORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function COHORT:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting %s v%s %s [%s]", self.ClassName, self.version, self.name, self.attribute) + self:I(self.lid..text) + + -- Start the status monitoring. + self:__Status(-1) +end + +--- Check asset status. +-- @param #COHORT self +function COHORT:_CheckAssetStatus() + + if self.verbose>=2 and #self.assets>0 then + + local text="" + for j,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- 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.legion and self.legion: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() + text=text..string.format("%s", status) + + if asset.flightgroup:IsFlightgroup() then + local fuelmin=asset.flightgroup:GetFuelMin() + local fuellow=asset.flightgroup:IsFuelLow() + local fuelcri=asset.flightgroup:IsFuelCritical() + text=text..string.format("Fuel=%d", fuelmin) + if fuelcri then + text=text.." (Critical!)" + elseif fuellow then + text=text.." (Low)" + end + 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. + if asset.flightgroup:IsFlightgroup() then + local payload=asset.payload and table.concat(self.legion:GetPayloadMissionTypes(asset.payload), ", ") or "None" + text=text..", Payload={"..payload.."}" + end + + 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:T(self.lid..text) + end + +end + +--- On after "Stop" event. +-- @param #COHORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function COHORT:onafterStop(From, Event, To) + + -- Debug info. + self:T(self.lid.."STOPPING Cohort and removing all assets!") + + -- Remove all assets. + for i=#self.assets,1,-1 do + local asset=self.assets[i] + self:DelAsset(asset) + end + + -- Clear call scheduler. + self.CallScheduler:Clear() + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if there is a cohort that can execute a given mission. +-- We check the mission type, the refuelling system, mission range. +-- @param #COHORT self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #boolean If true, Cohort can do that type of mission. +function COHORT:CanMission(Mission) + + local cando=true + + -- On duty?= + if not self:IsOnDuty() then + self:T(self.lid..string.format("Cohort 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 cohort can do the same mission types! + if not AUFTRAG.CheckMissionType(Mission.type, self:GetMissionTypes()) then + self:T(self.lid..string.format("INFO: Cohort cannot do mission type %s (%s, %s)", Mission.type, Mission.name, Mission:GetTargetName())) + return false + end + + -- Check that tanker mission has the correct refuelling system. + if Mission.type==AUFTRAG.Type.TANKER then + + if Mission.refuelSystem and Mission.refuelSystem==self.tankerSystem then + -- Correct refueling system. + self:T(self.lid..string.format("INFO: Correct refueling system requested=%s != %s=available", tostring(Mission.refuelSystem), tostring(self.tankerSystem))) + 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.legion: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 cohort engage range. + if TargetDistance>engagerange then + self:T(self.lid..string.format("INFO: Cohort 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 legion warehouse stock. +-- @param #COHORT self +-- @param #boolean InStock If `true`, only assets that are in the warehouse stock/inventory are counted. If `false`, only assets that are NOT in stock (i.e. spawned) are counted. If `nil`, all assets are counted. +-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. +-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. +-- @return #number Number of assets. +function COHORT:CountAssets(InStock, MissionTypes, Attributes) + + local N=0 + for _,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + if MissionTypes==nil or AUFTRAG.CheckMissionCapability(MissionTypes, self.missiontypes) then + if Attributes==nil or self:CheckAttribute(Attributes) then + if asset.spawned then + if InStock==false or InStock==nil then + N=N+1 --Spawned but we also count the spawned ones. + end + else + if InStock==true or InStock==nil then + N=N+1 --This is in stock. + end + end + end + end + end + + return N +end + +--- Get OPSGROUPs. +-- @param #COHORT self +-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. +-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. +-- @return Core.Set#SET_OPSGROUP Ops groups set. +function COHORT:GetOpsGroups(MissionTypes, Attributes) + + local set=SET_OPSGROUP:New() + + for _,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + if MissionTypes==nil or AUFTRAG.CheckMissionCapability(MissionTypes, self.missiontypes) then + if Attributes==nil or self:CheckAttribute(Attributes) then + if asset.flightgroup and asset.flightgroup:IsAlive() then + set:AddGroup(asset.flightgroup) + end + end + end + end + + return set +end + +--- Get assets for a mission. +-- @param #COHORT self +-- @param #string MissionType Mission type. +-- @param #number Npayloads Number of payloads available. +-- @return #table Assets that can do the required mission. +-- @return #number Number of payloads still available after recruiting the assets. +function COHORT:RecruitAssets(MissionType, Npayloads) + + -- Debug info. + self:T2(self.lid..string.format("Recruiting asset for Mission type=%s", MissionType)) + + -- Recruited assets. + local assets={} + + -- Loop over assets. + for _,_asset in pairs(self.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Get info. + local isRequested=asset.requested + local isReserved=asset.isReserved + local isSpawned=asset.spawned + local isOnMission=self.legion:IsAssetOnMission(asset) + + local opsgroup=asset.flightgroup + + -- Debug info. + self:T(self.lid..string.format("Asset %s: requested=%s, reserved=%s, spawned=%s, onmission=%s", + asset.spawngroupname, tostring(isRequested), tostring(isReserved), tostring(isSpawned), tostring(isOnMission))) + + -- First check that asset is not requested or reserved. This could happen if multiple requests are processed simultaniously. + if not (isRequested or isReserved) then + + -- Check if asset is currently on a mission (STARTED or QUEUED). + if self.legion:IsAssetOnMission(asset) then + --- + -- Asset is already on a mission. + --- + + -- Check if this asset is currently on a mission (STARTED or EXECUTING). + if MissionType==AUFTRAG.Type.RELOCATECOHORT then + + -- Relocation: Take all assets. Mission will be cancelled. + table.insert(assets, asset) + + elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.NOTHING) then + + -- Assets on mission NOTHING are considered. + table.insert(assets, asset) + + elseif self.legion:IsAssetOnMission(asset, {AUFTRAG.Type.GCICAP, AUFTRAG.Type.PATROLRACETRACK}) and MissionType==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:T(self.lid..string.format("Adding asset on GCICAP mission for an INTERCEPT mission")) + table.insert(assets, asset) + + elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.ONGUARD) and (MissionType==AUFTRAG.Type.ARTY or MissionType==AUFTRAG.Type.GROUNDATTACK) then + + if not opsgroup:IsOutOfAmmo() then + self:T(self.lid..string.format("Adding asset on ONGUARD mission for an XXX mission")) + table.insert(assets, asset) + end + + elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.PATROLZONE) and (MissionType==AUFTRAG.Type.ARTY or MissionType==AUFTRAG.Type.GROUNDATTACK) then + + if not opsgroup:IsOutOfAmmo() then + self:T(self.lid..string.format("Adding asset on PATROLZONE mission for an XXX mission")) + table.insert(assets, asset) + end + + elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.ALERT5) and AUFTRAG.CheckMissionCapability(MissionType, asset.payload.capabilities) and MissionType~=AUFTRAG.Type.ALERT5 then + + -- Check if the payload of this asset is compatible with the mission. + self:T(self.lid..string.format("Adding asset on ALERT 5 mission for %s mission", MissionType)) + 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) + --- + + -- Opsgroup. + local flightgroup=asset.flightgroup + + + if flightgroup and flightgroup:IsAlive() and not (flightgroup:IsDead() or flightgroup:IsStopped()) then + + --self:I("OpsGroup is alive") + + -- Assume we are ready and check if any condition tells us we are not. + local combatready=true + + -- Check if in a state where we really do not want to fight any more. + if flightgroup:IsFlightgroup() then + + --- + -- FLIGHTGROUP combat ready? + --- + + -- No more attacks if fuel is already low. Safety first! + if flightgroup:IsFuelLow() then + combatready=false + end + + if MissionType==AUFTRAG.Type.INTERCEPT and not flightgroup:CanAirToAir() then + combatready=false + else + local excludeguns=MissionType==AUFTRAG.Type.BOMBING or MissionType==AUFTRAG.Type.BOMBRUNWAY or MissionType==AUFTRAG.Type.BOMBCARPET or MissionType==AUFTRAG.Type.SEAD or MissionType==AUFTRAG.Type.ANTISHIP + if excludeguns and not flightgroup:CanAirToGround(excludeguns) then + combatready=false + end + end + + if flightgroup:IsHolding() or flightgroup:IsLanding() or flightgroup:IsLanded() or flightgroup:IsArrived() then + combatready=false + end + if asset.payload and not AUFTRAG.CheckMissionCapability(MissionType, asset.payload.capabilities) then + combatready=false + end + + else + + --- + -- ARMY/NAVYGROUP combat ready? + --- + + -- Disable this for now as it can cause problems - at least with transport and cargo assets. + --self:I("Attribute is: "..asset.attribute) + if flightgroup:IsArmygroup() then + -- check for fighting assets + if asset.attribute == WAREHOUSE.Attribute.GROUND_ARTILLERY or + asset.attribute == WAREHOUSE.Attribute.GROUND_TANK or + asset.attribute == WAREHOUSE.Attribute.GROUND_INFANTRY or + asset.attribute == WAREHOUSE.Attribute.GROUND_AAA or + asset.attribute == WAREHOUSE.Attribute.GROUND_SAM + then + combatready=true + end + else + combatready=false + end + + -- Not ready when rearming, retreating or returning! + if flightgroup:IsRearming() or flightgroup:IsRetreating() or flightgroup:IsReturning() then + combatready=false + end + + end + + -- Not ready when currently acting as ops transport carrier. + if flightgroup:IsLoading() or flightgroup:IsTransporting() or flightgroup:IsUnloading() or flightgroup:IsPickingup() or flightgroup:IsCarrier() then + combatready=false + end + -- Not ready when currently acting as ops transport cargo. + if flightgroup:IsCargo() or flightgroup:IsBoarding() or flightgroup:IsAwaitingLift() then + combatready=false + end + + -- This asset is "combatready". + if combatready then + self:T(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 we have payloads and asset is repaired. + if Npayloads>0 and self:IsRepaired(asset) 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 -- not requested check + end -- loop over assets + + self:T2(self.lid..string.format("Recruited %d assets for Mission type=%s", #assets, MissionType)) + + return assets, Npayloads +end + + +--- Get the time an asset needs to be repaired. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. +-- @return #number Time in seconds until asset is repaired. +function COHORT: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 + +--- Get max mission range. We add the largest weapon range, e.g. for arty or naval if weapon data is available. +-- @param #COHORT self +-- @param #table WeaponTypes (Optional) Weapon bit type(s) to add to the total range. Default is the max weapon type available. +-- @return #number Range in meters. +function COHORT:GetMissionRange(WeaponTypes) + + if WeaponTypes and type(WeaponTypes)~="table" then + WeaponTypes={WeaponTypes} + end + + local function checkWeaponType(Weapon) + local weapon=Weapon --Ops.OpsGroup#OPSGROUP.WeaponData + if WeaponTypes and #WeaponTypes>0 then + for _,weapontype in pairs(WeaponTypes) do + if weapontype==weapon.BitType then + return true + end + end + return false + end + return true + end + + -- Get max weapon range. + local WeaponRange=0 + for _,_weapon in pairs(self.weaponData or {}) do + local weapon=_weapon --Ops.OpsGroup#OPSGROUP.WeaponData + + if weapon.RangeMax>WeaponRange and checkWeaponType(weapon) then + WeaponRange=weapon.RangeMax + end + end + + return self.engageRange+WeaponRange +end + +--- Checks if a mission type is contained in a table of possible types. +-- @param #COHORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. +-- @return #boolean If true, the requested mission type is part of the possible mission types. +function COHORT: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 + +--- Check if the cohort attribute matches the given attribute(s). +-- @param #COHORT self +-- @param #table Attributes The requested attributes. See `WAREHOUSE.Attribute` enum. Can also be passed as a single attribute `#string`. +-- @return #boolean If true, the cohort has the requested attribute. +function COHORT:CheckAttribute(Attributes) + + if type(Attributes)~="table" then + Attributes={Attributes} + end + + for _,attribute in pairs(Attributes) do + if attribute==self.attribute then + return true + end + end + + return false +end + +--- Check ammo. +-- @param #COHORT self +-- @return Ops.OpsGroup#OPSGROUP.Ammo Ammo. +function COHORT:_CheckAmmo() + + -- Get units of group. + local units=self.templategroup:GetUnits() + + -- Init counter. + local nammo=0 + local nguns=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 + + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Output. + local text=string.format("Unit %s:\n", unit:GetName()) + + -- Get ammo table. + local ammotable=unit:GetAmmo() + + if ammotable then + + -- Debug info. + self:T3(ammotable) + + -- Loop over all weapons. + for w=1,#ammotable do + + -- Weapon table. + local weapon=ammotable[w] + + -- Descriptors. + local Desc=weapon["desc"] + + -- Warhead. + local Warhead=Desc["warhead"] + + -- Number of current weapon. + local Nammo=weapon["count"] + + -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3, torpedo=4 + local Category=Desc["category"] + + -- Get missile category: Weapon.MissileCategory AAM=1, SAM=2, BM=3, ANTI_SHIP=4, CRUISE=5, OTHER=6 + local MissileCategory = (Category==Weapon.Category.MISSILE) and Desc.missileCategory or nil + + -- Type name of current weapon. + local TypeName=Desc["typeName"] + + -- WeaponName + local weaponString = UTILS.Split(TypeName,"%.") + local WeaponName = weaponString[#weaponString] + + + -- Range in meters. Seems only to exist for missiles (not shells). + local Rmin=Desc["rangeMin"] or 0 + local Rmax=Desc["rangeMaxAltMin"] or 0 + + -- Caliber in mm. + local Caliber=Warhead and Warhead["caliber"] or 0 + + + -- We are specifically looking for shells or rockets here. + if Category==Weapon.Category.SHELL then + --- + -- SHELL + --- + + -- Add up all shells. + if Caliber<70 then + nguns=nguns+Nammo + else + nshells=nshells+Nammo + end + + -- Debug info. + text=text..string.format("- %d shells [%s]: caliber=%d mm, range=%d - %d meters\n", Nammo, WeaponName, Caliber, Rmin, Rmax) + + elseif Category==Weapon.Category.ROCKET then + --- + -- ROCKET + --- + + -- Add up all rockets. + nrockets=nrockets+Nammo + + -- Debug info. + text=text..string.format("- %d rockets [%s]: caliber=%d mm, range=%d - %d meters\n", Nammo, WeaponName, Caliber, Rmin, Rmax) + + elseif Category==Weapon.Category.BOMB then + --- + -- BOMB + --- + + -- Add up all rockets. + nbombs=nbombs+Nammo + + -- Debug info. + text=text..string.format("- %d bombs [%s]: caliber=%d mm, range=%d - %d meters\n", Nammo, WeaponName, Caliber, Rmin, Rmax) + + elseif Category==Weapon.Category.MISSILE then + --- + -- MISSILE + --- + + -- Add up all cruise missiles (category 5) + if MissileCategory==Weapon.MissileCategory.AAM then + nmissiles=nmissiles+Nammo + nmissilesAA=nmissilesAA+Nammo + -- Auto add range for AA missles. Useless here as this is not an aircraft. + if Rmax>0 then + self:AddWeaponRange(UTILS.MetersToNM(Rmin), UTILS.MetersToNM(Rmax), ENUMS.WeaponFlag.AnyAA) + end + elseif MissileCategory==Weapon.MissileCategory.SAM then + nmissiles=nmissiles+Nammo + nmissilesSA=nmissilesSA+Nammo + -- Dont think there is a bit type for SAM. + if Rmax>0 then + --self:AddWeaponRange(Rmin, Rmax, ENUMS.WeaponFlag.AnyASM) + end + elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then + nmissiles=nmissiles+Nammo + nmissilesAS=nmissilesAS+Nammo + -- Auto add weapon range for anti-ship missile. + if Rmax>0 then + self:AddWeaponRange(UTILS.MetersToNM(Rmin), UTILS.MetersToNM(Rmax), ENUMS.WeaponFlag.AntiShipMissile) + end + elseif MissileCategory==Weapon.MissileCategory.BM then + nmissiles=nmissiles+Nammo + nmissilesBM=nmissilesBM+Nammo + -- Don't think there is a good bit type for ballistic missiles. + if Rmax>0 then + --self:AddWeaponRange(Rmin, Rmax, ENUMS.WeaponFlag.AnyASM) + end + elseif MissileCategory==Weapon.MissileCategory.CRUISE then + nmissiles=nmissiles+Nammo + nmissilesCR=nmissilesCR+Nammo + -- Auto add weapon range for cruise missile. + if Rmax>0 then + self:AddWeaponRange(UTILS.MetersToNM(Rmin), UTILS.MetersToNM(Rmax), ENUMS.WeaponFlag.CruiseMissile) + end + elseif MissileCategory==Weapon.MissileCategory.OTHER then + nmissiles=nmissiles+Nammo + nmissilesAG=nmissilesAG+Nammo + end + + -- Debug info. + text=text..string.format("- %d %s missiles [%s]: caliber=%d mm, range=%d - %d meters\n", Nammo, self:_MissileCategoryName(MissileCategory), WeaponName, Caliber, Rmin, Rmax) + + elseif Category==Weapon.Category.TORPEDO then + + -- Add up all rockets. + ntorps=ntorps+Nammo + + -- Debug info. + text=text..string.format("- %d torpedos [%s]: caliber=%d mm, range=%d - %d meters\n", Nammo, WeaponName, Caliber, Rmin, Rmax) + + else + + -- Debug info. + text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n", Nammo, TypeName, Category, tostring(MissileCategory)) + + end + + end + end + + -- Debug text and send message. + if self.verbose>=5 then + self:I(self.lid..text) + else + self:T2(self.lid..text) + end + + end + + -- Total amount of ammunition. + nammo=nguns+nshells+nrockets+nmissiles+nbombs+ntorps + + local ammo={} --Ops.OpsGroup#OPSGROUP.Ammo + ammo.Total=nammo + ammo.Guns=nguns + ammo.Shells=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 #COHORT 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 COHORT:_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 + +--- Add an OPERATION. +-- @param #COHORT self +-- @param Ops.Operation#OPERATION Operation The operation this cohort is part of. +-- @return #COHORT self +function COHORT:_AddOperation(Operation) + + self.operations[Operation.name]=Operation + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Commander.lua b/Moose Development/Moose/Ops/Commander.lua new file mode 100644 index 000000000..89a2f50c0 --- /dev/null +++ b/Moose Development/Moose/Ops/Commander.lua @@ -0,0 +1,2192 @@ +--- **Ops** - Commander of Airwings, Brigades and Fleets. +-- +-- **Main Features:** +-- +-- * Manages AIRWINGS, BRIGADEs and FLEETs +-- * Handles missions (AUFTRAG) and finds the best assets for the job +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.Commander +-- @image OPS_Commander.png + + +--- COMMANDER class. +-- @type COMMANDER +-- @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 of the commander. +-- @field #string alias Alias name. +-- @field #table legions Table of legions which are commanded. +-- @field #table missionqueue Mission queue. +-- @field #table transportqueue Transport queue. +-- @field #table targetqueue Target queue. +-- @field #table opsqueue Operations queue. +-- @field #table rearmingZones Rearming zones. Each element is of type `#BRIGADE.SupplyZone`. +-- @field #table refuellingZones Refuelling zones. Each element is of type `#BRIGADE.SupplyZone`. +-- @field #table capZones CAP zones. Each element is of type `#AIRWING.PatrolZone`. +-- @field #table gcicapZones GCICAP zones. Each element is of type `#AIRWING.PatrolZone`. +-- @field #table awacsZones AWACS zones. Each element is of type `#AIRWING.PatrolZone`. +-- @field #table tankerZones Tanker zones. Each element is of type `#AIRWING.TankerZone`. +-- @field Ops.Chief#CHIEF chief Chief of staff. +-- @field #table limitMission Table of limits for mission types. +-- @extends Core.Fsm#FSM + +--- *He who has never leared to obey cannot be a good commander.* -- Aristotle +-- +-- === +-- +-- # The COMMANDER Concept +-- +-- A commander is the head of legions. He/she will find the best LEGIONs to perform an assigned AUFTRAG (mission) or OPSTRANSPORT. +-- A legion can be an AIRWING, BRIGADE or FLEET. +-- +-- # Constructor +-- +-- A new COMMANDER object is created with the @{#COMMANDER.New}(*Coalition, Alias*) function, where the parameter *Coalition* is the coalition side. +-- It can be `coalition.side.RED`, `coalition.side.BLUE` or `coalition.side.NEUTRAL`. This parameter is mandatory! +-- +-- The second parameter *Alias* is optional and can be used to give the COMMANDER a "name", which is used for output in the dcs.log file. +-- +-- local myCommander=COMANDER:New(coalition.side.BLUE, "General Patton") +-- +-- # Adding Legions +-- +-- Legions, i.e. AIRWINGS, BRIGADES and FLEETS can be added via the @{#COMMANDER.AddLegion}(*Legion*) command: +-- +-- myCommander:AddLegion(myLegion) +-- +-- ## Adding Airwings +-- +-- It is also possible to use @{#COMMANDER.AddAirwing}(*myAirwing*) function. This does the same as the `AddLegion` function but might be a bit more intuitive. +-- +-- ## Adding Brigades +-- +-- It is also possible to use @{#COMMANDER.AddBrigade}(*myBrigade*) function. This does the same as the `AddLegion` function but might be a bit more intuitive. +-- +-- ## Adding Fleets +-- +-- It is also possible to use @{#COMMANDER.AddFleet}(*myFleet*) function. This does the same as the `AddLegion` function but might be a bit more intuitive. +-- +-- # Adding Missions +-- +-- Mission can be added via the @{#COMMANDER.AddMission}(*myMission*) function. +-- +-- # Adding OPS Transports +-- +-- Transportation assignments can be added via the @{#COMMANDER.AddOpsTransport}(*myTransport*) function. +-- +-- # Adding CAP Zones +-- +-- A CAP zone can be added via the @{#COMMANDER.AddCapZone}() function. +-- +-- # Adding Rearming Zones +-- +-- A rearming zone can be added via the @{#COMMANDER.AddRearmingZone}() function. +-- +-- # Adding Refuelling Zones +-- +-- A refuelling zone can be added via the @{#COMMANDER.AddRefuellingZone}() function. +-- +-- +-- # FSM Events +-- +-- The COMMANDER will +-- +-- ## OPSGROUP on Mission +-- +-- Whenever an OPSGROUP (FLIGHTGROUP, ARMYGROUP or NAVYGROUP) is send on a mission, the `OnAfterOpsOnMission()` event is triggered. +-- Mission designers can hook into the event with the @{#COMMANDER.OnAfterOpsOnMission}() function +-- +-- function myCommander:OnAfterOpsOnMission(From, Event, To, OpsGroup, Mission) +-- -- Your code +-- end +-- +-- ## Canceling a Mission +-- +-- A mission can be cancelled with the @{#COMMMANDER.MissionCancel}() function +-- +-- myCommander:MissionCancel(myMission) +-- +-- or +-- myCommander:__MissionCancel(5*60, myMission) +-- +-- The last commander cancels the mission after 5 minutes (300 seconds). +-- +-- The cancel command will be forwarded to all assigned legions and OPS groups, which will abort their mission or remove it from their queue. +-- +-- @field #COMMANDER +COMMANDER = { + ClassName = "COMMANDER", + verbose = 0, + coalition = nil, + legions = {}, + missionqueue = {}, + transportqueue = {}, + targetqueue = {}, + opsqueue = {}, + rearmingZones = {}, + refuellingZones = {}, + capZones = {}, + gcicapZones = {}, + awacsZones = {}, + tankerZones = {}, + limitMission = {}, + maxMissionsAssignPerCycle = 1, +} + +--- COMMANDER class version. +-- @field #string version +COMMANDER.version="0.1.4" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Add CAP zones. +-- DONE: Add tanker zones. +-- DONE: Improve legion selection. Mostly done! +-- DONE: Find solution for missions, which require a transport. This is not as easy as it sounds since the selected mission assets restrict the possible transport assets. +-- DONE: Add ops transports. +-- DONE: Allow multiple Legions for one mission. +-- NOGO: Maybe it's possible to preselect the assets for the mission. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new COMMANDER object and start the FSM. +-- @param #COMMANDER self +-- @param #number Coalition Coaliton of the commander. +-- @param #string Alias Some name you want the commander to be called. +-- @return #COMMANDER self +function COMMANDER:New(Coalition, Alias) + + -- Inherit everything from INTEL class. + local self=BASE:Inherit(self, FSM:New()) --#COMMANDER + + if Coalition==nil then + env.error("ERROR: Coalition parameter is nil in COMMANDER:New() call!") + return nil + end + + -- Set coaliton. + self.coalition=Coalition + + -- Alias name. + self.alias=Alias + + -- Choose a name for red or blue. + if self.alias==nil then + if Coalition==coalition.side.BLUE then + self.alias="George S. Patton" + elseif Coalition==coalition.side.RED then + self.alias="Georgy Zhukov" + elseif Coalition==coalition.side.NEUTRAL then + self.alias="Mahatma Gandhi" + end + end + + -- Log ID. + self.lid=string.format("COMMANDER %s [%s] | ", self.alias, UTILS.GetCoalitionName(self.coalition)) + + -- Start state. + self:SetStartState("NotReadyYet") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("NotReadyYet", "Start", "OnDuty") -- Start COMMANDER. + self:AddTransition("*", "Status", "*") -- Status report. + self:AddTransition("*", "Stop", "Stopped") -- Stop COMMANDER. + + self:AddTransition("*", "MissionAssign", "*") -- Mission is assigned to a or multiple LEGIONs. + self:AddTransition("*", "MissionCancel", "*") -- COMMANDER cancels a mission. + + self:AddTransition("*", "TransportAssign", "*") -- Transport is assigned to a or multiple LEGIONs. + self:AddTransition("*", "TransportCancel", "*") -- COMMANDER cancels a Transport. + + self:AddTransition("*", "OpsOnMission", "*") -- An OPSGROUP was send on a Mission (AUFTRAG). + + self:AddTransition("*", "LegionLost", "*") -- Out of our legions was lost to the enemy. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the COMMANDER. + -- @function [parent=#COMMANDER] Start + -- @param #COMMANDER self + + --- Triggers the FSM event "Start" after a delay. Starts the COMMANDER. + -- @function [parent=#COMMANDER] __Start + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". Stops the COMMANDER. + -- @param #COMMANDER self + + --- Triggers the FSM event "Stop" after a delay. Stops the COMMANDER. + -- @function [parent=#COMMANDER] __Stop + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status". + -- @function [parent=#COMMANDER] Status + -- @param #COMMANDER self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#COMMANDER] __Status + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "MissionAssign". Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission! + -- @function [parent=#COMMANDER] MissionAssign + -- @param #COMMANDER self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + --- Triggers the FSM event "MissionAssign" after a delay. Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission! + -- @function [parent=#COMMANDER] __MissionAssign + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + --- On after "MissionAssign" event. + -- @function [parent=#COMMANDER] OnAfterMissionAssign + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The Legion(s) to which the mission is assigned. + + + --- Triggers the FSM event "MissionCancel". + -- @function [parent=#COMMANDER] MissionCancel + -- @param #COMMANDER self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionCancel" after a delay. + -- @function [parent=#COMMANDER] __MissionCancel + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionCancel" event. + -- @function [parent=#COMMANDER] OnAfterMissionCancel + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "TransportAssign". + -- @function [parent=#COMMANDER] TransportAssign + -- @param #COMMANDER self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + --- Triggers the FSM event "TransportAssign" after a delay. + -- @function [parent=#COMMANDER] __TransportAssign + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + --- On after "TransportAssign" event. + -- @function [parent=#COMMANDER] OnAfterTransportAssign + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + + --- Triggers the FSM event "TransportCancel". + -- @function [parent=#COMMANDER] TransportCancel + -- @param #COMMANDER self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportCancel" after a delay. + -- @function [parent=#COMMANDER] __TransportCancel + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportCancel" event. + -- @function [parent=#COMMANDER] OnAfterTransportCancel + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + + --- Triggers the FSM event "OpsOnMission". + -- @function [parent=#COMMANDER] OpsOnMission + -- @param #COMMANDER self + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "OpsOnMission" after a delay. + -- @function [parent=#COMMANDER] __OpsOnMission + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "OpsOnMission" event. + -- @function [parent=#COMMANDER] OnAfterOpsOnMission + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "LegionLost". + -- @function [parent=#COMMANDER] LegionLost + -- @param #COMMANDER self + -- @param Ops.Legion#LEGION Legion The legion that was lost. + -- @param DCS#coalition.side Coalition which captured the warehouse. + -- @param DCS#country.id Country which has captured the warehouse. + + --- Triggers the FSM event "LegionLost". + -- @function [parent=#COMMANDER] __LegionLost + -- @param #COMMANDER self + -- @param #number delay Delay in seconds. + -- @param Ops.Legion#LEGION Legion The legion that was lost. + -- @param DCS#coalition.side Coalition which captured the warehouse. + -- @param DCS#country.id Country which has captured the warehouse. + + --- On after "LegionLost" event. + -- @function [parent=#COMMANDER] OnAfterLegionLost + -- @param #COMMANDER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Legion#LEGION Legion The legion that was lost. + -- @param DCS#coalition.side Coalition which captured the warehouse. + -- @param DCS#country.id Country which has captured the warehouse. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set verbosity level. +-- @param #COMMANDER self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #COMMANDER self +function COMMANDER:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set limit for number of total or specific missions to be executed simultaniously. +-- @param #COMMANDER self +-- @param #number Limit Number of max. mission of this type. Default 10. +-- @param #string MissionType Type of mission, e.g. `AUFTRAG.Type.BAI`. Default `"Total"` for total number of missions. +-- @return #COMMANDER self +function COMMANDER:SetLimitMission(Limit, MissionType) + MissionType=MissionType or "Total" + if MissionType then + self.limitMission[MissionType]=Limit or 10 + else + self:E(self.lid.."ERROR: No mission type given for setting limit!") + end + return self +end + + +--- Get coalition. +-- @param #COMMANDER self +-- @return #number Coalition. +function COMMANDER:GetCoalition() + return self.coalition +end + +--- Add an AIRWING to the commander. +-- @param #COMMANDER self +-- @param Ops.Airwing#AIRWING Airwing The airwing to add. +-- @return #COMMANDER self +function COMMANDER:AddAirwing(Airwing) + + -- Add legion. + self:AddLegion(Airwing) + + return self +end + +--- Add a BRIGADE to the commander. +-- @param #COMMANDER self +-- @param Ops.Brigade#BRIGADE Brigade The brigade to add. +-- @return #COMMANDER self +function COMMANDER:AddBrigade(Brigade) + + -- Add legion. + self:AddLegion(Brigade) + + return self +end + +--- Add a FLEET to the commander. +-- @param #COMMANDER self +-- @param Ops.Fleet#FLEET Fleet The fleet to add. +-- @return #COMMANDER self +function COMMANDER:AddFleet(Fleet) + + -- Add legion. + self:AddLegion(Fleet) + + return self +end + + +--- Add a LEGION to the commander. +-- @param #COMMANDER self +-- @param Ops.Legion#LEGION Legion The legion to add. +-- @return #COMMANDER self +function COMMANDER:AddLegion(Legion) + + -- This legion is managed by the commander. + Legion.commander=self + + -- Add to legions. + table.insert(self.legions, Legion) + + return self +end + +--- Remove a LEGION to the commander. +-- @param #COMMANDER self +-- @param Ops.Legion#LEGION Legion The legion to be removed. +-- @return #COMMANDER self +function COMMANDER:RemoveLegion(Legion) + + for i,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + if legion.alias==Legion.alias then + table.remove(self.legions, i) + Legion.commander=nil + end + end + + return self +end + +--- Add mission to mission queue. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be added. +-- @return #COMMANDER self +function COMMANDER:AddMission(Mission) + + if not self:IsMission(Mission) then + + Mission.commander=self + + Mission.statusCommander=AUFTRAG.Status.PLANNED + + table.insert(self.missionqueue, Mission) + + end + + return self +end + +--- Add transport to queue. +-- @param #COMMANDER self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport to be added. +-- @return #COMMANDER self +function COMMANDER:AddOpsTransport(Transport) + + Transport.commander=self + + Transport.statusCommander=OPSTRANSPORT.Status.PLANNED + + table.insert(self.transportqueue, Transport) + + return self +end + +--- Remove mission from queue. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #COMMANDER self +function COMMANDER:RemoveMission(Mission) + + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==Mission.auftragsnummer then + self:T(self.lid..string.format("Removing mission %s (%s) status=%s from queue", Mission.name, Mission.type, Mission.status)) + mission.commander=nil + table.remove(self.missionqueue, i) + break + end + + end + + return self +end + +--- Remove transport from queue. +-- @param #COMMANDER self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport to be removed. +-- @return #COMMANDER self +function COMMANDER:RemoveTransport(Transport) + + for i,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + if transport.uid==Transport.uid then + self:T(self.lid..string.format("Removing transport UID=%d status=%s from queue", transport.uid, transport:GetState())) + transport.commander=nil + table.remove(self.transportqueue, i) + break + end + + end + + return self +end + +--- Add target. +-- @param #COMMANDER self +-- @param Ops.Target#TARGET Target Target object to be added. +-- @return #COMMANDER self +function COMMANDER:AddTarget(Target) + + if not self:IsTarget(Target) then + table.insert(self.targetqueue, Target) + end + + return self +end + +--- Add operation. +-- @param #COMMANDER self +-- @param Ops.Operation#OPERATION Operation The operation to be added. +-- @return #COMMANDER self +function COMMANDER:AddOperation(Operation) + + -- TODO: Check that is not already added. + + -- Add operation to table. + table.insert(self.opsqueue, Operation) + + return self +end + +--- Check if a TARGET is already in the queue. +-- @param #COMMANDER self +-- @param Ops.Target#TARGET Target Target object to be added. +-- @return #boolean If `true`, target exists in the target queue. +function COMMANDER:IsTarget(Target) + + for _,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + if target.uid==Target.uid or target:GetName()==Target:GetName() then + return true + end + end + + return false +end + +--- Remove target from queue. +-- @param #COMMANDER self +-- @param Ops.Target#TARGET Target The target. +-- @return #COMMANDER self +function COMMANDER:RemoveTarget(Target) + + for i,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + + if target.uid==Target.uid then + self:T(self.lid..string.format("Removing target %s from queue", Target.name)) + table.remove(self.targetqueue, i) + break + end + + end + + return self +end + +--- Add a rearming zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE RearmingZone Rearming zone. +-- @return Ops.Brigade#BRIGADE.SupplyZone The rearming zone data. +function COMMANDER:AddRearmingZone(RearmingZone) + + local rearmingzone={} --Ops.Brigade#BRIGADE.SupplyZone + + rearmingzone.zone=RearmingZone + rearmingzone.mission=nil + --rearmingzone.marker=MARKER:New(rearmingzone.zone:GetCoordinate(), "Rearming Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.rearmingZones, rearmingzone) + + return rearmingzone +end + +--- Add a refuelling zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE RefuellingZone Refuelling zone. +-- @return Ops.Brigade#BRIGADE.SupplyZone The refuelling zone data. +function COMMANDER:AddRefuellingZone(RefuellingZone) + + local rearmingzone={} --Ops.Brigade#BRIGADE.SupplyZone + + rearmingzone.zone=RefuellingZone + rearmingzone.mission=nil + --rearmingzone.marker=MARKER:New(rearmingzone.zone:GetCoordinate(), "Refuelling Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.refuellingZones, rearmingzone) + + return rearmingzone +end + +--- Add a CAP zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE Zone CapZone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet. +-- @param #number Speed Orbit speed in KIAS. 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 30 NM. +-- @return Ops.Airwing#AIRWING.PatrolZone The CAP zone data. +function COMMANDER:AddCapZone(Zone, Altitude, Speed, Heading, Leg) + + local patrolzone={} --Ops.Airwing#AIRWING.PatrolZone + + patrolzone.zone=Zone + patrolzone.altitude=Altitude or 12000 + patrolzone.heading=Heading or 270 + --patrolzone.speed=UTILS.KnotsToAltKIAS(Speed or 350, patrolzone.altitude) + patrolzone.speed=Speed or 350 + patrolzone.leg=Leg or 30 + patrolzone.mission=nil + --patrolzone.marker=MARKER:New(patrolzone.zone:GetCoordinate(), "CAP Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.capZones, patrolzone) + + return patrolzone +end + +--- Add a GCICAP zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE Zone CapZone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet. +-- @param #number Speed Orbit speed in KIAS. 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 30 NM. +-- @return Ops.Airwing#AIRWING.PatrolZone The CAP zone data. +function COMMANDER:AddGciCapZone(Zone, Altitude, Speed, Heading, Leg) + + local patrolzone={} --Ops.Airwing#AIRWING.PatrolZone + + patrolzone.zone=Zone + patrolzone.altitude=Altitude or 12000 + patrolzone.heading=Heading or 270 + --patrolzone.speed=UTILS.KnotsToAltKIAS(Speed or 350, patrolzone.altitude) + patrolzone.speed=Speed or 350 + patrolzone.leg=Leg or 30 + patrolzone.mission=nil + --patrolzone.marker=MARKER:New(patrolzone.zone:GetCoordinate(), "GCICAP Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.gcicapZones, patrolzone) + + return patrolzone +end + +--- Remove a GCI CAP. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE Zone Zone, where the flight orbits. +function COMMANDER:RemoveGciCapZone(Zone) + + local patrolzone={} --Ops.Airwing#AIRWING.PatrolZone + + patrolzone.zone=Zone + for i,_patrolzone in pairs(self.gcicapZones) do + if _patrolzone.zone == patrolzone.zone then + if _patrolzone.mission and _patrolzone.mission:IsNotOver() then + _patrolzone.mission:Cancel() + end + table.remove(self.gcicapZones, i) + break + end + end + return patrolzone +end + +--- Add an AWACS zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE Zone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet. +-- @param #number Speed Orbit speed in KIAS. 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 30 NM. +-- @return Ops.Airwing#AIRWING.PatrolZone The AWACS zone data. +function COMMANDER:AddAwacsZone(Zone, Altitude, Speed, Heading, Leg) + + local awacszone={} --Ops.Airwing#AIRWING.PatrolZone + + awacszone.zone=Zone + awacszone.altitude=Altitude or 12000 + awacszone.heading=Heading or 270 + --awacszone.speed=UTILS.KnotsToAltKIAS(Speed or 350, awacszone.altitude) + awacszone.speed=Speed or 350 + awacszone.speed=Speed or 350 + awacszone.leg=Leg or 30 + awacszone.mission=nil + --awacszone.marker=MARKER:New(awacszone.zone:GetCoordinate(), "AWACS Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.awacsZones, awacszone) + + return awacszone +end + +--- Remove a AWACS zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE Zone Zone, where the flight orbits. +function COMMANDER:RemoveAwacsZone(Zone) + + local awacszone={} --Ops.Airwing#AIRWING.PatrolZone + + awacszone.zone=Zone + for i,_awacszone in pairs(self.awacsZones) do + if _awacszone.zone == awacszone.zone then + if _awacszone.mission and _awacszone.mission:IsNotOver() then + _awacszone.mission:Cancel() + end + table.remove(self.awacsZones, i) + break + end + end + return awacszone +end + +--- Add a refuelling tanker zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE Zone Zone. +-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet. +-- @param #number Speed Orbit speed in KIAS. 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 30 NM. +-- @param #number RefuelSystem Refuelling system. +-- @return Ops.Airwing#AIRWING.TankerZone The tanker zone data. +function COMMANDER:AddTankerZone(Zone, Altitude, Speed, Heading, Leg, RefuelSystem) + + local tankerzone={} --Ops.Airwing#AIRWING.TankerZone + + tankerzone.zone=Zone + tankerzone.altitude=Altitude or 12000 + tankerzone.heading=Heading or 270 + --tankerzone.speed=UTILS.KnotsToAltKIAS(Speed or 350, tankerzone.altitude) -- speed translation to alt will be done by AUFTRAG anyhow + tankerzone.speed = Speed or 350 + tankerzone.leg=Leg or 30 + tankerzone.refuelsystem=RefuelSystem + tankerzone.mission=nil + tankerzone.marker=MARKER:New(tankerzone.zone:GetCoordinate(), "Tanker Zone"):ToCoalition(self:GetCoalition()) + + table.insert(self.tankerZones, tankerzone) + + return tankerzone +end + +--- Remove a refuelling tanker zone. +-- @param #COMMANDER self +-- @param Core.Zone#ZONE Zone Zone, where the flight orbits. +function COMMANDER:RemoveTankerZone(Zone) + + local tankerzone={} --Ops.Airwing#AIRWING.PatrolZone + + tankerzone.zone=Zone + for i,_tankerzone in pairs(self.tankerZones) do + if _tankerzone.zone == tankerzone.zone then + if _tankerzone.mission and _tankerzone.mission:IsNotOver() then + _tankerzone.mission:Cancel() + end + table.remove(self.tankerZones, i) + break + end + end + return tankerzone +end + +--- Check if this mission is already in the queue. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #boolean If `true`, this mission is in the queue. +function COMMANDER:IsMission(Mission) + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + if mission.auftragsnummer==Mission.auftragsnummer then + return true + end + end + + return false +end + +--- Relocate a cohort to another legion. +-- Assets in stock are spawned and routed to the new legion. +-- If assets are spawned, running missions will be cancelled. +-- Cohort assets will not be available until relocation is finished. +-- @param #COMMANDER self +-- @param Ops.Cohort#COHORT Cohort The cohort to be relocated. +-- @param Ops.Legion#LEGION Legion The legion where the cohort is relocated to. +-- @param #number Delay Delay in seconds before relocation takes place. Default `nil`, *i.e.* ASAP. +-- @param #number NcarriersMin Min number of transport carriers in case the troops should be transported. Default `nil` for no transport. +-- @param #number NcarriersMax Max number of transport carriers. +-- @param #table TransportLegions Legion(s) assigned for transportation. Default is all legions of the commander. +-- @return #COMMANDER self +function COMMANDER:RelocateCohort(Cohort, Legion, Delay, NcarriersMin, NcarriersMax, TransportLegions) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, COMMANDER.RelocateCohort, self, Cohort, Legion, 0, NcarriersMin, NcarriersMax, TransportLegions) + else + + -- Add cohort to legion. + if Legion:IsCohort(Cohort.name) then + self:E(self.lid..string.format("ERROR: Cohort %s is already part of new legion %s ==> CANNOT Relocate!", Cohort.name, Legion.alias)) + return self + else + table.insert(Legion.cohorts, Cohort) + end + + -- Old legion. + local LegionOld=Cohort.legion + + -- Check that cohort is part of this legion + if not LegionOld:IsCohort(Cohort.name) then + self:E(self.lid..string.format("ERROR: Cohort %s is NOT part of this legion %s ==> CANNOT Relocate!", Cohort.name, self.alias)) + return self + end + + -- Check that legions are different. + if LegionOld.alias==Legion.alias then + self:E(self.lid..string.format("ERROR: old legion %s is same as new legion %s ==> CANNOT Relocate!", LegionOld.alias, Legion.alias)) + return self + end + + -- Trigger Relocate event. + Cohort:Relocate() + + -- Create a relocation mission. + local mission=AUFTRAG:_NewRELOCATECOHORT(Legion, Cohort) + + -- Assign cohort to mission. + mission:AssignCohort(Cohort) + + -- All assets required. + mission:SetRequiredAssets(#Cohort.assets) + + -- Set transportation. + if NcarriersMin and NcarriersMin>0 then + mission:SetRequiredTransport(Legion.spawnzone, NcarriersMin, NcarriersMax) + end + + -- Assign transport legions. + if TransportLegions then + for _,legion in pairs(TransportLegions) do + mission:AssignTransportLegion(legion) + end + else + for _,legion in pairs(self.legions) do + mission:AssignTransportLegion(legion) + end + end + + -- Set mission range very large. Mission designer should know... + mission:SetMissionRange(10000) + + -- Add mission. + self:AddMission(mission) + + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #COMMANDER self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function COMMANDER:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting Commander") + self:I(self.lid..text) + + -- Start attached legions. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + if legion:GetState()=="NotReadyYet" then + legion:Start() + end + end + + self:__Status(-1) +end + +--- On after "Status" event. +-- @param #COMMANDER self +-- @param Wrapper.Group#GROUP Group Flight group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function COMMANDER:onafterStatus(From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + -- Status. + if self.verbose>=1 then + local text=string.format("Status %s: Legions=%d, Missions=%d, Targets=%d, Transports=%d", fsmstate, #self.legions, #self.missionqueue, #self.targetqueue, #self.transportqueue) + self:T(self.lid..text) + end + + -- Check Operations queue. + self:CheckOpsQueue() + + -- Check target queue and add missions. + self:CheckTargetQueue() + + -- Check mission queue and assign one PLANNED mission. + self:CheckMissionQueue() + + -- Check transport queue and assign one PLANNED transport. + self:CheckTransportQueue() + + -- Check rearming zones. + for _,_rearmingzone in pairs(self.rearmingZones) do + local rearmingzone=_rearmingzone --Ops.Brigade#BRIGADE.SupplyZone + -- Check if mission is nil or over. + if (not rearmingzone.mission) or rearmingzone.mission:IsOver() then + rearmingzone.mission=AUFTRAG:NewAMMOSUPPLY(rearmingzone.zone) + self:AddMission(rearmingzone.mission) + end + end + + -- Check refuelling zones. + for _,_supplyzone in pairs(self.refuellingZones) do + local supplyzone=_supplyzone --Ops.Brigade#BRIGADE.SupplyZone + -- Check if mission is nil or over. + if (not supplyzone.mission) or supplyzone.mission:IsOver() then + supplyzone.mission=AUFTRAG:NewFUELSUPPLY(supplyzone.zone) + self:AddMission(supplyzone.mission) + end + end + + + -- Check CAP zones. + for _,_patrolzone in pairs(self.capZones) do + local patrolzone=_patrolzone --Ops.Airwing#AIRWING.PatrolZone + -- Check if mission is nil or over. + if (not patrolzone.mission) or patrolzone.mission:IsOver() then + local Coordinate=patrolzone.zone:GetCoordinate() + patrolzone.mission=AUFTRAG:NewCAP(patrolzone.zone, patrolzone.altitude, patrolzone.speed, Coordinate, patrolzone.heading, patrolzone.leg) + self:AddMission(patrolzone.mission) + end + end + + -- Check GCICAP zones. + for _,_patrolzone in pairs(self.gcicapZones) do + local patrolzone=_patrolzone --Ops.Airwing#AIRWING.PatrolZone + -- Check if mission is nil or over. + if (not patrolzone.mission) or patrolzone.mission:IsOver() then + local Coordinate=patrolzone.zone:GetCoordinate() + patrolzone.mission=AUFTRAG:NewGCICAP(Coordinate, patrolzone.altitude, patrolzone.speed, patrolzone.heading, patrolzone.leg) + self:AddMission(patrolzone.mission) + end + end + + -- Check AWACS zones. + for _,_awacszone in pairs(self.awacsZones) do + local awacszone=_awacszone --Ops.Airwing#AIRWING.Patrol + -- Check if mission is nil or over. + if (not awacszone.mission) or awacszone.mission:IsOver() then + local Coordinate=awacszone.zone:GetCoordinate() + awacszone.mission=AUFTRAG:NewAWACS(Coordinate, awacszone.altitude, awacszone.speed, awacszone.heading, awacszone.leg) + self:AddMission(awacszone.mission) + end + end + + -- Check Tanker zones. + for _,_tankerzone in pairs(self.tankerZones) do + local tankerzone=_tankerzone --Ops.Airwing#AIRWING.TankerZone + -- Check if mission is nil or over. + if (not tankerzone.mission) or tankerzone.mission:IsOver() then + local Coordinate=tankerzone.zone:GetCoordinate() + tankerzone.mission=AUFTRAG:NewTANKER(Coordinate, tankerzone.altitude, tankerzone.speed, tankerzone.heading, tankerzone.leg, tankerzone.refuelsystem) + self:AddMission(tankerzone.mission) + end + end + + --- + -- LEGIONS + --- + + if self.verbose>=2 and #self.legions>0 then + + local text="Legions:" + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + local Nassets=legion:CountAssets() + local Nastock=legion:CountAssets(true) + text=text..string.format("\n* %s [%s]: Assets=%s stock=%s", legion.alias, legion:GetState(), Nassets, Nastock) + for _,aname in pairs(AUFTRAG.Type) do + local na=legion:CountAssets(true, {aname}) + local np=legion:CountPayloadsInStock({aname}) + local nm=legion:CountAssetsOnMission({aname}) + if na>0 or np>0 then + text=text..string.format("\n - %s: assets=%d, payloads=%d, on mission=%d", aname, na, np, nm) + end + end + end + self:T(self.lid..text) + + + if self.verbose>=3 then + + -- Count numbers + local Ntotal=0 + local Nspawned=0 + local Nrequested=0 + local Nreserved=0 + local Nstock=0 + + local text="\n===========================================\n" + text=text.."Assets:" + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + for _,_asset in pairs(cohort.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + local state="In Stock" + if asset.flightgroup then + state=asset.flightgroup:GetState() + local mission=legion:GetAssetCurrentMission(asset) + if mission then + state=state..string.format(", Mission \"%s\" [%s]", mission:GetName(), mission:GetType()) + end + else + if asset.spawned then + env.info("FF ERROR: asset has opsgroup but is NOT spawned!") + end + if asset.requested and asset.isReserved then + env.info("FF ERROR: asset is requested and reserved. Should not be both!") + state="Reserved+Requested!" + elseif asset.isReserved then + state="Reserved" + elseif asset.requested then + state="Requested" + end + end + + -- Text. + text=text..string.format("\n[UID=%03d] %s Legion=%s [%s]: State=%s [RID=%s]", + asset.uid, asset.spawngroupname, legion.alias, cohort.name, state, tostring(asset.rid)) + + + if asset.spawned then + Nspawned=Nspawned+1 + end + if asset.requested then + Nrequested=Nrequested+1 + end + if asset.isReserved then + Nreserved=Nreserved+1 + end + if not (asset.spawned or asset.requested or asset.isReserved) then + Nstock=Nstock+1 + end + + Ntotal=Ntotal+1 + + end + + end + + end + text=text.."\n-------------------------------------------" + text=text..string.format("\nNstock = %d", Nstock) + text=text..string.format("\nNreserved = %d", Nreserved) + text=text..string.format("\nNrequested = %d", Nrequested) + text=text..string.format("\nNspawned = %d", Nspawned) + text=text..string.format("\nNtotal = %d (=%d)", Ntotal, Nstock+Nspawned+Nrequested+Nreserved) + text=text.."\n===========================================" + self:I(self.lid..text) + end + + end + + --- + -- MISSIONS + --- + + -- Mission queue. + if self.verbose>=2 and #self.missionqueue>0 then + local text="Mission queue:" + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + local target=mission:GetTargetName() or "unknown" + text=text..string.format("\n[%d] %s (%s): status=%s, target=%s", i, mission.name, mission.type, mission.status, target) + end + self:I(self.lid..text) + end + + + --- + -- TARGETS + --- + + -- Target queue. + if self.verbose>=2 and #self.targetqueue>0 then + local text="Target queue:" + for i,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + text=text..string.format("\n[%d] %s: status=%s, life=%d", i, target:GetName(), target:GetState(), target:GetLife()) + end + self:I(self.lid..text) + end + + --- + -- TRANSPORTS + --- + + -- Transport queue. + if self.verbose>=2 and #self.transportqueue>0 then + local text="Transport queue:" + for i,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + text=text..string.format("\n[%d] UID=%d: status=%s", i, transport.uid, transport:GetState()) + end + self:I(self.lid..text) + end + + self:__Status(-30) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "MissionAssign" event. Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission already. +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @param #table Legions The Legion(s) to which the mission is assigned. +function COMMANDER:onafterMissionAssign(From, Event, To, Mission, Legions) + + -- Add mission to queue. + self:AddMission(Mission) + + -- Set mission commander status to QUEUED as it is now queued at a legion. + Mission.statusCommander=AUFTRAG.Status.QUEUED + + for _,_Legion in pairs(Legions) do + local Legion=_Legion --Ops.Legion#LEGION + + -- Debug info. + self:T(self.lid..string.format("Assigning mission \"%s\" [%s] to legion \"%s\"", Mission.name, Mission.type, Legion.alias)) + + -- Add mission to legion. + Legion:AddMission(Mission) + + -- Directly request the mission as the assets have already been selected. + Legion:MissionRequest(Mission) + + end + +end + +--- On after "MissionCancel" event. +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +function COMMANDER:onafterMissionCancel(From, Event, To, Mission) + + -- Debug info. + self:T(self.lid..string.format("Cancelling mission \"%s\" [%s] in status %s", Mission.name, Mission.type, Mission.status)) + + -- Set commander status. + Mission.statusCommander=AUFTRAG.Status.CANCELLED + + if Mission:IsPlanned() then + + -- Mission is still in planning stage. Should not have a legion assigned ==> Just remove it form the queue. + self:RemoveMission(Mission) + + else + + -- Legion will cancel mission. + if #Mission.legions>0 then + for _,_legion in pairs(Mission.legions) do + local legion=_legion --Ops.Legion#LEGION + + -- TODO: Should check that this legions actually belongs to this commander. + + -- Legion will cancel the mission. + legion:MissionCancel(Mission) + end + end + + end + +end + +--- On after "TransportAssign" event. Transport is added to a LEGION transport queue. +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. +function COMMANDER:onafterTransportAssign(From, Event, To, Transport, Legions) + + -- Set mission commander status to QUEUED as it is now queued at a legion. + Transport.statusCommander=OPSTRANSPORT.Status.QUEUED + + for _,_Legion in pairs(Legions) do + local Legion=_Legion --Ops.Legion#LEGION + + -- Debug info. + self:T(self.lid..string.format("Assigning transport UID=%d to legion \"%s\"", Transport.uid, Legion.alias)) + + -- Add mission to legion. + Legion:AddOpsTransport(Transport) + + -- Directly request the mission as the assets have already been selected. + Legion:TransportRequest(Transport) + + end + +end + +--- On after "TransportCancel" event. +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. +function COMMANDER:onafterTransportCancel(From, Event, To, Transport) + + -- Debug info. + self:T(self.lid..string.format("Cancelling Transport UID=%d in status %s", Transport.uid, Transport:GetState())) + + -- Set commander status. + Transport.statusCommander=OPSTRANSPORT.Status.CANCELLED + + if Transport:IsPlanned() then + + -- Transport is still in planning stage. Should not have a legion assigned ==> Just remove it form the queue. + self:RemoveTransport(Transport) + + else + + -- Legion will cancel mission. + if #Transport.legions>0 then + for _,_legion in pairs(Transport.legions) do + local legion=_legion --Ops.Legion#LEGION + + -- TODO: Should check that this legions actually belongs to this commander. + + -- Legion will cancel the mission. + legion:TransportCancel(Transport) + end + end + + end + +end + +--- On after "OpsOnMission". +-- @param #COMMANDER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup Ops group on mission +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function COMMANDER:onafterOpsOnMission(From, Event, To, OpsGroup, Mission) + -- Debug info. + self:T2(self.lid..string.format("Group \"%s\" on mission \"%s\" [%s]", OpsGroup:GetName(), Mission:GetName(), Mission:GetType())) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mission Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check OPERATIONs queue. +-- @param #COMMANDER self +function COMMANDER:CheckOpsQueue() + + -- Number of missions. + local Nops=#self.opsqueue + + -- Treat special cases. + if Nops==0 then + return nil + end + + -- Loop over operations. + for _,_ops in pairs(self.opsqueue) do + local operation=_ops --Ops.Operation#OPERATION + + if operation:IsRunning() then + + -- Loop over missions. + for _,_mission in pairs(operation.missions or {}) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.phase==nil or (mission.phase and mission.phase==operation.phase) and mission:IsPlanned() then + self:AddMission(mission) + end + end + + -- Loop over targets. + for _,_target in pairs(operation.targets or {}) do + local target=_target --Ops.Target#TARGET + + if (target.phase==nil or (target.phase and target.phase==operation.phase)) and (not self:IsTarget(target)) then + self:AddTarget(target) + end + end + + end + + end + +end + +--- Check target queue and assign ONE valid target by adding it to the mission queue of the COMMANDER. +-- @param #COMMANDER self +function COMMANDER:CheckTargetQueue() + + -- Number of missions. + local Ntargets=#self.targetqueue + + -- Treat special cases. + if Ntargets==0 then + return nil + end + + -- Remove done targets. + for i=#self.targetqueue,1,-1 do + local target=self.targetqueue[i] --Ops.Target#TARGET + if (not target:IsAlive()) or target:EvalConditionsAny(target.conditionStop) then + for _,_resource in pairs(target.resources) do + local resource=_resource --Ops.Target#TARGET.Resource + if resource.mission and resource.mission:IsNotOver() then + self:MissionCancel(resource.mission) + end + end + table.remove(self.targetqueue, i) + end + end + + -- Check if total number of missions is reached. + local NoLimit=self:_CheckMissionLimit("Total") + if NoLimit==false then + return nil + end + + -- Sort results table wrt prio and threatlevel. + local function _sort(a, b) + local taskA=a --Ops.Target#TARGET + local taskB=b --Ops.Target#TARGET + return (taskA.priotaskB.threatlevel0) + end + table.sort(self.targetqueue, _sort) + + -- Get the lowest importance value (lower means more important). + -- If a target with importance 1 exists, targets with importance 2 will not be assigned. Targets with no importance (nil) can still be selected. + local vip=math.huge + for _,_target in pairs(self.targetqueue) do + local target=_target --Ops.Target#TARGET + if target:IsAlive() and target.importance and target.importance Creating mission type %s: Nmin=%d, Nmax=%d", target:GetName(), missionType, resource.Nmin, resource.Nmax)) + + -- Create a mission. + local mission=AUFTRAG:NewFromTarget(target, missionType) + + if mission then + + -- Set mission parameters. + mission:SetRequiredAssets(resource.Nmin, resource.Nmax) + mission:SetRequiredAttribute(resource.Attributes) + mission:SetRequiredProperty(resource.Properties) + + -- Set operation (if any). + mission.operation=target.operation + + -- Set resource mission. + resource.mission=mission + + -- Add mission to queue. + self:AddMission(resource.mission) + + end + + end + + end + + end + end + +end + + +--- Check mission queue and assign ONE planned mission. +-- @param #COMMANDER self +function COMMANDER:CheckMissionQueue() + + -- Number of missions. + local Nmissions=#self.missionqueue + + -- Treat special cases. + if Nmissions==0 then + return nil + end + + local NoLimit=self:_CheckMissionLimit("Total") + if NoLimit==false 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.prio= (self.maxMissionsAssignPerCycle or 1) then + return + end + + end + + else + + --- + -- Missions NOT in PLANNED state + --- + + end + + end + +end + +--- Set how many missions can be assigned in a single status iteration. (eg. This is useful for persistent missions where you need to load all AUFTRAGs on mission start and then change it back to default) +--- Warning: Increasing this value will increase the number of missions started per iteration and thus may lead to performance issues if too many missions are started at once. +-- @param #COMMANDER self +-- @param #number Number of missions assigned per status iteration. Default is 1. +-- @return #COMMANDER self. +function COMMANDER:SetMaxMissionsAssignPerCycle(MaxMissionsAssignPerCycle) + self.maxMissionsAssignPerCycle = MaxMissionsAssignPerCycle or 1 + return self +end + +--- Get cohorts. +-- @param #COMMANDER self +-- @param #table Legions Special legions. +-- @param #table Cohorts Special cohorts. +-- @param Ops.Operation#OPERATION Operation Operation. +-- @return #table Cohorts. +function COMMANDER:_GetCohorts(Legions, Cohorts, Operation) + + --- Function that check if a legion or cohort is part of an operation. + local function CheckOperation(LegionOrCohort) + -- No operations ==> no problem! + if #self.opsqueue==0 then + return true + end + + -- Cohort is not dedicated to a running(!) operation. We assume so. + local isAvail=true + + -- Only available... + if Operation then + isAvail=false + end + + for _,_operation in pairs(self.opsqueue) do + local operation=_operation --Ops.Operation#OPERATION + + -- Legion is assigned to this operation. + local isOps=operation:IsAssignedCohortOrLegion(LegionOrCohort) + + if isOps and operation:IsRunning() then + + -- Is dedicated. + isAvail=false + + if Operation==nil then + -- No Operation given and this is dedicated to at least one operation. + return false + else + if Operation.uid==operation.uid then + -- Operation given and is part of it. + return true + end + end + end + end + + return isAvail + end + + -- Chosen cohorts. + local cohorts={} + + -- Check if there are any special legions and/or cohorts. + if (Legions and #Legions>0) or (Cohorts and #Cohorts>0) then + + -- Add cohorts of special legions. + for _,_legion in pairs(Legions or {}) do + local legion=_legion --Ops.Legion#LEGION + + -- Check that runway is operational. + local Runway=true + if legion:IsAirwing() then + Runway=legion:IsRunwayOperational() and legion.airbase and legion.airbase:GetCoalition() == legion:GetCoalition() + end + + -- Legion has to be running. + if legion:IsRunning() and Runway then + + -- Add cohorts of legion. + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + if CheckOperation(cohort.legion) or CheckOperation(cohort) then + table.insert(cohorts, cohort) + end + end + + end + end + + -- Add special cohorts. + for _,_cohort in pairs(Cohorts or {}) do + local cohort=_cohort --Ops.Cohort#COHORT + + if CheckOperation(cohort) then + table.insert(cohorts, cohort) + end + end + + else + + -- No special mission legions/cohorts found ==> take own legions. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + + -- Check that runway is operational. + local Runway=true + if legion:IsAirwing() then + Runway=legion:IsRunwayOperational() and legion.airbase and legion.airbase:GetCoalition() == legion:GetCoalition() + end + + -- Legion has to be running. + if legion:IsRunning() and Runway then + + -- Add cohorts of legion. + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + if CheckOperation(cohort.legion) or CheckOperation(cohort) then + table.insert(cohorts, cohort) + end + end + + end + end + + end + + return cohorts +end + +--- Recruit assets for a given mission. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #boolean If `true` enough assets could be recruited. +-- @return #table Recruited assets. +-- @return #table Legions that have recruited assets. +function COMMANDER:RecruitAssetsForMission(Mission) + + -- Debug info. + self:T2(self.lid..string.format("Recruiting assets for mission \"%s\" [%s]", Mission:GetName(), Mission:GetType())) + + -- Number of required assets. + local NreqMin, NreqMax=Mission:GetRequiredAssets() + + -- Target position. + local TargetVec2=Mission:GetTargetVec2() + + -- Special payloads. + local Payloads=Mission.payloads + + -- Largest cargo bay available of available carrier assets if mission assets need to be transported. + local MaxWeight=nil + + if Mission.NcarriersMin then + + local legions=self.legions + local cohorts=nil + if Mission.transportLegions or Mission.transportCohorts then + legions=Mission.transportLegions + cohorts=Mission.transportCohorts + end + + -- Get transport cohorts. + local Cohorts=LEGION._GetCohorts(legions, cohorts) + + -- Filter cohorts that can actually perform transport missions. + local transportcohorts={} + for _,_cohort in pairs(Cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + -- Check if cohort can perform transport to target. + local can=LEGION._CohortCan(cohort, AUFTRAG.Type.OPSTRANSPORT, Mission.carrierCategories, Mission.carrierAttributes, Mission.carrierProperties, nil, TargetVec2) + + -- MaxWeight of cargo assets is limited by the largets available cargo bay. We don't want to select, e.g., tanks that cannot be transported by APCs or helos. + if can and (MaxWeight==nil or cohort.cargobayLimit>MaxWeight) then + MaxWeight=cohort.cargobayLimit + end + end + + if MaxWeight then + self:T(self.lid..string.format("Largest cargo bay available=%.1f", MaxWeight)) + end + end + + local legions=self.legions + local cohorts=nil + if Mission.specialLegions or Mission.specialCohorts then + legions=Mission.specialLegions + cohorts=Mission.specialCohorts + end + + -- Get cohorts. + local Cohorts=LEGION._GetCohorts(legions, cohorts, Mission.operation, self.opsqueue) + + -- Debug info. + self:T(self.lid..string.format("Found %d cohort candidates for mission", #Cohorts)) + + -- Recruite assets. + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, Mission.type, Mission.alert5MissionType, NreqMin, NreqMax, TargetVec2, Payloads, + Mission.engageRange, Mission.refuelSystem, nil, nil, MaxWeight, nil, Mission.attributes, Mission.properties, {Mission.engageWeaponType}) + + return recruited, assets, legions +end + +--- Recruit assets performing an escort mission for a given asset. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @param #table Assets Table of assets to be escorted. +-- @return #boolean If `true`, enough assets could be recruited or no escort was required in the first place. +function COMMANDER:RecruitAssetsForEscort(Mission, Assets) + + -- Is an escort requested in the first place? + if Mission.NescortMin and Mission.NescortMax and (Mission.NescortMin>0 or Mission.NescortMax>0) then + + -- Cohorts. + local Cohorts=self:_GetCohorts(Mission.escortLegions, Mission.escortCohorts, Mission.operation) + + -- Call LEGION function but provide COMMANDER as self. + local assigned=LEGION.AssignAssetsForEscort(self, Cohorts, Assets, Mission.NescortMin, Mission.NescortMax, Mission.escortMissionType, Mission.escortTargetTypes, Mission.escortEngageRange) + + return assigned + end + + return true +end + +--- Recruit assets for a given TARGET. +-- @param #COMMANDER self +-- @param Ops.Target#TARGET Target The target. +-- @param #string MissionType Mission Type. +-- @param #number NassetsMin Min number of required assets. +-- @param #number NassetsMax Max number of required assets. +-- @return #boolean If `true` enough assets could be recruited. +-- @return #table Assets that have been recruited from all legions. +-- @return #table Legions that have recruited assets. +function COMMANDER:RecruitAssetsForTarget(Target, MissionType, NassetsMin, NassetsMax) + + -- Cohorts. + local Cohorts=self:_GetCohorts() + + -- Target position. + local TargetVec2=Target:GetVec2() + + -- Recruite assets. + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, MissionType, nil, NassetsMin, NassetsMax, TargetVec2) + + + return recruited, assets, legions +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Transport Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check transport queue and assign ONE planned transport. +-- @param #COMMANDER self +function COMMANDER:CheckTransportQueue() + + -- Number of missions. + local Ntransports=#self.transportqueue + + -- Treat special cases. + if Ntransports==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 + for _,_opsgroup in pairs(cargoOpsGroups) do + local opsgroup=_opsgroup --Ops.OpsGroup#OPSGROUP + local weight=opsgroup:GetWeightTotal() + if weight>weightGroup then + weightGroup=weight + end + TotalWeight=TotalWeight+weight + end + end + + if weightGroup>0 then + + -- Recruite assets from legions. + local recruited, assets, legions=self:RecruitAssetsForTransport(transport, weightGroup, TotalWeight) + + if recruited then + + -- Add asset to transport. + for _,_asset in pairs(assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + transport:AddAsset(asset) + end + + -- Assign transport to legion(s). + self:TransportAssign(transport, legions) + + -- Only ONE transport is assigned. + return + else + -- Not recruited. + LEGION.UnRecruitAssets(assets) + end + + end + + else + + --- + -- Missions NOT in PLANNED state + --- + + end + + end + +end + +--- Recruit assets for a given OPS transport. +-- @param #COMMANDER self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport. +-- @param #number CargoWeight Weight of the heaviest cargo group. +-- @param #number TotalWeight Total weight of all cargo groups. +-- @return #boolean If `true`, enough assets could be recruited. +-- @return #table Recruited assets. +-- @return #table Legions that have recruited assets. +function COMMANDER:RecruitAssetsForTransport(Transport, CargoWeight, TotalWeight) + + if CargoWeight==0 then + -- No cargo groups! + return false, {}, {} + end + + -- Cohorts. + local Cohorts=self:_GetCohorts() + + -- Target is the deploy zone. + local TargetVec2=Transport:GetDeployZone():GetVec2() + + -- Number of required carriers. + local NreqMin,NreqMax=Transport:GetRequiredCarriers() + + -- Recruit assets and legions. + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NreqMin, NreqMax, TargetVec2, nil, nil, nil, CargoWeight, TotalWeight) + + return recruited, assets, legions +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Resources +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if limit of missions has been reached. +-- @param #COMMANDER self +-- @param #string MissionType Type of mission. +-- @return #boolean If `true`, mission limit has **not** been reached. If `false`, limit has been reached. +function COMMANDER:_CheckMissionLimit(MissionType) + + local limit=self.limitMission[MissionType] + + if limit then + + if MissionType=="Total" then + MissionType=AUFTRAG.Type + end + + local N=self:CountMissions(MissionType, true) + + if N>=limit then + return false + end + end + + return true +end + + +--- Count assets of all assigned legions. +-- @param #COMMANDER self +-- @param #boolean InStock If true, only assets that are in the warehouse stock/inventory are counted. +-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. +-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. +-- @return #number Amount of asset groups. +function COMMANDER:CountAssets(InStock, MissionTypes, Attributes) + + local N=0 + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + N=N+legion:CountAssets(InStock, MissionTypes, Attributes) + end + + return N +end + +--- Count assets of all assigned legions. +-- @param #COMMANDER self +-- @param #table MissionTypes (Optional) Count only missions of these types. Default is all types. +-- @param #boolean OnlyRunning If `true`, only count running missions. +-- @return #number Amount missions. +function COMMANDER:CountMissions(MissionTypes, OnlyRunning) + + local N=0 + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if (not OnlyRunning) or (mission.statusCommander~=AUFTRAG.Status.PLANNED) then + + -- Check if this mission type is requested. + if AUFTRAG.CheckMissionType(mission.type, MissionTypes) then + N=N+1 + end + + end + end + + + return N +end + +--- Count assets of all assigned legions. +-- @param #COMMANDER self +-- @param #boolean InStock If true, only assets that are in the warehouse stock/inventory are counted. +-- @param #table Legions (Optional) Table of legions. Default is all legions. +-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. +-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. +-- @return #number Amount of asset groups. +function COMMANDER:GetAssets(InStock, Legions, MissionTypes, Attributes) + + -- Selected assets. + local assets={} + + for _,_legion in pairs(Legions or self.legions) do + local legion=_legion --Ops.Legion#LEGION + + --TODO Check if legion is running and maybe if runway is operational if air assets are requested. + + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + for _,_asset in pairs(cohort.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- TODO: Check if repaired. + -- TODO: currently we take only unspawned assets. + if not (asset.spawned or asset.isReserved or asset.requested) then + table.insert(assets, asset) + end + + end + end + end + + return assets +end + +--- Check all legions if they are able to do a specific mission type at a certain location with a given number of assets. +-- @param #COMMANDER self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #table Table of LEGIONs that can do the mission and have at least one asset available right now. +function COMMANDER:GetLegionsForMission(Mission) + + -- Table of legions that can do the mission. + local legions={} + + -- Loop over all legions. + for _,_legion in pairs(self.legions) do + local legion=_legion --Ops.Legion#LEGION + + -- Count number of assets in stock. + local Nassets=0 + if legion:IsAirwing() then + Nassets=legion:CountAssetsWithPayloadsInStock(Mission.payloads, {Mission.type}, Attributes) + else + Nassets=legion:CountAssets(true, {Mission.type}, Attributes) --Could also specify the attribute if Air or Ground mission. + end + + -- Has it assets that can? + if Nassets>0 and false then + + -- Get coordinate of the target. + local coord=Mission:GetTargetCoordinate() + + if coord then + + -- Distance from legion to target. + local distance=UTILS.MetersToNM(coord:Get2DDistance(legion:GetCoordinate())) + + -- Round: 55 NM ==> 5.5 ==> 6, 63 NM ==> 6.3 ==> 6 + local dist=UTILS.Round(distance/10, 0) + + -- Debug info. + self:T(self.lid..string.format("Got legion %s with Nassets=%d and dist=%.1f NM, rounded=%.1f", legion.alias, Nassets, distance, dist)) + + -- Add legion to table of legions that can. + table.insert(legions, {airwing=legion, distance=distance, dist=dist, targetcoord=coord, nassets=Nassets}) + + end + + end + + -- Add legion if it can provide at least 1 asset. + if Nassets>0 then + table.insert(legions, legion) + end + + end + + return legions +end + +--- Get assets on given mission or missions. +-- @param #COMMANDER self +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @return #table Assets on pending requests. +function COMMANDER:GetAssetsOnMission(MissionTypes) + + local assets={} + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Check if this mission type is requested. + if AUFTRAG.CheckMissionType(mission.type, MissionTypes) then + + for _,_asset in pairs(mission.assets or {}) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + table.insert(assets, asset) + end + end + end + + return assets +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/EasyGCICAP.lua b/Moose Development/Moose/Ops/EasyGCICAP.lua new file mode 100644 index 000000000..8a902cad5 --- /dev/null +++ b/Moose Development/Moose/Ops/EasyGCICAP.lua @@ -0,0 +1,1657 @@ +------------------------------------------------------------------------- +-- Easy CAP/GCI Class, based on OPS classes +------------------------------------------------------------------------- +-- +-- ## Documentation: +-- +-- https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Ops.EasyGCICAP.html +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/EasyGCICAP). +-- +------------------------------------------------------------------------- +-- Date: September 2023 +-- Last Update: Aug 2025 +------------------------------------------------------------------------- +-- +--- **Ops** - Easy GCI & CAP Manager +-- +-- === +-- +-- **Main Features:** +-- +-- * Automatically create and manage A2A CAP/GCI defenses using an AirWing and Squadrons for one coalition +-- * Easy set-up +-- * Add additional AirWings on other airbases +-- * Each wing can have more than one Squadron - tasking to Squadrons is done on a random basis per AirWing +-- * Create borders and zones of engagement +-- * Detection can be ground based and/or via AWACS +-- +-- === +-- +-- ### AUTHOR: **applevangelist** +-- +-- @module Ops.EasyGCICAP +-- @image AI_Combat_Air_Patrol.JPG + + +--- EASYGCICAP Class +-- @type EASYGCICAP +-- @field #string ClassName +-- @field #number overhead +-- @field #number engagerange +-- @field #number capgrouping +-- @field #string airbasename +-- @field Wrapper.Airbase#AIRBASE airbase +-- @field #number coalition +-- @field #string alias +-- @field #table wings +-- @field Ops.Intel#INTEL Intel +-- @field #number resurrection +-- @field #number capspeed +-- @field #number capalt +-- @field #number capdir +-- @field #number capleg +-- @field #number maxinterceptsize +-- @field #number missionrange +-- @field #number noalert5 +-- @field #table ManagedAW +-- @field #table ManagedSQ +-- @field #table ManagedCP +-- @field #table ManagedTK +-- @field #table ManagedEWR +-- @field #table ManagedREC +-- @field #number MaxAliveMissions +-- @field #boolean debug +-- @field #number repeatsonfailure +-- @field Core.Set#SET_ZONE GoZoneSet +-- @field Core.Set#SET_ZONE NoGoZoneSet +-- @field Core.Set#SET_ZONE ConflictZoneSet +-- @field #boolean Monitor +-- @field #boolean TankerInvisible +-- @field #number CapFormation +-- @field #table ReadyFlightGroups +-- @field #boolean DespawnAfterLanding +-- @field #boolean DespawnAfterHolding +-- @field #list ListOfAuftrag +-- @field #string defaulttakeofftype Take off type +-- @field #number FuelLowThreshold +-- @field #number FuelCriticalThreshold +-- @field #boolean showpatrolpointmarks +-- @field #table EngageTargetTypes +-- @extends Core.Fsm#FSM + +--- *“Airspeed, altitude, and brains. Two are always needed to successfully complete the flight.”* -- Unknown. +-- +-- === +-- +-- # The EasyGCICAP Concept +-- +-- The idea of this class is partially to make the OPS classes easier operational for an A2A CAP/GCI defense network, and to replace the legacy AI_A2A_Dispatcher system - not to it's +-- full extent, but make a basic system work very quickly. +-- +-- # Setup +-- +-- ## Basic understanding +-- +-- The basics are, there is **one** and only **one** AirWing per airbase. Each AirWing has **at least** one Squadron, who will do both CAP and GCI tasks. Squadrons will be randomly chosen for the task at hand. +-- Each AirWing has **at least** one CAP Point that it manages. CAP Points will be covered by the AirWing automatically as long as airframes are available. Detected intruders will be assigned to **one** +-- AirWing based on proximity (that is, if you have more than one). +-- +-- ## Assignment of tasks for intruders +-- +-- Either a CAP Plane or a newly spawned GCI plane will take care of the intruders. Standard overhead is 0.75, i.e. a group of 3 intrudes will +-- be managed by 2 planes from the assigned AirWing. There is an maximum missions limitation per AirWing, so we do not spam the skies. +-- +-- ## Basic set-up code +-- +-- ### Prerequisites +-- +-- You have to put a **STATIC WAREHOUSE** object on the airbase with the UNIT name according to the name of the airbase. **Do not put any other static type or it creates a conflict with the airbase name!** +-- E.g. for Kuitaisi this has to have the unit name Kutaisi. This object symbolizes the AirWing HQ. +-- Next put a late activated template group for your CAP/GCI Squadron on the map. Last, put a zone on the map for the CAP operations, let's name it "Blue Zone 1". Size of the zone plays no role. +-- Put an EW radar system on the map and name it aptly, like "Blue EWR". +-- +-- ### Zones +-- +-- For our example, you create a RED and a BLUE border, as a closed polygonal zone representing the borderlines. You can also have conflict zone, where - for our example - BLUE will attack +-- RED planes, despite being on RED territory. Think of a no-fly zone or an limited area of engagement. Conflict zones take precedence over borders, i.e. they can overlap all borders. +-- +-- ### Code it +-- +-- -- Set up a basic system for the blue side, we'll reside on Kutaisi, and use GROUP objects with "Blue EWR" in the name as EW Radar Systems. +-- local mywing = EASYGCICAP:New("Blue CAP Operations",AIRBASE.Caucasus.Kutaisi,"blue","Blue EWR") +-- +-- -- Add a CAP patrol point belonging to our airbase, we'll be at 30k ft doing 400 kn, initial direction 90 degrees (East), leg 20NM +-- mywing:AddPatrolPointCAP(AIRBASE.Caucasus.Kutaisi,ZONE:FindByName("Blue Zone 1"):GetCoordinate(),30000,400,90,20) +-- +-- -- Add a Squadron with template "Blue Sq1 M2000c", 20 airframes, skill good, Modex starting with 102 and skin "Vendee Jeanne" +-- mywing:AddSquadron("Blue Sq1 M2000c","CAP Kutaisi",AIRBASE.Caucasus.Kutaisi,20,AI.Skill.GOOD,102,"ec1.5_Vendee_Jeanne_clean") +-- +-- -- Add a couple of zones +-- -- We'll defend our own border +-- mywing:AddAcceptZone(ZONE_POLYGON:New( "Blue Border", GROUP:FindByName( "Blue Border" ) )) +-- -- We'll attack intruders also here - conflictzones can overlap borders(!) - limited zone of engagement +-- mywing:AddConflictZone(ZONE_POLYGON:New("Red Defense Zone", GROUP:FindByName( "Red Defense Zone" ))) +-- -- We'll leave the reds alone on their turf +-- mywing:AddRejectZone(ZONE_POLYGON:New( "Red Border", GROUP:FindByName( "Red Border" ) )) +-- +-- -- Optional - Draw the borders on the map so we see what's going on +-- -- Set up borders on map +-- local BlueBorder = ZONE_POLYGON:New( "Blue Border", GROUP:FindByName( "Blue Border" ) ) +-- BlueBorder:DrawZone(-1,{0,0,1},1,FillColor,FillAlpha,1,true) +-- local ConflictZone = ZONE_POLYGON:New("Red Defense Zone", GROUP:FindByName( "Red Defense Zone" )) +-- ConflictZone:DrawZone(-1,{1,1,0},1,FillColor,FillAlpha,2,true) +-- local BlueNoGoZone = ZONE_POLYGON:New( "Red Border", GROUP:FindByName( "Red Border" ) ) +-- BlueNoGoZone:DrawZone(-1,{1,0,0},1,FillColor,FillAlpha,4,true) +-- +-- ### Add a second airwing with squads and own CAP point (optional) +-- +-- -- Set this up at Sukhumi +-- mywing:AddAirwing(AIRBASE.Caucasus.Sukhumi_Babushara,"Blue CAP Sukhumi") +-- -- CAP Point "Blue Zone 2" +-- mywing:AddPatrolPointCAP(AIRBASE.Caucasus.Sukhumi_Babushara,ZONE:FindByName("Blue Zone 2"):GetCoordinate(),30000,400,90,20) +-- +-- -- This one has two squadrons to choose from +-- mywing:AddSquadron("Blue Sq3 F16","CAP Sukhumi II",AIRBASE.Caucasus.Sukhumi_Babushara,20,AI.Skill.GOOD,402,"JASDF 6th TFS 43-8526 Skull Riders") +-- mywing:AddSquadron("Blue Sq2 F15","CAP Sukhumi I",AIRBASE.Caucasus.Sukhumi_Babushara,20,AI.Skill.GOOD,202,"390th Fighter SQN") +-- +-- ### Add a tanker (optional) +-- +-- -- **Note** If you need different tanker types, i.e. Boom and Drogue, set them up at different AirWings! +-- -- Add a tanker point +-- mywing:AddPatrolPointTanker(AIRBASE.Caucasus.Kutaisi,ZONE:FindByName("Blue Zone Tanker"):GetCoordinate(),20000,280,270,50) +-- -- Add a tanker squad - Radio 251 AM, TACAN 51Y +-- mywing:AddTankerSquadron("Blue Tanker","Tanker Ops Kutaisi",AIRBASE.Caucasus.Kutaisi,20,AI.Skill.EXCELLENT,602,nil,251,radio.modulation.AM,51) +-- +-- ### Add an AWACS (optional) +-- +-- -- Add an AWACS point +-- mywing:AddPatrolPointAwacs(AIRBASE.Caucasus.Kutaisi,ZONE:FindByName("Blue Zone AWACS"):GetCoordinate(),25000,300,270,50) +-- -- Add an AWACS squad - Radio 251 AM, TACAN 51Y +-- mywing:AddAWACSSquadron("Blue AWACS","AWACS Ops Kutaisi",AIRBASE.Caucasus.Kutaisi,20,AI.Skill.AVERAGE,702,nil,271,radio.modulation.AM) +-- +-- # Fine-Tuning +-- +-- ## Change Defaults +-- +-- * @{#EASYGCICAP.SetDefaultResurrection}: Set how many seconds the AirWing stays inoperable after the AirWing STATIC HQ ist destroyed, default 900 secs. +-- * @{#EASYGCICAP.SetDefaultCAPSpeed}: Set how many knots the CAP flights should do (will be altitude corrected), default 300 kn. +-- * @{#EASYGCICAP.SetDefaultCAPAlt}: Set at which altitude (ASL) the CAP planes will fly, default 25,000 ft. +-- * @{#EASYGCICAP.SetDefaultCAPDirection}: Set the initial direction from the CAP point the planes will fly in degrees, default is 90°. +-- * @{#EASYGCICAP.SetDefaultCAPLeg}: Set the length of the CAP leg, default is 15 NM. +-- * @{#EASYGCICAP.SetDefaultCAPGrouping}: Set how many planes will be spawned per mission (CVAP/GCI), defaults to 2. +-- * @{#EASYGCICAP.SetDefaultMissionRange}: Set how many NM the planes can go from the home base, defaults to 100. +-- * @{#EASYGCICAP.SetDefaultNumberAlert5Standby}: Set how many planes will be spawned on cold standby (Alert5), default 2. +-- * @{#EASYGCICAP.SetDefaultEngageRange}: Set max engage range for CAP flights if they detect intruders, defaults to 50. +-- * @{#EASYGCICAP.SetMaxAliveMissions}: Set max parallel missions can be done (CAP+GCI+Alert5+Tanker+AWACS), defaults to 8. +-- * @{#EASYGCICAP.SetDefaultRepeatOnFailure}: Set max repeats on failure for intercepting/killing intruders, defaults to 3. +-- * @{#EASYGCICAP.SetTankerAndAWACSInvisible}: Set Tanker and AWACS to be invisible to enemy AI eyes. Is set to `true` by default. +-- +-- ## Debug and Monitor +-- +-- mywing.debug = true -- log information +-- mywing.Monitor = true -- show some statistics on screen +-- +-- +-- @field #EASYGCICAP +EASYGCICAP = { + ClassName = "EASYGCICAP", + overhead = 0.75, + capgrouping = 2, + airbasename = nil, + airbase = nil, + coalition = "blue", + alias = nil, + wings = {}, + Intel = nil, + resurrection = 900, + capspeed = 300, + capalt = 25000, + capdir = 45, + capleg = 15, + maxinterceptsize = 2, + missionrange = 100, + noalert5 = 4, + ManagedAW = {}, + ManagedSQ = {}, + ManagedCP = {}, + ManagedTK = {}, + ManagedEWR = {}, + ManagedREC = {}, + MaxAliveMissions = 8, + debug = false, + engagerange = 50, + repeatsonfailure = 3, + GoZoneSet = nil, + NoGoZoneSet = nil, + ConflictZoneSet = nil, + Monitor = false, + TankerInvisible = true, + CapFormation = nil, + ReadyFlightGroups = {}, + DespawnAfterLanding = false, + DespawnAfterHolding = true, + ListOfAuftrag = {}, + defaulttakeofftype = "hot", + FuelLowThreshold = 25, + FuelCriticalThreshold = 10, + showpatrolpointmarks = false, + EngageTargetTypes = {"Air"}, +} + +--- Internal Squadron data type +-- @type EASYGCICAP.Squad +-- @field #string TemplateName +-- @field #string SquadName +-- @field #string AirbaseName +-- @field #number AirFrames +-- @field #string Skill +-- @field #string Modex +-- @field #string Livery +-- @field #boolean Tanker +-- @field #boolean AWACS +-- @field #boolean RECON +-- @field #number Frequency +-- @field #number Modulation +-- @field #number TACAN + +--- Internal Wing data type +-- @type EASYGCICAP.Wing +-- @field #string AirbaseName +-- @field #string Alias +-- @field #string CapZoneName + +--- Internal CapPoint data type +-- @type EASYGCICAP.CapPoint +-- @field #string AirbaseName +-- @field Core.Point#COORDINATE Coordinate +-- @field #number Altitude +-- @field #number Speed +-- @field #number Heading +-- @field #number LegLength +-- @field Core.Zone#ZONE_BASE Zone + +--- EASYGCICAP class version. +-- @field #string version +EASYGCICAP.version="0.1.30" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: TBD + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new GCICAP Manager +-- @param #EASYGCICAP self +-- @param #string Alias A Name for this GCICAP +-- @param #string AirbaseName Name of the Home Airbase +-- @param #string Coalition Coalition, e.g. "blue" or "red" +-- @param #string EWRName (Partial) group name of the EWR system of the coalition, e.g. "Red EWR", can be handed in as table of names, e.g.{"EWR","Radar","SAM"} +-- @return #EASYGCICAP self +function EASYGCICAP:New(Alias, AirbaseName, Coalition, EWRName) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #EASYGCICAP + + -- defaults + self.alias = Alias or AirbaseName.." CAP Wing" + self.coalitionname = string.lower(Coalition) or "blue" + self.coalition = self.coalitionname == "blue" and coalition.side.BLUE or coalition.side.RED + self.wings = {} + if type(EWRName) == "string" then EWRName = {EWRName} end + self.EWRName = EWRName --or self.coalitionname.." EWR" + --self.CapZoneName = CapZoneName + self.airbasename = AirbaseName + self.airbase = AIRBASE:FindByName(self.airbasename) + self.GoZoneSet = SET_ZONE:New() + self.NoGoZoneSet = SET_ZONE:New() + self.ConflictZoneSet = SET_ZONE:New() + self.resurrection = 900 + self.capspeed = 300 + self.capalt = 25000 + self.capdir = 90 + self.capleg = 15 + self.capgrouping = 2 + self.missionrange = 100 + self.noalert5 = 2 + self.MaxAliveMissions = 8 + self.engagerange = 50 + self.repeatsonfailure = 3 + self.Monitor = false + self.TankerInvisible = true + self.CapFormation = ENUMS.Formation.FixedWing.FingerFour.Group + self.DespawnAfterLanding = false + self.DespawnAfterHolding = true + self.ListOfAuftrag = {} + self.defaulttakeofftype = "hot" + self.FuelLowThreshold = 25 + self.FuelCriticalThreshold = 10 + self.showpatrolpointmarks = false + self.EngageTargetTypes = {"Air"} + + -- Set some string id for output to DCS.log file. + self.lid=string.format("EASYGCICAP %s | ", self.alias) + + -- Add FSM transitions. + -- From State --> Event --> To State + self:SetStartState("Stopped") + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("Running", "Stop", "Stopped") + self:AddTransition("*", "Status", "*") + + self:AddAirwing(self.airbasename,self.alias,self.CapZoneName) + + self:I(self.lid.."Created new instance (v"..self.version..")") + + self:__Start(math.random(6,12)) + + return self +end + +------------------------------------------------------------------------- +-- Functions +------------------------------------------------------------------------- + +--- Get a specific managed AirWing by name +-- @param #EASYGCICAP self +-- @param #string AirbaseName Airbase name of the home of this wing. +-- @return Ops.AirWing#AIRWING Airwing or nil if not found +function EASYGCICAP:GetAirwing(AirbaseName) + self:T(self.lid.."GetAirwing") + if self.wings[AirbaseName] then + return self.wings[AirbaseName][1] + end + return nil +end + +--- Get a table of all managed AirWings +-- @param #EASYGCICAP self +-- @return #table Table of Ops.AirWing#AIRWING Airwings +function EASYGCICAP:GetAirwingTable() + self:T(self.lid.."GetAirwingTable") + local Wingtable = {} + for _,_object in pairs(self.wings or {}) do + table.insert(Wingtable,_object[1]) + end + return Wingtable +end + +--- Set "fuel low" threshold for CAP and INTERCEPT flights. +-- @param #EASYGCICAP self +-- @param #number Percent RTB if fuel at this percent. Values: 1..100, defaults to 25. +-- @return #EASYGCICAP self +function EASYGCICAP:SetFuelLow(Percent) + self:T(self.lid.."SetFuelLow") + self.FuelLowThreshold = Percent or 25 + return self +end + +--- Set markers on the map for Patrol Points. +-- @param #EASYGCICAP self +-- @param #boolean onoff Set to true to switch markers on. +-- @return #EASYGCICAP self +function EASYGCICAP:ShowPatrolPointMarkers(onoff) + if onoff then + self.showpatrolpointmarks = true + else + self.showpatrolpointmarks = false + end + return self +end + +--- Set "fuel critical" threshold for CAP and INTERCEPT flights. +-- @param #EASYGCICAP self +-- @param #number Percent RTB if fuel at this percent. Values: 1..100, defaults to 10. +-- @return #EASYGCICAP self +function EASYGCICAP:SetFuelCritical(Percent) + self:T(self.lid.."SetFuelCritical") + self.FuelCriticalThreshold = Percent or 10 + return self +end + +--- Set CAP formation. +-- @param #EASYGCICAP self +-- @param #number Formation Formation to fly, defaults to ENUMS.Formation.FixedWing.FingerFour.Group +-- @return #EASYGCICAP self +function EASYGCICAP:SetCAPFormation(Formation) + self:T(self.lid.."SetCAPFormation") + self.CapFormation = Formation + return self +end + +--- Set Tanker and AWACS to be invisible to enemy AI eyes +-- @param #EASYGCICAP self +-- @param #boolean Switch Set to true or false, by default this is set to true already +-- @return #EASYGCICAP self +function EASYGCICAP:SetTankerAndAWACSInvisible(Switch) + self:T(self.lid.."SetTankerAndAWACSInvisible") + self.TankerInvisible = Switch + return self +end + +--- (internal) Count alive missions in our internal stack. +-- @param #EASYGCICAP self +-- @return #number count +function EASYGCICAP:_CountAliveAuftrags() + local alive = 0 + for _,_auftrag in pairs(self.ListOfAuftrag) do + local auftrag = _auftrag -- Ops.Auftrag#AUFTRAG + if auftrag and (not (auftrag:IsCancelled() or auftrag:IsDone() or auftrag:IsOver())) then + alive = alive + 1 + end + end + return alive +end + +--- Set Maximum of alive missions created by this instance to stop airplanes spamming the map +-- @param #EASYGCICAP self +-- @param #number Maxiumum Maxmimum number of parallel missions allowed. Count is Intercept-Missions + Alert5-Missions, default is 8 +-- @return #EASYGCICAP self +function EASYGCICAP:SetMaxAliveMissions(Maxiumum) + self:T(self.lid.."SetMaxAliveMissions") + self.MaxAliveMissions = Maxiumum or 8 + return self +end + +--- Add default time to resurrect Airwing building if destroyed +-- @param #EASYGCICAP self +-- @param #number Seconds Seconds, defaults to 900 +-- @return #EASYGCICAP self +function EASYGCICAP:SetDefaultResurrection(Seconds) + self:T(self.lid.."SetDefaultResurrection") + self.resurrection = Seconds or 900 + return self +end + +--- Add default repeat attempts if an Intruder intercepts fails. +-- @param #EASYGCICAP self +-- @param #number Retries Retries, defaults to 3 +-- @return #EASYGCICAP self +function EASYGCICAP:SetDefaultRepeatOnFailure(Retries) + self:T(self.lid.."SetDefaultRepeatOnFailure") + self.repeatsonfailure = Retries or 3 + return self +end + +--- Add default take off type for the airwings. +-- @param #EASYGCICAP self +-- @param #string Takeoff Can be "hot", "cold", or "air" - default is "hot". +-- @return #EASYGCICAP self +function EASYGCICAP:SetDefaultTakeOffType(Takeoff) + self:T(self.lid.."SetDefaultTakeOffType") + self.defaulttakeofftype = Takeoff or "hot" + return self +end + +--- Set default CAP Speed in knots +-- @param #EASYGCICAP self +-- @param #number Speed Speed defaults to 300 +-- @return #EASYGCICAP self +function EASYGCICAP:SetDefaultCAPSpeed(Speed) + self:T(self.lid.."SetDefaultSpeed") + self.capspeed = Speed or 300 + return self +end + +--- Set default CAP Altitude in feet +-- @param #EASYGCICAP self +-- @param #number Altitude Altitude defaults to 25000 +-- @return #EASYGCICAP self +function EASYGCICAP:SetDefaultCAPAlt(Altitude) + self:T(self.lid.."SetDefaultAltitude") + self.capalt = Altitude or 25000 + return self +end + +--- Set default CAP lieg initial direction in degrees +-- @param #EASYGCICAP self +-- @param #number Direction Direction defaults to 90 (East) +-- @return #EASYGCICAP self +function EASYGCICAP:SetDefaultCAPDirection(Direction) + self:T(self.lid.."SetDefaultDirection") + self.capdir = Direction or 90 + return self +end + +--- Set default leg length in NM +-- @param #EASYGCICAP self +-- @param #number Leg Leg defaults to 15 +-- @return #EASYGCICAP self +function EASYGCICAP:SetDefaultCAPLeg(Leg) + self:T(self.lid.."SetDefaultLeg") + self.capleg = Leg or 15 + return self +end + +--- Set default grouping, i.e. how many airplanes per CAP point +-- @param #EASYGCICAP self +-- @param #number Grouping Grouping defaults to 2 +-- @return #EASYGCICAP self +function EASYGCICAP:SetDefaultCAPGrouping(Grouping) + self:T(self.lid.."SetDefaultCAPGrouping") + self.capgrouping = Grouping or 2 + return self +end + +--- Set default range planes can fly from their homebase in NM +-- @param #EASYGCICAP self +-- @param #number Range Range defaults to 100 NM +-- @return #EASYGCICAP self +function EASYGCICAP:SetDefaultMissionRange(Range) + self:T(self.lid.."SetDefaultMissionRange") + self.missionrange = Range or 100 + return self +end + +--- Set default number of airframes standing by for intercept tasks (visible on the airfield) +-- @param #EASYGCICAP self +-- @param #number Airframes defaults to 2 +-- @return #EASYGCICAP self +function EASYGCICAP:SetDefaultNumberAlert5Standby(Airframes) + self:T(self.lid.."SetDefaultNumberAlert5Standby") + self.noalert5 = math.abs(Airframes) or 2 + return self +end + +--- Set default engage range for intruders detected by CAP flights in NM. +-- @param #EASYGCICAP self +-- @param #number Range defaults to 50NM +-- @return #EASYGCICAP self +function EASYGCICAP:SetDefaultEngageRange(Range) + self:T(self.lid.."SetDefaultEngageRange") + self.engagerange = Range or 50 + return self +end + +--- Set default overhead for intercept calculations +-- @param #EASYGCICAP self +-- @param #number Overhead The overhead to use. +-- @return #EASYGCICAP self +-- @usage Either a CAP Plane or a newly spawned GCI plane will take care of intruders. Standard overhead is 0.75, i.e. a group of 3 intrudes will +-- be managed by 2 planes from the assigned AirWing. There is an maximum missions limitation per AirWing, so we do not spam the skies. +function EASYGCICAP:SetDefaultOverhead(Overhead) + self:T(self.lid.."SetDefaultOverhead") + self.overhead = Overhead or 0.75 + return self +end + +--- Set default despawning after landing. +-- @param #EASYGCICAP self +-- @return #EASYGCICAP self +function EASYGCICAP:SetDefaultDespawnAfterLanding() + self:T(self.lid.."SetDefaultDespawnAfterLanding") + self.DespawnAfterLanding = true + self.DespawnAfterHolding = false + return self +end + +--- Set default despawning after holding (despawn in air close to AFB). +-- @param #EASYGCICAP self +-- @return #EASYGCICAP self +function EASYGCICAP:SetDefaultDespawnAfterHolding() + self:T(self.lid.."SetDefaultDespawnAfterLanding") + self.DespawnAfterLanding = false + self.DespawnAfterHolding = true + return self +end + +--- Set CAP mission start to vary randomly between Start end End seconds. +-- @param #EASYGCICAP self +-- @param #number Start +-- @param #number End +-- @return #EASYGCICAP self +function EASYGCICAP:SetCapStartTimeVariation(Start, End) + self.capOptionVaryStartTime = Start or 5 + self.capOptionVaryEndTime = End or 60 + return self +end + + +--- Set which target types CAP flights will prefer to engage, defaults to {"Air"} +-- @param #EASYGCICAP self +-- @param #table types Table of comma separated #string entries, defaults to {"Air"} (everything that flies and is not a weapon). Useful other options are e.g. {"Bombers"}, {"Fighters"}, +-- or {"Helicopters"} or combinations like {"Bombers", "Fighters", "UAVs"}. See [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_enum_attributes). +-- @return #EASYGCICAP self +function EASYGCICAP:SetCAPEngageTargetTypes(types) + self.EngageTargetTypes = types or {"Air"} + return self +end + +--- Add an AirWing to the manager +-- @param #EASYGCICAP self +-- @param #string Airbasename +-- @param #string Alias +-- @return #EASYGCICAP self +function EASYGCICAP:AddAirwing(Airbasename, Alias) + self:T(self.lid.."AddAirwing "..Airbasename) + + -- Create Airwing data entry + local AWEntry = {} -- #EASYGCICAP.Wing + AWEntry.AirbaseName = Airbasename + AWEntry.Alias = Alias + --AWEntry.CapZoneName = CapZoneName + + self.ManagedAW[Airbasename] = AWEntry + + return self +end + +--- (Internal) Create actual AirWings from the list +-- @param #EASYGCICAP self +-- @return #EASYGCICAP self +function EASYGCICAP:_CreateAirwings() + self:T(self.lid.."_CreateAirwings") + for airbase,data in pairs(self.ManagedAW) do + local wing = data -- #EASYGCICAP.Wing + local afb = wing.AirbaseName + local alias = wing.Alias + --local cz = wing.CapZoneName + self:_AddAirwing(airbase,alias) + end + return self +end + +--- (internal) Create and add another AirWing to the manager +-- @param #EASYGCICAP self +-- @param #string Airbasename +-- @param #string Alias +-- @return #EASYGCICAP self +function EASYGCICAP:_AddAirwing(Airbasename, Alias) + self:T(self.lid.."_AddAirwing "..Airbasename) + + local CapFormation = self.CapFormation + local DespawnAfterLanding = self.DespawnAfterLanding + local DespawnAfterHolding = self.DespawnAfterHolding + + -- Check STATIC name + local check = STATIC:FindByName(Airbasename,false) or UNIT:FindByName(Airbasename) + if check == nil then + MESSAGE:New(self.lid.."There's no warehouse static on the map (wrong naming?) for airbase "..tostring(Airbasename).."!",30,"CHECK"):ToAllIf(self.debug):ToLog() + return + end + + -- Create Airwing + local CAP_Wing = AIRWING:New(Airbasename,Alias) + CAP_Wing:SetVerbosityLevel(0) + CAP_Wing:SetReportOff() + CAP_Wing:SetMarker(false) + CAP_Wing:SetAirbase(AIRBASE:FindByName(Airbasename)) + CAP_Wing:SetRespawnAfterDestroyed() + CAP_Wing:SetNumberCAP(self.capgrouping) + CAP_Wing:SetCapCloseRaceTrack(true) + + if self.showpatrolpointmarks then + CAP_Wing:ShowPatrolPointMarkers(true) + end + + if self.capOptionVaryStartTime then + CAP_Wing:SetCapStartTimeVariation(self.capOptionVaryStartTime,self.capOptionVaryEndTime) + end + + if CapFormation then + CAP_Wing:SetCAPFormation(CapFormation) + end + if #self.ManagedTK > 0 then + CAP_Wing:SetNumberTankerBoom(1) + CAP_Wing:SetNumberTankerProbe(1) + end + if #self.ManagedEWR > 0 then + CAP_Wing:SetNumberAWACS(1) + end + if #self.ManagedREC > 0 then + CAP_Wing:SetNumberRecon(1) + end + + CAP_Wing:SetTakeoffType(self.defaulttakeofftype) + CAP_Wing:SetLowFuelThreshold(0.3) + CAP_Wing.RandomAssetScore = math.random(50,100) + CAP_Wing:Start() + + local Intel = self.Intel + + local TankerInvisible = self.TankerInvisible + local engagerange = self.engagerange + local GoZoneSet = self.GoZoneSet + local NoGoZoneSet = self.NoGoZoneSet + local FuelLow = self.FuelLowThreshold or 25 + local FuelCritical = self.FuelCriticalThreshold or 10 + local EngageTypes = self.EngageTargetTypes or {"Air"} + + function CAP_Wing:onbeforeFlightOnMission(From, Event, To, Flightgroup, Mission) + local flightgroup = Flightgroup -- Ops.FlightGroup#FLIGHTGROUP + if DespawnAfterLanding then + flightgroup:SetDespawnAfterLanding() + elseif DespawnAfterHolding then + flightgroup:SetDespawnAfterHolding() + end + flightgroup:SetDestinationbase(AIRBASE:FindByName(Airbasename)) + flightgroup:GetGroup():CommandEPLRS(true,5) + flightgroup:GetGroup():SetOptionRadarUsingForContinousSearch() + flightgroup:GetGroup():SetOptionLandingOverheadBreak() + if Mission.type ~= AUFTRAG.Type.TANKER and Mission.type ~= AUFTRAG.Type.AWACS and Mission.type ~= AUFTRAG.Type.RECON then + flightgroup:SetDetection(true) + flightgroup:SetEngageDetectedOn(engagerange,EngageTypes,GoZoneSet,NoGoZoneSet) + flightgroup:SetOutOfAAMRTB() + flightgroup:SetFuelLowRTB(true) + flightgroup:SetFuelLowThreshold(FuelLow) + flightgroup:SetFuelCriticalRTB(true) + flightgroup:SetFuelCriticalThreshold(FuelCritical) + if CapFormation then + flightgroup:GetGroup():SetOption(AI.Option.Air.id.FORMATION,CapFormation) + end + end + if Mission.type == AUFTRAG.Type.TANKER or Mission.type == AUFTRAG.Type.AWACS or Mission.type == AUFTRAG.Type.RECON then + if TankerInvisible then + flightgroup:GetGroup():SetCommandInvisible(true) + end + if Mission.type == AUFTRAG.Type.RECON then + flightgroup:SetDetection(true) + end + end + flightgroup:GetGroup():OptionROTEvadeFire() + flightgroup:SetFuelLowRTB(true) + Intel:AddAgent(flightgroup) + if DespawnAfterHolding then + function flightgroup:onbeforeHolding(From,Event,To) + self:Despawn(1,true) + end + end + end + + if self.noalert5 > 0 then + local alert = AUFTRAG:NewALERT5(AUFTRAG.Type.INTERCEPT) + alert:SetRequiredAssets(self.noalert5) + alert:SetRepeat(99) + CAP_Wing:AddMission(alert) + table.insert(self.ListOfAuftrag,alert) + end + + self.wings[Airbasename] = { CAP_Wing, AIRBASE:FindByName(Airbasename):GetZone(), Airbasename } + + return self +end + +--- Add a CAP patrol point to a Wing +-- @param #EASYGCICAP self +-- @param #string AirbaseName Name of the Wing's airbase +-- @param Core.Point#COORDINATE Coordinate. Can be handed as a Core.Zone#ZONE object (e.g. in case you want the point to align with a moving zone). +-- @param #number Altitude Defaults to 25000 feet ASL. +-- @param #number Speed Defaults to 300 knots TAS. +-- @param #number Heading Defaults to 90 degrees (East). +-- @param #number LegLength Defaults to 15 NM. +-- @return #EASYGCICAP self +function EASYGCICAP:AddPatrolPointCAP(AirbaseName,Coordinate,Altitude,Speed,Heading,LegLength) + self:T(self.lid.."AddPatrolPointCAP")--..Coordinate:ToStringLLDDM()) + local coordinate = Coordinate + local EntryCAP = {} -- #EASYGCICAP.CapPoint + if Coordinate:IsInstanceOf("ZONE_BASE") then + -- adjust coordinate and get the coordinate from the zone + coordinate = Coordinate:GetCoordinate() + EntryCAP.Zone = Coordinate + end + EntryCAP.AirbaseName = AirbaseName + EntryCAP.Coordinate = coordinate + EntryCAP.Altitude = Altitude or 25000 + EntryCAP.Speed = Speed or 300 + EntryCAP.Heading = Heading or 90 + EntryCAP.LegLength = LegLength or 15 + self.ManagedCP[#self.ManagedCP+1] = EntryCAP + if self.debug then + local mark = MARKER:New(coordinate,self.lid.."Patrol Point"):ToAll() + end + return self +end + +--- Add a RECON patrol point to a Wing +-- @param #EASYGCICAP self +-- @param #string AirbaseName Name of the Wing's airbase +-- @param Core.Point#COORDINATE Coordinate. Can be handed as a Core.Zone#ZONE object (e.g. in case you want the point to align with a moving zone). +-- @param #number Altitude Defaults to 25000 feet. +-- @param #number Speed Defaults to 300 knots. +-- @param #number Heading Defaults to 90 degrees (East). +-- @param #number LegLength Defaults to 15 NM. +-- @return #EASYGCICAP self +function EASYGCICAP:AddPatrolPointRecon(AirbaseName,Coordinate,Altitude,Speed,Heading,LegLength) + self:T(self.lid.."AddPatrolPointRecon "..Coordinate:ToStringLLDDM()) + local EntryCAP = {} -- #EASYGCICAP.CapPoint + EntryCAP.AirbaseName = AirbaseName + EntryCAP.Coordinate = Coordinate + EntryCAP.Altitude = Altitude or 25000 + EntryCAP.Speed = Speed or 300 + EntryCAP.Heading = Heading or 90 + EntryCAP.LegLength = LegLength or 15 + self.ManagedREC[#self.ManagedREC+1] = EntryCAP + if self.debug then + local mark = MARKER:New(Coordinate,self.lid.."Patrol Point Recon"):ToAll() + end + return self +end + +--- Add a TANKER patrol point to a Wing +-- @param #EASYGCICAP self +-- @param #string AirbaseName Name of the Wing's airbase +-- @param Core.Point#COORDINATE Coordinate. Can be handed as a Core.Zone#ZONE object (e.g. in case you want the point to align with a moving zone). +-- @param #number Altitude Defaults to 25000 feet. +-- @param #number Speed Defaults to 300 knots. +-- @param #number Heading Defaults to 90 degrees (East). +-- @param #number LegLength Defaults to 15 NM. +-- @return #EASYGCICAP self +function EASYGCICAP:AddPatrolPointTanker(AirbaseName,Coordinate,Altitude,Speed,Heading,LegLength) + self:T(self.lid.."AddPatrolPointTanker "..Coordinate:ToStringLLDDM()) + local EntryCAP = {} -- #EASYGCICAP.CapPoint + EntryCAP.AirbaseName = AirbaseName + EntryCAP.Coordinate = Coordinate + EntryCAP.Altitude = Altitude or 25000 + EntryCAP.Speed = Speed or 300 + EntryCAP.Heading = Heading or 90 + EntryCAP.LegLength = LegLength or 15 + self.ManagedTK[#self.ManagedTK+1] = EntryCAP + if self.debug then + local mark = MARKER:New(Coordinate,self.lid.."Patrol Point Tanker"):ToAll() + end + return self +end + +--- Add an AWACS patrol point to a Wing +-- @param #EASYGCICAP self +-- @param #string AirbaseName Name of the Wing's airbase +-- @param Core.Point#COORDINATE Coordinate. Can be handed as a Core.Zone#ZONE object (e.g. in case you want the point to align with a moving zone). +-- @param #number Altitude Defaults to 25000 feet. +-- @param #number Speed Defaults to 300 knots. +-- @param #number Heading Defaults to 90 degrees (East). +-- @param #number LegLength Defaults to 15 NM. +-- @return #EASYGCICAP self +function EASYGCICAP:AddPatrolPointAwacs(AirbaseName,Coordinate,Altitude,Speed,Heading,LegLength) + self:T(self.lid.."AddPatrolPointAwacs "..Coordinate:ToStringLLDDM()) + local EntryCAP = {} -- #EASYGCICAP.CapPoint + EntryCAP.AirbaseName = AirbaseName + EntryCAP.Coordinate = Coordinate + EntryCAP.Altitude = Altitude or 25000 + EntryCAP.Speed = Speed or 300 + EntryCAP.Heading = Heading or 90 + EntryCAP.LegLength = LegLength or 15 + self.ManagedEWR[#self.ManagedEWR+1] = EntryCAP + if self.debug then + local mark = MARKER:New(Coordinate,self.lid.."Patrol Point AWACS"):ToAll() + end + return self +end + +--- (Internal) Set actual Tanker Points from the list +-- @param #EASYGCICAP self +-- @return #EASYGCICAP self +function EASYGCICAP:_SetTankerPatrolPoints() + self:T(self.lid.."_SetTankerPatrolPoints") + for _,_data in pairs(self.ManagedTK) do + local data = _data --#EASYGCICAP.CapPoint + self:T("Airbasename = "..data.AirbaseName) + if not self.wings[data.AirbaseName] then + MESSAGE:New(self.lid.."You are trying to create a TANKER point for which there is no wing! "..tostring(data.AirbaseName),30,"CHECK"):ToAllIf(self.debug):ToLog() + return + end + local Wing = self.wings[data.AirbaseName][1] -- Ops.Airwing#AIRWING + local Coordinate = data.Coordinate + local Altitude = data.Altitude + local Speed = data.Speed + local Heading = data.Heading + local LegLength = data.LegLength + Wing:AddPatrolPointTANKER(Coordinate,Altitude,Speed,Heading,LegLength) + end + + return self +end + +--- (Internal) Set actual Awacs Points from the list +-- @param #EASYGCICAP self +-- @return #EASYGCICAP self +function EASYGCICAP:_SetAwacsPatrolPoints() + self:T(self.lid.."_SetAwacsPatrolPoints") + for _,_data in pairs(self.ManagedEWR) do + local data = _data --#EASYGCICAP.CapPoint + self:T("Airbasename = "..data.AirbaseName) + if not self.wings[data.AirbaseName] then + MESSAGE:New(self.lid.."You are trying to create an AWACS point for which there is no wing! "..tostring(data.AirbaseName),30,"CHECK"):ToAllIf(self.debug):ToLog() + return + end + local Wing = self.wings[data.AirbaseName][1] -- Ops.Airwing#AIRWING + local Coordinate = data.Coordinate + local Altitude = data.Altitude + local Speed = data.Speed + local Heading = data.Heading + local LegLength = data.LegLength + Wing:AddPatrolPointAWACS(Coordinate,Altitude,Speed,Heading,LegLength) + end + + return self +end + +--- (Internal) Set actual PatrolPoints from the list +-- @param #EASYGCICAP self +-- @return #EASYGCICAP self +function EASYGCICAP:_SetCAPPatrolPoints() + self:T(self.lid.."_SetCAPPatrolPoints") + for _,_data in pairs(self.ManagedCP) do + local data = _data --#EASYGCICAP.CapPoint + self:T("Airbasename = "..data.AirbaseName) + if not self.wings[data.AirbaseName] then + MESSAGE:New(self.lid.."You are trying to create a CAP point for which there is no wing! "..tostring(data.AirbaseName),30,"CHECK"):ToAllIf(self.debug):ToLog() + return + end + local Wing = self.wings[data.AirbaseName][1] -- Ops.Airwing#AIRWING + local Coordinate = data.Coordinate + local Altitude = data.Altitude + local Speed = data.Speed + local Heading = data.Heading + local LegLength = data.LegLength + local Zone = _data.Zone + if Zone then + Wing:AddPatrolPointCAP(Zone,Altitude,Speed,Heading,LegLength) + else + Wing:AddPatrolPointCAP(Coordinate,Altitude,Speed,Heading,LegLength) + end + end + + return self +end + +--- (Internal) Set actual PatrolPoints from the list +-- @param #EASYGCICAP self +-- @return #EASYGCICAP self +function EASYGCICAP:_SetReconPatrolPoints() + self:T(self.lid.."_SetReconPatrolPoints") + for _,_data in pairs(self.ManagedREC) do + local data = _data --#EASYGCICAP.CapPoint + self:T("Airbasename = "..data.AirbaseName) + if not self.wings[data.AirbaseName] then + MESSAGE:New(self.lid.."You are trying to create a RECON point for which there is no wing! "..tostring(data.AirbaseName),30,"CHECK"):ToAllIf(self.debug):ToLog() + return + end + local Wing = self.wings[data.AirbaseName][1] -- Ops.Airwing#AIRWING + local Coordinate = data.Coordinate + local Altitude = data.Altitude + local Speed = data.Speed + local Heading = data.Heading + local LegLength = data.LegLength + Wing:AddPatrolPointRecon(Coordinate,Altitude,Speed,Heading,LegLength) + end + + return self +end + +--- (Internal) Create actual Squadrons from the list +-- @param #EASYGCICAP self +-- @return #EASYGCICAP self +function EASYGCICAP:_CreateSquads() + self:T(self.lid.."_CreateSquads") + for name,data in pairs(self.ManagedSQ) do + local squad = data -- #EASYGCICAP.Squad + local SquadName = name + local TemplateName = squad.TemplateName + local AirbaseName = squad.AirbaseName + local AirFrames = squad.AirFrames + local Skill = squad.Skill + local Modex = squad.Modex + local Livery = squad.Livery + local Frequeny = squad.Frequency + local Modulation = squad.Modulation + local TACAN = squad.TACAN + if squad.Tanker then + self:_AddTankerSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery,Frequeny,Modulation,TACAN) + elseif squad.AWACS then + self:_AddAWACSSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery,Frequeny,Modulation) + elseif squad.RECON then + self:_AddReconSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery) + else + self:_AddSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery) + end + end + return self +end + +--- Add a Squadron to an Airwing of the manager +-- @param #EASYGCICAP self +-- @param #string TemplateName Name of the group template. +-- @param #string SquadName Squadron name - must be unique! +-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi +-- @param #number AirFrames Number of available airframes, e.g. 20. +-- @param #string Skill (optional) Skill level, e.g. AI.Skill.AVERAGE +-- @param #string Modex (optional) Modex to be used,e.g. 402. +-- @param #string Livery (optional) Livery name to be used. +-- @return #EASYGCICAP self +function EASYGCICAP:AddSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery) + self:T(self.lid.."AddSquadron "..SquadName) + -- Add Squadron Data + local EntrySQ = {} -- #EASYGCICAP.Squad + EntrySQ.TemplateName = TemplateName + EntrySQ.SquadName = SquadName + EntrySQ.AirbaseName = AirbaseName + EntrySQ.AirFrames = AirFrames or 20 + EntrySQ.Skill = Skill or AI.Skill.AVERAGE + EntrySQ.Modex = Modex or 402 + EntrySQ.Livery = Livery + + self.ManagedSQ[SquadName] = EntrySQ + + return self +end + +--- Add a Recon Squadron to an Airwing of the manager +-- @param #EASYGCICAP self +-- @param #string TemplateName Name of the group template. +-- @param #string SquadName Squadron name - must be unique! +-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi +-- @param #number AirFrames Number of available airframes, e.g. 20. +-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE +-- @param #string Modex (optional) Modex to be used,e.g. 402. +-- @param #string Livery (optional) Livery name to be used. +-- @return #EASYGCICAP self +function EASYGCICAP:AddReconSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery) + self:T(self.lid.."AddReconSquadron "..SquadName) + -- Add Squadron Data + local EntrySQ = {} -- #EASYGCICAP.Squad + EntrySQ.TemplateName = TemplateName + EntrySQ.SquadName = SquadName + EntrySQ.AirbaseName = AirbaseName + EntrySQ.AirFrames = AirFrames or 20 + EntrySQ.Skill = Skill or AI.Skill.AVERAGE + EntrySQ.Modex = Modex or 402 + EntrySQ.Livery = Livery + EntrySQ.RECON = true + + self.ManagedSQ[SquadName] = EntrySQ + + return self +end + +--- Add a Tanker Squadron to an Airwing of the manager +-- @param #EASYGCICAP self +-- @param #string TemplateName Name of the group template. +-- @param #string SquadName Squadron name - must be unique! +-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi +-- @param #number AirFrames Number of available airframes, e.g. 20. +-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE +-- @param #string Modex (optional) Modex to be used,e.g. 402. +-- @param #string Livery (optional) Livery name to be used. +-- @param #number Frequency (optional) Radio Frequency to be used. +-- @param #number Modulation (optional) Radio Modulation to be used, e.g. radio.modulation.AM or radio.modulation.FM +-- @param #number TACAN (optional) TACAN channel, e.g. 71, resulting in Channel 71Y +-- @return #EASYGCICAP self +function EASYGCICAP:AddTankerSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery, Frequency, Modulation, TACAN) + self:T(self.lid.."AddTankerSquadron "..SquadName) + -- Add Squadron Data + local EntrySQ = {} -- #EASYGCICAP.Squad + EntrySQ.TemplateName = TemplateName + EntrySQ.SquadName = SquadName + EntrySQ.AirbaseName = AirbaseName + EntrySQ.AirFrames = AirFrames or 20 + EntrySQ.Skill = Skill or AI.Skill.AVERAGE + EntrySQ.Modex = Modex or 602 + EntrySQ.Livery = Livery + EntrySQ.Frequency = Frequency + EntrySQ.Modulation = Livery + EntrySQ.TACAN = TACAN + EntrySQ.Tanker = true + + self.ManagedSQ[SquadName] = EntrySQ + + return self +end + +--- Add an AWACS Squadron to an Airwing of the manager +-- @param #EASYGCICAP self +-- @param #string TemplateName Name of the group template. +-- @param #string SquadName Squadron name - must be unique! +-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi +-- @param #number AirFrames Number of available airframes, e.g. 20. +-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE +-- @param #string Modex (optional) Modex to be used,e.g. 402. +-- @param #string Livery (optional) Livery name to be used. +-- @param #number Frequency (optional) Radio Frequency to be used. +-- @param #number Modulation (optional) Radio Modulation to be used, e.g. radio.modulation.AM or radio.modulation.FM +-- @return #EASYGCICAP self +function EASYGCICAP:AddAWACSSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery, Frequency, Modulation) + self:T(self.lid.."AddAWACSSquadron "..SquadName) + -- Add Squadron Data + local EntrySQ = {} -- #EASYGCICAP.Squad + EntrySQ.TemplateName = TemplateName + EntrySQ.SquadName = SquadName + EntrySQ.AirbaseName = AirbaseName + EntrySQ.AirFrames = AirFrames or 20 + EntrySQ.Skill = Skill or AI.Skill.AVERAGE + EntrySQ.Modex = Modex or 702 + EntrySQ.Livery = Livery + EntrySQ.Frequency = Frequency + EntrySQ.Modulation = Livery + EntrySQ.AWACS = true + + self.ManagedSQ[SquadName] = EntrySQ + + return self +end + +--- (Internal) Add a Squadron to an Airwing of the manager +-- @param #EASYGCICAP self +-- @param #string TemplateName Name of the group template. +-- @param #string SquadName Squadron name - must be unique! +-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi +-- @param #number AirFrames Number of available airframes, e.g. 20. +-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE +-- @param #string Modex (optional) Modex to be used,e.g. 402. +-- @param #string Livery (optional) Livery name to be used. +-- @param #number Frequency (optional) Radio Frequency to be used. +-- @param #number Modulation (optional) Radio Modulation to be used, e.g. radio.modulation.AM or radio.modulation.FM +-- @return #EASYGCICAP self +function EASYGCICAP:_AddSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery, Frequency, Modulation) + self:T(self.lid.."_AddSquadron "..SquadName) + -- Add Squadrons + local Squadron_One = SQUADRON:New(TemplateName,AirFrames,SquadName) + Squadron_One:AddMissionCapability({AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.PATROLRACETRACK, AUFTRAG.Type.ALERT5}) + --Squadron_One:SetFuelLowRefuel(true) + Squadron_One:SetFuelLowThreshold(0.3) + Squadron_One:SetTurnoverTime(10,20) + Squadron_One:SetModex(Modex) + Squadron_One:SetLivery(Livery) + Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE) + Squadron_One:SetMissionRange(self.missionrange) + + local wing = self.wings[AirbaseName][1] -- Ops.Airwing#AIRWING + + wing:AddSquadron(Squadron_One) + wing:NewPayload(TemplateName,-1,{AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.PATROLRACETRACK, AUFTRAG.Type.ALERT5},75) + + return self +end + +--- (Internal) Add a Recon Squadron to an Airwing of the manager +-- @param #EASYGCICAP self +-- @param #string TemplateName Name of the group template. +-- @param #string SquadName Squadron name - must be unique! +-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi +-- @param #number AirFrames Number of available airframes, e.g. 20. +-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE +-- @param #string Modex (optional) Modex to be used,e.g. 402. +-- @param #string Livery (optional) Livery name to be used. +-- @return #EASYGCICAP self +function EASYGCICAP:_AddReconSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery) + self:T(self.lid.."_AddReconSquadron "..SquadName) + -- Add Squadrons + local Squadron_One = SQUADRON:New(TemplateName,AirFrames,SquadName) + Squadron_One:AddMissionCapability({AUFTRAG.Type.RECON}) + --Squadron_One:SetFuelLowRefuel(true) + Squadron_One:SetFuelLowThreshold(0.3) + Squadron_One:SetTurnoverTime(10,20) + Squadron_One:SetModex(Modex) + Squadron_One:SetLivery(Livery) + Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE) + Squadron_One:SetMissionRange(self.missionrange) + + local wing = self.wings[AirbaseName][1] -- Ops.Airwing#AIRWING + + wing:AddSquadron(Squadron_One) + wing:NewPayload(TemplateName,-1,{AUFTRAG.Type.RECON},75) + + return self +end + +--- (Internal) Add a Tanker Squadron to an Airwing of the manager +-- @param #EASYGCICAP self +-- @param #string TemplateName Name of the group template. +-- @param #string SquadName Squadron name - must be unique! +-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi +-- @param #number AirFrames Number of available airframes, e.g. 20. +-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE +-- @param #string Modex (optional) Modex to be used,e.g. 402. +-- @param #string Livery (optional) Livery name to be used. +-- @param #number Frequency (optional) Radio frequency of the Tanker +-- @param #number Modulation (Optional) Radio modulation of the Tanker +-- @param #number TACAN (Optional) TACAN Channel to be used, will always be an "Y" channel +-- @return #EASYGCICAP self +function EASYGCICAP:_AddTankerSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery, Frequency, Modulation, TACAN) + self:T(self.lid.."_AddTankerSquadron "..SquadName) + -- Add Squadrons + local Squadron_One = SQUADRON:New(TemplateName,AirFrames,SquadName) + Squadron_One:AddMissionCapability({AUFTRAG.Type.TANKER}) + --Squadron_One:SetFuelLowRefuel(true) + Squadron_One:SetFuelLowThreshold(0.3) + Squadron_One:SetTurnoverTime(10,20) + Squadron_One:SetModex(Modex) + Squadron_One:SetLivery(Livery) + Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE) + Squadron_One:SetMissionRange(self.missionrange) + Squadron_One:SetRadio(Frequency,Modulation) + if TACAN then + Squadron_One:AddTacanChannel(TACAN,TACAN) + end + + local wing = self.wings[AirbaseName][1] -- Ops.Airwing#AIRWING + + wing:AddSquadron(Squadron_One) + wing:NewPayload(TemplateName,-1,{AUFTRAG.Type.TANKER},75) + + return self +end + +--- (Internal) Add a AWACS Squadron to an Airwing of the manager +-- @param #EASYGCICAP self +-- @param #string TemplateName Name of the group template. +-- @param #string SquadName Squadron name - must be unique! +-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi +-- @param #number AirFrames Number of available airframes, e.g. 20. +-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE +-- @param #string Modex (optional) Modex to be used,e.g. 402. +-- @param #string Livery (optional) Livery name to be used. +-- @param #number Frequency (optional) Radio frequency of the AWACS +-- @param #number Modulation (Optional) Radio modulation of the AWACS +-- @return #EASYGCICAP self +function EASYGCICAP:_AddAWACSSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery, Frequency, Modulation) + self:T(self.lid.."_AddAWACSSquadron "..SquadName) + -- Add Squadrons + local Squadron_One = SQUADRON:New(TemplateName,AirFrames,SquadName) + Squadron_One:AddMissionCapability({AUFTRAG.Type.AWACS}) + --Squadron_One:SetFuelLowRefuel(true) + Squadron_One:SetFuelLowThreshold(0.3) + Squadron_One:SetTurnoverTime(10,20) + Squadron_One:SetModex(Modex) + Squadron_One:SetLivery(Livery) + Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE) + Squadron_One:SetMissionRange(self.missionrange) + Squadron_One:SetRadio(Frequency,Modulation) + local wing = self.wings[AirbaseName][1] -- Ops.Airwing#AIRWING + + wing:AddSquadron(Squadron_One) + wing:NewPayload(TemplateName,-1,{AUFTRAG.Type.AWACS},75) + + return self +end + +--- Add a zone to the accepted zones set. +-- @param #EASYGCICAP self +-- @param Core.Zone#ZONE_BASE Zone +-- @return #EASYGCICAP self +function EASYGCICAP:AddAcceptZone(Zone) + self:T(self.lid.."AddAcceptZone") + self.GoZoneSet:AddZone(Zone) + return self +end + +--- Add a zone to the rejected zones set. +-- @param #EASYGCICAP self +-- @param Core.Zone#ZONE_BASE Zone +-- @return #EASYGCICAP self +function EASYGCICAP:AddRejectZone(Zone) + self:T(self.lid.."AddRejectZone") + self.NoGoZoneSet:AddZone(Zone) + return self +end + +--- Add a zone to the conflict zones set. +-- @param #EASYGCICAP self +-- @param Core.Zone#ZONE_BASE Zone +-- @return #EASYGCICAP self +function EASYGCICAP:AddConflictZone(Zone) + self:T(self.lid.."AddConflictZone") + self.ConflictZoneSet:AddZone(Zone) + self.GoZoneSet:AddZone(Zone) + return self +end + + +--- (Internal) Try to assign the intercept to a FlightGroup already in air and ready. +-- @param #EASYGCICAP self +-- @param #table ReadyFlightGroups ReadyFlightGroups +-- @param Ops.Auftrag#AUFTRAG InterceptAuftrag The Auftrag +-- @param Wrapper.Group#GROUP Group The Target +-- @param #number WingSize Calculated number of Flights +-- @return #boolean assigned +-- @return #number leftover +function EASYGCICAP:_TryAssignIntercept(ReadyFlightGroups,InterceptAuftrag,Group,WingSize) + self:T("_TryAssignIntercept for size "..WingSize or 1) + local assigned = false + local wingsize = WingSize or 1 + local mindist = 0 + local disttable = {} + if Group and Group:IsAlive() then + local gcoord = Group:GetCoordinate() or COORDINATE:New(0,0,0) + self:T(self.lid..string.format("Assignment for %s",Group:GetName())) + for _name,_FG in pairs(ReadyFlightGroups or {}) do + local FG = _FG -- Ops.FlightGroup#FLIGHTGROUP + local fcoord = FG:GetCoordinate() + local dist = math.floor(UTILS.Round(fcoord:Get2DDistance(gcoord)/1000,1)) + self:T(self.lid..string.format("FG %s Distance %dkm",_name,dist)) + disttable[#disttable+1] = { FG=FG, dist=dist} + if dist>mindist then mindist=dist end + end + + local function sortDistance(a, b) + return a.dist < b.dist + end + + table.sort(disttable, sortDistance) + + for _,_entry in ipairs(disttable) do + local FG = _entry.FG -- Ops.FlightGroup#FLIGHTGROUP + FG:AddMission(InterceptAuftrag) + local cm = FG:GetMissionCurrent() + if cm then cm:Cancel() end + wingsize = wingsize - 1 + self:T(self.lid..string.format("Assigned to FG %s Distance %dkm",FG:GetName(),_entry.dist)) + if wingsize == 0 then + assigned = true + break + end + end + end + + return assigned, wingsize +end + +--- Here, we'll decide if we need to launch an intercepting flight, and from where +-- @param #EASYGCICAP self +-- @param Ops.Intel#INTEL.Cluster Cluster +-- @return #EASYGCICAP self +function EASYGCICAP:_AssignIntercept(Cluster) + -- Here, we'll decide if we need to launch an intercepting flight, and from where + local overhead = self.overhead + local capspeed = self.capspeed + 100 + local capalt = self.capalt + local maxsize = self.maxinterceptsize + local repeatsonfailure = self.repeatsonfailure + + local wings = self.wings + local ctlpts = self.ManagedCP + local MaxAliveMissions = self.MaxAliveMissions --* self.capgrouping + local nogozoneset = self.NoGoZoneSet + local conflictzoneset = self.ConflictZoneSet + local ReadyFlightGroups = self.ReadyFlightGroups + + -- Aircraft? + if Cluster.ctype ~= INTEL.Ctype.AIRCRAFT then return end + -- Threatlevel 0..10 + local contact = self.Intel:GetHighestThreatContact(Cluster) + local name = contact.groupname --#string + local threat = contact.threatlevel --#number + local position = self.Intel:CalcClusterFuturePosition(Cluster,300) + -- calculate closest zone + local bestdistance = 2000*1000 -- 2000km + local targetairwing = nil -- Ops.Airwing#AIRWING + local targetawname = "" -- #string + local clustersize = self.Intel:ClusterCountUnits(Cluster) or 1 + local wingsize = math.abs(overhead * (clustersize+1)) + if wingsize > maxsize then wingsize = maxsize end + -- existing mission, and if so - done? + local retrymission = true + if Cluster.mission and (not Cluster.mission:IsOver()) then + retrymission = false + end + if (retrymission) and (wingsize >= 1) then + MESSAGE:New(string.format("**** %s Interceptors need wingsize %d", UTILS.GetCoalitionName(self.coalition), wingsize),15,"CAPGCI"):ToAllIf(self.debug):ToLog() + for _,_data in pairs (wings) do + local airwing = _data[1] -- Ops.Airwing#AIRWING + local zone = _data[2] -- Core.Zone#ZONE + local zonecoord = zone:GetCoordinate() + local name = _data[3] -- #string + local coa = AIRBASE:FindByName(name):GetCoalition() + local distance = position:DistanceFromPointVec2(zonecoord) + local airframes = airwing:CountAssets(true) + local samecoalitionab = coa == self.coalition and true or false + if distance < bestdistance and airframes >= wingsize and samecoalitionab == true then + bestdistance = distance + targetairwing = airwing + targetawname = name + end + end + for _,_data in pairs (ctlpts) do + --local airwing = _data[1] -- Ops.Airwing#AIRWING + --local zone = _data[2] -- Core.Zone#ZONE + --local zonecoord = zone:GetCoordinate() + --local name = _data[3] -- #string + + local data = _data -- #EASYGCICAP.CapPoint + local name = data.AirbaseName + local zonecoord = data.Coordinate + if data.Zone then + -- refresh coordinate in case we have a (moving) zone + zonecoord = data.Zone:GetCoordinate() + end + local airwing = wings[name][1] + local coa = AIRBASE:FindByName(name):GetCoalition() + local samecoalitionab = coa == self.coalition and true or false + local distance = position:DistanceFromPointVec2(zonecoord) + local airframes = airwing:CountAssets(true) + if distance < bestdistance and airframes >= wingsize and samecoalitionab == true then + bestdistance = distance + targetairwing = airwing -- Ops.Airwing#AIRWING + targetawname = name + end + end + local text = string.format("Closest Airwing is %s", targetawname) + local m = MESSAGE:New(text,10,"CAPGCI"):ToAllIf(self.debug):ToLog() + -- Do we have a matching airwing? + if targetairwing then + local AssetCount = targetairwing:CountAssetsOnMission(MissionTypes,Cohort) + local missioncount = self:_CountAliveAuftrags() + -- Enough airframes on mission already? + self:T(self.lid.." Assets on Mission "..AssetCount) + if missioncount < MaxAliveMissions then + local repeats = repeatsonfailure + local InterceptAuftrag = AUFTRAG:NewINTERCEPT(contact.group) + :SetMissionRange(150) + :SetPriority(1,true,1) + --:SetRequiredAssets(wingsize) + :SetRepeatOnFailure(repeats) + :SetMissionSpeed(UTILS.KnotsToAltKIAS(capspeed,capalt)) + :SetMissionAltitude(capalt) + + if nogozoneset:Count() > 0 then + InterceptAuftrag:AddConditionSuccess( + function(group,zoneset,conflictset) + local success = false + if group and group:IsAlive() then + local coord = group:GetCoordinate() + if coord and zoneset:Count() > 0 and zoneset:IsCoordinateInZone(coord) then + success = true + end + if coord and conflictset:Count() > 0 and conflictset:IsCoordinateInZone(coord) then + success = false + end + end + return success + end, + contact.group, + nogozoneset, + conflictzoneset + ) + end + + table.insert(self.ListOfAuftrag,InterceptAuftrag) + local assigned, rest = self:_TryAssignIntercept(ReadyFlightGroups,InterceptAuftrag,contact.group,wingsize) + if not assigned then + InterceptAuftrag:SetRequiredAssets(rest) + targetairwing:AddMission(InterceptAuftrag) + end + Cluster.mission = InterceptAuftrag + end + else + MESSAGE:New("**** Not enough airframes available or max mission limit reached!",15,"CAPGCI"):ToAllIf(self.debug):ToLog() + end + end +end + +--- (Internal) Start detection. +-- @param #EASYGCICAP self +-- @return #EASYGCICAP self +function EASYGCICAP:_StartIntel() + self:T(self.lid.."_StartIntel") + -- Border GCI Detection + local BlueAir_DetectionSetGroup = SET_GROUP:New() + BlueAir_DetectionSetGroup:FilterPrefixes( self.EWRName ) + BlueAir_DetectionSetGroup:FilterStart() + + -- Intel type detection + local BlueIntel = INTEL:New(BlueAir_DetectionSetGroup,self.coalitionname, self.alias) + BlueIntel:SetClusterAnalysis(true,false,false) + BlueIntel:SetForgetTime(300) + BlueIntel:SetAcceptZones(self.GoZoneSet) + BlueIntel:SetRejectZones(self.NoGoZoneSet) + BlueIntel:SetConflictZones(self.ConflictZoneSet) + BlueIntel:SetVerbosity(0) + BlueIntel:Start() + + if self.debug then + BlueIntel.debug = true + end + + local function AssignCluster(Cluster) + self:_AssignIntercept(Cluster) + end + + function BlueIntel:onbeforeNewCluster(From,Event,To,Cluster) + AssignCluster(Cluster) + end + + self.Intel = BlueIntel + return self +end + +------------------------------------------------------------------------- +-- TODO FSM Functions +------------------------------------------------------------------------- + +--- (Internal) FSM Function onafterStart +-- @param #EASYGCICAP self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #EASYGCICAP self +function EASYGCICAP:onafterStart(From,Event,To) + self:T({From,Event,To}) + self:_StartIntel() + self:_CreateAirwings() + self:_CreateSquads() + self:_SetCAPPatrolPoints() + self:_SetTankerPatrolPoints() + self:_SetAwacsPatrolPoints() + self:_SetReconPatrolPoints() + self:__Status(-10) + return self +end + +--- (Internal) FSM Function onbeforeStatus +-- @param #EASYGCICAP self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #EASYGCICAP self +function EASYGCICAP:onbeforeStatus(From,Event,To) + self:T({From,Event,To}) + if self:GetState() == "Stopped" then return false end + return self +end + +--- (Internal) FSM Function onafterStatus +-- @param #EASYGCICAP self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #EASYGCICAP self +function EASYGCICAP:onafterStatus(From,Event,To) + self:T({From,Event,To}) + -- cleanup + local cleaned = false + local cleanlist = {} + for _,_auftrag in pairs(self.ListOfAuftrag) do + local auftrag = _auftrag -- Ops.Auftrag#AUFTRAG + if auftrag and (not (auftrag:IsCancelled() or auftrag:IsDone() or auftrag:IsOver())) then + table.insert(cleanlist,auftrag) + cleaned = true + end + end + if cleaned == true then + self.ListOfAuftrag = nil + self.ListOfAuftrag = cleanlist + end + -- Gather Some Stats + local function counttable(tbl) + local count = 0 + for _,_data in pairs(tbl) do + count = count + 1 + end + return count + end + local wings = counttable(self.ManagedAW) + local squads = counttable(self.ManagedSQ) + local caps = counttable(self.ManagedCP) + local assets = 0 + local instock = 0 + local capmission = 0 + local interceptmission = 0 + local reconmission = 0 + local awacsmission = 0 + local tankermission = 0 + for _,_wing in pairs(self.wings) do + local count = _wing[1]:CountAssetsOnMission(MissionTypes,Cohort) + local count2 = _wing[1]:CountAssets(true,MissionTypes,Attributes) + capmission = capmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.GCICAP,AUFTRAG.Type.PATROLRACETRACK}) + interceptmission = interceptmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.INTERCEPT}) + reconmission = reconmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.RECON}) + awacsmission = awacsmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.AWACS}) + tankermission = tankermission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.TANKER}) + assets = assets + count + instock = instock + count2 + local assetsonmission = _wing[1]:GetAssetsOnMission({AUFTRAG.Type.GCICAP,AUFTRAG.Type.PATROLRACETRACK}) + -- update ready groups + self.ReadyFlightGroups = nil + self.ReadyFlightGroups = {} + for _,_asset in pairs(assetsonmission or {}) do + local asset = _asset -- Functional.Warehouse#WAREHOUSE.Assetitem + local FG = asset.flightgroup -- Ops.FlightGroup#FLIGHTGROUP + if FG then + local name = FG:GetName() + local engage = FG:IsEngaging() + local hasmissiles = FG:IsOutOfMissiles() == nil and true or false + local ready = hasmissiles and FG:IsFuelGood() and FG:IsAirborne() + --self:T(string.format("Flightgroup %s Engaging = %s Ready = %s",tostring(name),tostring(engage),tostring(ready))) + if ready then + self.ReadyFlightGroups[name] = FG + end + end + end + end + if self.Monitor then + local threatcount = #self.Intel.Clusters or 0 + local text = "GCICAP "..self.alias + text = text.."\nWings: "..wings.."\nSquads: "..squads.."\nCapPoints: "..caps.."\nAssets on Mission: "..assets.."\nAssets in Stock: "..instock + text = text.."\nThreats: "..threatcount + text = text.."\nAirWing managed Missions: "..capmission+awacsmission+tankermission+reconmission + text = text.."\n - CAP: "..capmission + text = text.."\n - AWACS: "..awacsmission + text = text.."\n - TANKER: "..tankermission + text = text.."\n - Recon: "..reconmission + text = text.."\nSelf managed Missions:" + text = text.."\n - Mission Limit: "..self.MaxAliveMissions + text = text.."\n - Alert5+Intercept "..self:_CountAliveAuftrags() + MESSAGE:New(text,15,"GCICAP"):ToAll():ToLogIf(self.debug) + end + self:__Status(30) + return self +end + +--- (Internal) FSM Function onafterStop +-- @param #EASYGCICAP self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #EASYGCICAP self +function EASYGCICAP:onafterStop(From,Event,To) + self:T({From,Event,To}) + self.Intel:Stop() + for _,_wing in pairs(self.wings or {}) do + _wing:Stop() + end + return self +end diff --git a/Moose Development/Moose/Ops/Fleet.lua b/Moose Development/Moose/Ops/Fleet.lua new file mode 100644 index 000000000..22d465164 --- /dev/null +++ b/Moose Development/Moose/Ops/Fleet.lua @@ -0,0 +1,428 @@ +--- **Ops** - Fleet Warehouse. +-- +-- **Main Features:** +-- +-- * Manage flotillas +-- * Carry out ARTY and PATROLZONE missions (AUFTRAG) +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/Fleet). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.Fleet +-- @image OPS_Fleet.png + + +--- FLEET class. +-- @type FLEET +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity of output. +-- @field Core.Set#SET_ZONE retreatZones Retreat zone set. +-- @field #boolean pathfinding Set pathfinding on for all spawned navy groups. +-- @extends Ops.Legion#LEGION + +--- *A fleet of British ships at war are the best negotiators.* -- Horatio Nelson +-- +-- === +-- +-- # The FLEET Concept +-- +-- A FLEET consists of one or multiple FLOTILLAs. These flotillas "live" in a WAREHOUSE that has a phyiscal struction (STATIC or UNIT) and can be captured or destroyed. +-- +-- # Basic Setup +-- +-- A new `FLEET` object can be created with the @{#FLEET.New}(`WarehouseName`, `FleetName`) function, where `WarehouseName` is the name of the static or unit object hosting the fleet +-- and `FleetName` is the name you want to give the fleet. This must be *unique*! +-- +-- myFleet=FLEET:New("myWarehouseName", "1st Fleet") +-- myFleet:SetPortZone(ZonePort1stFleet) +-- myFleet:Start() +-- +-- A fleet needs a *port zone*, which is set via the @{#FLEET.SetPortZone}(`PortZone`) function. This is the zone where the naval assets are spawned and return to. +-- +-- Finally, the fleet needs to be started using the @{#FLEET.Start}() function. If the fleet is not started, it will not process any requests. +-- +-- ## Adding Flotillas +-- +-- Flotillas can be added via the @{#FLEET.AddFlotilla}(`Flotilla`) function. See @{Ops.Flotilla#FLOTILLA} for how to create a flotilla. +-- +-- myFleet:AddFlotilla(FlotillaTiconderoga) +-- myFleet:AddFlotilla(FlotillaPerry) +-- +-- +-- +-- @field #FLEET +FLEET = { + ClassName = "FLEET", + verbose = 0, + pathfinding = false, +} + +--- Supply Zone. +-- @type FLEET.SupplyZone +-- @field Core.Zone#ZONE zone The zone. +-- @field Ops.Auftrag#AUFTRAG mission Mission assigned to supply ammo or fuel. +-- @field #boolean markerOn If `true`, marker is on. +-- @field Wrapper.Marker#MARKER marker F10 marker. + +--- FLEET class version. +-- @field #string version +FLEET.version="0.0.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add routes? +-- DONE: Add weapon range. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new FLEET class object. +-- @param #FLEET self +-- @param #string WarehouseName Name of the warehouse STATIC or UNIT object representing the warehouse. +-- @param #string FleetName Name of the fleet. +-- @return #FLEET self +function FLEET:New(WarehouseName, FleetName) + + -- Inherit everything from LEGION class. + local self=BASE:Inherit(self, LEGION:New(WarehouseName, FleetName)) -- #FLEET + + -- 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("FLEET %s | ", self.alias) + + -- Defaults + self:SetRetreatZones() + + -- Turn ship into NAVYGROUP. + if self:IsShip() then + local wh=self.warehouse --Wrapper.Unit#UNIT + local group=wh:GetGroup() + self.warehouseOpsGroup=NAVYGROUP:New(group) --Ops.NavyGroup#NAVYGROUP + self.warehouseOpsElement=self.warehouseOpsGroup:GetElementByName(wh:GetName()) + end + + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "NavyOnMission", "*") -- An NAVYGROUP was send on a Mission (AUFTRAG). + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the FLEET. Initializes parameters and starts event handlers. + -- @function [parent=#FLEET] Start + -- @param #FLEET self + + --- Triggers the FSM event "Start" after a delay. Starts the FLEET. Initializes parameters and starts event handlers. + -- @function [parent=#FLEET] __Start + -- @param #FLEET self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". Stops the FLEET and all its event handlers. + -- @param #FLEET self + + --- Triggers the FSM event "Stop" after a delay. Stops the FLEET and all its event handlers. + -- @function [parent=#FLEET] __Stop + -- @param #FLEET self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "NavyOnMission". + -- @function [parent=#FLEET] NavyOnMission + -- @param #FLEET self + -- @param Ops.NavyGroup#NAVYGROUP ArmyGroup The NAVYGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "NavyOnMission" after a delay. + -- @function [parent=#FLEET] __NavyOnMission + -- @param #FLEET self + -- @param #number delay Delay in seconds. + -- @param Ops.NavyGroup#NAVYGROUP ArmyGroup The NAVYGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "NavyOnMission" event. + -- @function [parent=#FLEET] OnAfterNavyOnMission + -- @param #FLEET self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.NavyGroup#NAVYGROUP NavyGroup The NAVYGROUP on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add a flotilla to the fleet. +-- @param #FLEET self +-- @param Ops.Flotilla#FLOTILLA Flotilla The flotilla object. +-- @return #FLEET self +function FLEET:AddFlotilla(Flotilla) + + -- Add flotilla to fleet. + table.insert(self.cohorts, Flotilla) + + -- Add assets to flotilla. + self:AddAssetToFlotilla(Flotilla, Flotilla.Ngroups) + + -- Set fleet of flotilla. + Flotilla:SetFleet(self) + + -- Start flotilla. + if Flotilla:IsStopped() then + Flotilla:Start() + end + + return self +end + +--- Add asset group(s) to flotilla. +-- @param #FLEET self +-- @param Ops.Flotilla#FLOTILLA Flotilla The flotilla object. +-- @param #number Nassets Number of asset groups to add. +-- @return #FLEET self +function FLEET:AddAssetToFlotilla(Flotilla, Nassets) + + if Flotilla then + + -- Get the template group of the flotilla. + local Group=GROUP:FindByName(Flotilla.templatename) + + if Group then + + -- Debug text. + local text=string.format("Adding asset %s to flotilla %s", Group:GetName(), Flotilla.name) + self:T(self.lid..text) + + -- Add assets to airwing warehouse. + self:AddAsset(Group, Nassets, nil, nil, nil, nil, Flotilla.skill, Flotilla.livery, Flotilla.name) + + else + self:E(self.lid.."ERROR: Group does not exist!") + end + + else + self:E(self.lid.."ERROR: Flotilla does not exit!") + end + + return self +end + +--- Set pathfinding for all spawned naval groups. +-- @param #FLEET self +-- @param #boolean Switch If `true`, pathfinding is used. +-- @return #FLEET self +function FLEET:SetPathfinding(Switch) + self.pathfinding=Switch + return self +end + +--- Define a set of retreat zones. +-- @param #FLEET self +-- @param Core.Set#SET_ZONE RetreatZoneSet Set of retreat zones. +-- @return #FLEET self +function FLEET:SetRetreatZones(RetreatZoneSet) + self.retreatZones=RetreatZoneSet or SET_ZONE:New() + return self +end + +--- Add a retreat zone. +-- @param #FLEET self +-- @param Core.Zone#ZONE RetreatZone Retreat zone. +-- @return #FLEET self +function FLEET:AddRetreatZone(RetreatZone) + self.retreatZones:AddZone(RetreatZone) + return self +end + +--- Get retreat zones. +-- @param #FLEET self +-- @return Core.Set#SET_ZONE Set of retreat zones. +function FLEET:GetRetreatZones() + return self.retreatZones +end + +--- Get flotilla by name. +-- @param #FLEET self +-- @param #string FlotillaName Name of the flotilla. +-- @return Ops.Flotilla#FLOTILLA The Flotilla object. +function FLEET:GetFlotilla(FlotillaName) + local flotilla=self:_GetCohort(FlotillaName) + return flotilla +end + +--- Get flotilla of an asset. +-- @param #FLEET self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The flotilla asset. +-- @return Ops.Flotilla#FLOTILLA The flotilla object. +function FLEET:GetFlotillaOfAsset(Asset) + local flotilla=self:GetFlotilla(Asset.squadname) + return flotilla +end + +--- Remove asset from flotilla. +-- @param #FLEET self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The flotilla asset. +function FLEET:RemoveAssetFromFlotilla(Asset) + local flotilla=self:GetFlotillaOfAsset(Asset) + if flotilla then + flotilla:DelAsset(Asset) + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start FLEET FSM. +-- @param #FLEET self +function FLEET:onafterStart(From, Event, To) + + -- Start parent Warehouse. + self:GetParent(self, FLEET).onafterStart(self, From, Event, To) + + -- Info. + self:I(self.lid..string.format("Starting FLEET v%s", FLEET.version)) + +end + +--- Update status. +-- @param #FLEET self +function FLEET:onafterStatus(From, Event, To) + + -- Status of parent Warehouse. + self:GetParent(self).onafterStatus(self, From, Event, To) + + -- FSM state. + local fsmstate=self:GetState() + + ---------------- + -- Transport --- + ---------------- + + self:CheckTransportQueue() + + -------------- + -- Mission --- + -------------- + + -- Check if any missions should be cancelled. + self:CheckMissionQueue() + + ----------- + -- Info --- + ----------- + + -- Display tactival overview. + self:_TacticalOverview() + + -- General info: + if self.verbose>=1 then + + -- Count missions not over yet. + local Nmissions=self:CountMissionsInQueue() + + -- Asset count. + local Npq, Np, Nq=self:CountAssetsOnMission() + + -- Asset string. + 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, Flotillas=%d, Assets=%s", fsmstate, Nmissions, #self.cohorts, 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 or 0) + 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 + + -------------------- + -- Transport Info -- + -------------------- + if self.verbose>=2 then + local text=string.format("Transports Total=%d:", #self.transportqueue) + for i,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + local prio=string.format("%d/%s", transport.prio, tostring(transport.importance)) ; if transport.urgent then prio=prio.." (!)" end + local carriers=string.format("Ncargo=%d/%d, Ncarriers=%d", transport.Ncargo, transport.Ndelivered, transport.Ncarrier) + + text=text..string.format("\n[%d] UID=%d: Status=%s, Prio=%s, Cargo: %s", i, transport.uid, transport:GetState(), prio, carriers) + end + self:I(self.lid..text) + end + + ------------------- + -- Flotilla Info -- + ------------------- + if self.verbose>=3 then + local text="Flotillas:" + for i,_flotilla in pairs(self.cohorts) do + local flotilla=_flotilla --Ops.Flotilla#FLOTILLA + + local callsign=flotilla.callsignName and UTILS.GetCallsignName(flotilla.callsignName) or "N/A" + local modex=flotilla.modex and flotilla.modex or -1 + local skill=flotilla.skill and tostring(flotilla.skill) or "N/A" + + -- Flotilla text. + text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", flotilla.name, flotilla:GetState(), flotilla.aircrafttype, flotilla:CountAssets(true), #flotilla.assets, callsign, modex, skill) + end + self:I(self.lid..text) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "NavyOnMission". +-- @param #FLEET self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup Ops army group on mission. +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function FLEET:onafterNavyOnMission(From, Event, To, NavyGroup, Mission) + -- Debug info. + self:T(self.lid..string.format("Group %s on %s mission %s", NavyGroup:GetName(), Mission:GetType(), Mission:GetName())) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua new file mode 100644 index 000000000..bc13affeb --- /dev/null +++ b/Moose Development/Moose/Ops/FlightControl.lua @@ -0,0 +1,4764 @@ +--- **OPS** - Air Traffic Control for AI and human players. +-- +-- **Main Features:** +-- +-- * Manage aircraft departure and arrival +-- * Handles AI and human players +-- * Limit number of AI groups taxiing, taking off and landing simultaneously +-- * Immersive voice overs via SRS text-to-speech +-- * Define holding patterns for airdromes +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions: None +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module OPS.FlightControl +-- @image OPS_FlightControl.png + + +--- FLIGHTCONTROL class. +-- @type FLIGHTCONTROL +-- @field #string ClassName Name of the class. +-- @field #boolean verbose Verbosity level. +-- @field #string theatre The DCS map used in the mission. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string airbasename Name of airbase. +-- @field #string alias Radio alias, e.g. "Batumi Tower". +-- @field #number airbasetype Type of airbase. +-- @field Wrapper.Airbase#AIRBASE airbase Airbase object. +-- @field Core.Zone#ZONE zoneAirbase Zone around the airbase. +-- @field #table parking Parking spots table. +-- @field #table flights All flights table. +-- @field #table clients Table with all clients spawning at this airbase. +-- @field Ops.ATIS#ATIS atis ATIS object. +-- @field #number frequency ATC radio frequency in MHz. +-- @field #number modulation ATC radio modulation, *e.g.* `radio.modulation.AM`. +-- @field #number NlandingTot Max number of aircraft groups in the landing pattern. +-- @field #number NlandingTakeoff Max number of groups taking off to allow landing clearance. +-- @field #number NtaxiTot Max number of aircraft groups taxiing to runway for takeoff. +-- @field #boolean NtaxiInbound Include inbound taxiing groups. +-- @field #number NtaxiLanding Max number of aircraft landing for groups taxiing to runway for takeoff. +-- @field #number dTlanding Time interval in seconds between landing clearance. +-- @field #number Tlanding Time stamp (abs.) when last flight got landing clearance. +-- @field #number Nparkingspots Total number of parking spots. +-- @field Core.Spawn#SPAWN parkingGuard Parking guard spawner. +-- @field #table holdingpatterns Holding points. +-- @field #number hpcounter Counter for holding zones. +-- @field Sound.SRS#MSRSQUEUE msrsqueue Queue for TTS transmissions using MSRS class. +-- @field Sound.SRS#MSRS msrsTower Moose SRS wrapper. +-- @field Sound.SRS#MSRS msrsPilot Moose SRS wrapper. +-- @field #number Tlastmessage Time stamp (abs.) of last radio transmission. +-- @field #number dTmessage Time interval between messages. +-- @field #boolean markPatterns If `true`, park holding pattern. +-- @field #number speedLimitTaxi Taxi speed limit in m/s. +-- @field #number runwaydestroyed Time stamp (abs), when runway was destroyed. If `nil`, runway is operational. +-- @field #number runwayrepairtime Time in seconds until runway will be repaired after it was destroyed. Default is 3600 sec (one hour). +-- @field #boolean markerParking If `true`, occupied parking spots are marked. +-- @field #boolean nosubs If `true`, SRS TTS is without subtitles. +-- @field #number Nplayers Number of human players. Updated at each StatusUpdate call. +-- @field #boolean radioOnlyIfPlayers Activate to limit transmissions only if players are active at the airbase. +-- @extends Core.Fsm#FSM + +--- **Ground Control**: Airliner X, Good news, you are clear to taxi to the active. +-- **Pilot**: Roger, What's the bad news? +-- **Ground Control**: No bad news at the moment, but you probably want to get gone before I find any. +-- +-- === +-- +-- # The FLIGHTCONTROL Concept +-- +-- This class implements an ATC for human and AI controlled aircraft. It gives permission for take-off and landing based on a sophisticated queueing system. +-- Therefore, it solves (or reduces) a lot of common problems with the DCS implementation. +-- +-- You might be familiar with the `AIRBOSS` class. This class is the analogue for land based airfields. One major difference is that no pre-recorded sound files are +-- necessary. The radio transmissions use the SRS text-to-speech feature. +-- +-- ## Prerequisites +-- +-- * SRS is used for radio communications +-- +-- ## Limitations +-- +-- Some (DCS) limitations you should be aware of: +-- +-- * As soon as AI aircraft taxi or land, we completely loose control. All is governed by the internal DCS AI logic. +-- * We have no control over the active runway or which runway is used by the AI if there are multiple. +-- * Only one player/client per group as we can create menus only for a group and not for a specific unit. +-- * Only FLIGHTGROUPS are controlled. This means some older classes, *e.g.* RAT are not supported (yet). +-- * So far only airdromes are handled, *i.e.* no FARPs or ships. +-- * Helicopters are not treated differently from fixed wing aircraft until now. +-- * The active runway can only be determined by the wind direction. So at least set a very light wind speed in your mission. +-- +-- # Basic Usage +-- +-- A flight control for a given airdrome can be created with the @{#FLIGHTCONTROL.New}(*AirbaseName, Frequency, Modulation, PathToSRS*) function. You need to specify the name of the airbase, the +-- tower radio frequency, its modulation and the path, where SRS is located on the machine that is running this mission. +-- +-- For the FC to be operating, it needs to be started with the @{#FLIGHTCONTROL.Start}() function. +-- +-- ## Simple Script +-- +-- The simplest script looks like +-- +-- local FC_BATUMI=FLIGHTCONTROL:New(AIRBASE.Caucasus.Batumi, 251, nil, "D:\\SomeDirectory\\_SRS") +-- FC_BATUMI:Start() +-- +-- This will start the FC for at the Batumi airbase with tower frequency 251 MHz AM. SRS needs to be in the given directory. +-- +-- Like this, a default holding pattern (see below) is parallel to the direction of the active runway. +-- +-- # Holding Patterns +-- +-- Holding pattern are air spaces where incoming aircraft are guided to and have to hold until they get landing clearance. +-- +-- You can add a holding pattern with the @{#FLIGHTCONTROL.AddHoldingPattern}(*ArrivalZone, Heading, Length, FlightlevelMin, FlightlevelMax, Prio*) function, where +-- +-- * `ArrivalZone` is the zone where the aircraft enter the pattern. +-- * `Heading` is the direction into which the aircraft have to fly from the arrival zone. +-- * `Length` is the length of the pattern. +-- * `FlightLevelMin` is the lowest altitude at which aircraft can hold. +-- * `FlightLevelMax` is the highest altitude at which aircraft can hold. +-- * `Prio` is the priority of this holding stacks. If multiple patterns are defined, patterns with higher prio will be filled first. +-- +-- # Parking Guard +-- +-- A "parking guard" is a group or static object, that is spawned in front of parking aircraft. This is useful to stop AI groups from taxiing if they are spawned with hot engines. +-- It is also handy to forbid human players to taxi until they ask for clearance. +-- +-- You can activate the parking guard with the @{#FLIGHTCONTROL.SetParkingGuard}(*GroupName*) function, where the parameter `GroupName` is the name of a late activated template group. +-- This should consist of only *one* unit, *e.g.* a single infantry soldier. +-- +-- You can also use static objects as parking guards with the @{#FLIGHTCONTROL.SetParkingGuardStatic}(*StaticName*), where the parameter `StaticName` is the name of a static object placed +-- somewhere in the mission editor. +-- +-- # Limits for Inbound and Outbound Flights +-- +-- You can define limits on how many aircraft are simultaneously landing and taking off. This avoids (DCS) problems where taxiing aircraft cause a "traffic jam" on the taxi way(s) +-- and bring the whole airbase effectively to a stand still. +-- +-- ## Landing Limits +-- +-- The number of groups getting landing clearance can be set with the @{#FLIGHTCONTROL.SetLimitLanding}(*Nlanding, Ntakeoff*) function. +-- The first parameter, `Nlanding`, defines how many groups get clearance simultaneously. +-- +-- The second parameter, `Ntakeoff`, sets a limit on how many flights can take off whilst inbound flights still get clearance. By default, this is set to zero because the runway can only be used for takeoff *or* +-- landing. So if you have a flight taking off, inbound fights will have to wait until the runway is clear. +-- If you have an airport with more than one runway, *e.g.* Nellis AFB, you can allow simultanious landings and takeoffs by setting this number greater zero. +-- +-- The time interval between clerances can be set with the @{#FLIGHTCONTROL.SetLandingInterval}(`dt`) function, where the parameter `dt` specifies the time interval in seconds before +-- the next flight get clearance. This only has an effect if `Nlanding` is greater than one. +-- +-- ## Taxiing/Takeoff Limits +-- +-- The number of AI flight groups getting clearance to taxi to the runway can be set with the @{#FLIGHTCONTROL.SetLimitTaxi}(*Nlanding, Ntakeoff*) function. +-- The first parameter, `Ntaxi`, defines how many groups are allowed to taxi to the runway simultaneously. Note that once the AI starts to taxi, we loose complete control over it. +-- They will follow their internal logic to get the the runway and take off. Therefore, giving clearance to taxi is equivalent to giving them clearance for takeoff. +-- +-- By default, the parameter only counts the number of flights taxiing *to* the runway. If you set the second parameter, `IncludeInbound`, to `true`, this will also count the flights +-- that are taxiing to their parking spot(s) after they landed. +-- +-- The third parameter, `Nlanding`, defines how many aircraft can land whilst outbound fights still get taxi/takeoff clearance. By default, this is set to zero because the runway +-- can only be used for takeoff *or* landing. If you have an airport with more than one runway, *e.g.* Nellis AFB, you can allow aircraft to taxi to the runway while other flights are landing +-- by setting this number greater zero. +-- +-- Note that the limits here are only affecting **AI** aircraft groups. *Human players* are assumed to be a lot more well behaved and capable as they are able to taxi around obstacles, *e.g.* +-- other aircraft etc. Therefore, players will get taxi clearance independent of the number of inbound and/or outbound flights. Players will, however, still need to ask for takeoff clearance once +-- they are holding short of the runway. +-- +-- # Speeding Violations +-- +-- You can set a speed limit for taxiing players with the @{#FLIGHTCONTROL.SetSpeedLimitTaxi}(*SpeedLimit*) function, where the parameter `SpeedLimit` is the max allowed speed in knots. +-- If players taxi faster, they will get a radio message. Additionally, the FSM event `PlayerSpeeding` is triggered and can be captured with the `OnAfterPlayerSpeeding` function. +-- For example, this can be used to kick players that do not behave well. +-- +-- # Runway Destroyed +-- +-- Once a runway is damaged, DCS AI will stop taxiing. Therefore, this class monitors if a runway is destroyed. If this is the case, all AI taxi and landing clearances will be suspended for +-- one hour. This is the hard coded time in DCS until the runway becomes operational again. If that ever changes, you can manually set the repair time with the +-- @{#FLIGHTCONTROL.SetRunwayRepairtime} function. +-- +-- Note that human players we still get taxi, takeoff and landing clearances. +-- +-- If the runway is destroyed, the FSM event `RunwayDestroyed` is triggered and can be captured with the @{#FLIGHTCONTROL.OnAfterRunwayDestroyed} function. +-- +-- If the runway is repaired, the FSM event `RunwayRepaired` is triggered and can be captured with the @{#FLIGHTCONTROL.OnAfterRunwayRepaired} function. +-- +-- # SRS +-- +-- SRS text-to-speech is used to send radio messages from the tower and pilots. +-- +-- ## Tower +-- +-- You can set the options for the tower SRS voice with the @{#FLIGHTCONTROL.SetSRSTower}() function. +-- +-- ## Pilot +-- +-- You can set the options for the pilot SRS voice with the @{#FLIGHTCONTROL.SetSRSPilot}() function. +-- +-- # Runways +-- +-- First note, that we have extremely limited control over which runway the DCS AI groups use. The only parameter we can adjust is the direction of the wind. In many cases, the AI will try to takeoff and land +-- against the wind, which therefore determines the active runway. There are, however, cases where this does not hold true. For example, at Nellis AFB the runway for takeoff is `03L` while the runway for +-- landing is `21L`. +-- +-- By default, the runways for landing and takeoff are determined from the wind direction as described above. For cases where this gives wrong results, you can set the active runways manually. This is +-- done via @{Wrapper.Airbase#AIRBASE} class. +-- +-- More specifically, you can use the @{Wrapper.Airbase#AIRBASE.SetActiveRunwayLanding} function to set the landing runway and the @{Wrapper.Airbase#AIRBASE.SetActiveRunwayTakeoff} function to set +-- the runway for takeoff. +-- +-- ## Example for Nellis AFB +-- +-- For Nellis, you can use: +-- +-- -- Nellis AFB. +-- local Nellis=AIRBASE:FindByName(AIRBASE.Nevada.Nellis_AFB) +-- Nellis:SetActiveRunwayLanding("21L") +-- Nellis:SetActiveRunwayTakeoff("03L") +-- +-- # DCS ATC +-- +-- You can disable the DCS ATC with the @{Wrapper.Airbase#AIRBASE.SetRadioSilentMode}(*true*). This does not remove the DCS ATC airbase from the F10 menu but makes the ATC unresponsive. +-- +-- +-- # Examples +-- +-- In this section, you find examples for different airdromes. +-- +-- ## Nellis AFB +-- +-- -- Create a new FLIGHTCONTROL object at Nellis AFB. The tower frequency is 251 MHz AM. Path to SRS has to be adjusted. +-- local atcNellis=FLIGHTCONTROL:New(AIRBASE.Nevada.Nellis_AFB, 251, nil, "D:\\My SRS Directory") +-- -- Set a parking guard from a static named "Static Generator F Template". +-- atcNellis:SetParkingGuardStatic("Static Generator F Template") +-- -- Set taxi speed limit to 25 knots. +-- atcNellis:SetSpeedLimitTaxi(25) +-- -- Set that max 3 groups are allowed to taxi simultaneously. +-- atcNellis:SetLimitTaxi(3, false, 1) +-- -- Set that max 2 groups are allowd to land simultaneously and unlimited number (99) groups can land, while other groups are taking off. +-- atcNellis:SetLimitLanding(2, 99) +-- -- Use Google for text-to-speech. +-- atcNellis:SetSRSTower(nil, nil, "en-AU-Standard-A", nil, nil, "D:\\Path To Google\\GoogleCredentials.json") +-- atcNellis:SetSRSPilot(nil, nil, "en-US-Wavenet-I", nil, nil, "D:\\Path To Google\\GoogleCredentials.json") +-- -- Define two holding zones. +-- atcNellis:AddHoldingPattern(ZONE:New("Nellis Holding Alpha"), 030, 15, 6, 10, 10) +-- atcNellis:AddHoldingPattern(ZONE:New("Nellis Holding Bravo"), 090, 15, 6, 10, 20) +-- -- Start the ATC. +-- atcNellis:Start() +-- +-- @field #FLIGHTCONTROL +FLIGHTCONTROL = { + ClassName = "FLIGHTCONTROL", + verbose = 0, + lid = nil, + theatre = nil, + airbasename = nil, + airbase = nil, + airbasetype = nil, + zoneAirbase = nil, + parking = {}, + runways = {}, + flights = {}, + clients = {}, + atis = nil, + Nlanding = nil, + dTlanding = nil, + Nparkingspots = nil, + holdingpatterns = {}, + hpcounter = 0, + nosubs = false, + Nplayers = 0, +} + +--- Holding point. Contains holding stacks. +-- @type FLIGHTCONTROL.HoldingPattern +-- @field Core.Zone#ZONE arrivalzone Zone where aircraft should arrive. +-- @field #number uid Unique ID. +-- @field #string name Name of the zone, which is -. +-- @field Core.Point#COORDINATE pos0 First position of racetrack holding pattern. +-- @field Core.Point#COORDINATE pos1 Second position of racetrack holding pattern. +-- @field #number angelsmin Smallest holding altitude in angels. +-- @field #number angelsmax Largest holding alitude in angels. +-- @field #table stacks Holding stacks. +-- @field #number markArrival Marker ID of the arrival zone. +-- @field #number markArrow Marker ID of the direction. + +--- Holding stack. +-- @type FLIGHTCONTROL.HoldingStack +-- @field Ops.FlightGroup#FLIGHTGROUP flightgroup Flight group of this stack. +-- @field #number angels Holding altitude in Angels. +-- @field Core.Point#COORDINATE pos0 First position of racetrack holding pattern. +-- @field Core.Point#COORDINATE pos1 Second position of racetrack holding pattern. +-- @field #number heading Heading. + + +--- Parking spot data. +-- @type FLIGHTCONTROL.ParkingSpot +-- @field Wrapper.Group#GROUP ParkingGuard Parking guard for this spot. +-- @extends Wrapper.Airbase#AIRBASE.ParkingSpot + +--- Flight status. +-- @type FLIGHTCONTROL.FlightStatus +-- @field #string UNKNOWN Flight is unknown. +-- @field #string INBOUND Flight is inbound. +-- @field #string HOLDING Flight is holding. +-- @field #string LANDING Flight is landing. +-- @field #string TAXIINB Flight is taxiing to parking area. +-- @field #string ARRIVED Flight arrived at parking spot. +-- @field #string TAXIOUT Flight is taxiing to runway for takeoff. +-- @field #string READYTX Flight is ready to taxi. +-- @field #string READYTO Flight is ready for takeoff. +-- @field #string TAKEOFF Flight is taking off. +FLIGHTCONTROL.FlightStatus={ + UNKNOWN="Unknown", + PARKING="Parking", + READYTX="Ready To Taxi", + TAXIOUT="Taxi To Runway", + READYTO="Ready For Takeoff", + TAKEOFF="Takeoff", + INBOUND="Inbound", + HOLDING="Holding", + LANDING="Landing", + TAXIINB="Taxi To Parking", + ARRIVED="Arrived", +} + +--- FlightControl class version. +-- @field #string version +FLIGHTCONTROL.version="0.7.7" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list + +-- TODO: Switch to enable/disable AI messages. +-- TODO: Talk me down option. +-- TODO: Check runways and clean up. +-- TODO: Add FARPS? +-- DONE: Improve ATC TTS messages. +-- DONE: ATIS option. +-- DONE: Runway destroyed. +-- DONE: Accept and forbit parking spots. DONE via AIRBASE black/white lists and airwing features. +-- DONE: Support airwings. Dont give clearance for Alert5 or if mission has not started. +-- DONE: Define holding zone. +-- DONE: Basic ATC voice overs. +-- DONE: Add SRS TTS. +-- DONE: Add parking guard. +-- DONE: Interface with FLIGHTGROUP. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new FLIGHTCONTROL class object for an associated airbase. +-- @param #FLIGHTCONTROL self +-- @param #string AirbaseName Name of the airbase. +-- @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. +-- @param #string PathToSRS Path to the directory, where SRS is located. +-- @param #number Port Port of SRS Server, defaults to 5002 +-- @param #string GoogleKey Path to the Google JSON-Key. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:New(AirbaseName, Frequency, Modulation, PathToSRS, Port, GoogleKey) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #FLIGHTCONTROL + + -- Try to get the airbase. + self.airbase=AIRBASE:FindByName(AirbaseName) + + -- Name of the airbase. + self.airbasename=AirbaseName + + -- Set some string id for output to DCS.log file. + self.lid=string.format("FLIGHTCONTROL %s | ", AirbaseName) + + -- Check if the airbase exists. + if not self.airbase then + self:E(string.format("ERROR: Could not find airbase %s!", tostring(AirbaseName))) + return nil + end + -- Check if airbase is an airdrome. + if self.airbase:GetAirbaseCategory()~=Airbase.Category.AIRDROME then + self:E(string.format("ERROR: Airbase %s is not an AIRDROME! Script does not handle FARPS or ships.", tostring(AirbaseName))) + return nil + end + + -- Airbase category airdrome, FARP, SHIP. + self.airbasetype=self.airbase:GetAirbaseCategory() + + -- Current map. + self.theatre=env.mission.theatre + + -- 5 NM zone around the airbase. + self.zoneAirbase=ZONE_RADIUS:New("FC", self:GetCoordinate():GetVec2(), UTILS.NMToMeters(5)) + + -- Add backup holding pattern. + self:_AddHoldingPatternBackup() + + -- Set alias. + self.alias=self.airbasename.." Tower" + + -- Defaults: + self:SetLimitLanding(2, 0) + self:SetLimitTaxi(2, false, 0) + self:SetLandingInterval() + self:SetFrequency(Frequency, Modulation) + self:SetMarkHoldingPattern(true) + self:SetRunwayRepairtime() + self.nosubs = false + + -- Set Callsign Options + self:SetCallSignOptions(true,true) + + -- Init msrs queue. + self.msrsqueue=MSRSQUEUE:New(self.alias) + + -- Set that transmission is only if alive players on the server. + self:SetTransmitOnlyWithPlayers(true) + + -- Init msrs bases + local path = PathToSRS or MSRS.path + local port = Port or MSRS.port or 5002 + + -- Set SRS Port + self:SetSRSPort(port) + + -- SRS for Tower. + self.msrsTower=MSRS:New(path, Frequency, Modulation) + self.msrsTower:SetPort(port) + if GoogleKey then + self.msrsTower:SetProviderOptionsGoogle(GoogleKey,GoogleKey) + self.msrsTower:SetProvider(MSRS.Provider.GOOGLE) + end + self.msrsTower:SetCoordinate(self:GetCoordinate()) + self:SetSRSTower() + + -- SRS for Pilot. + self.msrsPilot=MSRS:New(PathToSRS, Frequency, Modulation) + self.msrsPilot:SetPort(self.Port) + if GoogleKey then + self.msrsPilot:SetProviderOptionsGoogle(GoogleKey,GoogleKey) + self.msrsPilot:SetProvider(MSRS.Provider.GOOGLE) + end + self.msrsTower:SetCoordinate(self:GetCoordinate()) + self:SetSRSPilot() + + -- Wait at least 10 seconds after last radio message before calling the next status update. + self.dTmessage=10 + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "StatusUpdate", "*") -- Update status. + + self:AddTransition("*", "PlayerKilledGuard", "*") -- Player killed parking guard + self:AddTransition("*", "PlayerSpeeding", "*") -- Player speeding on taxi way. + + self:AddTransition("*", "RunwayDestroyed", "*") -- Runway of the airbase was destroyed. + self:AddTransition("*", "RunwayRepaired", "*") -- Runway of the airbase was repaired. + + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + -- Add to data base. + _DATABASE:AddFlightControl(self) + + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". + -- @function [parent=#FLIGHTCONTROL] Start + -- @param #FLIGHTCONTROL self + + --- Triggers the FSM event "Start" after a delay. + -- @function [parent=#FLIGHTCONTROL] __Start + -- @param #FLIGHTCONTROL self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". + -- @function [parent=#FLIGHTCONTROL] Stop + -- @param #FLIGHTCONTROL self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#FLIGHTCONTROL] __Stop + -- @param #FLIGHTCONTROL self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "StatusUpdate". + -- @function [parent=#FLIGHTCONTROL] StatusUpdate + -- @param #FLIGHTCONTROL self + + --- Triggers the FSM event "StatusUpdate" after a delay. + -- @function [parent=#FLIGHTCONTROL] __StatusUpdate + -- @param #FLIGHTCONTROL self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RunwayDestroyed". + -- @function [parent=#FLIGHTCONTROL] RunwayDestroyed + -- @param #FLIGHTCONTROL self + + --- Triggers the FSM event "RunwayDestroyed" after a delay. + -- @function [parent=#FLIGHTCONTROL] __RunwayDestroyed + -- @param #FLIGHTCONTROL self + -- @param #number delay Delay in seconds. + + --- On after "RunwayDestroyed" event. + -- @function [parent=#FLIGHTCONTROL] OnAfterRunwayDestroyed + -- @param #FLIGHTCONTROL self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "RunwayRepaired". + -- @function [parent=#FLIGHTCONTROL] RunwayRepaired + -- @param #FLIGHTCONTROL self + + --- Triggers the FSM event "RunwayRepaired" after a delay. + -- @function [parent=#FLIGHTCONTROL] __RunwayRepaired + -- @param #FLIGHTCONTROL self + -- @param #number delay Delay in seconds. + + --- On after "RunwayRepaired" event. + -- @function [parent=#FLIGHTCONTROL] OnAfterRunwayRepaired + -- @param #FLIGHTCONTROL self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "PlayerSpeeding". + -- @function [parent=#FLIGHTCONTROL] PlayerSpeeding + -- @param #FLIGHTCONTROL self + -- @param Ops.FlightGroup#FLIGHTGROUP.PlayerData Player data. + + --- Triggers the FSM event "PlayerSpeeding" after a delay. + -- @function [parent=#FLIGHTCONTROL] __PlayerSpeeding + -- @param #FLIGHTCONTROL self + -- @param #number delay Delay in seconds. + -- @param Ops.FlightGroup#FLIGHTGROUP.PlayerData Player data. + + --- On after "PlayerSpeeding" event. + -- @function [parent=#FLIGHTCONTROL] OnAfterPlayerSpeeding + -- @param #FLIGHTCONTROL self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.FlightGroup#FLIGHTGROUP.PlayerData Player data. + + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set verbosity level. +-- @param #FLIGHTCONTROL self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Limit radio transmissions only if human players are registered at the airbase. +-- This can be used to reduce TTS messages on heavy missions. +-- @param #FLIGHTCONTROL self +-- @param #boolean Switch If `true` or `nil` no transmission if there are no players. Use `false` enable TTS with no players. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetRadioOnlyIfPlayers(Switch) + if Switch==nil or Switch==true then + self.radioOnlyIfPlayers=true + else + self.radioOnlyIfPlayers=false + end + return self +end + + +--- Set whether to only transmit TTS messages if there are players on the server. +-- @param #FLIGHTCONTROL self +-- @param #boolean Switch If `true`, only send TTS messages if there are alive Players. If `false` or `nil`, transmission are done also if no players are on the server. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetTransmitOnlyWithPlayers(Switch) + self.msrsqueue:SetTransmitOnlyWithPlayers(Switch) + return self +end + + +--- Set subtitles to appear on SRS TTS messages. +-- @param #FLIGHTCONTROL self +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SwitchSubtitlesOn() + self.nosubs = false + return self +end + +--- Set subtitles to appear on SRS TTS messages. +-- @param #FLIGHTCONTROL self +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SwitchSubtitlesOff() + self.nosubs = true + return self +end + +--- Set the tower frequency. +-- @param #FLIGHTCONTROL self +-- @param #number Frequency Frequency in MHz. Default 305 MHz. +-- @param #number Modulation Modulation `radio.modulation.AM`=0, `radio.modulation.FM`=1. Default `radio.modulation.AM`. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetFrequency(Frequency, Modulation) + + self.frequency=Frequency or 305 + self.modulation=Modulation or radio.modulation.AM + + if self.msrsPilot then + self.msrsPilot:SetFrequencies(Frequency) + self.msrsPilot:SetModulations(Modulation) + end + + if self.msrsTower then + self.msrsTower:SetFrequencies(Frequency) + self.msrsTower:SetModulations(Modulation) + end + + return self +end + +--- Set the SRS server port. +-- @param #FLIGHTCONTROL self +-- @param #number Port Port to be used. Defaults to 5002. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetSRSPort(Port) + self.Port = Port or 5002 + return self +end + +--- Set SRS options for a given MSRS object. +-- @param #FLIGHTCONTROL self +-- @param Sound.SRS#MSRS msrs Moose SRS object. +-- @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 Volume Volume. Default 1.0. +-- @param #string Label Name under which SRS transmits. +-- @param #string PathToGoogleCredentials Path to google credentials json file. +-- @param #number Port Server port for SRS +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:_SetSRSOptions(msrs, Gender, Culture, Voice, Volume, Label, PathToGoogleCredentials, Port) + + -- Defaults: + Gender=Gender or "female" + Culture=Culture or "en-GB" + Volume=Volume or 1.0 + + if msrs then + msrs:SetGender(Gender) + msrs:SetCulture(Culture) + msrs:SetVoice(Voice) + msrs:SetVolume(Volume) + msrs:SetLabel(Label) + msrs:SetCoalition(self:GetCoalition()) + msrs:SetPort(Port or self.Port or 5002) + end + + return self +end + +--- Set SRS options for tower voice. +-- @param #FLIGHTCONTROL self +-- @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`. See [Google Voices](https://cloud.google.com/text-to-speech/docs/voices). +-- @param #number Volume Volume. Default 1.0. +-- @param #string Label Name under which SRS transmits. Default `self.alias`. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetSRSTower(Gender, Culture, Voice, Volume, Label) + + if self.msrsTower then + self:_SetSRSOptions(self.msrsTower, Gender or "female", Culture or "en-GB", Voice, Volume, Label or self.alias) + end + + return self +end + +--- Set SRS options for pilot voice. +-- @param #FLIGHTCONTROL self +-- @param #string Gender Gender: "male" (default) or "female". +-- @param #string Culture Culture, e.g. "en-US" (default). +-- @param #string Voice Specific voice. Overrides `Gender` and `Culture`. +-- @param #number Volume Volume. Default 1.0. +-- @param #string Label Name under which SRS transmits. Default "Pilot". +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetSRSPilot(Gender, Culture, Voice, Volume, Label) + + if self.msrsPilot then + self:_SetSRSOptions(self.msrsPilot, Gender or "male", Culture or "en-US", Voice, Volume, Label or "Pilot") + end + + return self +end + + +--- Set the number of aircraft groups, that are allowed to land simultaneously. +-- Note that this restricts AI and human players. +-- +-- By default, up to two groups get landing clearance. They are spaced out in time, i.e. after the first one got cleared, the second has to wait a bit. +-- This +-- +-- By default, landing clearance is only given when **no** other flight is taking off. You can adjust this for airports with more than one runway or +-- in cases where simultaneous takeoffs and landings are unproblematic. Note that only because there are multiple runways, it does not mean the AI uses them. +-- +-- @param #FLIGHTCONTROL self +-- @param #number Nlanding Max number of aircraft landing simultaneously. Default 2. +-- @param #number Ntakeoff Allowed number of aircraft taking off for groups to get landing clearance. Default 0. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetLimitLanding(Nlanding, Ntakeoff) + + self.NlandingTot=Nlanding or 2 + + self.NlandingTakeoff=Ntakeoff or 0 + + return self +end + +--- Set time interval between landing clearance of groups. +-- @param #FLIGHTCONTROL self +-- @param #number dt Time interval in seconds. Default 180 sec (3 min). +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetLandingInterval(dt) + + self.dTlanding=dt or 180 + + return self +end + + +--- Set the number of **AI** aircraft groups, that are allowed to taxi simultaneously. +-- If the limit is reached, other AI groups not get taxi clearance to taxi to the runway. +-- +-- By default, this only counts the number of AI that taxi from their parking position to the runway. +-- You can also include inbound AI that taxi from the runway to their parking position. +-- This can be handy for problematic (usually smaller) airdromes, where there is only one taxiway inbound and outbound flights. +-- +-- By default, AI will not get cleared for taxiing if at least one other flight is currently landing. If this is an unproblematic airdrome, you can +-- also allow groups to taxi if planes are landing, *e.g.* if there are two separate runways. +-- +-- NOTE that human players are *not* restricted as they should behave better (hopefully) than the AI. +-- +-- @param #FLIGHTCONTROL self +-- @param #number Ntaxi Max number of groups allowed to taxi. Default 2. +-- @param #boolean IncludeInbound If `true`, the above +-- @param #number Nlanding Max number of landing flights. Default 0. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetLimitTaxi(Ntaxi, IncludeInbound, Nlanding) + + self.NtaxiTot=Ntaxi or 2 + + self.NtaxiInbound=IncludeInbound + + self.NtaxiLanding=Nlanding or 0 + + return self +end + +--- Add a holding pattern. +-- This is a zone where the aircraft... +-- @param #FLIGHTCONTROL self +-- @param Core.Zone#ZONE ArrivalZone Zone where planes arrive. +-- @param #number Heading Heading in degrees. +-- @param #number Length Length in nautical miles. Default 15 NM. +-- @param #number FlightlevelMin Min flight level. Default 5. +-- @param #number FlightlevelMax Max flight level. Default 15. +-- @param #number Prio Priority. Lower is higher. Default 50. +-- @return #FLIGHTCONTROL.HoldingPattern Holding pattern table. +function FLIGHTCONTROL:AddHoldingPattern(ArrivalZone, Heading, Length, FlightlevelMin, FlightlevelMax, Prio) + + -- Get ZONE if passed as string. + if type(ArrivalZone)=="string" then + ArrivalZone=ZONE:New(ArrivalZone) + end + + -- Increase counter. + self.hpcounter=self.hpcounter+1 + + local hp={} --#FLIGHTCONTROL.HoldingPattern + hp.uid=self.hpcounter + hp.arrivalzone=ArrivalZone + hp.name=string.format("%s-%d", ArrivalZone:GetName(), hp.uid) + hp.pos0=ArrivalZone:GetCoordinate() + hp.pos1=hp.pos0:Translate(UTILS.NMToMeters(Length or 15), Heading) + hp.angelsmin=FlightlevelMin or 5 + hp.angelsmax=FlightlevelMax or 15 + hp.prio=Prio or 50 + + hp.stacks={} + for i=hp.angelsmin, hp.angelsmax do + local stack={} --#FLIGHTCONTROL.HoldingStack + stack.angels=i + stack.flightgroup=nil + stack.pos0=UTILS.DeepCopy(hp.pos0) + stack.pos0:SetAltitude(UTILS.FeetToMeters(i*1000)) + stack.pos1=UTILS.DeepCopy(hp.pos1) + stack.pos1:SetAltitude(UTILS.FeetToMeters(i*1000)) + stack.heading=Heading + table.insert(hp.stacks, stack) + end + + -- Add to table. + table.insert(self.holdingpatterns, hp) + + -- Sort holding patterns wrt to prio. + local function _sort(a,b) + return a.prio0 then + -- Debug info. + local text=string.format("Still got %d messages in the radio queue. Will call status again in %.1f sec", #self.msrsqueue, Tqueue) + self:T(self.lid..text) + + -- Call status again in dt seconds. + self:__StatusUpdate(-Tqueue) + + -- Deny transition. + return false + end + + return true +end + +--- Update status. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:onafterStatusUpdate() + + -- Debug message. + self:T2(self.lid.."Status update") + + -- Check markers of holding patterns. + self:_CheckMarkHoldingPatterns() + + -- Check if runway was repaired. + if self:IsRunwayOperational()==false then + local Trepair=self:GetRunwayRepairtime() + if Trepair==0 then + self:RunwayRepaired() + else + self:I(self.lid..string.format("Runway still destroyed! Will be repaired in %d sec", Trepair)) + end + end + + -- Check status of all registered flights. + self:_CheckFlights() + + -- Check parking spots. + --self:_CheckParking() + + -- Check waiting and landing queue. + self:_CheckQueues() + + -- Get runway. + local rwyLanding=self:GetActiveRunwayText() + local rwyTakeoff=self:GetActiveRunwayText(true) + + -- Count flights. + local Nflights= self:CountFlights() + local NQparking=self:CountFlights(FLIGHTCONTROL.FlightStatus.PARKING) + local NQreadytx=self:CountFlights(FLIGHTCONTROL.FlightStatus.READYTX) + local NQtaxiout=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIOUT) + local NQreadyto=self:CountFlights(FLIGHTCONTROL.FlightStatus.READYTO) + local NQtakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF) + local NQinbound=self:CountFlights(FLIGHTCONTROL.FlightStatus.INBOUND) + local NQholding=self:CountFlights(FLIGHTCONTROL.FlightStatus.HOLDING) + local NQlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING) + local NQtaxiinb=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIINB) + local NQarrived=self:CountFlights(FLIGHTCONTROL.FlightStatus.ARRIVED) + -- ========================================================================================================= + local Nqueues = (NQparking+NQreadytx+NQtaxiout+NQreadyto+NQtakeoff) + (NQinbound+NQholding+NQlanding+NQtaxiinb+NQarrived) + + -- Count free parking spots. + --TODO: get and substract number of reserved parking spots. + local nfree=self.Nparkingspots-NQarrived-NQparking + + local Nfree=self:CountParking(AIRBASE.SpotStatus.FREE) + local Noccu=self:CountParking(AIRBASE.SpotStatus.OCCUPIED) + local Nresv=self:CountParking(AIRBASE.SpotStatus.RESERVED) + + if Nfree+Noccu+Nresv~=self.Nparkingspots then + self:E(self.lid..string.format("WARNING: Number of parking spots does not match! Nfree=%d, Noccu=%d, Nreserved=%d != %d total", Nfree, Noccu, Nresv, self.Nparkingspots)) + end + + -- Info text. + if self.verbose>=1 then + local text=string.format("State %s - Runway Landing=%s, Takeoff=%s - Parking F=%d/O=%d/R=%d of %d - Flights=%s: Qpark=%d Qtxout=%d Qready=%d Qto=%d | Qinbound=%d Qhold=%d Qland=%d Qtxinb=%d Qarr=%d", + self:GetState(), rwyLanding, rwyTakeoff, Nfree, Noccu, Nresv, self.Nparkingspots, Nflights, NQparking, NQtaxiout, NQreadyto, NQtakeoff, NQinbound, NQholding, NQlanding, NQtaxiinb, NQarrived) + self:I(self.lid..text) + end + + if Nflights==Nqueues then + --Check! + else + self:E(string.format("WARNING: Number of total flights %d!=%d number of flights in all queues!", Nflights, Nqueues)) + end + + if self.verbose>=2 then + local text="Holding Patterns:" + for i,_pattern in pairs(self.holdingpatterns) do + local pattern=_pattern --#FLIGHTCONTROL.HoldingPattern + + -- Pattern info. + text=text..string.format("\n[%d] Pattern %s [Prio=%d, UID=%d]: Stacks=%d, Angels %d - %d", i, pattern.name, pattern.prio, pattern.uid, #pattern.stacks, pattern.angelsmin, pattern.angelsmax) + + if self.verbose>=4 then + -- Explicit stack info. + for _,_stack in pairs(pattern.stacks) do + local stack=_stack --#FLIGHTCONTROL.HoldingStack + local text=string.format("", stack.angels, stack) + end + end + end + self:I(self.lid..text) + end + + -- Next status update in ~30 seconds. + self:__StatusUpdate(-30) +end + +--- Stop FLIGHTCONTROL FSM. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:onafterStop() + + -- Unhandle events. + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.EngineStartup) + self:UnHandleEvent(EVENTS.Takeoff) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.EngineShutdown) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.Kill) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event handler for event birth. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData +function FLIGHTCONTROL:OnEventBirth(EventData) + self:F3({EvendData=EventData}) + + if EventData and EventData.IniGroupName and EventData.IniUnit then + + self:T3(self.lid..string.format("BIRTH: unit = %s", tostring(EventData.IniUnitName))) + self:T3(self.lid..string.format("BIRTH: group = %s", tostring(EventData.IniGroupName))) + + -- Unit that was born. + local unit=EventData.IniUnit + + -- We delay this, to have all elements of the group in the game. + if unit:IsAir() then + + local bornhere=EventData.Place and EventData.Place:GetName()==self.airbasename or false + --env.info("FF born here ".. tostring(bornhere)) + + -- We got a player? + local playerunit, playername=self:_GetPlayerUnitAndName(EventData.IniUnitName) + + if playername or bornhere then + + -- Create player menu. + self:ScheduleOnce(0.5, self._CreateFlightGroup, self, EventData.IniGroup) + + end + + -- Spawn parking guard. + if bornhere then + self:SpawnParkingGuard(unit) + end + + end + + end + +end + +--- Event handling function. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData Event data. +function FLIGHTCONTROL:OnEventCrashOrDead(EventData) + + if EventData then + + -- Check if out runway was destroyed. + if EventData.IniUnitName then + if self.airbase and self.airbasename and self.airbasename==EventData.IniUnitName then + self:RunwayDestroyed() + end + end + + end + +end + +--- Event handler for event land. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData +function FLIGHTCONTROL:OnEventLand(EventData) + self:F3({EvendData=EventData}) + + self:T2(self.lid..string.format("LAND: unit = %s", tostring(EventData.IniUnitName))) + self:T3(self.lid..string.format("LAND: group = %s", tostring(EventData.IniGroupName))) + +end + +--- Event handler for event takeoff. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData +function FLIGHTCONTROL:OnEventTakeoff(EventData) + self:F3({EvendData=EventData}) + + self:T2(self.lid..string.format("TAKEOFF: unit = %s", tostring(EventData.IniUnitName))) + self:T3(self.lid..string.format("TAKEOFF: group = %s", tostring(EventData.IniGroupName))) + + -- This would be the closest airbase. + local airbase=EventData.Place + + -- Unit that took off. + local unit=EventData.IniUnit + + -- Nil check for airbase. Crashed as player gave me no airbase. + if not (airbase or unit) then + self:E(self.lid.."WARNING: Airbase or IniUnit is nil in takeoff event!") + return + end + +end + +--- Event handler for event engine startup. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData +function FLIGHTCONTROL:OnEventEngineStartup(EventData) + self:F3({EvendData=EventData}) + + self:T2(self.lid..string.format("ENGINESTARTUP: unit = %s", tostring(EventData.IniUnitName))) + self:T3(self.lid..string.format("ENGINESTARTUP: group = %s", tostring(EventData.IniGroupName))) + +end + +--- Event handler for event engine shutdown. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData +function FLIGHTCONTROL:OnEventEngineShutdown(EventData) + self:F3({EvendData=EventData}) + + self:T2(self.lid..string.format("ENGINESHUTDOWN: unit = %s", tostring(EventData.IniUnitName))) + self:T3(self.lid..string.format("ENGINESHUTDOWN: group = %s", tostring(EventData.IniGroupName))) + +end + +--- Event handler for event kill. +-- @param #FLIGHTCONTROL self +-- @param Core.Event#EVENTDATA EventData +function FLIGHTCONTROL:OnEventKill(EventData) + self:F3({EvendData=EventData}) + + -- Debug info. + self:T2(self.lid..string.format("KILL: ini unit = %s", tostring(EventData.IniUnitName))) + self:T3(self.lid..string.format("KILL: ini group = %s", tostring(EventData.IniGroupName))) + self:T2(self.lid..string.format("KILL: tgt unit = %s", tostring(EventData.TgtUnitName))) + self:T3(self.lid..string.format("KILL: tgt group = %s", tostring(EventData.TgtGroupName))) + + -- Parking guard name prefix. + local guardPrefix=string.format("Parking Guard %s", self.airbasename) + + local victimName=EventData.IniUnitName + local killerName=EventData.TgtUnitName + + if victimName and victimName:find(guardPrefix) then + + env.info(string.format("Parking guard %s killed!", victimName)) + + for _,_flight in pairs(self.flights) do + local flight=_flight --Ops.FlightGroup#FLIGHTGROUP + local element=flight:GetElementByName(killerName) + if element then + env.info(string.format("Parking guard %s killed by %s!", victimName, killerName)) + return + end + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "RunwayDestroyed" event. +-- @param #FLIGHTCONTROL self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTCONTROL:onafterRunwayDestroyed(From, Event, To) + + -- Debug Message. + self:T(self.lid..string.format("Runway destoyed!")) + + -- Set time stamp. + self.runwaydestroyed=timer.getAbsTime() + + self:TransmissionTower("All flights, our runway was destroyed. All operations are suspended for one hour.",Flight,Delay) + +end + +--- On after "RunwayRepaired" event. +-- @param #FLIGHTCONTROL self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTCONTROL:onafterRunwayRepaired(From, Event, To) + + -- Debug Message. + self:T(self.lid..string.format("Runway repaired!")) + + -- Set parameter. + self.runwaydestroyed=nil + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Queue Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check takeoff and landing queues. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:_CheckQueues() + + -- Print queue. + if self.verbose>=2 then + self:_PrintQueue(self.flights, "All flights") + end + + -- Get next flight in line: either holding or parking. + local flight, isholding, parking=self:_GetNextFlight() + + + -- Check if somebody wants something. + if flight then + + if isholding then + + -------------------- + -- Holding flight -- + -------------------- + + -- No other flight is taking off and number of landing flights is below threshold. + if self:_CheckFlightLanding(flight) then + + -- Get interval to last flight that got landing clearance. + local dTlanding=99999 + if self.Tlanding then + dTlanding=timer.getAbsTime()-self.Tlanding + end + + if parking and dTlanding>=self.dTlanding then + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- Runway. + local runway=self:GetActiveRunwayText() + + -- Message. + local text=string.format("%s, %s, you are cleared to land, runway %s", callsign, self.alias, runway) + + -- Transmit message. + self:TransmissionTower(text, flight) + + -- Give AI the landing signal. + if flight.isAI then + + -- Message. + local text=string.format("Runway %s, cleared to land, %s", runway, callsign) + + -- Transmit message. + self:TransmissionPilot(text, flight, 10) + + -- Land AI. + self:_LandAI(flight, parking) + else + + -- We set this flight to landing. With this he is allowed to leave the pattern. + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.LANDING) + + end + + -- Set time last flight got landing clearance. + self.Tlanding=timer.getAbsTime() + + end + else + self:T3(self.lid..string.format("FYI: Landing clearance for flight %s denied", flight.groupname)) + end + + else + + -------------------- + -- Takeoff flight -- + -------------------- + + -- No other flight is taking off or landing. + if self:_CheckFlightTakeoff(flight) then + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- Runway. + local runway=self:GetActiveRunwayText(true) + + -- Message. + local text=string.format("%s, %s, taxi to runway %s, hold short", callsign, self.alias, runway) + + if self:GetFlightStatus(flight)==FLIGHTCONTROL.FlightStatus.READYTO then + text=string.format("%s, %s, cleared for take-off, runway %s", callsign, self.alias, runway) + end + + -- Transmit message. + self:TransmissionTower(text, flight) + + -- Check if flight is AI. Humans have to request taxi via F10 menu. + if flight.isAI then + + --- + -- AI + --- + + -- Message. + local text="Wilco, " + + -- Start uncontrolled aircraft. + if flight:IsUncontrolled() then + + -- Message. + text=text..string.format("starting engines, ") + + -- Start uncontrolled aircraft. + flight:StartUncontrolled() + end + + -- Message. + text=text..string.format("runway %s, %s", runway, callsign) + + -- Transmit message. + self:TransmissionPilot(text, flight, 10) + + -- Remove parking guards. + for _,_element in pairs(flight.elements) do + local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element + if element and element.parking then + local spot=self:GetParkingSpotByID(element.parking.TerminalID) + self:RemoveParkingGuard(spot) + end + end + + -- Set flight to takeoff. No way we can stop the AI now. + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAKEOFF) + + else + + --- + -- PLAYER + --- + + if self:GetFlightStatus(flight)==FLIGHTCONTROL.FlightStatus.READYTO then + + -- Player is ready for takeoff + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAKEOFF) + + else + + -- Remove parking guards. + for _,_element in pairs(flight.elements) do + local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element + if element.parking then + local spot=self:GetParkingSpotByID(element.parking.TerminalID) + if element.ai then + self:RemoveParkingGuard(spot, 15) + else + self:RemoveParkingGuard(spot, 10) + end + end + end + + end + + end + + else + -- Debug message. + self:T3(self.lid..string.format("FYI: Take off for flight %s denied", flight.groupname)) + end + end + else + -- Debug message. + self:T2(self.lid..string.format("FYI: No flight in queue for takeoff or landing")) + end + +end + +--- Check if a flight can get clearance for taxi/takeoff. +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight.. +-- @return #boolean If true, flight can. +function FLIGHTCONTROL:_CheckFlightTakeoff(flight) + + -- Number of groups landing. + local nlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING) + + -- Number of groups taking off. + local ntakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF, nil, true) + + -- Current status. + local status=self:GetFlightStatus(flight) + + if flight.isAI then + --- + -- AI + --- + + if nlanding>self.NtaxiLanding then + self:T(self.lid..string.format("AI flight %s [status=%s] NOT cleared for taxi/takeoff as %d>%d flight(s) landing", flight.groupname, status, nlanding, self.NtaxiLanding)) + return false + end + + local ninbound=0 + if self.NtaxiInbound then + ninbound=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIINB, nil, true) + end + + if ntakeoff+ninbound>=self.NtaxiTot then + self:T(self.lid..string.format("AI flight %s [status=%s] NOT cleared for taxi/takeoff as %d>=%d flight(s) taxi/takeoff", flight.groupname, status, ntakeoff, self.NtaxiTot)) + return false + end + + self:T(self.lid..string.format("AI flight %s [status=%s] cleared for taxi/takeoff! nLanding=%d, nTakeoff=%d", flight.groupname, status, nlanding, ntakeoff)) + return true + else + --- + -- Player + -- + -- We allow unlimited number of players to taxi to runway. + -- We do not allow takeoff if at least one flight is landing. + --- + + if status==FLIGHTCONTROL.FlightStatus.READYTO then + + if nlanding>self.NtaxiLanding then + -- Traffic landing. No permission to + self:T(self.lid..string.format("Player flight %s [status=%s] not cleared for taxi/takeoff as %d>%d flight(s) landing", flight.groupname, status, nlanding, self.NtaxiLanding)) + return false + end + + end + + self:T(self.lid..string.format("Player flight %s [status=%s] cleared for taxi/takeoff", flight.groupname, status)) + return true + end + + +end + +--- Check if a flight can get clearance for taxi/takeoff. +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight.. +-- @return #boolean If true, flight can. +function FLIGHTCONTROL:_CheckFlightLanding(flight) + + -- Number of groups landing. + local nlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING) + + -- Number of groups taking off. + local ntakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF, nil, true) + + -- Current status. + local status=self:GetFlightStatus(flight) + + if flight.isAi then + --- + -- AI + --- + + if ntakeoff<=self.NlandingTakeoff and nlanding land + return flightholding, true, parking + else + -- Not enough parking ==> take off + return flightparking, false, nil + end + end + + local text=string.format("Flight holding for %d sec, flight parking for %d sec", flightholding:GetHoldingTime(), flightparking:GetParkingTime()) + self:T(self.lid..text) + + -- Return the flight which is waiting longer. NOTE that Tholding and Tparking are abs. mission time. So a smaller value means waiting longer. + if flightholding.Tholding and flightparking.Tparking and flightholding.TholdingTholdingMin then + return fg + end + end + + -- Sort flights by low fuel. + local function _sortByFuel(a, b) + local flightA=a --Ops.FlightGroup#FLIGHTGROUP + local flightB=b --Ops.FlightGroup#FLIGHTGROUP + local fuelA=flightA.group:GetFuelMin() + local fuelB=flightB.group:GetFuelMin() + return fuelATholdingMin then + return fg + end + + return nil +end + + +--- Get next flight waiting for taxi and takeoff clearance. +-- @param #FLIGHTCONTROL self +-- @return Ops.FlightGroup#FLIGHTGROUP Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready. +function FLIGHTCONTROL:_GetNextFightParking() + + -- Return only AI or human player flights. + local OnlyAI=nil + if self:IsRunwayDestroyed() then + OnlyAI=false -- If false, we return only player flights. + end + + -- Get flights ready for take off. + local QreadyTO=self:GetFlights(FLIGHTCONTROL.FlightStatus.READYTO, OPSGROUP.GroupStatus.TAXIING, OnlyAI) + + -- First check human players. + if #QreadyTO>0 then + -- First come, first serve. + return QreadyTO[1] + end + + -- Get flights ready to taxi. + local QreadyTX=self:GetFlights(FLIGHTCONTROL.FlightStatus.READYTX, OPSGROUP.GroupStatus.PARKING, OnlyAI) + + -- First check human players. + if #QreadyTX>0 then + -- First come, first serve. + return QreadyTX[1] + end + + -- Check if runway is destroyed. + if self:IsRunwayDestroyed() then + -- Runway destroyed. As we only look for AI later on, we return nil here. + return nil + end + + -- Get AI flights parking. + local Qparking=self:GetFlights(FLIGHTCONTROL.FlightStatus.PARKING, nil, true) + + -- Number of flights parking. + local Nparking=#Qparking + + -- Check special cases where only up to one flight is waiting for takeoff. + if Nparking==0 then + return nil + end + + -- Sort flights parking time. + local function _sortByTparking(a, b) + local flightA=a --Ops.FlightGroup#FLIGHTGROUP + local flightB=b --Ops.FlightGroup#FLIGHTGROUP + return flightA.Tparking=2 then + local text="Parking flights:" + for i,_flight in pairs(Qparking) do + local flight=_flight --Ops.FlightGroup#FLIGHTGROUP + text=text..string.format("\n[%d] %s [%s], state=%s [%s]: Tparking=%.1f sec", i, flight.groupname, tostring(flight.actype), flight:GetState(), self:GetFlightStatus(flight), flight:GetParkingTime()) + end + self:I(self.lid..text) + end + + -- Get the first AI flight. + for i,_flight in pairs(Qparking) do + local flight=_flight --Ops.FlightGroup#FLIGHTGROUP + if flight.isAI and flight.isReadyTO then + return flight + end + end + + return nil +end + +--- Print queue. +-- @param #FLIGHTCONTROL self +-- @param #table queue Queue to print. +-- @param #string name Queue name. +-- @return #string Queue text. +function FLIGHTCONTROL:_PrintQueue(queue, name) + + local text=string.format("%s Queue N=%d:", name, #queue) + if #queue==0 then + -- Queue is empty. + text=text.." empty." + else + + local time=timer.getAbsTime() + + -- Loop over all flights in queue. + for i,_flight in ipairs(queue) do + local flight=_flight --Ops.FlightGroup#FLIGHTGROUP + + -- Gather info. + local fuel=flight.group:GetFuelMin()*100 + local ai=tostring(flight.isAI) + local actype=tostring(flight.actype) + + -- Holding and parking time. + local holding=flight.Tholding and UTILS.SecondsToClock(time-flight.Tholding, true) or "X" + local parking=flight.Tparking and UTILS.SecondsToClock(time-flight.Tparking, true) or "X" + + local holding=flight:GetHoldingTime() + if holding>=0 then + holding=UTILS.SecondsToClock(holding, true) + else + holding="X" + end + local parking=flight:GetParkingTime() + if parking>=0 then + parking=UTILS.SecondsToClock(parking, true) + else + parking="X" + end + + -- Number of elements. + local nunits=flight:CountElements() + + -- Status. + local state=flight:GetState() + local status=self:GetFlightStatus(flight) + + -- Main info. + text=text..string.format("\n[%d] %s (%s*%d): status=%s | %s, ai=%s, fuel=%d, holding=%s, parking=%s", + i, flight.groupname, actype, nunits, state, status, ai, fuel, holding, parking) + + -- Elements info. + for j,_element in pairs(flight.elements) do + local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element + local life=element.unit:GetLife() + local life0=element.unit:GetLife0() + local park=element.parking and tostring(element.parking.TerminalID) or "N/A" + text=text..string.format("\n (%d) %s (%s): status=%s, ai=%s, airborne=%s life=%d/%d spot=%s", + j, tostring(element.modex), element.name, tostring(element.status), tostring(element.ai), tostring(element.unit:InAir()), life, life0, park) + end + end + end + + -- Display text. + self:I(self.lid..text) + + return text +end + +--- Set flight status. +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. +-- @param #string status New status. +function FLIGHTCONTROL:SetFlightStatus(flight, status) + + -- Debug message. + self:T(self.lid..string.format("New status %s-->%s for flight %s", flight.controlstatus or "unknown", status, flight:GetName())) + + -- Update menu when flight status changed. + if flight.controlstatus~=status and not flight.isAI then + self:T(self.lid.."Updating menu in 0.2 sec after flight status change") + flight:_UpdateMenu(0.2) + end + + -- Set new status + flight.controlstatus=status + +end + +--- Get flight status. +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. +-- @return #string Flight status +function FLIGHTCONTROL:GetFlightStatus(flight) + + if flight then + return flight.controlstatus or "unkonwn" + end + + return "unknown" +end + +--- Check if FC has control over this flight. +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. +-- @return #boolean +function FLIGHTCONTROL:IsControlling(flight) + + -- Check that we are controlling this flight. + local is=flight.flightcontrol and flight.flightcontrol.airbasename==self.airbasename or false + + return is +end + +--- Check if a group is in a queue. +-- @param #FLIGHTCONTROL 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 FLIGHTCONTROL:_InQueue(queue, group) + local name=group:GetName() + + for _,_flight in pairs(queue) do + local flight=_flight --Ops.FlightGroup#FLIGHTGROUP + if name==flight.groupname then + return true + end + end + + return false +end + +--- Get flights. +-- @param #FLIGHTCONTROL self +-- @param #string Status Return only flights in this flightcontrol status, e.g. `FLIGHTCONTROL.Status.XXX`. +-- @param #string GroupStatus Return only flights in this FSM status, e.g. `OPSGROUP.GroupStatus.TAXIING`. +-- @param #boolean AI If `true` only AI flights are returned. If `false`, only flights with clients are returned. If `nil` (default), all flights are returned. +-- @return #table Table of flights. +function FLIGHTCONTROL:GetFlights(Status, GroupStatus, AI) + + if Status~=nil or GroupStatus~=nil or AI~=nil then + + local flights={} + + for _,_flight in pairs(self.flights) do + local flight=_flight --Ops.FlightGroup#FLIGHTGROUP + + local status=self:GetFlightStatus(flight, Status) + + if status==Status then + if AI==nil or AI==flight.isAI then + if GroupStatus==nil or GroupStatus==flight:GetState() then + table.insert(flights, flight) + end + end + end + + end + + return flights + else + return self.flights + end + +end + +--- Count flights in a given status. +-- @param #FLIGHTCONTROL self +-- @param #string Status Return only flights in this status. +-- @param #string GroupStatus Count only flights in this FSM status, e.g. `OPSGROUP.GroupStatus.TAXIING`. +-- @param #boolean AI If `true` only AI flights are counted. If `false`, only flights with clients are counted. If `nil` (default), all flights are counted. +-- @return #number Number of flights. +function FLIGHTCONTROL:CountFlights(Status, GroupStatus, AI) + + if Status~=nil or GroupStatus~=nil or AI~=nil then + + local flights=self:GetFlights(Status, GroupStatus, AI) + + return #flights + + else + return #self.flights + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Runway Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get the active runway based on current wind direction. +-- @param #FLIGHTCONTROL self +-- @return Wrapper.Airbase#AIRBASE.Runway Active runway. +function FLIGHTCONTROL:GetActiveRunway() + local rwy=self.airbase:GetActiveRunway() + return rwy +end + +--- Get the active runway for landing. +-- @param #FLIGHTCONTROL self +-- @return Wrapper.Airbase#AIRBASE.Runway Active runway. +function FLIGHTCONTROL:GetActiveRunwayLanding() + local rwy=self.airbase:GetActiveRunwayLanding() + return rwy +end + +--- Get the active runway for takeoff. +-- @param #FLIGHTCONTROL self +-- @return Wrapper.Airbase#AIRBASE.Runway Active runway. +function FLIGHTCONTROL:GetActiveRunwayTakeoff() + local rwy=self.airbase:GetActiveRunwayTakeoff() + return rwy +end + + +--- Get the name of the active runway. +-- @param #FLIGHTCONTROL self +-- @param #boolean Takeoff If true, return takeoff runway name. Default is landing. +-- @return #string Runway text, e.g. "31L" or "09". +function FLIGHTCONTROL:GetActiveRunwayText(Takeoff) + + local runway + if Takeoff then + runway=self:GetActiveRunwayTakeoff() + else + runway=self:GetActiveRunwayLanding() + end + + local name=self.airbase:GetRunwayName(runway, true) + + return name or "XX" +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Parking Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Init parking spots. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:_InitParkingSpots() + + -- Parking spots of airbase. + local parkingdata=self.airbase:GetParkingSpotsTable() + + -- Init parking spots table. + self.parking={} + + self.Nparkingspots=0 + for _,_spot in pairs(parkingdata) do + local spot=_spot --Wrapper.Airbase#AIRBASE.ParkingSpot + + -- Mark position. + local text=string.format("Parking ID=%d, Terminal=%d: Free=%s, Client=%s, Dist=%.1f", spot.TerminalID, spot.TerminalType, tostring(spot.Free), tostring(spot.ClientName), spot.DistToRwy) + self:T3(self.lid..text) + + -- Add to table. + self.parking[spot.TerminalID]=spot + + -- Marker. + --spot.Marker=MARKER:New(spot.Coordinate, "Spot"):ReadOnly():ToCoalition(self:GetCoalition()) + + -- Check if spot is initially free or occupied. + if spot.Free then + + -- Parking spot is free. + self:SetParkingFree(spot) + + else + + -- Scan for the unit sitting here. + local unit=spot.Coordinate:FindClosestUnit(20) + + + if unit then + + local unitname=unit and unit:GetName() or "unknown" + + local isalive=unit:IsAlive() + + self:T2(self.lid..string.format("FF parking spot %d is occupied by unit %s alive=%s", spot.TerminalID, unitname, tostring(isalive))) + + if isalive then + + -- Set parking occupied. + self:SetParkingOccupied(spot, unitname) + + -- Spawn parking guard. + self:SpawnParkingGuard(unit) + + else + + -- TODO + --env.info(string.format("FF parking spot %d is occupied by NOT ALIVE unit %s", spot.TerminalID, unitname)) + + -- Parking spot is free. + self:SetParkingFree(spot) + + end + + else + self:E(self.lid..string.format("ERROR: Parking spot is NOT FREE but no unit could be found there!")) + end + end + + -- Increase counter + self.Nparkingspots=self.Nparkingspots+1 + end + +end + +--- Get parking spot by its Terminal ID. +-- @param #FLIGHTCONTROL self +-- @param #number TerminalID +-- @return #FLIGHTCONTROL.ParkingSpot Parking spot data table. +function FLIGHTCONTROL:GetParkingSpotByID(TerminalID) + return self.parking[TerminalID] +end + +--- Set parking spot to FREE and update F10 marker. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table. +-- @param #string status New status. +-- @param #string unitname Name of the unit. +function FLIGHTCONTROL:_UpdateSpotStatus(spot, status, unitname) + + -- Debug message. + self:T2(self.lid..string.format("Updating parking spot %d status: %s --> %s (unit=%s)", spot.TerminalID, tostring(spot.Status), status, tostring(unitname))) + + -- Set new status. + spot.Status=status + +end + +--- Set parking spot to FREE and update F10 marker. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table. +function FLIGHTCONTROL:SetParkingFree(spot) + + -- Get spot. + local spot=self:GetParkingSpotByID(spot.TerminalID) + + -- Update spot status. + self:_UpdateSpotStatus(spot, AIRBASE.SpotStatus.FREE, spot.OccupiedBy or spot.ReservedBy) + + -- Not occupied or reserved. + spot.OccupiedBy=nil + spot.ReservedBy=nil + + -- Remove parking guard. + self:RemoveParkingGuard(spot) + + -- Update marker. + self:UpdateParkingMarker(spot) + +end + +--- Set parking spot to RESERVED and update F10 marker. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table. +-- @param #string unitname Name of the unit occupying the spot. Default "unknown". +function FLIGHTCONTROL:SetParkingReserved(spot, unitname) + + -- Get spot. + local spot=self:GetParkingSpotByID(spot.TerminalID) + + -- Update spot status. + self:_UpdateSpotStatus(spot, AIRBASE.SpotStatus.RESERVED, unitname) + + -- Reserved. + spot.ReservedBy=unitname or "unknown" + + -- Update marker. + self:UpdateParkingMarker(spot) + +end + +--- Set parking spot to OCCUPIED and update F10 marker. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table. +-- @param #string unitname Name of the unit occupying the spot. Default "unknown". +function FLIGHTCONTROL:SetParkingOccupied(spot, unitname) + + -- Get spot. + local spot=self:GetParkingSpotByID(spot.TerminalID) + + -- Update spot status. + self:_UpdateSpotStatus(spot, AIRBASE.SpotStatus.OCCUPIED, unitname) + + -- Occupied. + spot.OccupiedBy=unitname or "unknown" + + -- Update marker. + self:UpdateParkingMarker(spot) + +end + +--- Update parking markers. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table. +function FLIGHTCONTROL:UpdateParkingMarker(spot) + + if self.markerParking then + + -- Get spot. + local spot=self:GetParkingSpotByID(spot.TerminalID) + + -- Only mark OCCUPIED and RESERVED spots. + if spot.Status==AIRBASE.SpotStatus.FREE then + + if spot.Marker then + spot.Marker:Remove() + end + + else + + local text=string.format("Spot %d (type %d): %s", spot.TerminalID, spot.TerminalType, spot.Status:upper()) + if spot.OccupiedBy then + text=text..string.format("\nOccupied by %s", tostring(spot.OccupiedBy)) + end + if spot.ReservedBy then + text=text..string.format("\nReserved for %s", tostring(spot.ReservedBy)) + end + if spot.ClientSpot then + text=text..string.format("\nClient %s", tostring(spot.ClientName)) + end + + if spot.Marker then + + if text~=spot.Marker.text or not spot.Marker.shown then + spot.Marker:UpdateText(text) + end + + else + + spot.Marker=MARKER:New(spot.Coordinate, text):ToAll() + + end + + end + end + +end + +--- Check if parking spot is free. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot data. +-- @return #boolean If true, parking spot is free. +function FLIGHTCONTROL:IsParkingFree(spot) + return spot.Status==AIRBASE.SpotStatus.FREE +end + +--- Check if a parking spot is reserved by a flight group. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot to check. +-- @return #string Name of element or nil. +function FLIGHTCONTROL:IsParkingOccupied(spot) + + if spot.Status==AIRBASE.SpotStatus.OCCUPIED then + return tostring(spot.OccupiedBy) + else + return false + end +end + +--- Check if a parking spot is reserved by a flight group. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot to check. +-- @return #string Name of element or *nil*. +function FLIGHTCONTROL:IsParkingReserved(spot) + + if spot.Status==AIRBASE.SpotStatus.RESERVED then + return tostring(spot.ReservedBy) + else + return false + end +end + +--- Get free parking spots. +-- @param #FLIGHTCONTROL self +-- @param #number terminal Terminal type or nil. +-- @return #number Number of free spots. Total if terminal=nil or of the requested terminal type. +-- @return #table Table of free parking spots of data type #FLIGHCONTROL.ParkingSpot. +function FLIGHTCONTROL:_GetFreeParkingSpots(terminal) + + local freespots={} + + local n=0 + for _,_parking in pairs(self.parking) do + local parking=_parking --Wrapper.Airbase#AIRBASE.ParkingSpot + + if self:IsParkingFree(parking) then + if terminal==nil or terminal==parking.terminal then + n=n+1 + table.insert(freespots, parking) + end + end + end + + return n,freespots +end + +--- Get closest parking spot. +-- @param #FLIGHTCONTROL self +-- @param Core.Point#COORDINATE Coordinate Reference coordinate. +-- @param #number TerminalType (Optional) Check only this terminal type. +-- @param #boolean Status (Optional) Only consider spots that have this status. +-- @return #FLIGHTCONTROL.ParkingSpot Closest parking spot. +function FLIGHTCONTROL:GetClosestParkingSpot(Coordinate, TerminalType, Status) + + local distmin=math.huge + local spotmin=nil + + for TerminalID, Spot in pairs(self.parking) do + local spot=Spot --Wrapper.Airbase#AIRBASE.ParkingSpot + + --env.info(self.lid..string.format("FF Spot %d: %s", spot.TerminalID, spot.Status)) + + if (Status==nil or Status==spot.Status) and AIRBASE._CheckTerminalType(spot.TerminalType, TerminalType) then + + -- Get distance from coordinate to spot. + local dist=Coordinate:Get2DDistance(spot.Coordinate) + + -- Check if distance is smaller. + if dist0 then + text=text..string.format("\n- Parking %d", NQparking) + end + if NQreadytx>0 then + text=text..string.format("\n- Ready to taxi %d", NQreadytx) + end + if NQtaxiout>0 then + text=text..string.format("\n- Taxi to runway %d", NQtaxiout) + end + if NQreadyto>0 then + text=text..string.format("\n- Ready for takeoff %d", NQreadyto) + end + if NQtakeoff>0 then + text=text..string.format("\n- Taking off %d", NQtakeoff) + end + if NQinbound>0 then + text=text..string.format("\n- Inbound %d", NQinbound) + end + if NQholding>0 then + text=text..string.format("\n- Holding pattern %d", NQholding) + end + if NQlanding>0 then + text=text..string.format("\n- Landing %d", NQlanding) + end + if NQtaxiinb>0 then + text=text..string.format("\n- Taxi to parking %d", NQtaxiinb) + end + if NQarrived>0 then + text=text..string.format("\n- Arrived at parking %d", NQarrived) + end + + -- Message to flight + self:TextMessageToFlight(text, flight, 15, true) + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Menu: Inbound +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Player calls inbound. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerRequestInbound(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + if flight:IsAirborne() then + + -- Call sign. + local callsign=self:_GetCallsignName(flight) + + -- Get player element. + local player=flight:GetPlayerElement() + + -- Pilot calls inbound for landing. + local text=string.format("%s, %s, inbound for landing", self.alias, callsign) + + -- Radio message. + self:TransmissionPilot(text, flight) + + -- Current player coord. + local flightcoord=flight:GetCoordinate(nil, player.name) + + -- Distance from player to airbase. + local dist=flightcoord:Get2DDistance(self:GetCoordinate()) + + if distself.NlandingTakeoff then + + -- Message text. + local text=string.format("%s, negative! We have currently traffic taking off!", callsign) + + -- Send message. + self:TransmissionTower(text, flight, 10) + + else + + -- Runway. + local runway=self:GetActiveRunwayText() + + -- Message text. + local text=string.format("%s, affirmative, runway %s. Confirm approach!", callsign, runway) + + -- Send message. + self:TransmissionTower(text, flight, 10) + + -- Set flight status to landing. + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.LANDING) + + end + + else + + -- Error you are not airborne! + local text=string.format("Negative, you must be INBOUND and CONTROLLED by us!") + + -- Send message. + self:TextMessageToFlight(text, flight, 10) + end + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Menu: Taxi +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Player requests taxi. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerRequestTaxi(groupname) + + -- Get flight. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- Pilot request for taxi. + local text=string.format("%s, %s, request taxi to runway.", self.alias, callsign) + self:TransmissionPilot(text, flight) + + if flight:IsParking() then + + -- Tell pilot to wait until cleared. + local text=string.format("%s, %s, hold position until further notice.", callsign, self.alias) + self:TransmissionTower(text, flight, 10) + + -- Set flight status to "Ready to Taxi". + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.READYTX) + + elseif flight:IsTaxiing() then + + -- Runway for takeoff. + local runway=self:GetActiveRunwayText(true) + + -- Tell pilot to wait until cleared. + local text=string.format("%s, %s, taxi to runway %s, hold short.", callsign, self.alias, runway) + self:TransmissionTower(text, flight, 10) + + -- Taxi out. + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAXIOUT) + + -- Get player element. + local playerElement=flight:GetPlayerElement() + + -- Set parking to free. Could be reserved. + if playerElement and playerElement.parking then + self:SetParkingFree(playerElement.parking) + end + + else + self:TextMessageToFlight(string.format("Negative, you must be PARKING to request TAXI!"), flight) + end + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + end + +end + +--- Player aborts taxi. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerAbortTaxi(groupname) + + -- Get flight. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- Pilot request for taxi. + local text=string.format("%s, %s, cancel my taxi request.", self.alias, callsign) + self:TransmissionPilot(text, flight) + + if flight:IsParking() then + + -- Tell pilot remain parking. + local text=string.format("%s, %s, roger, remain on your parking position.", callsign, self.alias) + self:TransmissionTower(text, flight, 10) + + -- Set flight status to "Parking". + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING) + + -- Get player element. + local playerElement=flight:GetPlayerElement() + + -- Set parking guard. + if playerElement then + self:SpawnParkingGuard(playerElement.unit) + end + + elseif flight:IsTaxiing() then + + -- Tell pilot to return to parking. + local text=string.format("%s, %s, roger, return to your parking position.", callsign, self.alias) + self:TransmissionTower(text, flight, 10) + + -- Set flight status to "Taxi Inbound". + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAXIINB) + + else + self:TextMessageToFlight(string.format("Negative, you must be PARKING or TAXIING to abort TAXI!"), flight) + end + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Menu: Takeoff +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Player requests takeoff. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerRequestTakeoff(groupname) + + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + if flight:IsTaxiing() then + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- Pilot request for taxi. + local text=string.format("%s, %s, ready for departure. Request takeoff.", self.alias, callsign) + self:TransmissionPilot(text, flight) + + -- Get number of flights landing. + local Nlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING) + + -- Get number of flights taking off. + local Ntakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF) + + --[[ + local text="" + if Nlanding==0 and Ntakeoff==0 then + text="No current traffic. You are cleared for takeoff." + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAKEOFF) + elseif Nlanding>0 and Ntakeoff>0 then + text=string.format("Negative, we got %d flights inbound and %d outbound ahead of you. Hold position until futher notice.", Nlanding, Ntakeoff) + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.READYTO) + elseif Nlanding>0 then + if Nlanding==1 then + text=string.format("Negative, we got %d flight inbound before it's your turn. Wait until futher notice.", Nlanding) + else + text=string.format("Negative, we got %d flights inbound. Wait until futher notice.", Nlanding) + end + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.READYTO) + elseif Ntakeoff>0 then + text=string.format("Negative, %d flights ahead of you are waiting for takeoff. Talk to you soon.", Ntakeoff) + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.READYTO) + end + ]] + + -- We only check for landing flights. + local text=string.format("%s, %s, ", callsign, self.alias) + if Nlanding==0 then + + -- No traffic. + text=text.."no current traffic. You are cleared for takeoff." + + -- Set status to "Take off". + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAKEOFF) + elseif Nlanding>0 then + if Nlanding==1 then + text=text..string.format("negative, we got %d flight inbound before it's your turn. Hold position until futher notice.", Nlanding) + else + text=text..string.format("negative, we got %d flights inbound. Hold positon until futher notice.", Nlanding) + end + end + + -- Message from tower. + self:TransmissionTower(text, flight, 10) + + else + self:TextMessageToFlight(string.format("Negative, you must request TAXI before you can request TAKEOFF!"), flight) + end + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + end + +end + +--- Player wants to abort takeoff. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerAbortTakeoff(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + -- Flight status. + local status=self:GetFlightStatus(flight) + + -- Check that we are taking off or ready for takeoff. + if status==FLIGHTCONTROL.FlightStatus.TAKEOFF or status==FLIGHTCONTROL.FlightStatus.READYTO then + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- Pilot request for taxi. + local text=string.format("%s, %s, abort takeoff.", self.alias, callsign) + self:TransmissionPilot(text, flight) + + -- Set new flight status. + if flight:IsParking() then + + text=string.format("%s, %s, affirm, remain on your parking position.", callsign, self.alias) + + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING) + + -- Get player element. + local playerElement=flight:GetPlayerElement() + + -- Set parking guard. + if playerElement then + self:SpawnParkingGuard(playerElement.unit) + end + + elseif flight:IsTaxiing() then + text=string.format("%s, %s, roger, report whether you want to taxi back or takeoff later.", callsign, self.alias) + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAXIOUT) + else + env.info(self.lid.."ERROR") + end + + -- Message from tower. + self:TransmissionTower(text, flight, 10) + + else + self:TextMessageToFlight("Negative, You are NOT in the takeoff queue", flight) + end + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Menu: Parking +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Player reserves a parking spot. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerRequestParking(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- Get player element. + local player=flight:GetPlayerElement() + + -- Set terminal type. + local TerminalType=AIRBASE.TerminalType.FighterAircraft + if flight.isHelo then + TerminalType=AIRBASE.TerminalType.HelicopterUsable + end + -- Current coordinate. + local coord=flight:GetCoordinate(nil, player.name) + + -- Get spawn position if any. + local spot=self:_GetPlayerSpot(player.name) + + -- Get closest FREE parking spot if player was not spawned here or spot is already taken. + if not spot then + spot=self:GetClosestParkingSpot(coord, TerminalType, AIRBASE.SpotStatus.FREE) + end + + if spot then + + -- Message text. + local text=string.format("%s, your assigned parking position is terminal ID %d.", callsign, spot.TerminalID) + + -- Transmit message. + self:TransmissionTower(text, flight) + + -- If player already has a spot. + if player.parking then + self:SetParkingFree(player.parking) + end + + -- Reserve parking for player. + player.parking=spot + self:SetParkingReserved(spot, player.name) + + -- Update menu ==> Cancel Parking. + flight:_UpdateMenu(0.2) + + else + + -- Message text. + local text=string.format("%s, no free parking spot available. Try again later.", callsign) + + -- Transmit message. + self:TransmissionTower(text, flight) + + end + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + end + +end + +--- Player cancels parking spot reservation. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerCancelParking(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- Get player element. + local player=flight:GetPlayerElement() + + -- If player already has a spot. + if player.parking then + self:SetParkingFree(player.parking) + player.parking=nil + self:TextMessageToFlight(string.format("%s, your parking spot reservation at terminal ID %d was cancelled.", callsign, player.parking.TerminalID), flight) + else + self:TextMessageToFlight("You did not have a valid parking spot reservation.", flight) + end + + -- Update menu ==> Reserve Parking. + flight:_UpdateMenu(0.2) + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + end + +end + +--- Player arrived at parking position. +-- @param #FLIGHTCONTROL self +-- @param #string groupname Name of the flight group. +function FLIGHTCONTROL:_PlayerArrived(groupname) + + -- Get flight group. + local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP + + if flight then + + -- Player element. + local player=flight:GetPlayerElement() + + -- Get current coordinate. + local coord=flight:GetCoordinate(nil, player.name) + + -- Parking spot. + local spot=self:_GetPlayerSpot(player.name) --#FLIGHTCONTROL.ParkingSpot + if player.parking then + spot=self:GetParkingSpotByID(player.parking.TerminalID) + else + if not spot then + spot=self:GetClosestParkingSpot(coord) + end + end + + if spot then + + -- Get callsign. + local callsign=self:_GetCallsignName(flight) + + -- Distance to parking spot. + local dist=coord:Get2DDistance(spot.Coordinate) + + if dist<12 then + + -- Message text. + local text=string.format("%s, %s, arrived at parking position. Terminal ID %d.", self.alias, callsign, spot.TerminalID) + + -- Transmit message. + self:TransmissionPilot(text, flight) + -- Message text. + local text="" + if spot.ReservedBy and spot.ReservedBy~=player.name then + + -- Reserved by someone else. + text=string.format("%s, this spot is already reserved for %s. Find yourself a different parking position.", callsign, self.alias, spot.ReservedBy) + + else + + -- Okay, have a drink... + text=string.format("%s, %s, roger. Enjoy a cool bevarage in the officers' club.", callsign, self.alias) + + -- Set player element to parking. + flight:ElementParking(player, spot) + + -- Set flight status to PARKING. + self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING) + + -- Set parking guard. + if player then + self:SpawnParkingGuard(player.unit) + end + + end + + -- Transmit message. + self:TransmissionTower(text, flight, 10) + + else + + -- Message text. + local text=string.format("%s, %s, arrived at parking position.", self.alias, callsign) + + -- Transmit message. + self:TransmissionPilot(text, flight) + + local text="" + if spot.ReservedBy then + if spot.ReservedBy==player.name then + -- To far from reserved spot. + text=string.format("%s, %s, you are still %d meters away from your reserved parking position at terminal ID %d. Continue taxiing!", callsign, self.alias, dist, spot.TerminalID) + else + -- Closest spot is reserved by someone else. + --local spotFree=self:GetClosestParkingSpot(coord, nil, AIRBASE.SpotStatus.Free) + text=string.format("%s, %s, the closest parking spot is already reserved. Continue taxiing to a free spot!", callsign, self.alias) + end + else + -- Too far from closest spot. + text=string.format("%s, %s, you are still %d meters away from the closest parking position. Continue taxiing to a proper spot!", callsign, self.alias, dist) + end + + -- Transmit message. + self:TransmissionTower(text, flight, 10) + + end + + else + -- TODO: No spot + end + + else + self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname))) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Flight and Element Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new flight group. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return Ops.FlightGroup#FLIGHTGROUP Flight group. +function FLIGHTCONTROL:_CreateFlightGroup(group) + + -- Check if not already in flights + if self:_InQueue(self.flights, group) then + self:E(self.lid..string.format("WARNING: Flight group %s does already exist!", group:GetName())) + return + end + + -- Debug info. + self:T(self.lid..string.format("Creating new flight for group %s of aircraft type %s.", group:GetName(), group:GetTypeName())) + + -- Get flightgroup from data base. + local flight=_DATABASE:GetOpsGroup(group:GetName()) + + -- If it does not exist yet, create one. + if not flight then + flight=FLIGHTGROUP:New(group:GetName()) + end + + -- Set flightcontrol. + if flight.homebase and flight.homebase:GetName()==self.airbasename then + flight:SetFlightControl(self) + end + + return flight +end + +--- Remove flight from all queues. +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP Flight The flight to be removed. +function FLIGHTCONTROL:_RemoveFlight(Flight) + + -- Loop over all flights in group. + for i,_flight in pairs(self.flights) do + local flight=_flight --Ops.FlightGroup#FLIGHTGROUP + + -- Check for name. + if flight.groupname==Flight.groupname then + + -- Debug message. + self:T(self.lid..string.format("Removing flight group %s", flight.groupname)) + + -- Remove table entry. + table.remove(self.flights, i) + + -- Remove myself. + Flight.flightcontrol=nil + + -- Set flight status to unknown. + self:SetFlightStatus(Flight, FLIGHTCONTROL.FlightStatus.UNKNOWN) + + return true + end + end + + -- Debug message. + self:E(self.lid..string.format("WARNING: Could NOT remove flight group %s", Flight.groupname)) +end + +--- Get flight from group. +-- @param #FLIGHTCONTROL 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 Ops.FlightGroup#FLIGHTGROUP Flight group or nil. +-- @return #number Queue index or nil. +function FLIGHTCONTROL:_GetFlightFromGroup(group) + + if group then + + -- Group name + local name=group:GetName() + + -- Loop over all flight groups in queue + for i,_flight in pairs(self.flights) do + local flight=_flight --Ops.FlightGroup#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 of flight from its unit name. +-- @param #FLIGHTCONTROL self +-- @param #string unitname Name of the unit. +-- @return Ops.OpsGroup#OPSGROUP.Element Element of the flight or nil. +-- @return #number Element index or nil. +-- @return Ops.FlightGroup#FLIGHTGROUP The Flight group or nil. +function FLIGHTCONTROL:_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:_GetFlightFromGroup(unit:GetGroup()) + + -- 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 --Ops.OpsGroup#OPSGROUP.Element + + 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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Check Sanity Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check status of all registered flights and do some sanity checks. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:_CheckFlights() + + -- First remove all dead flights. + for i=#self.flights,1,-1 do + local flight=self.flights[i] --Ops.FlightGroup#FLIGHTGROUP + if flight:IsDead() then + self:T(self.lid..string.format("Removing DEAD flight %s", tostring(flight.groupname))) + self:_RemoveFlight(flight) + end + end + + -- Count number of players + self.Nplayers=0 + for _,_flight in pairs(self.flights) do + local flight=_flight --Ops.FlightGroup#FLIGHTGROUP + if not flight.isAI then + self.Nplayers=self.Nplayers+1 + end + end + + -- Check speeding. + if self.speedLimitTaxi then + + for _,_flight in pairs(self.flights) do + local flight=_flight --Ops.FlightGroup#FLIGHTGROUP + + if not flight.isAI then + + -- Get player element. + local playerElement=flight:GetPlayerElement() + + -- Current flight status. + local flightstatus=self:GetFlightStatus(flight) + + if playerElement then + + -- Check if speeding while taxiing. + if (flightstatus==FLIGHTCONTROL.FlightStatus.TAXIINB or flightstatus==FLIGHTCONTROL.FlightStatus.TAXIOUT) and self.speedLimitTaxi then + + -- Current speed in m/s. + local speed=playerElement.unit:GetVelocityMPS() + + -- Current position. + local coord=playerElement.unit:GetCoord() + + -- We do not want to check speed on runways. + local onRunway=self:IsCoordinateRunway(coord) + + -- Debug output. + self:T(self.lid..string.format("Player %s speed %.1f knots (max=%.1f) onRunway=%s", playerElement.playerName, UTILS.MpsToKnots(speed), UTILS.MpsToKnots(self.speedLimitTaxi), tostring(onRunway))) + + if speed and speed>self.speedLimitTaxi and not onRunway then + + -- Callsign. + local callsign=self:_GetCallsignName(flight) + + -- Radio text. + local text=string.format("%s, slow down, you are taxiing too fast!", callsign) + + -- Radio message to player. + self:TransmissionTower(text, flight) + + -- Get player data. + local PlayerData=flight:_GetPlayerData() + + -- Trigger FSM speeding event. + self:PlayerSpeeding(PlayerData) + + end + + end + + end + end + end + + end + +end + +--- Check status of all registered flights and do some sanity checks. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:_CheckParking() + + for TerminalID,_spot in pairs(self.parking) do + local spot=_spot --Wrapper.Airbase#AIRBASE.ParkingSpot + + if spot.Reserved then + if spot.MarkerID then + spot.Coordinate:RemoveMark(spot.MarkerID) + end + spot.MarkerID=spot.Coordinate:MarkToCoalition(string.format("Parking reserved for %s", tostring(spot.Reserved)), self:GetCoalition()) + end + + -- First remove all dead flights. + for i=1,#self.flights do + local flight=self.flights[i] --Ops.FlightGroup#FLIGHTGROUP + for _,_element in pairs(flight.elements) do + local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element + if element.parking and element.parking.TerminalID==TerminalID then + if spot.MarkerID then + spot.Coordinate:RemoveMark(spot.MarkerID) + end + spot.MarkerID=spot.Coordinate:MarkToCoalition(string.format("Parking spot occupied by %s", tostring(element.name)), self:GetCoalition()) + end + end + end + + end + + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Routing Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Tell AI to land at the airbase. Flight is added to the landing queue. +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. +-- @param #table parking Free parking spots table. +function FLIGHTCONTROL:_LandAI(flight, parking) + + -- Debug info. + self:T(self.lid..string.format("Landing AI flight %s.", flight.groupname)) + + + -- Respawn? + local respawn=false + + if respawn then + + -- Get group template. + local Template=flight.group:GetTemplate() + + -- TODO: get landing waypoints from flightgroup. + + -- Set route points. + Template.route.points=wp + + for i,unit in pairs(Template.units) do + local spot=parking[i] --Wrapper.Airbase#AIRBASE.ParkingSpot + + local element=flight:GetElementByName(unit.name) + if element then + + -- Set the parking spot at the destination airbase. + unit.parking_landing=spot.TerminalID + + local text=string.format("Reserving parking spot %d for unit %s", spot.TerminalID, tostring(unit.name)) + self:T(self.lid..text) + + -- Set parking to RESERVED. + self:SetParkingReserved(spot, element.name) + + else + env.info("FF error could not get element to assign parking!") + end + end + + -- Debug message. + self:TextMessageToFlight(string.format("Respawning group %s", flight.groupname), flight) + + --Respawn the group. + flight:Respawn(Template) + + else + + -- Give signal to land. + flight:ClearToLand() + + end + +end + +--- Get holding stack. +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. +-- @return #FLIGHTCONTROL.HoldingStack Holding point. +function FLIGHTCONTROL:_GetHoldingStack(flight) + + -- Debug message. + self:T(self.lid..string.format("Getting holding point for flight %s", flight:GetName())) + + for i,_hp in pairs(self.holdingpatterns) do + local holdingpattern=_hp --#FLIGHTCONTROL.HoldingPattern + + self:T(self.lid..string.format("Checking holding point %s", holdingpattern.name)) + + for j,_stack in pairs(holdingpattern.stacks) do + local stack=_stack --#FLIGHTCONTROL.HoldingStack + local name=stack.flightgroup and stack.flightgroup:GetName() or "empty" + self:T(self.lid..string.format("Stack %d: %s", j, name)) + if not stack.flightgroup then + return stack + end + end + + end + + return nil +end + + +--- Count flights in holding pattern. +-- @param #FLIGHTCONTROL self +-- @param #FLIGHTCONTROL.HoldingPattern Pattern The pattern. +-- @return #FLIGHTCONTROL.HoldingStack Holding point. +function FLIGHTCONTROL:_CountFlightsInPattern(Pattern) + + local N=0 + + for _,_stack in pairs(Pattern.stacks) do + local stack=_stack --#FLIGHTCONTROL.HoldingStack + if stack.flightgroup then + N=N+1 + end + end + + return N +end + + +--- AI flight on final. +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:_FlightOnFinal(flight) + + -- Callsign. + local callsign=self:_GetCallsignName(flight) + + -- Message text. + local text=string.format("%s, final", callsign) + + -- Transmit message. + self:TransmissionPilot(text, flight) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Radio Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Radio transmission from tower. +-- @param #FLIGHTCONTROL self +-- @param #string Text The text to transmit. +-- @param Ops.FlightGroup#FLIGHTGROUP Flight The flight. +-- @param #number Delay Delay in seconds before the text is transmitted. Default 0 sec. +function FLIGHTCONTROL:TransmissionTower(Text, Flight, Delay) + + if self.radioOnlyIfPlayers==true and self.Nplayers==0 then + self:T(self.lid.."No players ==> skipping TOWER radio transmission") + return + end + + -- Spoken text. + local text=self:_GetTextForSpeech(Text) + + -- "Subtitle". + local subgroups=nil + if Flight and not Flight.isAI then + local playerData=Flight:_GetPlayerData() + if playerData.subtitles and (not self.nosubs) then + subgroups=subgroups or {} + table.insert(subgroups, Flight.group) + end + end + + -- New transmission. + local transmission=self.msrsqueue:NewTransmission(text, nil, self.msrsTower, nil, 1, subgroups, Text) + + -- Set time stamp. Can be in the future. + self.Tlastmessage=timer.getAbsTime() + (Delay or 0) + + -- Debug message. + self:T(self.lid..string.format("Radio Tower: %s", Text)) + +end + +--- Radio transmission. +-- @param #FLIGHTCONTROL self +-- @param #string Text The text to transmit. +-- @param Ops.FlightGroup#FLIGHTGROUP Flight The flight. +-- @param #number Delay Delay in seconds before the text is transmitted. Default 0 sec. +function FLIGHTCONTROL:TransmissionPilot(Text, Flight, Delay) + + if self.radioOnlyIfPlayers==true and self.Nplayers==0 then + self:T(self.lid.."No players ==> skipping PILOT radio transmission") + return + end + + + -- Get player data. + local playerData=Flight:_GetPlayerData() + + -- Check if player enabled his "voice". + if playerData==nil or playerData.myvoice then + + -- Spoken text. + local text=self:_GetTextForSpeech(Text) + + -- MSRS instance to use. + local msrs=self.msrsPilot -- Sound.SRS#MSRS + + if Flight.useSRS and Flight.msrs then + + -- Pilot radio call using settings of the FLIGHTGROUP. We just overwrite the frequency. + msrs=Flight.msrs + + end + + -- "Subtitle". + local subgroups=nil + if Flight and not Flight.isAI then + local playerData=Flight:_GetPlayerData() + if playerData.subtitles and (not self.nosubs) then + subgroups=subgroups or {} + table.insert(subgroups, Flight.group) + end + end + + -- Add transmission to msrsqueue. + local coordinate = Flight:GetCoordinate(true) + msrs:SetCoordinate() + self.msrsqueue:NewTransmission(text, nil, msrs, nil, 1, subgroups, Text, nil, self.frequency, self.modulation) + + end + + -- Set time stamp. + self.Tlastmessage=timer.getAbsTime() + (Delay or 0) + + -- Debug message. + self:T(self.lid..string.format("Radio Pilot: %s", Text)) + +end + + +--- Text message to group. +-- @param #FLIGHTCONTROL self +-- @param #string Text The text to transmit. +-- @param Ops.FlightGroup#FLIGHTGROUP Flight The flight. +-- @param #number Duration Duration in seconds. Default 5. +-- @param #boolean Clear Clear screen. +-- @param #number Delay Delay in seconds before the text is transmitted. Default 0 sec. +function FLIGHTCONTROL:TextMessageToFlight(Text, Flight, Duration, Clear, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, FLIGHTCONTROL.TextMessageToFlight, self, Text, Flight, Duration, Clear, 0) + else + + if Flight and Flight.group and Flight.group:IsAlive() then + + -- Group ID. + local gid=Flight.group:GetID() + + -- Out text. + trigger.action.outTextForGroup(gid, self:_CleanText(Text), Duration or 5, Clear) + + end + + end + +end + +--- Clean text. Remove control sequences. +-- @param #FLIGHTCONTROL self +-- @param #string Text The text. +-- @param #string Cleaned text. +function FLIGHTCONTROL:_CleanText(Text) + + local text=Text:gsub("\n$",""):gsub("\n$","") + + return text +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- [INTERNAL] Add parking guard in front of a parking aircraft - delayed for MP. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Unit#UNIT unit The aircraft. +function FLIGHTCONTROL:_SpawnParkingGuard(unit) + -- Position of the unit. + local coordinate=unit:GetCoordinate() + + -- Parking spot. + local spot=self:GetClosestParkingSpot(coordinate) + + if not spot.ParkingGuard then + + -- Current heading of the unit. + local heading=unit:GetHeading() + + -- Length of the unit + 3 meters. + local size, x, y, z=unit:GetObjectSize() + + local xdiff = 3 + --Fix for hangars, puts the guy out front and not on top. + if AIRBASE._CheckTerminalType(spot.TerminalType, AIRBASE.TerminalType.Shelter) then + xdiff = 27-(x*0.5) + end + + if (AIRBASE._CheckTerminalType(spot.TerminalType, AIRBASE.TerminalType.OpenMed) or AIRBASE._CheckTerminalType(spot.TerminalType, AIRBASE.TerminalType.Shelter)) and self.airbasename == AIRBASE.Sinai.Ramon_Airbase then + xdiff = 12 + end + + -- Debug message. + self:T2(self.lid..string.format("Parking guard for %s: heading=%d, length x=%.1f m, xdiff=%.1f m", unit:GetName(), heading, x, xdiff)) + + -- Coordinate for the guard. + local Coordinate=coordinate:Translate(x*0.5+xdiff, heading) + + -- Let him face the aircraft. + local lookat=heading-180 + + -- Set heading and AI off to save resources. + self.parkingGuard:InitHeading(lookat) + + -- Turn AI Off. + if self.parkingGuard:IsInstanceOf("SPAWN") then + --self.parkingGuard:InitAIOff() + end + + -- Group that is spawned. + spot.ParkingGuard=self.parkingGuard:SpawnFromCoordinate(Coordinate) + + else + self:E(self.lid.."ERROR: Parking Guard already exists!") + end +end + +--- Add parking guard in front of a parking aircraft. +-- @param #FLIGHTCONTROL self +-- @param Wrapper.Unit#UNIT unit The aircraft. +function FLIGHTCONTROL:SpawnParkingGuard(unit) + + if unit and self.parkingGuard then + + -- Schedule delay so in MP we get the heading of the client's plane + self:ScheduleOnce(1,FLIGHTCONTROL._SpawnParkingGuard,self,unit) + + end + +end + +--- Remove parking guard. +-- @param #FLIGHTCONTROL self +-- @param #FLIGHTCONTROL.ParkingSpot spot +-- @param #number delay Delay in seconds. +function FLIGHTCONTROL:RemoveParkingGuard(spot, delay) + + if delay and delay>0 then + self:ScheduleOnce(delay, FLIGHTCONTROL.RemoveParkingGuard, self, spot) + else + + if spot.ParkingGuard then + spot.ParkingGuard:Destroy() + spot.ParkingGuard=nil + end + + end + +end + +--- Check if a flight is on a runway +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP flight +-- @param Wrapper.Airbase#AIRBASE.Runway Runway or nil. +function FLIGHTCONTROL:_IsFlightOnRunway(flight) + + for _,_runway in pairs(self.airbase.runways) do + local runway=_runway --Wrapper.Airbase#AIRBASE.Runway + + local inzone=flight:IsInZone(runway.zone) + + if inzone then + return runway + end + + end + + return nil +end + +--- [User] Set callsign options for TTS output. See @{Wrapper.Group#GROUP.GetCustomCallSign}() on how to set customized callsigns. +-- @param #FLIGHTCONTROL self +-- @param #boolean ShortCallsign If true, only call out the major flight number. Default = `true`. +-- @param #boolean Keepnumber If true, keep the **customized callsign** in the #GROUP name for players as-is, no amendments or numbers. Default = `true`. +-- @param #table CallsignTranslations (optional) Table to translate between DCS standard callsigns and bespoke ones. Does not apply if using customized +-- callsigns from playername or group name. +-- @return #FLIGHTCONTROL self +function FLIGHTCONTROL:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations) + if not ShortCallsign or ShortCallsign == false then + self.ShortCallsign = false + else + self.ShortCallsign = true + end + self.Keepnumber = Keepnumber or false + self.CallsignTranslations = CallsignTranslations + return self +end + +--- Get callsign name of a given flight. +-- @param #FLIGHTCONTROL self +-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group. +-- @return #string Callsign or "Ghostrider 1-1". +function FLIGHTCONTROL:_GetCallsignName(flight) + + local callsign=flight:GetCallsignName(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations) + + --local name=string.match(callsign, "%a+") + --local number=string.match(callsign, "%d+") + + return callsign +end + + +--- Get text for text-to-speech. +-- Numbers are spaced out, e.g. "Heading 180" becomes "Heading 1 8 0 ". +-- @param #FLIGHTCONTROL self +-- @param #string text Original text. +-- @return #string Spoken text. +function FLIGHTCONTROL:_GetTextForSpeech(text) + + --- Function to space out text. + local function space(text) + + local res="" + + for i=1, #text do + local char=text:sub(i,i) + res=res..char.." " + end + + return res + end + + -- Space out numbers. + local t=text:gsub("(%d+)", space) + + --TODO: 9 to niner. + + return t +end + + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FLIGHTCONTROL 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 FLIGHTCONTROL:_GetPlayerUnitAndName(unitName) + + if unitName 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) + + -- 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 + +--- Check holding pattern markers. Draw if they should exists and remove if they should not. +-- @param #FLIGHTCONTROL self +function FLIGHTCONTROL:_CheckMarkHoldingPatterns() + + for _,pattern in pairs(self.holdingpatterns) do + local Pattern=pattern + + if self.markPatterns then + + self:_MarkHoldingPattern(Pattern) + + else + + self:_UnMarkHoldingPattern(Pattern) + + end + + end + +end + +--- Draw marks of holding pattern (if they do not exist. +-- @param #FLIGHTCONTROL self +-- @param #FLIGHTCONTROL.HoldingPattern Pattern Holding pattern table. +function FLIGHTCONTROL:_MarkHoldingPattern(Pattern) + + if not Pattern.markArrow then + Pattern.markArrow=Pattern.pos0:ArrowToAll(Pattern.pos1, nil, {1,0,0}, 1, {1,1,0}, 0.5, 2, true) + end + + if not Pattern.markArrival then + Pattern.markArrival=Pattern.arrivalzone:DrawZone() + end + +end + +--- Removem markers of holding pattern (if they exist). +-- @param #FLIGHTCONTROL self +-- @param #FLIGHTCONTROL.HoldingPattern Pattern Holding pattern table. +function FLIGHTCONTROL:_UnMarkHoldingPattern(Pattern) + + if Pattern.markArrow then + UTILS.RemoveMark(Pattern.markArrow) + Pattern.markArrow=nil + end + + if Pattern.markArrival then + UTILS.RemoveMark(Pattern.markArrival) + Pattern.markArrival=nil + end + +end + +--- Add a holding pattern. +-- @param #FLIGHTCONTROL self +-- @return #FLIGHTCONTROL.HoldingPattern Holding pattern table. +function FLIGHTCONTROL:_AddHoldingPatternBackup() + + local runway=self:GetActiveRunway() + + local heading=runway.heading + + local vec2=self.airbase:GetVec2() + + local Vec2=UTILS.Vec2Translate(vec2, UTILS.NMToMeters(5), heading+90) + + local ArrivalZone=ZONE_RADIUS:New("Arrival Zone", Vec2, 5000) + + -- Add holding pattern with very low priority. + self.holdingBackup=self:AddHoldingPattern(ArrivalZone, heading, 15, 5, 25, 999) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua new file mode 100644 index 000000000..269dae89e --- /dev/null +++ b/Moose Development/Moose/Ops/FlightGroup.lua @@ -0,0 +1,5360 @@ +--- **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/Flightgroup). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.FlightGroup +-- @image OPS_FlightGroup.png + + +--- FLIGHTGROUP class. +-- @type FLIGHTGROUP +-- @field #string actype Type name of the aircraft. +-- @field #number rangemax Max range in meters. +-- @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.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 despawnAfterLanding If `true`, group is despawned after landed at an airbase. +-- @field #boolean despawnAfterHolding If `true`, group is despawned after reaching the holding point. +-- @field #number RTBRecallCount Number that counts RTB calls. +-- @field OPS.FlightControl#FLIGHTCONTROL.HoldingStack stack Holding stack. +-- @field #boolean isReadyTO Flight is ready for takeoff. This is for FLIGHTCONTROL. +-- @field #boolean prohibitAB Disallow (true) or allow (false) AI to use the afterburner. +-- @field #boolean jettisonEmptyTanks Allow (true) or disallow (false) AI to jettison empty fuel tanks. +-- @field #boolean jettisonWeapons Allow (true) or disallow (false) AI to jettison weapons if in danger. +-- @field #number holdtime Time [s] flight is holding before going on final. Set to nil for indefinitely. +-- +-- @extends Ops.OpsGroup#OPSGROUP + +--- *To invent an airplane is nothing; to build one is something; to fly is everything.* -- Otto Lilienthal +-- +-- === +-- +-- # 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 = false, + outofAGMrtb = false, + flightcontrol = nil, + flaghold = nil, + Tholding = nil, + Tparking = nil, + Twaiting = nil, + menu = nil, + isHelo = nil, + RTBRecallCount = 0, + playerSettings = {}, + playerWarnings = {}, + prohibitAB = false, + jettisonEmptyTanks = true, + jettisonWeapons = true, -- that's actually a negative option like prohibitAB +} + + +--- 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", +} + +--- Radio Text. +-- @type FLIGHTGROUP.RadioText +-- @field #string normal +-- @field #string enhanced + +--- Radio messages. +-- @type FLIGHTGROUP.RadioMessage +-- @field #FLIGHTGROUP.RadioText AIRBORNE +-- @field #FLIGHTGROUP.RadioText TAXIING +FLIGHTGROUP.RadioMessage = { + AIRBORNE={normal="Airborn", enhanced="Airborn"}, + TAXIING={normal="Taxiing", enhanced="Taxiing"}, +} + +--- Skill level. +-- @type FLIGHTGROUP.PlayerSkill +-- @field #string STUDENT Flight Student. Shows tips and hints in important phases of the approach. +-- @field #string AVIATOR Naval aviator. Moderate number of hints but not really zip lip. +-- @field #string GRADUATE TOPGUN graduate. For people who know what they are doing. Nearly *ziplip*. +-- @field #string INSTRUCTOR TOPGUN instructor. For people who know what they are doing. Nearly *ziplip*. +FLIGHTGROUP.PlayerSkill = { + STUDENT = "Student", + AVIATOR = "Aviator", + GRADUATE = "Graduate", + INSTRUCTOR = "Instructor", +} + +--- Player data. +-- @type FLIGHTGROUP.PlayerData +-- @field #string name Player name. +-- @field #boolean subtitles Display subtitles. +-- @field #string skill Skill level. + +--- FLIGHTGROUP players. +-- @field #table Players Player data. +FLIGHTGROUP.Players={} + +--- FLIGHTGROUP class version. +-- @field #string version +FLIGHTGROUP.version="1.0.3" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: VTOL aircraft. +-- TODO: Mark assigned parking spot on F10 map. +-- TODO: Let user request a parking spot via F10 marker :) +-- DONE: Use new UnitLost event instead of crash/dead. +-- DONE: 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 TACAN beacon. +-- 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 og=_DATABASE:GetOpsGroup(group) + if og then + og:I(og.lid..string.format("WARNING: OPS group already exists in data base!")) + return og + 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 or "N/A") + + -- Defaults + self:SetDefaultROE() + self:SetDefaultROT() + self:SetDefaultEPLRS(self.isEPLRS) + self:SetDetection() + self:SetFuelLowThreshold() + self:SetFuelLowRTB() + self:SetFuelCriticalThreshold() + self:SetFuelCriticalRTB() + + -- Holding flag. + self.flaghold=USERFLAG:New(string.format("%s_FlagHold", self.groupname)) + self.flaghold:Set(0) + self.holdtime=2*60 + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "LandAtAirbase", "Inbound") -- Group is ordered to land at an airbase. + self:AddTransition("*", "RTB", "Inbound") -- Group is returning to (home/destination) airbase. + 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", "Cruising") -- 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("*", "FuelLow", "*") -- Fuel state of group is low. Default ~25%. + self:AddTransition("*", "FuelCritical", "*") -- Fuel state of group is critical. Default ~10%. + + self:AddTransition("Cruising", "EngageTarget", "Engaging") -- Engage targets. + self:AddTransition("Engaging", "Disengage", "Cruising") -- 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("*", "Cruise", "Cruising") -- The whole flight group is cruising. + 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. + + --- FSM Function OnAfterElementSpawned. + -- @function [parent=#FLIGHTGROUP] OnAfterElementSpawned + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. + + --- FSM Function OnAfterElementParking. + -- @function [parent=#FLIGHTGROUP] OnAfterElementParking + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. + -- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot. + + --- FSM Function OnAfterElementEngineOn. + -- @function [parent=#FLIGHTGROUP] OnAfterElementEngineOn + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. + + --- FSM Function OnAfterElementTaxiing. + -- @function [parent=#FLIGHTGROUP] OnAfterElementTaxiing + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. + + --- FSM Function OnAfterElementTakeoff. + -- @function [parent=#FLIGHTGROUP] OnAfterElementTakeoff + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil. + + --- FSM Function OnAfterElementAirborne. + -- @function [parent=#FLIGHTGROUP] OnAfterElementAirborne + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. + + --- FSM Function OnAfterElementLanded. + -- @function [parent=#FLIGHTGROUP] OnAfterElementLanded + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil. + + --- FSM Function OnAfterElementArrived. + -- @function [parent=#FLIGHTGROUP] OnAfterElementArrived + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP.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. + + --- FSM Function OnAfterElementDestroyed. + -- @function [parent=#FLIGHTGROUP] OnAfterElementDestroyed + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. + + --- FSM Function OnAfterElementDead. + -- @function [parent=#FLIGHTGROUP] OnAfterElementDead + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. + + --- FSM Function OnAfterSpawned. + -- @function [parent=#FLIGHTGROUP] OnAfterSpawned + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- FSM Function OnAfterParking. + -- @function [parent=#FLIGHTGROUP] OnAfterParking + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- FSM Function OnAfterTaxiing. + -- @function [parent=#FLIGHTGROUP] OnAfterTaxiing + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- FSM Function OnAfterTakeoff. + -- @function [parent=#FLIGHTGROUP] OnAfterTakeoff + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- FSM Function OnAfterAirborne. + -- @function [parent=#FLIGHTGROUP] OnAfterAirborne + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- FSM Function OnAfterCruise. + -- @function [parent=#FLIGHTGROUP] OnAfterCruise + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- FSM Function OnAfterLanding. + -- @function [parent=#FLIGHTGROUP] OnAfterLanding + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- FSM Function OnAfterLanded. + -- @function [parent=#FLIGHTGROUP] OnAfterLanded + -- @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. + + --- FSM Function OnAfterLandedAt. + -- @function [parent=#FLIGHTGROUP] OnAfterLandedAt + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- FSM Function OnAfterArrived. + -- @function [parent=#FLIGHTGROUP] OnAfterArrived + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- FSM Function OnAfterDead. + -- @function [parent=#FLIGHTGROUP] OnAfterDead + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- FSM Function OnAfterUpdateRoute. + -- @function [parent=#FLIGHTGROUP] OnAfterUpdateRoute + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. + -- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. + + --- FSM Function OnAfterOutOfMissilesAA. + -- @function [parent=#FLIGHTGROUP] OnAfterOutOfMissilesAA + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- FSM Function OnAfterOutOfMissilesAG. + -- @function [parent=#FLIGHTGROUP] OnAfterOutOfMissilesAG + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- FSM Function OnAfterRTB. + -- @function [parent=#FLIGHTGROUP] OnAfterRTB + -- @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. + + --- FSM Function OnAfterLandAtAirbase. + -- @function [parent=#FLIGHTGROUP] OnAfterLandAtAirbase + -- @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. + + --- FSM Function OnAfterWait. + -- @function [parent=#FLIGHTGROUP] OnAfterWait + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever). + -- @param #number Altitude Altitude in feet. Default 10,000 ft for airplanes and 1,000 feet for helos. + -- @param #number Speed Speed in knots. Default 250 kts for airplanes and 20 kts for helos. + + --- FSM Function OnAfterRefuel. + -- @function [parent=#FLIGHTGROUP] OnAfterRefuel + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Coordinate The coordinate. + + --- FSM Function OnAfterRefueled. + -- @function [parent=#FLIGHTGROUP] OnAfterRefueled + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- FSM Function OnAfterDisengage. + -- @function [parent=#FLIGHTGROUP] OnAfterDisengage + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Set#SET_UNIT TargetUnitSet + + --- FSM Function OnAfterEngageTarget. + -- @function [parent=#FLIGHTGROUP] OnAfterEngageTarget + -- @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. + + --- FSM Function OnAfterLandAt. + -- @function [parent=#FLIGHTGROUP] OnAfterLandAt + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Set#SET_UNIT TargetUnitSet + + --- FSM Function OnAfterFuelLow. + -- @function [parent=#FLIGHTGROUP] OnAfterFuelLow + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- FSM Function OnAfterFuelCritical. + -- @function [parent=#FLIGHTGROUP] OnAfterFuelCritical + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- FSM Function OnBeforeUpdateRoute. + -- @function [parent=#FLIGHTGROUP] OnBeforeUpdateRoute + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. + -- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. + -- @return #boolean Transision allowed? + + --- FSM Function OnBeforeRTB. + -- @function [parent=#FLIGHTGROUP] OnBeforeRTB + -- @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. + + --- FSM Function OnBeforeLandAtAirbase. + -- @function [parent=#FLIGHTGROUP] OnBeforeLandAtAirbase + -- @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. + + --- FSM Function OnBeforeWait. + -- @function [parent=#FLIGHTGROUP] OnBeforeWait + -- @param #FLIGHTGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever). + -- @param #number Altitude Altitude in feet. Default 10,000 ft for airplanes and 1,000 feet for helos. + -- @param #number Speed Speed in knots. Default 250 kts for airplanes and 20 kts for helos. + + --- FSM Function OnBeforeLandAt. + -- @function [parent=#FLIGHTGROUP] OnBeforeLandAt + -- @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). + + -- TODO: Add pseudo functions ? Normally done, but should be double check + + -- 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) + self:HandleEvent(EVENTS.PlayerLeaveUnit, self.OnEventPlayerLeaveUnit) + + -- Initialize group. + self:_InitGroup() + + -- Init waypoints. + self:_InitWaypoints() + + + + -- Start the status monitoring. + self.timerStatus=TIMER:New(self.Status, self):Start(1, 30) + + -- 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) + + -- Add OPSGROUP to _DATABASE. + _DATABASE:AddOpsGroup(self) + + 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 + +--- Get airwing the flight group belongs to. +-- @param #FLIGHTGROUP self +-- @return Ops.Airwing#AIRWING The AIRWING object (if any). +function FLIGHTGROUP:GetAirwing() + return self.legion +end + +--- Get name of airwing the flight group belongs to. +-- @param #FLIGHTGROUP self +-- @return #string Name of the airwing or "None" if the flightgroup does not belong to any airwing. +function FLIGHTGROUP:GetAirwingName() + local name=self.legion and self.legion.alias or "None" + return name +end + +--- Get squadron the flight group belongs to. +-- @param #FLIGHTGROUP self +-- @return Ops.Squadron#SQUADRON The SQUADRON of this flightgroup or #nil if the flightgroup does not belong to any squadron. +function FLIGHTGROUP:GetSquadron() + return self.cohort +end + +--- Get squadron name the flight group belongs to. +-- @param #FLIGHTGROUP self +-- @return #string The squadron name or "None" if the flightgroup does not belon to any squadron. +function FLIGHTGROUP:GetSquadronName() + local name=self.cohort and self.cohort:GetName() or "None" + return name +end + +--- Set if aircraft is VTOL capable. Unfortunately, there is no DCS way to determine this via scripting. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetVTOL() + self.isVTOL=true + return self +end + +--- Set if aircraft is **not** allowed to use afterburner. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetProhibitAfterburner() + self.prohibitAB = true + if self:GetGroup():IsAlive() then + self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_AB, true) + end + return self +end + +--- Set if aircraft is allowed to use afterburner. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetAllowAfterburner() + self.prohibitAB = false + if self:GetGroup():IsAlive() then + self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_AB, false) + end + return self +end + +--- Set if aircraft is allowed to drop empty fuel tanks - set to true to allow, and false to forbid it. +-- @param #FLIGHTGROUP self +-- @param #boolean Switch true or false +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetJettisonEmptyTanks(Switch) + self.jettisonEmptyTanks = Switch + if self:GetGroup():IsAlive() then + self:GetGroup():SetOption(AI.Option.Air.id.JETT_TANKS_IF_EMPTY, Switch) + end + return self +end + +--- Set if aircraft is allowed to drop weapons to escape danger - set to true to allow, and false to forbid it. +-- @param #FLIGHTGROUP self +-- @param #boolean Switch true or false +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetJettisonWeapons(Switch) + self.jettisonWeapons = not Switch + if self:GetGroup():IsAlive() then + self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_JETT, not Switch) + end + return self +end + +--- Set the aircraft to land straight in. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetOptionLandingStraightIn() + self.OptionLandingStraightIn = true + if self:GetGroup():IsAlive() then + self:GetGroup():SetOptionLandingStraightIn() + end + return self +end + +--- Set the aircraft to land in pairs. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetOptionLandingForcePair() + self.OptionLandingForcePair = true + if self:GetGroup():IsAlive() then + self:GetGroup():SetOptionLandingForcePair() + end + return self +end + +--- Set the aircraft to NOT land in pairs. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetOptionLandingRestrictPair() + self.OptionLandingRestrictPair = true + if self:GetGroup():IsAlive() then + self:GetGroup():SetOptionLandingRestrictPair() + end + return self +end + +--- Set the aircraft to land after overhead break. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetOptionLandingOverheadBreak() + self.OptionLandingOverheadBreak = true + if self:GetGroup():IsAlive() then + self:GetGroup():SetOptionLandingOverheadBreak() + end + return self +end + +--- [HELICOPTER] Set the aircraft to prefer takeoff and landing vertically. +-- @param #FLIGHTGROUP self +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetOptionPreferVertical() + self.OptionPreferVertical = true + if self:GetGroup():IsAlive() then + self:GetGroup():OptionPreferVerticalLanding() + end + return self +end + +--- Set if group is ready for taxi/takeoff if controlled by a `FLIGHTCONTROL`. +-- @param #FLIGHTGROUP self +-- @param #boolean ReadyTO If `true`, flight is ready for takeoff. +-- @param #number Delay Delay in seconds before value is set. Default 0 sec. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:SetReadyForTakeoff(ReadyTO, Delay) + if Delay and Delay>0 then + self:ScheduleOnce(Delay, FLIGHTGROUP.SetReadyForTakeoff, self, ReadyTO, 0) + else + self:T(self.lid.."Set Ready for Takeoff switch for flightcontrol") + self.isReadyTO=ReadyTO + end + return self +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:IsControlling(self) then + -- Flight control is already controlling this flight! + return + else + -- Remove flight from previous FC. + self.flightcontrol:_RemoveFlight(self) + end + end + + -- Set FC. + self:T(self.lid..string.format("Setting FLIGHTCONTROL to airbase %s", flightcontrol.airbasename)) + self.flightcontrol=flightcontrol + + -- Add flight to all flights. + if not flightcontrol:IsFlight(self) then + table.insert(flightcontrol.flights, self) + 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) + if type(HomeAirbase)=="string" then + HomeAirbase=AIRBASE:FindByName(HomeAirbase) + end + 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) + if type(DestinationAirbase)=="string" then + DestinationAirbase=AIRBASE:FindByName(DestinationAirbase) + end + 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 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 + +--- Enable that the group is despawned after holding. 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:SetDespawnAfterHolding() + self.despawnAfterHolding=true + return self +end + + +--- Check if flight is parking. +-- @param #FLIGHTGROUP self +-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element. +-- @return #boolean If true, flight is parking after spawned. +function FLIGHTGROUP:IsParking(Element) + local is=self:Is("Parking") + if Element then + is=Element.status==OPSGROUP.ElementStatus.PARKING + end + return is +end + +--- Check if is taxiing to the runway. +-- @param #FLIGHTGROUP self +-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element. +-- @return #boolean If true, flight is taxiing after engine start up. +function FLIGHTGROUP:IsTaxiing(Element) + local is=self:Is("Taxiing") + if Element then + is=Element.status==OPSGROUP.ElementStatus.TAXIING + end + return is +end + +--- Check if flight is airborne or cruising. +-- @param #FLIGHTGROUP self +-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element. +-- @return #boolean If true, flight is airborne. +function FLIGHTGROUP:IsAirborne(Element) + local is=self:Is("Airborne") or self:Is("Cruising") + if Element then + is=Element.status==OPSGROUP.ElementStatus.AIRBORNE + end + return is +end + +--- Check if flight is airborne or cruising. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is airborne. +function FLIGHTGROUP:IsCruising() + local is=self:Is("Cruising") + return is +end + +--- Check if flight is landing. +-- @param #FLIGHTGROUP self +-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element. +-- @return #boolean If true, flight is landing, i.e. on final approach. +function FLIGHTGROUP:IsLanding(Element) + local is=self:Is("Landing") + if Element then + is=Element.status==OPSGROUP.ElementStatus.LANDING + end + return is +end + +--- Check if flight has landed and is now taxiing to its parking spot. +-- @param #FLIGHTGROUP self +-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element. +-- @return #boolean If true, flight has landed +function FLIGHTGROUP:IsLanded(Element) + local is=self:Is("Landed") + if Element then + is=Element.status==OPSGROUP.ElementStatus.LANDED + end + return is +end + +--- Check if flight has arrived at its destination parking spot. +-- @param #FLIGHTGROUP self +-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element. +-- @return #boolean If true, flight has arrived at its destination and is parking. +function FLIGHTGROUP:IsArrived(Element) + local is=self:Is("Arrived") + if Element then + is=Element.status==OPSGROUP.ElementStatus.ARRIVED + end + return is +end + +--- Check if flight is inbound and traveling to holding pattern. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is holding. +function FLIGHTGROUP:IsInbound() + local is=self:Is("Inbound") + return is +end + +--- Check if flight is holding and waiting for landing clearance. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is holding. +function FLIGHTGROUP:IsHolding() + local is=self:Is("Holding") + return is +end + +--- Check if flight is going for fuel. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is refueling. +function FLIGHTGROUP:IsGoing4Fuel() + local is=self:Is("Going4Fuel") + return is +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() + local is=self:Is("LandingAt") + return is +end + +--- Check if helo(!) flight has landed at a specific point. +-- @param #FLIGHTGROUP self +-- @return #boolean If true, has landed somewhere. +function FLIGHTGROUP:IsLandedAt() + local is=self:Is("LandedAt") + return is +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 is good on fuel (not below low or even critical state). +-- @param #FLIGHTGROUP self +-- @return #boolean If true, flight is good on fuel. +function FLIGHTGROUP:IsFuelGood() + local isgood=not (self.fuellow or self.fuelcritical) + return isgood +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 + + local alive=self:IsAlive() + + if alive~=nil then + -- Check if group is already active. + local _delay=0 + if alive==false then + self:Activate() + _delay=1 + end + self:T(self.lid.."Starting uncontrolled group") + self.group:StartUncontrolled(_delay) + self.isUncontrolled=false + else + self:T(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 + + -- Set flag. + self:T(self.lid..string.format("Clear to land ==> setting holding flag to 1 (true)")) + self.flaghold:Set(1) + + -- Not holding any more. + self.Tholding=nil + + -- Clear holding stack. + if self.stack then + self.stack.flightgroup=nil + self.stack=nil + end + + 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 --Ops.OpsGroup#OPSGROUP.Element + + local unit=element.unit + + local life=unit:GetLife() + + if unit and unit:IsAlive() and life>1 then + local fuel=unit:GetFuel() + if fuelself.Twaiting+self.dTwait then + --self.Twaiting=nil + --self.dTwait=nil + --self:_CheckGroupDone() + end + end + end + + --- check if we need to end holding + --self:T(self.lid.."Checking if we are holding at a holding point...") + if mission and mission.missionHoldingCoord and self.isHoldingAtHoldingPoint == true then + self:T(self.lid.."...yes") + if mission:IsReadyToPush() then + --self:T(self.lid.."Ready to push -> YES") + -- move flag to 1 + self.flaghold:Set(1) + -- Not waiting any more. + self.Twaiting=nil + self.dTwait=nil + self.isHoldingAtHoldingPoint = false + --else + --self:T(self.lid.."Ready to push -> NO!") + end + --else + --self:T(self.lid.."...no") + end + + -- If mission, check if DCS task needs to be updated. + if mission and mission.updateDCSTask then + + -- Orbit missions might need updates. + if (mission:GetType()==AUFTRAG.Type.ORBIT or mission:GetType()==AUFTRAG.Type.RECOVERYTANKER or mission:GetType()==AUFTRAG.Type.CAP) and mission.orbitVec2 then + + -- Get 2D vector of orbit target. + local vec2=mission:GetTargetVec2() + + -- Heading. + local hdg=mission:GetTargetHeading() + + -- Heading change? + local hdgchange=false + if mission.orbitLeg then + if UTILS.HdgDiff(hdg, mission.targetHeading)>0 then + hdgchange=true + end + end + + -- Distance to previous position. + local dist=UTILS.VecDist2D(vec2, mission.orbitVec2) + + -- Distance change? + local distchange=dist>mission.orbitDeltaR + + -- Debug info. + self:T3(self.lid..string.format("Checking orbit mission dist=%d meters", dist)) + + -- Check if distance is larger than threshold. + if distchange or hdgchange then + + -- Debug info. + self:T3(self.lid..string.format("Updating orbit!")) + + -- Update DCS task. This also sets the new mission.orbitVec2. + local DCSTask=mission:GetDCSMissionTask() --DCS#Task + + -- Get task. + local Task=mission:GetGroupWaypointTask(self) + + -- Reset current orbit task. + self.controller:resetTask() + + -- Push task after one second. We need to give resetTask some time or it will not work! + self:_SandwitchDCSTask(DCSTask, Task, false, 1) + + end + + elseif mission.type==AUFTRAG.Type.CAPTUREZONE then + + -- Get task. + local Task=mission:GetGroupWaypointTask(self) + + -- Update task: Engage or get new zone. + if mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.EXECUTING or mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.STARTED then + self:_UpdateTask(Task, mission) + end + + end + end + + + -- TODO: _CheckParking() function + + -- Check if flight began to taxi (if it was parking). + if self:IsParking() then + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + + -- Check for parking spot. + if element.parking then + + -- Get distance to assigned parking spot. + local dist=self:_GetDistToParking(element.parking, element.unit:GetCoord()) + + -- Debug info. + self:T(self.lid..string.format("Distance to parking spot %d = %.1f meters", element.parking.TerminalID, dist)) + + -- If distance >10 meters, we consider the unit as taxiing. At least for fighters, the initial distance seems to be around 1.8 meters. + if dist>12 and element.engineOn then + self:ElementTaxiing(element) + end + + else + --self:T(self.lid..string.format("Element %s is in PARKING queue but has no parking spot assigned!", element.name)) + end + end + end + + else + -- Check damage. + self:_CheckDamage() + end + + --- + -- Group + --- + + -- Short info. + if self.verbose>=1 then + + -- Number of elements. + local nelem=self:CountElements() + local Nelem=#self.elements + + -- Get number of tasks and missions. + local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() + local nMissions=self:CountRemainingMissison() + + -- ROE and Alarm State. + local roe=self:GetROE() or -1 + local rot=self:GetROT() or -1 + + -- 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 wpN=#self.waypoints or 0 + local wpF=tostring(self.passedfinalwp) + + -- Speed. + local speed=UTILS.MpsToKnots(self.velocity or 0) + local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed()) + + -- Altitude. + local alt=self.position and self.position.y or 0 + + -- Heading in degrees. + local hdg=self.heading or 0 + + -- TODO: GetFormation function. + local formation=self.option.Formation or "unknown" + + -- Life points. + local life=self.life or 0 + + -- Total ammo. + local ammo=self:GetAmmoTot().Total + + -- Detected units. + local ndetected=self.detectionOn and tostring(self.detectedunits:Count()) or "Off" + + -- Get cargo weight. + local cargo=0 + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + cargo=cargo+element.weightCargo + end + + -- Home and destination base. + local home=self.homebase and self.homebase:GetName() or "unknown" + local dest=self.destbase and self.destbase:GetName() or "unknown" + local curr=self.currbase and self.currbase:GetName() or "N/A" + + -- Info text. + local text=string.format("%s [%d/%d]: ROE/ROT=%d/%d | T/M=%d/%d | Wp=%d[%d]-->%d[%d]/%d [%s] | Life=%.1f | v=%.1f (%d) | Hdg=%03d | Ammo=%d | Detect=%s | Cargo=%.1f | Base=%s [%s-->%s]", + fsmstate, nelem, Nelem, roe, rot, nTaskTot, nMissions, wpidxCurr, wpuidCurr, wpidxNext, wpuidNext, wpN, wpF, life, speed, speedEx, hdg, ammo, ndetected, cargo, curr, home, dest) + 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 --Ops.OpsGroup#OPSGROUP.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 lp=unit:GetLife() + local lp0=unit:GetLife0() + local parking=element.parking and tostring(element.parking.TerminalID) or "X" + + -- Get ammo. + local ammo=self:GetAmmoElement(element) + + -- Output text for element. + text=text..string.format("\n[%d] %s: status=%s, fuel=%.1f, life=%.1f [%.1f/%.1f], guns=%d, rockets=%d, bombs=%d, missiles=%d (AA=%d, AG=%d, AS=%s), parking=%s", + i, name, status, fuel*100, life*100, lp, lp0, 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 alive then + + -- TODO: _Check distance travelled. + + -- 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 --Ops.OpsGroup#OPSGROUP.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:T(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 + + --- + -- Track flight + --- + if false then + + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + + local unit=element.unit + + if unit and unit:IsAlive() then + + local vec3=unit:GetVec3() + + if vec3 and element.pos then + + local id=UTILS.GetMarkID() + + trigger.action.lineToAll(-1, id, vec3, element.pos, {1,1,1,0.5}, 1) + + end + + element.pos=vec3 + + end + + end + + end + + --- + -- Fuel State + --- + + -- TODO: _CheckFuelState() function. + + -- Only if group is in air. + if alive and self.group:IsAirborne(true) then + + local fuelmin=self:GetFuelMin() + + -- Debug info. + self:T2(self.lid..string.format("Fuel state=%d", fuelmin)) + + if fuelmin>=self.fuellowthresh then + self.fuellow=false + end + + if fuelmin>=self.fuelcriticalthresh then + self.fuelcritical=false + end + + + -- Low fuel? + if fuelmin See also OPSGROUP +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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)) + + -- Element started engies. + self:ElementEngineOn(element) + + -- Engines are on. + element.engineOn=true + + --[[ + -- 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) + self:T3(self.lid.."EVENT: TakeOff") + + -- 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:T2(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 + + -- Engines are off. + element.engineOn=false + + if element.unit and element.unit:IsAlive() then + + local airbase=self:GetClosestAirbase() + local parking=self:GetParkingSpot(element, 100, 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:T(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 + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "ElementSpawned" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. +function FLIGHTGROUP:onafterElementSpawned(From, Event, To, Element) + + -- Debug info. + self:T(self.lid..string.format("Element spawned %s", Element.name)) + + if Element.playerName then + self:_InitPlayerData(Element.playerName) + end + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.SPAWNED) + + if Element.unit:InAir(not self.isHelo) then -- Setting check because of problems with helos dynamically spawned where inAir WRONGLY returned true if spawned at an airbase or farp! + -- 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 Ops.OpsGroup#OPSGROUP.Element Element The flight group element. +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot. +function FLIGHTGROUP:onafterElementParking(From, Event, To, Element, Spot) + + -- Set parking spot. + if Spot then + self:_SetElementParkingAt(Element, Spot) + end + + -- Debug info. + 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 self:IsTakeoffCold() then + -- Wait for engine startup event. + elseif self:IsTakeoffHot() then + self:__ElementEngineOn(0.5, Element) -- delay a bit to allow all elements + Element.engineOn=true + elseif self:IsTakeoffRunway() then + self:__ElementEngineOn(0.5, Element) + Element.engineOn=true + end + +end + +--- On after "ElementEngineOn" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP.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 Ops.OpsGroup#OPSGROUP.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 Ops.OpsGroup#OPSGROUP.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(0.01, Element) + +end + +--- On after "ElementAirborne" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element. +function FLIGHTGROUP:onafterElementAirborne(From, Event, To, Element) + + -- Debug info. + self:T2(self.lid..string.format("Element airborne %s", Element.name)) + + -- Set parking spot to free. Also for FC. This is usually done after taxiing but doing it here in case the group is teleported. + self:_SetElementParkingFree(Element) + + -- 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 Ops.OpsGroup#OPSGROUP.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) + + -- Debug info. + self:T2(self.lid..string.format("Element landed %s at %s airbase", Element.name, airbase and airbase:GetName() or "unknown")) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.LANDED, airbase) + + -- Helos with skids land directly on parking spots. + if self.isHelo then + + local Spot=self:GetParkingSpot(Element, 10, airbase) + + if Spot then + self:_SetElementParkingAt(Element, Spot) + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.ARRIVED) + end + + end + + -- Despawn after landing. + if self.despawnAfterLanding then + + if self.legion then + + if airbase and self.legion.airbase and airbase.AirbaseName==self.legion.airbase.AirbaseName then + + if self:IsLanded() then + -- Everybody landed ==> Return to legion. Will despawn the last one. + self:ReturnToLegion() + else + -- Despawn the element. + self:DespawnElement(Element) + end + + end + + else + + -- Despawn the element. + self:DespawnElement(Element) + + end + end +end + +--- On after "ElementArrived" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP.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)) + + -- Set element parking. + 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 Ops.OpsGroup#OPSGROUP.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 Ops.OpsGroup#OPSGROUP.Element Element The flight group element. +function FLIGHTGROUP:onafterElementDead(From, Event, To, Element) + + -- Check for flight control. + if self.flightcontrol and Element.parking then + self.flightcontrol:SetParkingFree(Element.parking) + end + + -- Call OPSGROUP function. This will remove the flightcontrol. Therefore, has to be after setting parking free. + self:GetParent(self).onafterElementDead(self, From, Event, To, Element) + + -- Not parking any more. + Element.parking=nil + +end + + +--- On after "Spawned" event. +-- @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")) + + -- 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", tostring(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("Weight = %.1f kg\n", self:GetWeightTotal()) + text=text..string.format("Cargo bay = %.1f kg\n", self:GetFreeCargobay()) + 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("Has EPLRS = %s\n", tostring(self.isEPLRS)) + text=text..string.format("Helicopter = %s\n", tostring(self.isHelo)) + 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())) + text=text..string.format("Elements:") + for i,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + text=text..string.format("\n[%d] %s: callsign=%s, modex=%s, player=%s", i, element.name, tostring(element.callsign), tostring(element.modex), tostring(element.playerName)) + end + self:I(self.lid..text) + end + + -- Update position. + self:_UpdatePosition() + + -- Not dead or destroyed yet. + self.isDead=false + self.isDestroyed=false + + if self.isAI then + + -- TODO: Could be that element is spawned UNCONTROLLED. + -- In that case, the commands are not yet used. + -- This should be shifted to something like after ACTIVATED + + -- Set ROE. + self:SwitchROE(self.option.ROE) + + -- Set ROT. + self:SwitchROT(self.option.ROT) + + -- Set default EPLRS. + self:SwitchEPLRS(self.option.EPLRS) + + -- Set default Invisible. + self:SwitchInvisible(self.option.Invisible) + + -- Set default Immortal. + self:SwitchImmortal(self.option.Immortal) + + -- 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, self.jettisonWeapons) + self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_AB, self.prohibitAB) -- Does not seem to work. AI still used the after burner. + self:GetGroup():SetOption(AI.Option.Air.id.RTB_ON_BINGO, false) + self:GetGroup():SetOption(AI.Option.Air.id.JETT_TANKS_IF_EMPTY, self.jettisonEmptyTanks) + --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 + + -- Set flightcontrol. + if self.currbase then + local flightcontrol=_DATABASE:GetFlightControl(self.currbase:GetName()) + if flightcontrol then + self:SetFlightControl(flightcontrol) + else + -- F10 other menu. + self:_UpdateMenu(0.5) + end + else + self:_UpdateMenu(0.5) + end + + 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) + + -- Get closest airbase + local airbase=self:GetClosestAirbase() + local airbasename=airbase:GetName() or "unknown" + + -- Debug info + self:T(self.lid..string.format("Flight is parking at airbase %s", airbasename)) + + -- Set current airbase. + self.currbase=airbase + + -- Set homebase to current airbase if not defined yet. + -- This is necessary, e.g, when flights are spawned at an airbase because they do not have a takeoff waypoint. + if not self.homebase then + self.homebase=airbase + end + + -- 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. This also updates the menu. + self:SetFlightControl(flightcontrol) + + if self.flightcontrol then + + -- Set flight status. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.PARKING) + + end + + else + self:T3(self.lid.."INFO: No flight control in onAfterParking!") + 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 + + if self.flightcontrol and self.flightcontrol:IsControlling(self) 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) + 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")) + + -- No current airbase any more. + self.currbase=nil + + -- Cruising. + self:__Cruise(-0.01) + +end + +--- On after "Cruising" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FLIGHTGROUP:onafterCruise(From, Event, To) + self:T(self.lid..string.format("Flight cruising")) + + -- Not waiting anymore. + self.Twaiting=nil + self.dTwait=nil + + if self.isAI then + + --- + -- AI + --- + + -- Check group Done. + self:_CheckGroupDone(nil, 120) + + else + + --- + -- CLIENT + --- + + -- Had this commented out (forgot why, probably because it was not necessary) but re-enabling it because of carrier launch. + self:_UpdateMenu(0.1) + + 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")) + + -- Everyone is landing now. + self:_SetElementStatusAll(OPSGROUP.ElementStatus.LANDING) + + if self.flightcontrol and self.flightcontrol:IsControlling(self) then + -- Add flight to landing queue. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.LANDING) + end + + -- Not holding any more. + self.Tholding=nil + + -- Clear holding stack. + if self.stack then + self.stack.flightgroup=nil + self.stack=nil + end + +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 self.flightcontrol:IsControlling(self) 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")) + + -- Trigger (un-)loading process. + if self:IsPickingup() then + self:__Loading(-1) + elseif self:IsTransporting() then + self:__Unloading(-1) + end + +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 + + if not self.isAI then + -- Player landed. No despawn. + return + end + + --TODO: Check that current base is airwing base. + local airwing=self:GetAirwing() --airwing:GetAirbaseName()==self.currbase:GetName() + + -- Check what to do. + if airwing and not (self:IsPickingup() or self:IsTransporting()) then + + -- Debug info. + self:T(self.lid..string.format("Airwing asset group %s arrived ==> Adding asset back to stock of airwing %s", self.groupname, airwing.alias)) + + -- Add the asset back to the airwing. + --airwing:AddAsset(self.group, 1) + self:ReturnToLegion(1) + + elseif self.isLandingAtAirbase then + + local Template=UTILS.DeepCopy(self.template) --DCS#Template + + -- No late activation. + self.isLateActivated=false + Template.lateActivation=self.isLateActivated + + -- Spawn in uncontrolled state. + self.isUncontrolled=true + Template.uncontrolled=self.isUncontrolled + + -- First waypoint of the group. + local SpawnPoint=Template.route.points[1] + + -- These are only for ships and FARPS. + SpawnPoint.linkUnit = nil + SpawnPoint.helipadId = nil + SpawnPoint.airdromeId = nil + + -- Airbase. + local airbase=self.isLandingAtAirbase --Wrapper.Airbase#AIRBASE + + -- Get airbase ID and category. + local AirbaseID = airbase:GetID() + + -- Set airdromeId. + if airbase:IsShip() then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif airbase:IsHelipad() then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif airbase:IsAirdrome() then + SpawnPoint.airdromeId = AirbaseID + end + + -- Set waypoint type/action. + SpawnPoint.alt = 0 + SpawnPoint.type = COORDINATE.WaypointType.TakeOffParking + SpawnPoint.action = COORDINATE.WaypointAction.FromParkingArea + + local units=Template.units + + for i=#units,1,-1 do + local unit=units[i] + local element=self:GetElementByName(unit.name) + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + unit.parking=element.parking and element.parking.TerminalID or nil + unit.parking_id=nil + local vec3=element.unit:GetVec3() + local heading=element.unit:GetHeading() + unit.x=vec3.x + unit.y=vec3.z + unit.alt=vec3.y + unit.heading=math.rad(heading) + unit.psi=-unit.heading + else + table.remove(units, i) + end + end + + -- Respawn with this template. + self:_Respawn(0, Template) + + -- Reset. + self.isLandingAtAirbase=nil + + -- Init (un-)loading process. + if self:IsPickingup() then + self:__Loading(-1) + elseif self:IsTransporting() then + self:__Unloading(-1) + end + + else + -- Depawn after 5 min. Important to trigger dead events before DCS despawns on its own without any notification. + self:T(self.lid..string.format("Despawning group in 5 minutes after arrival!")) + 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 + + -- 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 Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. +-- @return #boolean Transision allowed? +function FLIGHTGROUP:onbeforeUpdateRoute(From, Event, To, n, N) + + -- Is transition allowed? We assume yes until proven otherwise. + local allowed=true + local trepeat=nil + + if self:IsAlive() 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:T(self.lid.."Update route denied. Group is DEAD!") + allowed=false + elseif self:IsInUtero() then + self:T(self.lid.."Update route denied. Group is INUTERO!") + 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 + + -- Check if group is uncontrolled. If so, the mission task cannot be set yet! + if allowed and self:IsUncontrolled() then + self:T(self.lid.."Update route denied. Group is UNCONTROLLED!") + local mission=self:GetMissionCurrent() + if mission and mission.type==AUFTRAG.Type.ALERT5 then + trepeat=nil --Alert 5 is just waiting for the real mission. No need to try to update the route. + else + trepeat=-5 + end + allowed=false + end + + -- Requested waypoint index <1. Something is seriously wrong here! + if n and n<1 then + self:T(self.lid.."Update route denied because waypoint n<1!") + allowed=false + end + + -- No current waypoint. Something is serously wrong! + if not self.currentwp then + self:T(self.lid.."Update route denied because self.currentwp=nil!") + allowed=false + end + + local Nn=n or self.currentwp+1 + if not Nn or Nn<1 then + self:T(self.lid.."Update route denied because N=nil or N<1") + trepeat=-5 + allowed=false + end + + -- Check for a current task. + if self.taskcurrent>0 then + + -- Get the current task. Must not be executing already. + local task=self:GetTaskByID(self.taskcurrent) + + if task then + if task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then + -- For patrol zone, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: PatrolZone") + elseif task.dcstask.id==AUFTRAG.SpecialTask.CAPTUREZONE then + -- For patrol zone, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: CaptureZone") + elseif task.dcstask.id==AUFTRAG.SpecialTask.RECON then + -- For recon missions, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: ReconMission") + elseif task.dcstask.id==AUFTRAG.SpecialTask.PATROLRACETRACK then + -- For recon missions, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: Patrol Race Track") + elseif task.dcstask.id==AUFTRAG.SpecialTask.HOVER then + -- For recon missions, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: Hover") + elseif task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then + -- For relocate + self:T2(self.lid.."Allowing update route for Task: Relocate Cohort") + elseif task.description and task.description=="Task_Land_At" then + -- We allow this + self:T2(self.lid.."Allowing update route for Task: Task_Land_At") + else + local taskname=task and task.description or "No description" + self:T(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. Therefore, also directly executed tasks should be added to the queue! + 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 in state %s: allowed=%s (repeat in %s)", self:GetState(), tostring(allowed), tostring(trepeat))) + + -- Try again? + 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 Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. +function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n, N) + + -- Update route from this waypoint number onwards. + n=n or self.currentwp+1 + + -- Max index. + N=N or #self.waypoints + N=math.min(N, #self.waypoints) + + -- Waypoints. + local wp={} + + -- Current velocity. + local speed=self.group and self.group:GetVelocityKMH() or 100 + + -- Waypoint type. + local waypointType=COORDINATE.WaypointType.TurningPoint + local waypointAction=COORDINATE.WaypointAction.TurningPoint + if self:IsLanded() or self:IsLandedAt() or self:IsAirborne()==false then + -- Had some issues with passing waypoint function of the next WP called too ealy when the type is TurningPoint. Setting it to TakeOff solved it! + waypointType=COORDINATE.WaypointType.TakeOff + --waypointType=COORDINATE.WaypointType.TakeOffGroundHot + --waypointAction=COORDINATE.WaypointAction.FromGroundAreaHot + end + + -- 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:GetCoordinate():WaypointAir(COORDINATE.WaypointAltType.BARO, waypointType, waypointAction, speed, true, nil, {}, "Current") + table.insert(wp, current) + + -- Add remaining waypoints to route. + for i=n, N do + table.insert(wp, self.waypoints[i]) + end + + if wp[2] then + self.speedWp=wp[2].speed + 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 [%s], homebase=%s destination=%s", n, #wp, self:GetState(), 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 "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:T(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:T(self.lid.."Calling RTB in onafterOutOfMissilesAA") + 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:T(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:T(self.lid.."Calling RTB in onafterOutOfMissilesAG") + 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. +-- @param #number waittime Time to wait if group is done. +function FLIGHTGROUP:_CheckGroupDone(delay, waittime) + + -- FSM state. + local fsmstate=self:GetState() + + if self:IsAlive() and self.isAI then + + if delay and delay>0 then + -- Debug info. + self:T(self.lid..string.format("Check FLIGHTGROUP [state=%s] done in %.3f seconds... (t=%.4f)", fsmstate, delay, timer.getTime())) + + -- Delayed call. + self:ScheduleOnce(delay, FLIGHTGROUP._CheckGroupDone, self) + else + + -- Debug info. + self:T(self.lid..string.format("Check FLIGHTGROUP [state=%s] done? (t=%.4f)", fsmstate, timer.getTime())) + + -- Group is currently engaging. + if self:IsEngaging() then + self:T(self.lid.."Engaging! Group NOT done...") + return + end + -- Check if group is going for fuel. + if self:IsGoing4Fuel() then + self:T(self.lid.."Going for FUEL! Group NOT done...") + return + end + + -- Number of tasks remaining. + local nTasks=self:CountRemainingTasks() + + -- Number of mission remaining. + local nMissions=self:CountRemainingMissison() + + -- Number of cargo transports remaining. + local nTransports=self:CountRemainingTransports() + + -- Number of paused missions. + local nPaused=self:_CountPausedMissions() + + -- First check if there is a paused mission and that all remaining missions are paused. If there are other missions in the queue, we will run those. + if nPaused>0 and nPaused==nMissions then + local missionpaused=self:_GetPausedMission() + self:T(self.lid..string.format("Found paused mission %s [%s]. Unpausing mission...", missionpaused.name, missionpaused.type)) + self:UnpauseMission() + return + end + + -- Group is ordered to land at an airbase. + if self.isLandingAtAirbase then + self:T(self.lid..string.format("Landing at airbase %s! Group NOT done...", self.isLandingAtAirbase:GetName())) + return + end + + -- Group is waiting. + if self:IsWaiting() then + self:T(self.lid.."Waiting! Group NOT done...") + return + end + + -- Debug info. + self:T(self.lid..string.format("Remaining (final=%s): missions=%d, tasks=%d, transports=%d", tostring(self.passedfinalwp), nMissions, nTasks, nTransports)) + + -- Final waypoint passed? + -- Or next waypoint index is the first waypoint. Could be that the group was on a mission and the mission waypoints were deleted. then the final waypoint is FALSE but no real waypoint left. + -- Since we do not do ad infinitum, this leads to a rapid oscillation between UpdateRoute and CheckGroupDone! + if self:HasPassedFinalWaypoint() or self:GetWaypointIndexNext()==1 then + + --- + -- Final Waypoint PASSED + --- + + -- Got current mission or task? + if self.currentmission==nil and self.taskcurrent==0 and (self.cargoTransport==nil or self.cargoTransport:GetCarrierTransportStatus(self)==OPSTRANSPORT.Status.DELIVERED) then + + -- Number of remaining tasks/missions? + if nTasks==0 and nMissions==0 and nTransports==0 then + + local destbase=self.destbase or self.homebase --Wrapper.Airbase#AIRBASE + local destzone=self.destzone or self.homezone --Wrapper.Airbase#AIRBASE + + -- Send flight to destination. + if waittime then + self:T(self.lid..string.format("Passed Final WP and No current and/or future missions/tasks/transports. Waittime given ==> Waiting for %d sec!", waittime)) + self:Wait(waittime) + elseif destbase then + if self.currbase and self.currbase.AirbaseName==destbase.AirbaseName and self:IsParking() then + self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports AND parking at destination airbase ==> Arrived!") + self:Arrived() + else + -- Only send RTB if current base is not yet the destination + if self.currbase==nil or self.currbase.AirbaseName~=destbase.AirbaseName then + self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports ==> RTB!") + self:__RTB(-0.1, destbase) + end + end + elseif destzone then + self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports ==> RTZ!") + self:__RTZ(-0.1, destzone) + else + self:T(self.lid.."Passed Final WP and NO Tasks/Missions left. No DestBase or DestZone ==> Wait!") + self:__Wait(-1) + end + + else + -- Check if not parking (could be on ALERT5 and just spawned (current mission=nil) + if not self:IsParking() then + 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 + 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 + + --- + -- Final Waypoint NOT PASSED + --- + + -- Debug info. + self:T(self.lid..string.format("Flight (status=%s) did NOT pass the final waypoint yet ==> update route in -0.01 sec", self:GetState())) + + -- Update route. + self:__UpdateRoute(-0.01) + + 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) + + -- Debug info. + self:T(self.lid..string.format("RTB: before event=%s: %s --> %s to %s", Event, From, To, airbase and airbase:GetName() or "None")) + + if self:IsAlive() then + + local allowed=true + local Tsuspend=nil + + if airbase==nil then + self:T(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:T(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())) + return false + end + + if self.currbase and self.currbase:GetName()==airbase:GetName() then + self:T(self.lid.."WARNING: Currbase is already same as RTB airbase. RTB canceled!") + return false + end + + -- Check if the group has landed at an airbase. If so, we lost control and RTBing is not possible (only after a respawn). + if self:IsLanded() then + self:T(self.lid.."WARNING: Flight has already landed. RTB canceled!") + return 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:T(self.lid..string.format("WARNING: Group [%s] is not AIRBORNE ==> RTB event is suspended for 20 sec", self:GetState())) + allowed=false + Tsuspend=-20 + local groupspeed = self.group:GetVelocityMPS() + if groupspeed<=1 and not self:IsParking() then + self.RTBRecallCount = self.RTBRecallCount+1 + end + if self.RTBRecallCount>6 then + self:T(self.lid..string.format("WARNING: Group [%s] is not moving and was called RTB %d times. Assuming a problem and despawning!", self:GetState(), self.RTBRecallCount)) + self.RTBRecallCount=0 + self:Despawn(5) + return + end + end + + -- Only if fuel is not low or critical. + if self:IsFuelGood() then + + -- Check if there are remaining tasks. + local Ntot,Nsched, Nwp=self:CountRemainingTasks() + + if self.taskcurrent>0 then + self:T(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:T(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:T(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 + + if self.Twaiting and self.dTwait then + self:T(self.lid..string.format("WARNING: Group is Waiting for a specific duration ==> RTB event is canceled", Nwp)) + allowed=false + end + + end + + if Tsuspend and not allowed then + self:T(self.lid.."Calling RTB in onbeforeRTB") + self:__RTB(Tsuspend, airbase, SpeedTo, SpeedHold) + end + + return allowed + + else + self:T(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 + + -- Cancel all missions. + self:CancelAllMissions() + + -- Land at airbase. + self:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) + +end + + +--- On before "LandAtAirbase" 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. +function FLIGHTGROUP:onbeforeLandAtAirbase(From, Event, To, airbase) + + if self:IsAlive() then + + local allowed=true + local Tsuspend=nil + + if airbase==nil then + self:T(self.lid.."ERROR: Airbase is nil in LandAtAirbase() 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:T(self.lid..string.format("ERROR: Wrong airbase coalition %d in LandAtAirbase() call! We allow only same as group %d or neutral airbases 0", airbase:GetCoalition(), self.group:GetCoalition())) + return false + end + + if self.currbase and self.currbase:GetName()==airbase:GetName() then + self:T(self.lid.."WARNING: Currbase is already same as LandAtAirbase airbase. LandAtAirbase canceled!") + return false + end + + -- Check if the group has landed at an airbase. If so, we lost control and RTBing is not possible (only after a respawn). + if self:IsLanded() then + self:T(self.lid.."WARNING: Flight has already landed. LandAtAirbase canceled!") + return false + end + + if self:IsParking() then + allowed=false + Tsuspend=-30 + self:T(self.lid.."WARNING: Flight is parking. LandAtAirbase call delayed by 30 sec") + elseif self:IsTaxiing() then + allowed=false + Tsuspend=-1 + self:T(self.lid.."WARNING: Flight is parking. LandAtAirbase call delayed by 1 sec") + end + + if Tsuspend and not allowed then + self:__LandAtAirbase(Tsuspend, airbase) + end + + return allowed + else + self:T(self.lid.."WARNING: Group is not alive! LandAtAirbase call not allowed") + return false + end + +end + + +--- On after "LandAtAirbase" 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. +function FLIGHTGROUP:onafterLandAtAirbase(From, Event, To, airbase) + + self.isLandingAtAirbase=airbase + + self:_LandAtAirbase(airbase) + +end + +--- Land at an airbase. +-- @param #FLIGHTGROUP self +-- @param Wrapper.Airbase#AIRBASE airbase Airbase where the group shall land. +-- @param #number SpeedTo Speed used for travelling from current position to holding point in knots. +-- @param #number SpeedHold Holding speed in knots. +-- @param #number SpeedLand Landing speed in knots. Default 170 kts. +function FLIGHTGROUP:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand) + + -- Set current airbase. + self.currbase=airbase + + -- Passed final waypoint! + self:_PassedFinalWaypoint(true, "_LandAtAirbase") + + -- Not waiting any more. + self.Twaiting=nil + self.dTwait=nil + + -- 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) + + -- Clear holding time in any case. + self.Tholding=nil + + -- 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) + + -- Holding altitude. + local althold=self.isHelo and 1000+math.random(10)*100 or math.random(4,10)*1000 + + -- Holding points. + local c0=self: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 and self.isAI then + + -- Get holding stack from flight control. + local stack=fc:_GetHoldingStack(self) + + if stack then + + stack.flightgroup=self + self.stack=stack + + -- Race track points. + p0=stack.pos0 + p1=stack.pos1 + + -- Debug marks. + if false then + p0:MarkToAll(string.format("%s: Holding stack P0, alt=%d meters", self:GetName(), p0.y)) + p1:MarkToAll(string.format("%s: Holding stack P1, alt=%d meters", self:GetName(), p0.y)) + end + + else + + end + + -- Set flightcontrol for this flight. + self:SetFlightControl(fc) + + -- Add flight to inbound queue. + self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.INBOUND) + + -- Callsign. + local callsign=self:GetCallsignName() + + -- Pilot calls inbound for landing. + local text=string.format("%s, %s, inbound for landing", fc.alias, callsign) + + -- Radio message. + fc:TransmissionPilot(text, self) + + -- Message text. + local text=string.format("%s, %s, roger, hold at angels %d. Report entering the pattern.", callsign, fc.alias, stack.angels) + + -- Send message. + fc:TransmissionTower(text, self, 10) + + end + + -- Some intermediate coordinate to climb to the default cruise alitude. + local c1=c0:GetIntermediateCoordinate(p0, 0.25):SetAltitude(self.altitudeCruise, true) + local c2=c0:GetIntermediateCoordinate(p0, 0.75):SetAltitude(self.altitudeCruise, true) + + -- Altitude above ground for a glide slope of 3 degrees. + local x1=self.isHelo and UTILS.NMToMeters(2.0) or UTILS.NMToMeters(10) + local x2=self.isHelo and UTILS.NMToMeters(1.0) or UTILS.NMToMeters(5) + local alpha=math.rad(3) + local h1=x1*math.tan(alpha) + local h2=x2*math.tan(alpha) + + -- Get active runway. + local runway=airbase:GetActiveRunwayLanding() + + -- Set holding flag to 0=false. + self.flaghold:Set(0) + + -- Set holding time. + local holdtime=self.holdtime + 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={} + -- NOTE: Currently, this first waypoint confuses the AI. It makes them go in circles. Looks like they cannot find the waypoint and are flying around it. + --wp[#wp+1]=c0:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Current Pos") + wp[#wp+1]=c1:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Climb") + wp[#wp+1]=c2:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Descent") + wp[#wp+1]=p0:WaypointAir("BARO", 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:IsAirdrome() then + + --- + -- Airdrome + --- + + -- Call a function to tell everyone we are on final. + local TaskFinal = self.group:TaskFunction("FLIGHTGROUP._OnFinal", self) + + -- Final approach waypoint. + local rheading + if runway then + rheading = runway.heading-180 + else + -- AB HeloBase w/o runway eg Naqoura + local wind = airbase:GetCoordinate():GetWind() + rheading = -wind + end + + local papp=airbase:GetCoordinate():Translate(x1, rheading):SetAltitude(h1) + wp[#wp+1]=papp:WaypointAirTurningPoint("BARO", UTILS.KnotsToKmph(SpeedLand), {TaskFinal}, "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, rheading):SetAltitude(h2) + wp[#wp+1]=pland:WaypointAirLanding(UTILS.KnotsToKmph(SpeedLand), airbase, {}, "Landing") + + elseif airbase:IsShip() or airbase:IsHelipad() then + + --- + -- Ship or Helipad + --- + + local pland=airbase:GetCoordinate() + wp[#wp+1]=pland:WaypointAirLanding(UTILS.KnotsToKmph(SpeedLand), airbase, {}, "Landing") + + end + + if self.isAI then + + -- Clear all tasks. + -- Warning, looks like this can make DCS CRASH! Had this after calling RTB once passed the final waypoint. + --self:ClearTasks() + + -- Just route the group. Respawn might happen when going from holding to final. + -- NOTE: I have delayed that here because of RTB calling _LandAtAirbase which resets current task immediately. + -- So the stop flag change to 1 will not trigger TaskDone() and a current mission is not done either! + -- Looks like a delay of 0.1 sec was not enough for the stopflag to take effect. Increasing this to 1.0 sec. + -- This delay is looking better. Hopefully not any unwanted side effects in other situations. + self:Route(wp, 1.0) + + end + +end + +--- On before "Wait" event. +-- @param #FLIGHTGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever). +-- @param #number Altitude Altitude in feet. Default 10,000 ft for airplanes and 1,000 feet for helos. +-- @param #number Speed Speed in knots. Default 250 kts for airplanes and 20 kts for helos. +function FLIGHTGROUP:onbeforeWait(From, Event, To, Duration, Altitude, Speed) + + local allowed=true + local Tsuspend=nil + + -- Check for a current task. + if self.taskcurrent>0 and not self:IsLandedAt() then + self:T(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 30 sec!")) + Tsuspend=-30 + allowed=false + end + + -- Check for a current transport assignment. + if self.cargoTransport and not self:IsLandedAt() then + --self:T(self.lid..string.format("WARNING: Got current TRANSPORT assignment ==> WAIT event is suspended for 30 sec!")) + --Tsuspend=-30 + --allowed=false + end + + -- Call wait again. + if Tsuspend and not allowed then + self:__Wait(Tsuspend, Duration, 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 #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever). +-- @param #number Altitude Altitude in feet. Default 10,000 ft for airplanes and 1,000 feet for helos. +-- @param #number Speed Speed in knots. Default 250 kts for airplanes and 20 kts for helos. +function FLIGHTGROUP:onafterWait(From, Event, To, Duration, Altitude, Speed) + + -- Group will orbit at its current position. + local Coord=self:GetCoordinate() + + -- Set altitude: 1000 ft for helos and 10,000 ft for panes. + if Altitude then + Altitude=UTILS.FeetToMeters(Altitude) + else + Altitude=self.altitudeCruise + end + + -- Set speed. + Speed=Speed or (self.isHelo and 20 or 250) + + -- Debug message. + local text=string.format("Group set to wait/orbit at altitude %d m and speed %.1f km/h for %s seconds", Altitude, Speed, tostring(Duration)) + self:T(self.lid..text) + + --TODO: set ROE passive. introduce roe event/state/variable. + + -- Orbit until flaghold=1 (true) but max 5 min if no FC is giving the landing clearance. + self.flaghold:Set(0) + local TaskOrbit = self.group:TaskOrbit(Coord, Altitude, UTILS.KnotsToMps(Speed)) + local TaskStop = self.group:TaskCondition(nil, self.flaghold.UserFlagName, 1, nil, Duration) + local TaskCntr = self.group:TaskControlled(TaskOrbit, TaskStop) + local TaskOver = self.group:TaskFunction("FLIGHTGROUP._FinishedWaiting", self) + + local DCSTasks + if Duration or true then + DCSTasks=self.group:TaskCombo({TaskCntr, TaskOver}) + else + DCSTasks=self.group:TaskCombo({TaskOrbit, TaskOver}) + end + + + -- Set task. + self:PushTask(DCSTasks) + + -- Set time stamp. + self.Twaiting=timer.getAbsTime() + + -- Max waiting time in seconds. + self.dTwait=Duration + +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:T(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: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) + + -- Set RTB on Bingo option. Currently DCS does not execute the refueling task if RTB_ON_BINGO is set to "NO RTB ON BINGO" + self.group:SetOption(AI.Option.Air.id.RTB_ON_BINGO, true) + +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:T(self.lid..text) + + -- Set RTB on Bingo option to "NO RTB ON BINGO" + self.group:SetOption(AI.Option.Air.id.RTB_ON_BINGO, false) + + -- 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) + + -- Despawn after holding. + if self.despawnAfterHolding then + if self.legion then + self:ReturnToLegion(1) + else + self:Despawn(1) + end + return + end + + -- Holding time stamp. + self.Tholding=timer.getAbsTime() + + -- Debug message. + 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 self.isAI then + + -- Callsign. + local callsign=self:GetCallsignName() + + -- Pilot arrived at holding pattern. + local text=string.format("%s, %s, arrived at holding pattern", self.flightcontrol.alias, callsign) + + if self.stack then + text=text..string.format(", angels %d.", self.stack.angels) + end + + -- Radio message. + self.flightcontrol:TransmissionPilot(text, self) + + -- Message to flight + local text=string.format("%s, roger, fly heading %d and wait for landing clearance", callsign, self.stack.heading) + + -- Radio message from tower. + self.flightcontrol:TransmissionTower(text, self, 10) + + 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:T("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 `nil` = forever. +function FLIGHTGROUP:onafterLandAt(From, Event, To, Coordinate, Duration) + + -- Duration. + --Duration=Duration or 600 + + self:T(self.lid..string.format("Landing at Coordinate for %s seconds", tostring(Duration))) + + Coordinate=Coordinate or self:GetCoordinate() + + local DCStask=self.group:TaskLandAtVec2(Coordinate:GetVec2(), Duration) + + local Task=self:NewTaskScheduled(DCStask, 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) + + -- Current min fuel. + local fuel=self:GetFuelMin() or 0 + + -- Debug message. + local text=string.format("Low fuel %d for flight group %s", fuel, self.groupname) + self:T(self.lid..text) + + -- Set switch to true. + self.fuellow=true + + -- Back to destination or home. + local airbase=self.destbase or self.homebase + + if self.fuellowrefuel and self.refueltype then + + -- Find nearest tanker within 50 NM. + local tanker=self:FindNearestTanker(50) + + if tanker then + + -- Debug message. + self:T(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) + + -- Trigger refuel even. + self:Refuel(coordinate) + + return + end + end + + -- Send back to airbase. + if airbase and self.fuellowrtb then + self:T(self.lid.."Calling RTB in onafterFuelLow") + self:RTB(airbase) + --TODO: RTZ + 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:T(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:T(self.lid.."Calling RTB in onafterFuelCritical") + self:RTB(airbase) + --TODO: RTZ + end +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 is on final. +-- @param Wrapper.Group#GROUP group Group object. +-- @param #FLIGHTGROUP flightgroup Flight group object. +function FLIGHTGROUP._OnFinal(group, flightgroup) + flightgroup:T2(flightgroup.lid..string.format("Group on final approach")) + + local fc=flightgroup.flightcontrol + + if fc and fc:IsControlling(flightgroup) then + fc:_FlightOnFinal(flightgroup) + end + +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 + +--- Function called when flight finished waiting. +-- @param Wrapper.Group#GROUP group Group object. +-- @param #FLIGHTGROUP flightgroup Flight group object. +function FLIGHTGROUP._FinishedWaiting(group, flightgroup) + flightgroup:T(flightgroup.lid..string.format("Group finished waiting")) + + -- Not waiting any more. + flightgroup.Twaiting=nil + flightgroup.dTwait=nil + + -- Check group done. + flightgroup:_CheckGroupDone(0.1) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. +-- @param #FLIGHTGROUP self +-- @param #table Template Template used to init the group. Default is `self.template`. +-- @param #number Delay Delay in seconds before group is initialized. Default `nil`, *i.e.* instantaneous. +-- @return #FLIGHTGROUP self +function FLIGHTGROUP:_InitGroup(Template, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, FLIGHTGROUP._InitGroup, self, Template, 0) + else + + -- First check if group was already initialized. + if self.groupinitialized then + self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!") + return + end + + -- Group object. + local group=self.group --Wrapper.Group#GROUP + + -- Helo group. + self.isHelo=group:IsHelicopter() + + -- Max speed in km/h. + self.speedMax=group:GetSpeedMax() + + -- Is group mobile? + if self.speedMax and self.speedMax>3.6 then + self.isMobile=true + else + self.isMobile=false + self.speedMax = 0 + end + + -- Cruise speed limit 380 kts for fixed and 110 knots for rotary wings. + local speedCruiseLimit=self.isHelo and UTILS.KnotsToKmph(110) or UTILS.KnotsToKmph(380) + + -- Cruise speed: 70% of max speed but within limit. + self.speedCruise=math.min(self.speedMax*0.7, speedCruiseLimit) + + -- Group ammo. + self.ammo=self:GetAmmoTot() + + -- Get template of group. + local template=Template or self:_GetTemplate() + + -- Is (template) group uncontrolled. + self.isUncontrolled=template~=nil and template.uncontrolled or false + + -- Is (template) group late activated. + self.isLateActivated=template~=nil and template.lateActivation or false + + if template then + + -- Radio parameters from template. Default is set on spawn if not modified by user. + self.radio.Freq=tonumber(template.frequency) + self.radio.Modu=tonumber(template.modulation) + self.radio.On=template.communication + + -- Set callsign. Default is set on spawn if not modified by user. + local callsign=template.units[1].callsign + --self:I({callsign=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=tonumber(callsign[1]) + self.callsign.NumberGroup=tonumber(callsign[2]) + self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) + + end + + -- 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. + if not self.tacanDefault then + self:SetDefaultTACAN(nil, nil, nil, nil, true) + end + if not self.tacan then + self.tacan=UTILS.DeepCopy(self.tacanDefault) + end + + -- 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 {} --#table + self.menu.atc.root=self.menu.atc.root or MENU_GROUP:New(self.group, "ATC") --Core.Menu#MENU_GROUP + self.menu.atc.help=self.menu.atc.help or MENU_GROUP:New(self.group, "Help", self.menu.atc.root) --Core.Menu#MENU_GROUP + end + + -- Units of the group. + local units=self.group:GetUnits() + + -- DCS group. + local dcsgroup=Group.getByName(self.groupname) + local size0=dcsgroup:getInitialSize() + + -- Quick check. + if #units~=size0 then + self:T(self.lid..string.format("ERROR: Got #units=%d but group consists of %d units!", #units, size0)) + end + + -- Add elemets. + for _,unit in pairs(units) do + self:_AddElementByName(unit:GetName()) + end + + -- Init done. + self.groupinitialized=true + end + + return self +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.waypoints0 and self.waypoints0[1] or nil --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 dself.currentwp then + self:_PassedFinalWaypoint(false, "AddWaypointLanding") + end + + -- Speed in knots. + Speed=Speed or self.speedCruise + + -- Get coordinate of airbase. + local Coordinate=Airbase:GetCoordinate() + + -- Create air waypoint. + local wp=Coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, nil, Airbase, {}, "Landing Temp", 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 + +--- Get player element. +-- @param #FLIGHTGROUP self +-- @return Ops.OpsGroup#OPSGROUP.Element The element. +function FLIGHTGROUP:GetPlayerElement() + + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + if not element.ai then + return element + end + end + + return nil +end + +--- Get player element. +-- @param #FLIGHTGROUP self +-- @return #string Player name or `nil`. +function FLIGHTGROUP:GetPlayerName() + + local playerElement=self:GetPlayerElement() + + if playerElement then + return playerElement.playerName + end + + return nil +end + +--- Set parking spot of element. +-- @param #FLIGHTGROUP self +-- @param Ops.OpsGroup#OPSGROUP.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)) + + -- Get flightcontrol. + local fc=_DATABASE:GetFlightControl(Spot.AirbaseName) + + if fc and not self.flightcontrol then + self:SetFlightControl(fc) + end + + 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 Ops.OpsGroup#OPSGROUP.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 Ops.OpsGroup#OPSGROUP.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) + + -- Coordinate of unit landed + local coord=element.unit:GetCoordinate() + + -- Airbase. + airbase=airbase or self:GetClosestAirbase() + + if airbase == nil then + self:T(self.lid.."No airbase found for element "..element.name) + return nil + end + + -- Parking table of airbase. + local parking=airbase.parking --:GetParkingSpotsTable() + + -- If airbase is ship, translate parking coords. Alternatively, we just move the coordinate of the unit to the origin of the map, which is way more efficient. + if airbase and airbase:IsShip() then + -- No need to compute the relative position if there is only one parking spot. + if #parking > 1 then + coord = airbase:GetRelativeCoordinate( coord.x, coord.y, coord.z ) + else + coord.x=0 + coord.z=0 + maxdist=500 -- 100 meters was not enough, e.g. on the Seawise Giant, where the spot is 139 meters from the "center". + end + end + + 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 + + -- Distance to spot. + 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) + if template then + 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 + 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 --Ops.OpsGroup#OPSGROUP.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)) + + end + + end -- check terminal type + end -- loop over parking spots + + -- No parking spot for at least one asset :( + if not gotit then + self:T(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 or _attribute==FLIGHTGROUP.Attribute.AIR_UAV 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 + +--- Check if group got stuck. This overwrites the OPSGROUP function. +-- Here we only check if stuck whilst taxiing. +-- @param #FLIGHTGROUP self +-- @param #boolean Despawn If `true`, despawn group if stuck. +-- @return #number Time in seconds the group got stuck or nil if not stuck. +function FLIGHTGROUP:_CheckStuck(Despawn) + + -- Cases we are not stuck. + if not self:IsTaxiing() then + return nil + end + + -- Current time. + local Tnow=timer.getTime() + + -- Expected speed in m/s. + local ExpectedSpeed=5 + + -- Current speed in m/s. + local speed=self:GetVelocity() + + -- Check speed. + if speed<0.1 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 + + local holdtime=nil + + -- Somehow we are not moving... + if self.stuckTimestamp then + + -- Time we are holding. + holdtime=Tnow-self.stuckTimestamp + + -- Trigger stuck event. + self:Stuck(holdtime) + + if holdtime>=5*60 and holdtime<15*60 then + + -- Debug warning. + self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) + + elseif holdtime>=15*60 then + + -- Debug warning. + self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) + + -- Look for a current mission and cancel it as we do not seem to be able to perform it. + local mission=self:GetMissionCurrent() + + if mission then + self:T(self.lid..string.format("WARNING: Cancelling mission %s [%s] due to being stuck", mission:GetName(), mission:GetType())) + self:MissionCancel(mission) + end + + if self.stuckDespawn then + if self.legion then + self:T(self.lid..string.format("Asset is returned to its legion after being stuck!")) + self:ReturnToLegion() + else + self:T(self.lid..string.format("Despawning group after being stuck!")) + self:Despawn() + end + end + + end + + end + + return holdtime +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- OPTION FUNCTIONS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- MENU FUNCTIONS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Update menu. +--@param #FLIGHTGROUP self +--@param #number delay Delay in seconds. +function FLIGHTGROUP:_UpdateMenu(delay) + + if delay and delay>0 then + -- Delayed call. + self:ScheduleOnce(delay, FLIGHTGROUP._UpdateMenu, self) + else + + -- Player element. + local player=self:GetPlayerElement() + + if player and player.status~=OPSGROUP.ElementStatus.DEAD then + + -- Debug text. + if self.verbose>=2 then + local text=string.format("Updating MENU: State=%s, ATC=%s [%s]", self:GetState(), + self.flightcontrol and self.flightcontrol.airbasename or "None", self.flightcontrol and self.flightcontrol:GetFlightStatus(self) or "Unknown") + + -- Message to group. + MESSAGE:New(text, 5):ToGroup(self.group) + self:T(self.lid..text) + end + + -- Get current position of player. + local position=self:GetCoordinate(nil, player.name) + + -- Get all FLIGHTCONTROLS + local fc={} + for airbasename,_flightcontrol in pairs(_DATABASE.FLIGHTCONTROLS) do + local flightcontrol=_flightcontrol --OPS.FlightControl#FLIGHTCONTROL + + -- Get coord of airbase. + local coord=flightcontrol:GetCoordinate() + + -- Distance to flight. + local dist=coord:Get2DDistance(position) + + -- Add to table. + table.insert(fc, {airbasename=airbasename, dist=dist}) + end + + -- Sort table wrt distance to airbases. + local function _sort(a,b) + return a.dist=1 then + + -- FSM state. + local fsmstate=self:GetState() + + local callsign=self.callsignName and UTILS.GetCallsignName(self.callsignName) or "N/A" + local skill=self.skill and tostring(self.skill) or "N/A" + + local NassetsTot=#self.assets + local NassetsInS=self:CountAssets(true) + local NassetsQP=0 ; local NassetsP=0 ; local NassetsQ=0 + if self.legion then + NassetsQP, NassetsP, NassetsQ=self.legion:CountAssetsOnMission(nil, self) + end + + -- Short info. + local text=string.format("%s [Type=%s, Call=%s, Skill=%s]: Assets Total=%d, Stock=%d, Mission=%d [Active=%d, Queue=%d]", + fsmstate, self.aircrafttype, callsign, skill, NassetsTot, NassetsInS, NassetsQP, NassetsP, NassetsQ) + self:T(self.lid..text) + + -- Weapon data info. + if self.verbose>=3 and self.weaponData then + local text="Weapon Data:" + for bit,_weapondata in pairs(self.weaponData) do + local weapondata=_weapondata --Ops.OpsGroup#OPSGROUP.WeaponData + text=text..string.format("\n- Bit=%s: Rmin=%.1f km, Rmax=%.1f km", bit, weapondata.RangeMin/1000, weapondata.RangeMax/1000) + end + self:I(self.lid..text) + end + + -- Check if group has detected any units. + self:_CheckAssetStatus() + + end + + if not self:IsStopped() then + self:__Status(-60) + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Intelligence.lua b/Moose Development/Moose/Ops/Intelligence.lua new file mode 100644 index 000000000..f582f0675 --- /dev/null +++ b/Moose Development/Moose/Ops/Intelligence.lua @@ -0,0 +1,2606 @@ +--- **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 Core.Set#SET_ZONE conflictzoneset Set of conflict zones. Contacts in these zones are considered, even if they are not in accept zones or if they are in reject 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 in meters in which groups/units are considered to belong to a cluster. +-- @field #number prediction Seconds default to be used with CalcClusterFuturePosition. +-- @field #boolean detectStatics If `true`, detect STATIC objects. Default `false`. +-- @field #number statusupdate Time interval in seconds after which the status is refreshed. Default 60 sec. Should be negative. +-- @extends Core.Fsm#FSM + +--- Top Secret! +-- +-- === +-- +-- # 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") +-- MESSAGE:New(text, 15, "KGB"):ToAll() +-- end +-- +-- ## And/or new clusters found +-- +-- function RedIntel:OnAfterNewCluster(From, Event, To, Cluster) +-- local text = string.format("NEW cluster #%d of size %d", Cluster.index, Cluster.size) +-- 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 = 15000, + clusteranalysis = true, + clustermarkers = false, + clusterarrows = false, + prediction = 300, + detectStatics = false, +} + +--- 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 If `true`, contact is a naval group. +-- @field #boolean ishelo If `true`, contact is a helo group. +-- @field #boolean isground If `true`, contact is a ground group. +-- @field #boolean isStatic If `true`, contact is a STATIC object. +-- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this contact. +-- @field Ops.Target#TARGET target The Target attached to this contact. +-- @field #string recce The name of the recce unit that detected this contact. +-- @field #string ctype Contact type of #INTEL.Ctype. +-- @field #string platform [AIR] Contact platform name, e.g. Foxbat, Flanker_E, defaults to Bogey if unknown +-- @field #number heading [AIR] Heading of the contact, if available. +-- @field #boolean maneuvering [AIR] Contact has changed direction by >10 deg. +-- @field #number altitude [AIR] Flight altitude of the contact in meters. + +--- 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 #number markerID Marker ID. +-- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this cluster. +-- @field #string ctype Cluster type of #INTEL.Ctype. +-- @field #number altitude [AIR] Average flight altitude of the cluster in meters. + +--- Contact or cluster type. +-- @type INTEL.Ctype +-- @field #string GROUND Ground. +-- @field #string NAVAL Ship. +-- @field #string AIRCRAFT Airpane or helicopter. +-- @field #string STRUCTURE Static structure. +INTEL.Ctype={ + GROUND="Ground", + NAVAL="Naval", + AIRCRAFT="Aircraft", + STRUCTURE="Structure" +} + +--- INTEL class version. +-- @field #string version +INTEL.version="0.3.6" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add min cluster size. Only create new clusters if they have a certain group size. +-- TODO: process detected set asynchroniously for better performance. +-- DONE: Add statics. +-- DONE: Filter detection methods. +-- 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="INTEL SPECTRE" + if self.coalition then + if self.coalition==coalition.side.RED then + self.alias="INTEL KGB" + elseif self.coalition==coalition.side.BLUE then + self.alias="INTEL 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("%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("*", "Stop", "Stopped") -- Stop FSM. + + 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. + + + -- Defaults + self:SetForgetTime() + self:SetAcceptZones() + self:SetRejectZones() + self:SetConflictZones() + + ------------------------ + --- 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. + + + --- Triggers the FSM event "NewContact". + -- @function [parent=#INTEL] NewContact + -- @param #INTEL self + -- @param #INTEL.Contact Contact Detected contact. + + --- Triggers the FSM event "NewContact" after a delay. + -- @function [parent=#INTEL] NewContact + -- @param #INTEL self + -- @param #number delay Delay in seconds. + -- @param #INTEL.Contact Contact Detected contact. + + --- 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. + + + --- Triggers the FSM event "LostContact". + -- @function [parent=#INTEL] LostContact + -- @param #INTEL self + -- @param #INTEL.Contact Contact Lost contact. + + --- Triggers the FSM event "LostContact" after a delay. + -- @function [parent=#INTEL] LostContact + -- @param #INTEL self + -- @param #number delay Delay in seconds. + -- @param #INTEL.Contact Contact Lost 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. + + + --- Triggers the FSM event "NewCluster". + -- @function [parent=#INTEL] NewCluster + -- @param #INTEL self + -- @param #INTEL.Cluster Cluster Detected cluster. + + --- Triggers the FSM event "NewCluster" after a delay. + -- @function [parent=#INTEL] NewCluster + -- @param #INTEL self + -- @param #number delay Delay in seconds. + -- @param #INTEL.Cluster Cluster Detected cluster. + + --- 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.Cluster Cluster Detected cluster. + + + --- Triggers the FSM event "LostCluster". + -- @function [parent=#INTEL] LostCluster + -- @param #INTEL self + -- @param #INTEL.Cluster Cluster Lost cluster. + -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`. + + --- Triggers the FSM event "LostCluster" after a delay. + -- @function [parent=#INTEL] LostCluster + -- @param #INTEL self + -- @param #number delay Delay in seconds. + -- @param #INTEL.Cluster Cluster Lost cluster. + -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`. + + --- 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 an 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 an 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 conflict zones. Contacts detected in this/these zone(s) are reported by the detection. +-- Note that conflict zones overrule all other zones, i.e. if a unit is outside of an accept zone and inside a reject zone, it is still reported if inside a conflict zone. +-- @param #INTEL self +-- @param Core.Set#SET_ZONE ConflictZoneSet Set of conflict zone(s). +-- @return #INTEL self +function INTEL:SetConflictZones(ConflictZoneSet) + self.conflictzoneset=ConflictZoneSet or SET_ZONE:New() + return self +end + +--- Add a conflict zone. Contacts detected in this zone are conflicted and not reported by the detection. +-- Note that conflict zones overrule all other zones, i.e. if a unit is outside of an accept zone and inside a reject zone, it is still reported if inside a conflict zone. +-- @param #INTEL self +-- @param Core.Zone#ZONE ConflictZone Add a zone to the conflict zone set. +-- @return #INTEL self +function INTEL:AddConflictZone(ConflictZone) + self.conflictzoneset:AddZone(ConflictZone) + return self +end + +--- Remove a conflict zone from the conflict zone set. +-- Note that conflict zones overrule all other zones, i.e. if a unit is outside of an accept zone and inside a reject zone, it is still reported if inside a conflict zone. +-- @param #INTEL self +-- @param Core.Zone#ZONE ConflictZone Remove a zone from the conflict zone set. +-- @return #INTEL self +function INTEL:RemoveConflictZone(ConflictZone) + self.conflictzoneset:Remove(ConflictZone:GetName(), true) + return self +end + +--- **OBSOLETE, will be removed in next version!** 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) + 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 + +--- Method to make the radar detection less accurate, e.g. for WWII scenarios. +-- @param #INTEL self +-- @param #number minheight Minimum flight height to be detected, in meters AGL (above ground) +-- @param #number thresheight Threshold to escape the radar if flying below minheight, defaults to 90 (90% escape chance) +-- @param #number thresblur Threshold to be detected by the radar overall, defaults to 85 (85% chance to be found) +-- @param #number closing Closing-in in km - the limit of km from which on it becomes increasingly difficult to escape radar detection if flying towards the radar position. Should be about 1/3 of the radar detection radius in kilometers, defaults to 20. +-- @return #INTEL self +function INTEL:SetRadarBlur(minheight,thresheight,thresblur,closing) + self.RadarBlur = true + self.RadarBlurMinHeight = minheight or 250 -- meters + self.RadarBlurThresHeight = thresheight or 90 -- 10% chance to find a low flying group + self.RadarBlurThresBlur = thresblur or 85 -- 25% chance to escape the radar overall + self.RadarBlurClosing = closing or 20 -- 20km + self.RadarBlurClosingSquare = self.RadarBlurClosing * self.RadarBlurClosing + return self +end + +--- Set the accept range in kilometers from each of the recce. Only object closer than this range will be detected. +-- @param #INTEL self +-- @param #number Range Range in kilometers +-- @return #INTEL self +function INTEL:SetAcceptRange(Range) + self.RadarAcceptRange = true + self.RadarAcceptRangeKilometers = Range or 75 + 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 + +--- Add a group to the detection set. +-- @param #INTEL self +-- @param Wrapper.Group#GROUP AgentGroup Group of agents. Can also be an @{Ops.OpsGroup#OPSGROUP} object. +-- @return #INTEL self +function INTEL:AddAgent(AgentGroup) + + -- Check if this was an OPS group. + if AgentGroup:IsInstanceOf("OPSGROUP") then + AgentGroup=AgentGroup:GetGroup() + end + + -- Add to detection set. + self.detectionset:AddGroup(AgentGroup,true) + 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. +-- @param #boolean Arrows If true, draws arrows on F10 map. +-- @return #INTEL self +function INTEL:SetClusterAnalysis(Switch, Markers, Arrows) + self.clusteranalysis=Switch + self.clustermarkers=Markers + self.clusterarrows=Arrows + return self +end + +--- Set whether STATIC objects are detected. +-- @param #INTEL self +-- @param #boolean Switch If `true`, statics are detected. +-- @return #INTEL self +function INTEL:SetDetectStatics(Switch) + if Switch and Switch==true then + self.detectStatics=true + else + self.detectStatics=false + end + 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 in kilometers. Default 15 km. +-- @return #INTEL self +function INTEL:SetClusterRadius(radius) + self.clusterradius = (radius or 15)*1000 + 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 + +--- Get name of a contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return #string Name of the contact. +function INTEL:GetContactName(Contact) + return Contact.groupname +end + +--- Get group of a contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return Wrapper.Group#GROUP Group object. +function INTEL:GetContactGroup(Contact) + return Contact.group +end + +--- Get threatlevel of a contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return #number Threat level. +function INTEL:GetContactThreatlevel(Contact) + return Contact.threatlevel +end + + +--- Get type name of a contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return #string Type name. +function INTEL:GetContactTypeName(Contact) + return Contact.typename +end + +--- Get category name of a contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return #string Category name. +function INTEL:GetContactCategoryName(Contact) + return Contact.categoryname +end + +--- Get coordinate of a contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return Core.Point#COORDINATE Coordinates. +function INTEL:GetContactCoordinate(Contact) + return Contact.position +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)) + return self +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.isStatic and 1 or 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) + return self +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 + + local inconflictzone=false + -- Check if unit is in any of the conflict zones. + if self.conflictzoneset:Count()>0 then + for _,_zone in pairs(self.conflictzoneset.Set) do + local zone=_zone --Core.Zone#ZONE + if unit:IsInZone(zone) then + inconflictzone=true + break + end + end + end + + -- 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) and (not inconflictzone) 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 and (not inconflictzone) then + table.insert(remove, unitname) + end + end + + -- Filter unit categories. Added check that we have a UNIT and not a STATIC object because :GetUnitCategory() is only available for units. + if #self.filterCategory>0 and unit:IsInstanceOf("UNIT") 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 DetectedStatics={} + local RecceGroups={} + for unitname,_unit in pairs(DetectedUnits) do + local unit=_unit --Wrapper.Unit#UNIT + if unit:IsInstanceOf("UNIT") then + local group=unit:GetGroup() + if group then + local groupname = group:GetName() + DetectedGroups[groupname]=group + RecceGroups[groupname]=RecceDetecting[unitname] + end + else + if self.detectStatics then + DetectedStatics[unitname]=unit + RecceGroups[unitname]=RecceDetecting[unitname] + end + end + end + + -- Create detected contacts. + self:CreateDetectedItems(DetectedGroups, DetectedStatics, RecceGroups) + + -- Paint a picture of the battlefield. + if self.clusteranalysis then + self:PaintPicture() + end + + return self +end + +--- Update an #INTEL.Contact item. +-- @param #INTEL self +-- @param #INTEL.Contact Contact Contact. +-- @return #INTEL.Contact The contact. +function INTEL:_UpdateContact(Contact) + + if Contact.isStatic then + + -- Statics don't need to be updated. + + else + + if Contact.group and Contact.group:IsAlive() then + + Contact.Tdetected=timer.getAbsTime() + Contact.position=Contact.group:GetCoordinate() + Contact.velocity=Contact.group:GetVelocityVec3() + Contact.speed=Contact.group:GetVelocityMPS() + if Contact.group:IsAir() then + Contact.altitude=Contact.group:GetAltitude() + local oldheading = Contact.heading or 1 + local newheading = Contact.group:GetHeading() + if newheading == 0 then newheading = 1 end + local changeh = math.abs(((oldheading - newheading) + 360) % 360) + Contact.heading = newheading + if changeh > 10 then + Contact.maneuvering = true + else + Contact.maneuvering = false + end + end + end + + end + return self +end + +--- Create an #INTEL.Contact item from a given GROUP or STATIC object. +-- @param #INTEL self +-- @param Wrapper.Positionable#POSITIONABLE Positionable The GROUP or STATIC object. +-- @param #string RecceName The name of the recce group that has detected this contact. +-- @return #INTEL.Contact The contact. +function INTEL:_CreateContact(Positionable, RecceName) + + if Positionable and Positionable:IsAlive() then + + -- Create new contact. + local item={} --#INTEL.Contact + + if Positionable:IsInstanceOf("GROUP") then + + local group=Positionable --Wrapper.Group#GROUP + + item.groupname=group:GetName() + item.group=group + item.Tdetected=timer.getAbsTime() + 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=RecceName + item.isground = group:IsGround() or false + item.isship = group:IsShip() or false + item.isStatic=false + if group:IsAir() then + item.platform=group:GetNatoReportingName() + item.heading = group:GetHeading() + item.maneuvering = false + item.altitude = group:GetAltitude() + else + -- TODO optionally add ground types? + item.platform="Unknown" + item.altitude = group:GetAltitude(true) + end + if item.category==Group.Category.AIRPLANE or item.category==Group.Category.HELICOPTER then + item.ctype=INTEL.Ctype.AIRCRAFT + elseif item.category==Group.Category.GROUND or item.category==Group.Category.TRAIN then + item.ctype=INTEL.Ctype.GROUND + elseif item.category==Group.Category.SHIP then + item.ctype=INTEL.Ctype.NAVAL + end + + return item + + elseif Positionable:IsInstanceOf("STATIC") then + + local static=Positionable --Wrapper.Static#STATIC + + item.groupname=static:GetName() + item.group=static + item.Tdetected=timer.getAbsTime() + item.typename=static:GetTypeName() or "Unknown" + item.attribute="Static" + item.category=3 --static:GetCategory() + item.categoryname=static:GetCategoryName() or "Unknown" + item.threatlevel=static:GetThreatLevel() or 0 + item.position=static:GetCoordinate() + item.velocity=static:GetVelocityVec3() + item.speed=0 + item.recce=RecceName + item.isground = true + item.isship = false + item.isStatic=true + item.ctype=INTEL.Ctype.STRUCTURE + + return item + else + self:E(self.lid..string.format("ERROR: object needs to be a GROUP or STATIC!")) + end + + end + + return nil +end + +--- Create detected items. +-- @param #INTEL self +-- @param #table DetectedGroups Table of detected Groups. +-- @param #table DetectedStatics Table of detected Statics. +-- @param #table RecceDetecting Table of detecting recce names. +function INTEL:CreateDetectedItems(DetectedGroups, DetectedStatics, RecceDetecting) + self:F({RecceDetecting=RecceDetecting}) + + -- Current time. + local Tnow=timer.getAbsTime() + + -- Loop over groups. + for groupname,_group in pairs(DetectedGroups) do + local group=_group --Wrapper.Group#GROUP + + -- Create or update contact for this group. + self:KnowObject(group, RecceDetecting[groupname]) + + end + + -- Loop over statics. + for staticname,_static in pairs(DetectedStatics) do + local static=_static --Wrapper.Static#STATIC + + -- Create or update contact for this group. + self:KnowObject(static, RecceDetecting[staticname]) + + 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 + return self +end + +--- (Internal) Return the detected target groups of the controllable as a @{Core.Set#SET_GROUP}. +-- The optional parameters 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 + + -- NOTE: Got an object that exists but when trying UNIT:Find() the DCS getName() function failed. ID of the object was 5,000,031 + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then + + -- Protected call to get the name of the object. + local status,name = pcall( + function() + local name=DetectedObject:getName() + return name + end) + + if status then + + local unit=UNIT:FindByName(name) + + if unit and unit:IsAlive() then + local DetectionAccepted = true + + if self.RadarAcceptRange then + local reccecoord = Unit:GetCoordinate() + local coord = unit:GetCoordinate() + local dist = math.floor(coord:Get2DDistance(reccecoord)/1000) -- km + if dist > self.RadarAcceptRangeKilometers then DetectionAccepted = false end + end + + if self.RadarBlur then + local reccecoord = Unit:GetCoordinate() + local coord = unit:GetCoordinate() + local dist = math.floor(coord:Get2DDistance(reccecoord)/1000) -- km + local AGL = unit:GetAltitude(true) + local minheight = self.RadarBlurMinHeight or 250 -- meters + local thresheight = self.RadarBlurThresHeight or 90 -- 10% chance to find a low flying group + local thresblur = self.RadarBlurThresBlur or 85 -- 25% chance to escape the radar overall + --local dist = math.floor(Distance) + if dist <= self.RadarBlurClosing then + thresheight = (((dist*dist)/self.RadarBlurClosingSquare)*thresheight) + thresblur = (((dist*dist)/self.RadarBlurClosingSquare)*thresblur) + end + local fheight = math.floor(math.random(1,10000)/100) + local fblur = math.floor(math.random(1,10000)/100) + if fblur > thresblur then DetectionAccepted = false end + if AGL <= minheight and fheight < thresheight then DetectionAccepted = false end + if self.debug or self.verbose > 1 then + MESSAGE:New("Radar Blur",10):ToLogIf(self.debug):ToAllIf(self.verbose>1) + MESSAGE:New("Unit "..name.." is at "..math.floor(AGL).."m. Distance "..math.floor(dist).."km.",10):ToLogIf(self.debug):ToAllIf(self.verbose>1) + MESSAGE:New(string.format("fheight = %d/%d | fblur = %d/%d",fheight,thresheight,fblur,thresblur),10):ToLogIf(self.debug):ToAllIf(self.verbose>1) + MESSAGE:New("Detection Accepted = "..tostring(DetectionAccepted),10):ToLogIf(self.debug):ToAllIf(self.verbose>1) + end + end + + if DetectionAccepted then + DetectedUnits[name]=unit + RecceDetecting[name]=reccename + self:T(string.format("Unit %s detect by %s", name, reccename)) + end + else + if self.detectStatics then + local static=STATIC:FindByName(name, false) + if static then + --env.info("FF found static "..name) + DetectedUnits[name]=static + RecceDetecting[name]=reccename + end + end + end + + else + -- Warning! + self:T(self.lid..string.format("WARNING: Could not get name of detected object ID=%s! Detected by %s", DetectedObject.id_, 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) + + -- Debug text. + self:F(self.lid..string.format("NEW contact %s", Contact.groupname)) + + -- Add to table of unknown contacts. + table.insert(self.ContactsUnknown, Contact) + return self +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 Lost contact. +function INTEL:onafterLostContact(From, Event, To, Contact) + + -- Debug text. + self:F(self.lid..string.format("LOST contact %s", Contact.groupname)) + + -- Add to table of lost contacts. + table.insert(self.ContactsLost, Contact) + return self +end + +--- On after "NewCluster" event. +-- @param #INTEL self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #INTEL.Cluster Cluster Detected cluster. +function INTEL:onafterNewCluster(From, Event, To, Cluster) + + -- Debug text. + self:F(self.lid..string.format("NEW cluster #%d [%s] of size %d", Cluster.index, Cluster.ctype, Cluster.size)) + + -- Add cluster to table. + self:_AddCluster(Cluster) + return self +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) + + -- Debug text. + local text = self.lid..string.format("LOST cluster #%d [%s]", Cluster.index, Cluster.ctype) + + 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) + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Make the INTEL aware of a object that was not detected (yet). This will add the object to the contacts table and trigger a `NewContact` event. +-- @param #INTEL self +-- @param Wrapper.Positionable#POSITIONABLE Positionable Group or static object. +-- @param #string RecceName Name of the recce group that detected this object. +-- @param #number Tdetected Abs. mission time in seconds, when the object is detected. Default now. +-- @return #INTEL self +function INTEL:KnowObject(Positionable, RecceName, Tdetected) + + local Tnow=timer.getAbsTime() + Tdetected=Tdetected or Tnow + + if Positionable and Positionable:IsAlive() then + + if Tdetected>Tnow then + -- Delay call. + self:ScheduleOnce(Tdetected-Tnow, self.KnowObject, self, Positionable, RecceName) + else + + -- Name of the object. + local name=Positionable:GetName() + + -- Try to get the contact by name. + local contact=self:GetContactByName(name) + + if contact then + + -- Update contact info. + self:_UpdateContact(contact) + + else + + -- Create new contact. + contact=self:_CreateContact(Positionable, RecceName) + + if contact then + + -- Debug info. + self:T(string.format("%s contact detected by %s", contact.groupname, RecceName or "unknown")) + + -- Add contact to table. + self:AddContact(contact) + + -- Trigger new contact event. + self:NewContact(contact) + + end + + end + end + end + + return self +end + +--- 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 + +--- Check if a Contact is already known. It is checked, whether the contact is in the contacts table. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact to be added. +-- @return #boolean If `true`, contact is already known. +function INTEL:_IsContactKnown(Contact) + + for i,_contact in pairs(self.Contacts) do + local contact=_contact --#INTEL.Contact + if contact.groupname==Contact.groupname then + return true + end + end + + return false +end + + +--- Add a contact to our list. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact to be added. +-- @return #INTEL self +function INTEL:AddContact(Contact) + + -- First check if the contact is already in the table. + if self:_IsContactKnown(Contact) then + self:E(self.lid..string.format("WARNING: Contact %s is already in the contact table!", tostring(Contact.groupname))) + else + self:T(self.lid..string.format("Adding new Contact %s to table", tostring(Contact.groupname))) + table.insert(self.Contacts, Contact) + end + + return self +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 + return self +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 + + -- We never forget statics as they don't move. + if Contact.isStatic then + return false + end + + -- Time since last detected. + local dT=timer.getAbsTime()-Contact.Tdetected + + local dTforget=nil + + 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() + self:F(self.lid.."Painting Picture!") + + -- First remove all lost contacts from clusters. + for _,_contact in pairs(self.ContactsLost) do + local contact=_contact --#INTEL.Contact + + -- Get cluster this contact belongs to (if any). + local cluster=self:GetClusterOfContact(contact) + + if cluster then + self:RemoveContactFromCluster(contact, cluster) + end + end + + -- Clean up cluster table. + local ClusterSet = {} + + -- Now check if whole clusters were lost. + for _i,_cluster in pairs(self.Clusters) do + local cluster=_cluster --#INTEL.Cluster + + if cluster.size>0 and self:ClusterCountUnits(cluster)>0 then + -- This one has size>0 and units>0 + table.insert(ClusterSet,_cluster) + else + + -- This cluster is gone. + + -- Remove marker. + if cluster.marker then + cluster.marker:Remove() + end + + -- Marker of the arrow. + if cluster.markerID then + COORDINATE:RemoveMark(cluster.markerID) + end + + -- Lost cluster. + self:LostCluster(cluster, cluster.mission) + end + end + + -- Set Clusters. + self.Clusters = ClusterSet + + -- Update positions. + self:_UpdateClusterPositions() + + + for _,_contact in pairs(self.Contacts) do + local contact=_contact --#INTEL.Contact + + -- Debug info. + self:T(string.format("Paint Picture: checking for %s",contact.groupname)) + + -- Get the current cluster (if any) this contact belongs to. + local currentcluster=self:GetClusterOfContact(contact) + + if currentcluster then + --- + -- Contact is currently part of a cluster. + --- + + -- Check if the contact is still connected to the cluster. + local isconnected=self:IsContactConnectedToCluster(contact, currentcluster) + + if isconnected then + + else + + --- Not connected to current cluster any more. + + -- Remove from current cluster. + self:RemoveContactFromCluster(contact, currentcluster) + + -- Find new cluster. + local cluster=self:_GetClosestClusterOfContact(contact) + + if cluster then + -- Add contact to cluster. + self:AddContactToCluster(contact, cluster) + else + + -- Create a new cluster. + local newcluster=self:_CreateClusterFromContact(contact) + + -- Trigger new cluster event. + self:NewCluster(newcluster) + end + + end + + else + + --- + -- Contact is not in any cluster yet. + --- + + -- Debug info. + self:T(self.lid..string.format("Paint Picture: contact %s has NO current cluster", contact.groupname)) + + -- Get the closest existing cluster of this contact. + local cluster=self:_GetClosestClusterOfContact(contact) + + if cluster then + + -- Debug info. + self:T(self.lid..string.format("Paint Picture: contact %s has closest cluster #%d",contact.groupname, cluster.index)) + + -- Add contact to this cluster. + self:AddContactToCluster(contact, cluster) + + else + + -- Debug info. + self:T(self.lid..string.format("Paint Picture: contact %s has no closest cluster ==> Create new cluster", contact.groupname)) + + -- Create a brand new cluster. + local newcluster=self:_CreateClusterFromContact(contact) + + -- Trigger event for a new cluster. + self:NewCluster(newcluster) + end + + end + + end + + -- Update positions. + self:_UpdateClusterPositions() + + -- 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. + if self.verbose >= 1 then + BASE:I("Updating cluster marker and future position") + end + + -- Update cluster markers. + self:UpdateClusterMarker(cluster) + + -- Extrapolate future position of the cluster. + self:CalcClusterFuturePosition(cluster, 300) + + end + end + + return self +end + +--- Create a new cluster. +-- @param #INTEL self +-- @return #INTEL.Cluster cluster The cluster. +function INTEL:_CreateCluster() + + -- Create new cluster. + local cluster={} --#INTEL.Cluster + + cluster.index=self.clustercounter + cluster.coordinate=COORDINATE:New(0, 0, 0) + cluster.threatlevelSum=0 + cluster.threatlevelMax=0 + cluster.size=0 + cluster.Contacts={} + cluster.altitude=0 + + -- Increase counter. + self.clustercounter=self.clustercounter+1 + + return cluster +end + +--- Create a new cluster from a first contact. The contact is automatically added to the cluster. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The first contact. +-- @return #INTEL.Cluster cluster The cluster. +function INTEL:_CreateClusterFromContact(Contact) + + local cluster=self:_CreateCluster() + + self:T(self.lid..string.format("Created NEW cluster #%d with first contact %s", cluster.index, Contact.groupname)) + + cluster.coordinate:UpdateFromCoordinate(Contact.position) + + cluster.ctype=Contact.ctype + + self:AddContactToCluster(Contact, cluster) + + return cluster +end + +--- Add cluster to table. +-- @param #INTEL self +-- @param #INTEL.Cluster Cluster The cluster to add. +function INTEL:_AddCluster(Cluster) + + --TODO: Check if cluster is already in the table. + + -- Add cluster. + table.insert(self.Clusters, Cluster) + + return self +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) + + -- Add to threat level sum. + cluster.threatlevelSum=cluster.threatlevelSum+contact.threatlevel + + -- Increase size. + cluster.size=cluster.size+1 + + -- alt + self:GetClusterAltitude(cluster,true) + + -- Debug info. + self:T(self.lid..string.format("Adding contact %s to cluster #%d [%s] ==> New size=%d", contact.groupname, cluster.index, cluster.ctype, cluster.size)) + end + + return self +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=#cluster.Contacts,1,-1 do + local Contact=cluster.Contacts[i] --#INTEL.Contact + + if Contact.groupname==contact.groupname then + + -- Remove threat level sum. + cluster.threatlevelSum=cluster.threatlevelSum-contact.threatlevel + + -- Decrease cluster size. + cluster.size=cluster.size-1 + + -- Remove from table. + table.remove(cluster.Contacts, i) + + -- Debug info. + self:T(self.lid..string.format("Removing contact %s from cluster #%d ==> New cluster size=%d", contact.groupname, cluster.index, cluster.size)) + + return self + end + + end + + end + return self +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 speedsum = 0 + local n=0 + for _,_contact in pairs(cluster.Contacts) do + local contact=_contact --#INTEL.Contact + + if (not contact.isStatic) and contact.group:IsAlive() then + local speed = contact.group:GetVelocityKNOTS() + direction = direction + (contact.group:GetHeading()*speed) + n=n+1 + speedsum = speedsum + speed + end + end + + --TODO: This calculation is WRONG! + -- Simple example for two groups: + -- First group is going West, i.e. heading 090 + -- Second group is going East, i.e. heading 270 + -- Total is 360/2=180, i.e. South! + -- It should not go anywhere as the two movements cancel each other. + -- Apple - Correct, edge case for N=2^x, but when 2 pairs of groups drive in exact opposite directions, the cluster will split at some point? + -- maybe add the speed as weight to get a weighted factor: + + if n==0 then + return 0 + else + return math.floor(direction / (speedsum * n )) + end + +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 contact=_contact --#INTEL.Contact + + if (not contact.isStatic) and contact.group:IsAlive() then + velocity = velocity + contact.group:GetVelocityMPS() + n=n+1 + end + + end + + if n==0 then + return 0 + else + return math.floor(velocity / n) + end +end + +--- Calculate cluster velocity vector. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @return DCS#Vec3 Velocity vector in m/s. +function INTEL:CalcClusterVelocityVec3(cluster) + + local v={x=0, y=0, z=0} --DCS#Vec3 + + for _,_contact in pairs(cluster.Contacts) do + local contact=_contact --#INTEL.Contact + + if (not contact.isStatic) and contact.group:IsAlive() then + local vec=contact.group:GetVelocityVec3() + v.x=v.x+vec.x + v.y=v.y+vec.y + v.z=v.y+vec.z + end + end + + return v +end + +--- Calculate cluster future position after given seconds. +-- @param #INTEL self +-- @param #INTEL.Cluster cluster The cluster of contacts. +-- @param #number seconds Time interval in seconds. Default is `self.prediction`. +-- @return Core.Point#COORDINATE Calculated future position of the cluster. +function INTEL:CalcClusterFuturePosition(cluster, seconds) + + -- Get current position of the cluster. + local p=self:GetClusterCoordinate(cluster) + + -- Velocity vector in m/s. + local v=self:CalcClusterVelocityVec3(cluster) + + -- Time in seconds. + local t=seconds or self.prediction + + -- Extrapolated vec3. + local Vec3={x=p.x+v.x*t, y=p.y+v.y*t, z=p.z+v.z*t} + + -- Future position. + local futureposition=COORDINATE:NewFromVec3(Vec3) + + -- Create an arrow pointing in the direction of the movement. + if self.clustermarkers and self.clusterarrows then + if cluster.markerID then + COORDINATE:RemoveMark(cluster.markerID) + end + cluster.markerID = p:ArrowToAll(futureposition, self.coalition, {1,0,0}, 1, {1,1,0}, 0.5, 2, true, "Position 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. +-- @return #number Distance to cluster in meters. +function INTEL:IsContactConnectedToCluster(contact, cluster) + + -- Must be of the same type. We do not want to mix aircraft with ground units. + if contact.ctype~=cluster.ctype then + return false, math.huge + end + + for _,_contact in pairs(cluster.Contacts) do + local Contact=_contact --#INTEL.Contact + + -- Do not calcuate the distance to the contact itself unless it is the only contact in the cluster. + if Contact.groupname~=contact.groupname or cluster.size==1 then + + --local dist=Contact.position:Get2DDistance(contact.position) + local dist=Contact.position:DistanceFromPointVec2(contact.position) + + -- AIR - check for spatial proximity (corrected because airprox was always false for ctype~=INTEL.Ctype.AIRCRAFT) + local airprox = true + if contact.ctype == INTEL.Ctype.AIRCRAFT then + self:T(string.format("Cluster Alt=%d | Contact Alt=%d",cluster.altitude,contact.altitude)) + local adist = math.abs(cluster.altitude - contact.altitude) + if adist > UTILS.FeetToMeters(10000) then -- limit to 10kft + airprox = false + end + end + + if dist UTILS.FeetToMeters(10000) then + airprox = false + end + end + + if dist0 then + avgalt = newalt/n + end + + -- Update cluster coordinate. + Cluster.altitude = avgalt + + self:T(string.format("Updating Cluster Altitude: %d",Cluster.altitude)) + + return Cluster.altitude +end + +--- Get the coordinate of a cluster. +-- @param #INTEL self +-- @param #INTEL.Cluster Cluster The cluster. +-- @param #boolean Update If `true`, update the coordinate. Default is to just return the last stored position. +-- @return Core.Point#COORDINATE The coordinate of this cluster. +function INTEL:GetClusterCoordinate(Cluster, Update) + + -- Init. + local x=0 ; local y=0 ; local z=0 ; local n=0 + + -- Loop over all contacts. + for _,_contact in pairs(Cluster.Contacts) do + local contact=_contact --#INTEL.Contact + + local vec3=nil --DCS#Vec3 + + if Update and contact.group and contact.group:IsAlive() then + vec3 = contact.group:GetVec3() + end + + if not vec3 then + vec3=contact.position + end + + if vec3 then + + -- Sum up posits. + x=x+vec3.x + y=y+vec3.y + z=z+vec3.z + + -- Increase counter. + n=n+1 + + end + + end + + if n>0 then + + -- Average. + local Vec3={x=x/n, y=y/n, z=z/n} --DCS#Vec3 + + -- Update cluster coordinate. + Cluster.coordinate:UpdateFromVec3(Vec3) + + end + + return Cluster.coordinate +end + +--- Check if the coorindate of the cluster changed. +-- @param #INTEL self +-- @param #INTEL.Cluster Cluster The cluster. +-- @param #number Threshold in meters. Default 100 m. +-- @param Core.Point#COORDINATE Coordinate Reference coordinate. Default is the last known coordinate of the cluster. +-- @return #boolean If `true`, the coordinate changed by more than the given threshold. +function INTEL:_CheckClusterCoordinateChanged(Cluster, Coordinate, Threshold) + + Threshold=Threshold or 100 + + Coordinate=Coordinate or Cluster.coordinate + + -- Positions of cluster. + local a=Coordinate:GetVec3() + local b=self:GetClusterCoordinate(Cluster, true):GetVec3() + + local dist=UTILS.VecDist3D(a,b) + + if dist>Threshold 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 cluster=_cluster --#INTEL.Cluster + + -- Update cluster coordinate. + local coord = self:GetClusterCoordinate(cluster, true) + local alt = self:GetClusterAltitude(cluster,true) + + -- Debug info. + self:T(self.lid..string.format("Updating Cluster position size: %s", cluster.size)) + end + return self +end + +--- Count number of alive units in contact. +-- @param #INTEL self +-- @param #INTEL.Contact Contact The contact. +-- @return #number unitcount +function INTEL:ContactCountUnits(Contact) + if Contact.isStatic then + if Contact.group and Contact.group:IsAlive() then + return 1 + else + return 0 + end + else + if Contact.group then + local n=Contact.group:CountAliveUnits() + return n + else + return 0 + end + end +end + +--- Count number of alive units in cluster. +-- @param #INTEL self +-- @param #INTEL.Cluster Cluster The cluster +-- @return #number unitcount +function INTEL:ClusterCountUnits(Cluster) + local unitcount = 0 + for _,_contact in pairs (Cluster.Contacts) do + local contact=_contact --#INTEL.Contact + unitcount = unitcount + self:ContactCountUnits(contact) + 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: %s\nSize %d\nUnits %d\nTLsum=%d", cluster.index, cluster.ctype, cluster.size, unitcount, cluster.threatlevelSum) + + if not cluster.marker then + + -- First time ==> need to create a new marker object. + cluster.marker=MARKER:New(cluster.coordinate, text):ToCoalition(self.coalition) + + else + + -- Need to refresh? + local refresh=false + + -- Check if marker text changed. + if cluster.marker.text~=text then + cluster.marker.text=text + refresh=true + end + + -- Check if coordinate changed. + local coordchange=self:_CheckClusterCoordinateChanged(cluster, cluster.marker.coordinate) + if coordchange then + cluster.marker.coordinate:UpdateFromCoordinate(cluster.coordinate) + refresh=true + end + + if refresh then + cluster.marker:Refresh() + end + + end + + return self +end + +--- Get the contact with the highest threat level from the cluster. +-- @param #INTEL self +-- @param #INTEL.Cluster Cluster The cluster. +-- @return #INTEL.Contact the contact or nil if none +function INTEL:GetHighestThreatContact(Cluster) + local threatlevel=-1 + local rcontact = nil + + for _,_contact in pairs(Cluster.Contacts) do + + local contact=_contact --Ops.Intel#INTEL.Contact + + if contact.threatlevel>threatlevel then + threatlevel=contact.threatlevel + rcontact = contact + end + + end + return rcontact +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.Intel#INTEL.Contact contacts. +-- @field #table clusters Table of Ops.Intel#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 = 120, + interval = 20, + contacts = {}, + clusters = {}, + contactcoords = {}, +} + +--- Version string +-- @field #string version +INTEL_DLINK.version = "0.0.2" + +--- Function to instantiate a new object +-- @param #INTEL_DLINK self +-- @param #table Intels Table of Ops.Intel#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_DLINK + + self.intels = Intels or {} + self.contacts = {} + self.clusters = {} + self.contactcoords = {} + + -- Set alias. + if Alias then + self.alias=tostring(Alias) + else + self.alias="SPECTRE" + end + + -- 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) + + -- Cache time + self:SetDLinkCacheTime(Cachetime or 120) + + -- 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.Intel#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 set how long INTEL DLINK remembers contacts. + -- @param #INTEL_DLINK self + -- @param #number seconds Remember this many seconds. Defaults to 180. + -- @return #INTEL_DLINK self + function INTEL_DLINK:SetDLinkCacheTime(seconds) + self.cachetime = math.abs(seconds or 120) + self:I(self.lid.."Caching for "..self.cachetime.." seconds.") + return self + end + +--- Function to collect data from the various #INTEL +-- @param #INTEL_DLINK self +-- @param #string From The From state +-- @param #string Event The Event triggering this call +-- @param #string To The To state +-- @return #INTEL_DLINK self +function INTEL_DLINK:onbeforeCollect(From, Event, To) + self:T({From, Event, To}) + -- run through our #INTEL objects and gather the contacts tables + self:T("Contacts Data Gathering") + local newcontacts = {} + local intels = self.intels -- #table + for _,_intel in pairs (intels) do + _intel = _intel -- #INTEL + if _intel:Is("Running") then + local ctable = _intel:GetContactTable() or {} -- #INTEL.Contact + for _,_contact in pairs (ctable) do + local _ID = string.format("%s-%d",_contact.groupname, _contact.Tdetected) + self:T(string.format("Adding %s",_ID)) + newcontacts[_ID] = _contact + end + end + end + -- clean up for stale contacts and dupes + self:T("Cleanup") + local contacttable = {} + local coordtable = {} + local TNow = timer.getAbsTime() + local Tcache = self.cachetime + for _ind, _contact in pairs(newcontacts) do -- #string, #INTEL.Contact + if TNow - _contact.Tdetected < Tcache then + if (not contacttable[_contact.groupname]) or (contacttable[_contact.groupname] and contacttable[_contact.groupname].Tdetected < _contact.Tdetected) then + self:T(string.format("Adding %s",_contact.groupname)) + contacttable[_contact.groupname] = _contact + table.insert(coordtable,_contact.position) + end + end + end + -- run through our #INTEL objects and gather the clusters tables + self:T("Clusters Data Gathering") + local newclusters = {} + local intels = self.intels -- #table + for _,_intel in pairs (intels) do + _intel = _intel -- #INTEL + if _intel:Is("Running") then + local ctable = _intel:GetClusterTable() or {} -- #INTEL.Cluster + for _,_cluster in pairs (ctable) do + local _ID = string.format("%s-%d", _intel.alias, _cluster.index) + self:T(string.format("Adding %s",_ID)) + table.insert(newclusters,_cluster) + end + end + end + -- update self tables + self.contacts = contacttable + self.contactcoords = coordtable + self.clusters = newclusters + self:__Collected(1, contacttable, newclusters) -- make table available via FSM Event + -- schedule next round + local interv = self.interval * -1 + self:__Collect(interv) + return self +end + +--- Function called after collection is done +-- @param #INTEL_DLINK self +-- @param #string From The From state +-- @param #string Event The Event triggering this call +-- @param #string To The To state +-- @param #table Contacts The table of collected #INTEL.Contact contacts +-- @param #table Clusters The table of collected #INTEL.Cluster clusters +-- @return #INTEL_DLINK self +function INTEL_DLINK:onbeforeCollected(From, Event, To, Contacts, Clusters) + self:T({From, Event, To}) + return self +end + +--- Function to stop +-- @param #INTEL_DLINK self +-- @param #string From The From state +-- @param #string Event The Event triggering this call +-- @param #string To The To state +-- @return #INTEL_DLINK self +function INTEL_DLINK:onafterStop(From, Event, To) + self:T({From, Event, To}) + local text = string.format("Version %s stopped.", self.version) + self:I(self.lid .. text) + return self +end + +--- Function to query the detected contacts +-- @param #INTEL_DLINK self +-- @return #table Table of #INTEL.Contact contacts +function INTEL_DLINK:GetContactTable() + self:T(self.lid .. "GetContactTable") + return self.contacts +end + +--- Function to query the detected clusters +-- @param #INTEL_DLINK self +-- @return #table Table of #INTEL.Cluster clusters +function INTEL_DLINK:GetClusterTable() + self:T(self.lid .. "GetClusterTable") + return self.clusters +end + +--- Function to query the detected contact coordinates +-- @param #INTEL_DLINK self +-- @return #table Table of the contacts' Core.Point#COORDINATE objects. +function INTEL_DLINK:GetDetectedItemCoordinates() + self:T(self.lid .. "GetDetectedItemCoordinates") + return self.contactcoords +end + +---------------------------------------------------------------------------------------------- +-- End INTEL_DLINK +---------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Legion.lua b/Moose Development/Moose/Ops/Legion.lua new file mode 100644 index 000000000..513ed07f0 --- /dev/null +++ b/Moose Development/Moose/Ops/Legion.lua @@ -0,0 +1,3484 @@ +--- **Ops** - Legion Warehouse. +-- +-- Parent class of Airwings, Brigades and Fleets. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.Legion +-- @image OPS_Legion.png + + +--- LEGION class. +-- @type LEGION +-- @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 missionqueue Mission queue table. +-- @field #table transportqueue Transport queue. +-- @field #table cohorts Cohorts of this legion. +-- @field Ops.Commander#COMMANDER commander Commander of this legion. +-- @field Ops.Chief#CHIEF chief Chief of this legion. +-- @field #boolean tacview If `true`, show tactical overview on status update. +-- @extends Functional.Warehouse#WAREHOUSE + +--- *Per aspera ad astra.* +-- +-- === +-- +-- # The LEGION Concept +-- +-- The LEGION class contains all functions that are common for the AIRWING, BRIGADE and FLEET classes, which inherit the LEGION class. +-- +-- An LEGION consists of multiple COHORTs. These cohorts "live" in a WAREHOUSE, i.e. a physical structure that can be destroyed or captured. +-- +-- ** The LEGION class is not meant to be used directly. Use AIRWING, BRIGADE or FLEET instead! ** +-- +-- @field #LEGION +LEGION = { + ClassName = "LEGION", + verbose = 0, + lid = nil, + missionqueue = {}, + transportqueue = {}, + cohorts = {}, +} + +--- Random score that is added to the asset score in the selection process. +-- @field #number RandomAssetScore +LEGION.RandomAssetScore=1 + + +--- LEGION class version. +-- @field #string version +LEGION.version="0.5.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Create FLEED class. +-- DONE: Relocate cohorts. +-- DONE: Aircraft will not start hot on Alert5. +-- DONE: OPS transport. +-- DONE: Make general so it can be inherited by AIRWING and BRIGADE classes. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new LEGION class object. +-- @param #LEGION self +-- @param #string WarehouseName Name of the warehouse STATIC or UNIT object representing the warehouse. +-- @param #string LegionName Name of the legion. Must be **unique**! +-- @return #LEGION self +function LEGION:New(WarehouseName, LegionName) + + -- Inherit everything from WAREHOUSE class. + local self=BASE:Inherit(self, WAREHOUSE:New(WarehouseName, LegionName)) -- #LEGION + + -- 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("LEGION %s | ", self.alias) + + -- Defaults: + self:SetMarker(false) + + -- Dead and crash events are handled via opsgroups. + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.Dead) + + -- 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("*", "MissionAssign", "*") -- Recruit assets, add to queue and request immediately. + + self:AddTransition("*", "TransportRequest", "*") -- Add a (mission) request to the warehouse. + self:AddTransition("*", "TransportCancel", "*") -- Cancel transport. + self:AddTransition("*", "TransportAssign", "*") -- Recruit assets, add to queue and request immediately. + + self:AddTransition("*", "OpsOnMission", "*") -- An OPSGROUP was send on a Mission (AUFTRAG). + + self:AddTransition("*", "LegionAssetReturned", "*") -- An asset returned (from a mission) to the Legion warehouse. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the LEGION. Initializes parameters and starts event handlers. + -- @function [parent=#LEGION] Start + -- @param #LEGION self + + --- Triggers the FSM event "Start" after a delay. Starts the LEGION. Initializes parameters and starts event handlers. + -- @function [parent=#LEGION] __Start + -- @param #LEGION self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the LEGION and all its event handlers. + -- @param #LEGION self + + --- Triggers the FSM event "Stop" after a delay. Stops the LEGION and all its event handlers. + -- @function [parent=#LEGION] __Stop + -- @param #LEGION self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "MissionCancel". + -- @function [parent=#LEGION] MissionCancel + -- @param #LEGION self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "MissionAssign". + -- @function [parent=#LEGION] MissionAssign + -- @param #LEGION self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The legion(s) from which the mission assets are requested. + + --- Triggers the FSM event "MissionAssign" after a delay. + -- @function [parent=#LEGION] __MissionAssign + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The legion(s) from which the mission assets are requested. + + --- On after "MissionAssign" event. + -- @function [parent=#LEGION] OnAfterMissionAssign + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Legions The legion(s) from which the mission assets are requested. + + + --- Triggers the FSM event "MissionRequest". + -- @function [parent=#LEGION] MissionRequest + -- @param #LEGION self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Assets (Optional) Assets to add. + + --- Triggers the FSM event "MissionRequest" after a delay. + -- @function [parent=#LEGION] __MissionRequest + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Assets (Optional) Assets to add. + + --- On after "MissionRequest" event. + -- @function [parent=#LEGION] OnAfterMissionRequest + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + -- @param #table Assets (Optional) Assets to add. + + + --- Triggers the FSM event "MissionCancel" after a delay. + -- @function [parent=#LEGION] __MissionCancel + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionCancel" event. + -- @function [parent=#LEGION] OnAfterMissionCancel + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "TransportAssign". + -- @function [parent=#LEGION] TransportAssign + -- @param #LEGION self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + --- Triggers the FSM event "TransportAssign" after a delay. + -- @function [parent=#LEGION] __TransportAssign + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + --- On after "TransportAssign" event. + -- @function [parent=#LEGION] OnAfterTransportAssign + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + -- @param #table Legions The legion(s) to which this transport is assigned. + + + --- Triggers the FSM event "TransportRequest". + -- @function [parent=#LEGION] TransportRequest + -- @param #LEGION self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportRequest" after a delay. + -- @function [parent=#LEGION] __TransportRequest + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportRequest" event. + -- @function [parent=#LEGION] OnAfterTransportRequest + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + + --- Triggers the FSM event "TransportCancel". + -- @function [parent=#LEGION] TransportCancel + -- @param #LEGION self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportCancel" after a delay. + -- @function [parent=#LEGION] __TransportCancel + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportCancel" event. + -- @function [parent=#LEGION] OnAfterTransportCancel + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + + --- Triggers the FSM event "OpsOnMission". + -- @function [parent=#LEGION] OpsOnMission + -- @param #LEGION self + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "OpsOnMission" after a delay. + -- @function [parent=#LEGION] __OpsOnMission + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "OpsOnMission" event. + -- @function [parent=#LEGION] OnAfterOpsOnMission + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "LegionAssetReturned". + -- @function [parent=#LEGION] LegionAssetReturned + -- @param #LEGION self + -- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to. + -- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned. + + --- Triggers the FSM event "LegionAssetReturned" after a delay. + -- @function [parent=#LEGION] __LegionAssetReturned + -- @param #LEGION self + -- @param #number delay Delay in seconds. + -- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to. + -- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned. + + --- On after "LegionAssetReturned" event. Triggered when an asset group returned to its Legion. + -- @function [parent=#LEGION] OnAfterLegionAssetReturned + -- @param #LEGION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to. + -- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned. + + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set verbosity level. +-- @param #LEGION self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #LEGION self +function LEGION:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set tactical overview on. +-- @param #LEGION self +-- @return #LEGION self +function LEGION:SetTacticalOverviewOn() + self.tacview=true + return self +end + +--- Add a mission for the legion. It will pick the best available assets for the mission and lauch it when ready. +-- @param #LEGION self +-- @param Ops.Auftrag#AUFTRAG Mission Mission for this legion. +-- @return #LEGION self +function LEGION:AddMission(Mission) + + -- Set status to QUEUED. This event is only allowed for the first legion that calls it. + Mission:Queued() + + -- Set legion status. + Mission:SetLegionStatus(self, AUFTRAG.Status.QUEUED) + + -- Add legion to mission. + Mission:AddLegion(self) + + -- Set target for ALERT 5. + if Mission.type==AUFTRAG.Type.ALERT5 then + Mission:_TargetFromObject(self:GetCoordinate()) + end + + -- 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 #LEGION self +-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed. +-- @return #LEGION self +function LEGION:RemoveMission(Mission) + + for i,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if mission.auftragsnummer==Mission.auftragsnummer then + mission:RemoveLegion(self) + table.remove(self.missionqueue, i) + break + end + + end + + return self +end + +--- Add transport assignment to queue. +-- @param #LEGION self +-- @param Ops.OpsTransport#OPSTRANSPORT OpsTransport Transport assignment. +-- @return #LEGION self +function LEGION:AddOpsTransport(OpsTransport) + + -- Is not queued at a legion. + OpsTransport:Queued() + + -- Set legion status. + OpsTransport:SetLegionStatus(self, AUFTRAG.Status.QUEUED) + + -- Add mission to queue. + table.insert(self.transportqueue, OpsTransport) + + -- Add this legion to the transport. + OpsTransport:AddLegion(self) + + -- Info text. + local text=string.format("Added Transport %s. Starting at %s-%s", + tostring(OpsTransport.uid), UTILS.SecondsToClock(OpsTransport.Tstart, true), OpsTransport.Tstop and UTILS.SecondsToClock(OpsTransport.Tstop, true) or "INF") + self:T(self.lid..text) + + return self +end + +--- Add cohort to cohort table of this legion. +-- @param #LEGION self +-- @param Ops.Cohort#COHORT Cohort The cohort to be added. +-- @return #LEGION self +function LEGION:AddCohort(Cohort) + + if self:IsCohort(Cohort.name) then + + self:E(self.lid..string.format("ERROR: A cohort with name %s already exists in this legion. Cohorts must have UNIQUE names!")) + + else + + -- Add cohort to legion. + table.insert(self.cohorts, Cohort) + + end + + return self +end + +--- Remove cohort from cohor table of this legion. +-- @param #LEGION self +-- @param Ops.Cohort#COHORT Cohort The cohort to be added. +-- @return #LEGION self +function LEGION:DelCohort(Cohort) + + for i=#self.cohorts,1,-1 do + local cohort=self.cohorts[i] --Ops.Cohort#COHORT + if cohort.name==Cohort.name then + self:T(self.lid..string.format("Removing Cohort %s", tostring(cohort.name))) + table.remove(self.cohorts, i) + end + end + + return self +end + +--- Remove specific asset from legion. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. +-- @return #LEGION self +function LEGION:DelAsset(Asset) + + if Asset.cohort then + Asset.cohort:DelAsset(Asset) + else + self:E(self.lid..string.format("ERROR: Asset has not cohort attached. Cannot remove it from legion!")) + end + + return self +end + + +--- Relocate a cohort to another legion. +-- Assets in stock are spawned and routed to the new legion. +-- If assets are spawned, running missions will be cancelled. +-- Cohort assets will not be available until relocation is finished. +-- @param #LEGION self +-- @param Ops.Cohort#COHORT Cohort The cohort to be relocated. +-- @param Ops.Legion#LEGION Legion The legion where the cohort is relocated to. +-- @param #number Delay Delay in seconds before relocation takes place. Default `nil`, *i.e.* ASAP. +-- @param #number NcarriersMin Min number of transport carriers in case the troops should be transported. Default `nil` for no transport. +-- @param #number NcarriersMax Max number of transport carriers. +-- @param #table TransportLegions Legion(s) assigned for transportation. Default is that transport assets can only be recruited from this legion. +-- @return #LEGION self +function LEGION:RelocateCohort(Cohort, Legion, Delay, NcarriersMin, NcarriersMax, TransportLegions) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, LEGION.RelocateCohort, self, Cohort, Legion, 0, NcarriersMin, NcarriersMax, TransportLegions) + else + + -- Add cohort to legion. + if Legion:IsCohort(Cohort.name) then + self:E(self.lid..string.format("ERROR: Cohort %s is already part of new legion %s ==> CANNOT Relocate!", Cohort.name, Legion.alias)) + return self + else + table.insert(Legion.cohorts, Cohort) + end + + -- Check that cohort is part of this legion + if not self:IsCohort(Cohort.name) then + self:E(self.lid..string.format("ERROR: Cohort %s is NOT part of this legion %s ==> CANNOT Relocate!", Cohort.name, self.alias)) + return self + end + + -- Check that legions are different. + if self.alias==Legion.alias then + self:E(self.lid..string.format("ERROR: old legion %s is same as new legion %s ==> CANNOT Relocate!", self.alias, Legion.alias)) + return self + end + + -- Trigger Relocate event. + Cohort:Relocate() + + -- Create a relocation mission. + local mission=AUFTRAG:_NewRELOCATECOHORT(Legion, Cohort) + + if false then + --- Disabled for now. + + -- Add assets to mission. + mission:_AddAssets(Cohort.assets) + + -- Debug info. + self:I(self.lid..string.format("Relocating Cohort %s [nassets=%d] to legion %s", Cohort.name, #Cohort.assets, Legion.alias)) + + -- Assign mission to this legion. + self:MissionAssign(mission, {self}) + + else + + -- Assign cohort to mission. + mission:AssignCohort(Cohort) + + -- All assets required. + mission:SetRequiredAssets(#Cohort.assets) + + -- Set transportation. + if NcarriersMin and NcarriersMin>0 then + mission:SetRequiredTransport(Legion.spawnzone, NcarriersMin, NcarriersMax) + end + + -- Assign transport legions. + if TransportLegions then + for _,legion in pairs(TransportLegions) do + mission:AssignTransportLegion(legion) + end + end + + -- Set mission range very large. Mission designer should know... + mission:SetMissionRange(10000) + + -- Add mission. + self:AddMission(mission) + end + + end + + return self +end + +--- Get cohort by name. +-- @param #LEGION self +-- @param #string CohortName Name of the platoon. +-- @return Ops.Cohort#COHORT The Cohort object. +function LEGION:_GetCohort(CohortName) + + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + if cohort.name==CohortName then + return cohort + end + + end + + return nil +end + +--- Check if cohort is part of this legion. +-- @param #LEGION self +-- @param #string CohortName Name of the platoon. +-- @return #boolean If `true`, cohort is part of this legion. +function LEGION:IsCohort(CohortName) + + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + if cohort.name==CohortName then + return true + end + + end + + return false +end + +--- Get name of legion. This is the alias of the warehouse. +-- @param #LEGION self +-- @return #string Name of legion. +function LEGION:GetName() + return self.alias +end + +--- Get cohort of an asset. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset. +-- @return Ops.Cohort#COHORT The Cohort object. +function LEGION:_GetCohortOfAsset(Asset) + local cohort=self:_GetCohort(Asset.squadname) + return cohort +end + + +--- Check if a BRIGADE class is calling. +-- @param #LEGION self +-- @return #boolean If true, this is a BRIGADE. +function LEGION:IsBrigade() + local is=self.ClassName==BRIGADE.ClassName + return is +end + +--- Check if the AIRWING class is calling. +-- @param #LEGION self +-- @return #boolean If true, this is an AIRWING. +function LEGION:IsAirwing() + local is=self.ClassName==AIRWING.ClassName + return is +end + +--- Check if the FLEET class is calling. +-- @param #LEGION self +-- @return #boolean If true, this is a FLEET. +function LEGION:IsFleet() + local is=self.ClassName==FLEET.ClassName + return is +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start LEGION FSM. +-- @param #LEGION self +function LEGION:onafterStart(From, Event, To) + + -- Start parent Warehouse. + self:GetParent(self, LEGION).onafterStart(self, From, Event, To) + + -- Info. + self:T3(self.lid..string.format("Starting LEGION v%s", LEGION.version)) + +end + +--- Check mission queue and assign ONE mission. +-- @param #LEGION self +-- @return #boolean If `true`, a mission was found and requested. +function LEGION:CheckMissionQueue() + + -- Number of missions. + local Nmissions=#self.missionqueue + + -- Treat special cases. + if Nmissions==0 then + return nil + end + + -- 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 + + -- Housekeeping + local TNow = timer.getTime() + if mission:IsOver() and mission:IsNotRepeatable() and mission.DeletionTimstamp == nil then + mission.DeletionTimstamp = TNow + end + if mission.DeletionTimstamp ~= nil and TNow - mission.DeletionTimstamp > 1800 then + mission = nil + end + end + + -- Check that runway is operational and that carrier is not recovering. + if self:IsAirwing() then + if self:IsRunwayOperational()==false then + return nil + end + local airboss=self.airboss --Ops.Airboss#AIRBOSS + if airboss then + if not airboss:IsIdle() then + return nil + end + end + 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 + + -- Number of current opsgroups. + local N=mission.Nassigned-mission.Ndead + + if N Reinforce=%s", + mission.reinforce, N, mission.Nassigned, mission.Ndead, mission.NassetsMin, tostring(reinforce))) + end + + -- Firstly, check if mission is due? + if (mission:IsQueued(self) or reinforce) and mission:IsReadyToGo() and (mission.importance==nil or mission.importance<=vip) then + + -- Recruit best assets for the job. + local recruited, assets, legions=self:RecruitAssetsForMission(mission) + + -- Did we find enough assets? + if recruited then + + -- Add to mission. + --mission:_AddAssets(assets) + + -- Recruit asset for escorting recruited mission assets. + local EscortAvail=self:RecruitAssetsForEscort(mission, assets) + + -- Transport available (or not required). + local TransportAvail=true + + -- Is escort required and available? + if EscortAvail then + + -- Recruit carrier assets for transport. + local Transport=nil + if mission.NcarriersMin then + + -- Transport legions. + local Legions=mission.transportLegions or {self} + + -- Assign carrier assets for transport. + TransportAvail, Transport=self:AssignAssetsForTransport(Legions, assets, mission.NcarriersMin, mission.NcarriersMax, mission.transportDeployZone, mission.transportDisembarkZone, mission.carrierCategories, mission.carrierAttributes, mission.carrierProperties) + end + + -- Add opstransport to mission. + if TransportAvail and Transport then + mission.opstransport=Transport + end + + end + + if EscortAvail and TransportAvail then + + -- Got a mission. + self:MissionRequest(mission, assets) + + -- Reduce number of reinforcements. + if reinforce then + mission.reinforce=mission.reinforce-#assets + self:T(self.lid..string.format("Reinforced with N=%d Nreinforce=%d", #assets, mission.reinforce)) + end + + return true + else + -- Recruited assets but no requested escort available. Unrecruit assets! + LEGION.UnRecruitAssets(assets, mission) + end + + end -- recruited mission assets + + end -- mission due? + end -- mission loop + + return nil +end + +--- Check transport queue and assign ONE transport. +-- @param #LEGION self +-- @return #boolean If `true`, a transport was found and requested. +function LEGION:CheckTransportQueue() + + -- Number of missions. + local Ntransports=#self.transportqueue + + -- Treat special cases. + if Ntransports==0 then + return nil + end + + -- TODO: Remove transports that are over! + + -- 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.prio Request and return. + self:TransportRequest(transport) + return true + end + + end + end + + -- No transport found. + return nil +end + + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "MissionAssign" event. Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission already. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @param #table Legions The LEGIONs. +function LEGION:onafterMissionAssign(From, Event, To, Mission, Legions) + + for _,_Legion in pairs(Legions) do + local Legion=_Legion --Ops.Legion#LEGION + + -- Debug info. + self:T(self.lid..string.format("Assigning mission %s (%s) to legion %s", Mission.name, Mission.type, Legion.alias)) + + -- Add mission to legion. + Legion:AddMission(Mission) + + -- Directly request the mission as the assets have already been selected. + Legion:MissionRequest(Mission) + + end + +end + +--- Create a request and add it to the warehouse queue. +-- @param #LEGION self +-- @param Functional.Warehouse#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 #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. +-- @return Functional.Warehouse#WAREHOUSE.Queueitem The request. +function LEGION:_AddRequest(AssetDescriptor, AssetDescriptorValue, nAsset, Prio, Assignment) + + -- Defaults. + nAsset=nAsset or 1 + Prio=Prio or 50 + + -- Increase id. + self.queueid=self.queueid+1 + + -- Request queue table item. + local request={ + uid=self.queueid, + prio=Prio, + warehouse=self, + assetdesc=AssetDescriptor, + assetdescval=AssetDescriptorValue, + nasset=nAsset, + transporttype=WAREHOUSE.TransportType.SELFPROPELLED, + ntransport=0, + assignment=tostring(Assignment), + airbase=self:GetAirbase(), + category=self:GetAirbaseCategory(), + ndelivered=0, + ntransporthome=0, + assets={}, + toself=true, + } --Functional.Warehouse#WAREHOUSE.Queueitem + + + -- Add request to queue. + table.insert(self.queue, request) + + local descval="assetlist" + if request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST then + + else + descval=tostring(request.assetdescval) + end + + local text=string.format("Warehouse %s: New request from warehouse %s.\nDescriptor %s=%s, #assets=%s; Transport=%s, #transports=%s.", + self.alias, self.alias, request.assetdesc, descval, tostring(request.nasset), request.transporttype, tostring(request.ntransport)) + self:_DebugMessage(text, 5) + + return request +end + + +--- On after "MissionRequest" event. Performs a self request to the warehouse for the mission assets. Sets mission status to REQUESTED. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +-- @param #table Assets (Optional) Assets to add. +function LEGION:onafterMissionRequest(From, Event, To, Mission, Assets) + + -- Debug info. + self:T(self.lid..string.format("MissionRequest for mission %s [%s]", Mission:GetName(), Mission:GetType())) + + -- Take provided assets or that of the mission. + Assets=Assets or Mission.assets + + -- Set mission status from QUEUED to REQUESTED. + Mission:Requested() + + -- Set legion status. Ensures that it is not considered in the next selection. + Mission:SetLegionStatus(self, AUFTRAG.Status.REQUESTED) + + --- + -- Some assets might already be spawned and even on a different mission (orbit). + -- Need to dived to set into spawned and instock assets and handle the other + --- + + -- Assets to be requested. + local Assetlist={} + + for _,_asset in pairs(Assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Check that this asset belongs to this Legion warehouse. + if asset.wid==self.uid then + + if asset.spawned then + + --- + -- Spawned Assets + --- + + if asset.flightgroup and not asset.flightgroup:IsMissionInQueue(Mission) then + + -- Add new mission. + asset.flightgroup:AddMission(Mission) + + --- + -- Special Missions + --- + + -- Get current mission. + local currM=asset.flightgroup:GetMissionCurrent() + + if currM then + + -- Cancel? + local cancel=false + -- Pause? + local pause=false + + -- Check if mission is INTERCEPT and asset is currently on GCI mission. If so, GCI is paused. + if (currM.type==AUFTRAG.Type.GCICAP or currM.type==AUFTRAG.Type.PATROLRACETRACK) and Mission.type==AUFTRAG.Type.INTERCEPT then + pause=true + elseif (currM.type==AUFTRAG.Type.ONGUARD or currM.type==AUFTRAG.Type.PATROLZONE) and (Mission.type==AUFTRAG.Type.ARTY or Mission.type==AUFTRAG.Type.GROUNDATTACK) then + pause=true + elseif currM.type==AUFTRAG.Type.NOTHING then + pause=true + end + + -- Cancel current ALERT5 mission. + if currM.type==AUFTRAG.Type.ALERT5 then + cancel=true + end + + -- Cancel current mission for relcation. + if Mission.type==AUFTRAG.Type.RELOCATECOHORT then + cancel=true + + -- Get request. + local request=currM:_GetRequest(self) + + if request then + self:T2(self.lid.."Removing group from cargoset") + request.cargogroupset:Remove(asset.spawngroupname, true) + else + self:E(self.lid.."ERROR: no request for spawned asset!") + end + + end + + -- Cancel mission. + if cancel then + self:T(self.lid..string.format("Cancel current mission %s [%s] to send group on mission %s [%s]", currM.name, currM.type, Mission.name, Mission.type)) + asset.flightgroup:MissionCancel(currM) + elseif pause then + self:T(self.lid..string.format("Pausing current mission %s [%s] to send group on mission %s [%s]", currM.name, currM.type, Mission.name, Mission.type)) + asset.flightgroup:PauseMission() + end + + -- Not reserved any more. + asset.isReserved=false + + end + + -- Add asset to mission. + Mission:AddAsset(asset) + + -- Trigger event. + self:__OpsOnMission(2, asset.flightgroup, Mission) + + else + self:T(self.lid.."ERROR: OPSGROUP for asset does NOT exist but it seems to be SPAWNED (asset.spawned=true)!") + end + + else + + --- + -- Stock Assets + --- + + -- These assets need to be requested and spawned. + table.insert(Assetlist, asset) + + end + + end + end + + -- Add request to legion warehouse. + if #Assetlist>0 then + + --local text=string.format("Requesting assets for mission %s:", Mission.name) + for i,_asset in pairs(Assetlist) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Set asset to requested! Important so that new requests do not use this asset! + asset.requested=true + + -- Spawned asset are not requested. + if asset.spawned then + asset.requested=false + end + + -- Not reserved and more. + asset.isReserved=false + + -- Set mission task so that the group is spawned with the right one. + if Mission.missionTask then + asset.missionTask=Mission.missionTask + end + + -- Set takeoff type to parking for ALERT5 missions. We dont want them to take off without a proper mission if squadron start is hot. + if Mission.type==AUFTRAG.Type.ALERT5 then + asset.takeoffType=COORDINATE.WaypointType.TakeOffParking + end + + -- Add asset to mission. + Mission:AddAsset(asset) + + end + + -- Set assignment. + -- TODO: Get/set functions for assignment string. + local assignment=string.format("Mission-%d", Mission.auftragsnummer) + + --local request=Mission:_GetRequest(self) + + -- Add request to legion warehouse. + --self:AddRequest(self, WAREHOUSE.Descriptor.ASSETLIST, Assetlist, #Assetlist, nil, nil, Mission.prio, assignment) + local request=self:_AddRequest(WAREHOUSE.Descriptor.ASSETLIST, Assetlist, #Assetlist, Mission.prio, assignment) + + -- Debug Info. + self:T(self.lid..string.format("Added request=%d for Nasssets=%d", request.uid, #Assetlist)) + + -- The queueid has been increased in the onafterAddRequest function. So we can simply use it here. + --Mission.requestID[self.alias]=self.queueid + Mission:_SetRequestID(self, self.queueid) + + -- Get request. + --local request=self:GetRequestByID(self.queueid) + + -- Debug info. + self:T(self.lid..string.format("Mission %s [%s] got Request ID=%d", Mission:GetName(), Mission:GetType(), self.queueid)) + + -- Request ship. + if request then + if self:IsShip() then + self:T(self.lid.."Warehouse physical structure is SHIP. Requestes assets will be late activated!") + request.lateActivation=true + end + end + + end + +end + +--- On after "TransportAssign" event. Transport is added to a LEGION transport queue and assets are requested from the LEGION warehouse. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. +-- @param #table Legions The legion(s) to which the transport is assigned. +function LEGION:onafterTransportAssign(From, Event, To, Transport, Legions) + + for _,_Legion in pairs(Legions) do + local Legion=_Legion --Ops.Legion#LEGION + + -- Debug info. + self:T(self.lid..string.format("Assigning transport %d to legion %s", Transport.uid, Legion.alias)) + + -- Add mission to legion. + Legion:AddOpsTransport(Transport) + + -- Directly request the mission as the assets have already been selected. + Legion:TransportRequest(Transport) + + end + +end + +--- On after "TransportRequest" event. Performs a self request to the warehouse for the transport assets. Sets transport status to REQUESTED. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Opstransport The requested mission. +function LEGION:onafterTransportRequest(From, Event, To, OpsTransport) + + -- List of assets that will be requested. + local AssetList={} + + --TODO: Find spawned assets on ALERT 5 mission OPSTRANSPORT. + + --local text=string.format("Requesting assets for mission %s:", Mission.name) + for i,_asset in pairs(OpsTransport.assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Check that this asset belongs to this Legion warehouse. + if asset.wid==self.uid then + + -- Set asset to requested! Important so that new requests do not use this asset! + asset.requested=true + asset.isReserved=false + + -- Set transport mission task. + asset.missionTask=ENUMS.MissionTask.TRANSPORT + + -- Add asset to list. + table.insert(AssetList, asset) + end + end + + if #AssetList>0 then + + -- Set mission status from QUEUED to REQUESTED. + OpsTransport:Requested() + + -- Set legion status. Ensures that it is not considered in the next selection. + OpsTransport:SetLegionStatus(self, OPSTRANSPORT.Status.REQUESTED) + + -- TODO: Get/set functions for assignment string. + local assignment=string.format("Transport-%d", OpsTransport.uid) + + -- Add request to legion warehouse. + --self:AddRequest(self, WAREHOUSE.Descriptor.ASSETLIST, AssetList, #AssetList, nil, nil, OpsTransport.prio, assignment) + self:_AddRequest(WAREHOUSE.Descriptor.ASSETLIST, AssetList, #AssetList, OpsTransport.prio, assignment) + + -- The queueid has been increased in the onafterAddRequest function. So we can simply use it here. + OpsTransport.requestID[self.alias]=self.queueid + end + +end + +--- On after "TransportCancel" event. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport to be cancelled. +function LEGION:onafterTransportCancel(From, Event, To, Transport) + + -- Info message. + self:T(self.lid..string.format("Cancel transport UID=%d", Transport.uid)) + + -- Set status to cancelled. + Transport:SetLegionStatus(self, OPSTRANSPORT.Status.CANCELLED) + + for i=#Transport.assets, 1, -1 do + local asset=Transport.assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Asset should belong to this legion. + if asset.wid==self.uid then + + local opsgroup=asset.flightgroup + + if opsgroup then + opsgroup:TransportCancel(Transport) + end + + -- Delete awaited transport. + local cargos=Transport:GetCargoOpsGroups(false) + for _,_cargo in pairs(cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP + + -- Remover my lift. + cargo:_DelMyLift(Transport) + + -- Legion of cargo group + local legion=cargo.legion + + -- Add asset back to legion. + if legion then + legion:T(self.lid..string.format("Adding cargo group %s back to legion", cargo:GetName())) + legion:__AddAsset(0.1, cargo.group, 1) + end + end + + -- Remove asset from mission. + Transport:DelAsset(asset) + + -- Not requested any more (if it was). + asset.requested=nil + asset.isReserved=nil + + end + end + + -- Remove queued request (if any). + if Transport.requestID[self.alias] then + self:_DeleteQueueItemByID(Transport.requestID[self.alias], self.queue) + end + +end + + +--- On after "MissionCancel" event. Cancels the missions of all flightgroups. Deletes request from warehouse queue. +-- @param #LEGION 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 LEGION:onafterMissionCancel(From, Event, To, Mission) + + -- Info message. + self:T(self.lid..string.format("Cancel mission %s", Mission.name)) + + -- Set status to cancelled. + Mission:SetLegionStatus(self, AUFTRAG.Status.CANCELLED) + + for i=#Mission.assets, 1, -1 do + local asset=Mission.assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Asset should belong to this legion. + if asset.wid==self.uid then + + local opsgroup=asset.flightgroup + + if opsgroup then + opsgroup:MissionCancel(Mission) + end + + -- Remove asset from mission. + Mission:DelAsset(asset) + + -- Not requested any more (if it was). + asset.requested=nil + asset.isReserved=nil + + end + end + + -- Remove queued request (if any). + local requestID=Mission:_GetRequestID(self) + if requestID then + self:_DeleteQueueItemByID(requestID, self.queue) + end + +end + +--- On after "OpsOnMission". +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup Ops group on mission +-- @param Ops.Auftrag#AUFTRAG Mission The requested mission. +function LEGION:onafterOpsOnMission(From, Event, To, OpsGroup, Mission) + -- Debug info. + self:T2(self.lid..string.format("Group %s on mission %s [%s]", OpsGroup:GetName(), Mission:GetName(), Mission:GetType())) + + if self:IsAirwing() then + -- Trigger event for Airwings. + self:FlightOnMission(OpsGroup, Mission) + elseif self:IsBrigade() then + -- Trigger event for Brigades. + self:ArmyOnMission(OpsGroup, Mission) + else + -- Trigger event for Fleets. + self:NavyOnMission(OpsGroup, Mission) + end + + -- Load group as cargo because it cannot swim! We pause the mission. + if self:IsBrigade() and self:IsShip() then + OpsGroup:PauseMission() + self.warehouseOpsGroup:Load(OpsGroup, self.warehouseOpsElement) + end + + -- Trigger event for chief. + if self.chief then + self.chief:OpsOnMission(OpsGroup, Mission) + end + + -- Trigger event for commander. + if self.commander then + self.commander:OpsOnMission(OpsGroup, Mission) + end + +end + +--- On after "NewAsset" event. Asset is added to the given cohort (asset assignment). +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset that has just been added. +-- @param #string assignment The (optional) assignment for the asset. +function LEGION:onafterNewAsset(From, Event, To, asset, assignment) + + -- Call parent WAREHOUSE function first. + self:GetParent(self, LEGION).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:T(self.lid..text) + + -- Get cohort. + local cohort=self:_GetCohort(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 cohort then + + if asset.assignment==assignment then + + --- + -- Asset is added to the COHORT for the first time + --- + + local nunits=#asset.template.units + + -- Debug text. + local text=string.format("Adding asset to cohort %s: assignment=%s, type=%s, attribute=%s, nunits=%d ngroup=%s", cohort.name, assignment, asset.unittype, asset.attribute, nunits, tostring(cohort.ngrouping)) + self:T(self.lid..text) + + -- Adjust number of elements in the group. + if cohort.ngrouping then + local template=asset.template + + local N=math.max(#template.units, cohort.ngrouping) + + -- We need to recalc the total weight and cargo bay. + asset.weight=0 + asset.cargobaytot=0 + + -- 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])) + asset.cargobaytot=asset.cargobaytot+asset.cargobay[1] + asset.weight=asset.weight+asset.weights[1] + template.units[i].x=template.units[1].x+5*(i-nunits) + template.units[i].y=template.units[1].y+5*(i-nunits) + else + if i<=cohort.ngrouping then + asset.weight=asset.weight+asset.weights[i] + asset.cargobaytot=asset.cargobaytot+asset.cargobay[i] + end + end + + -- Remove units if original template contains more than in grouping. + if i>cohort.ngrouping then + template.units[i]=nil + end + end + + -- Set number of units. + asset.nunits=cohort.ngrouping + + -- Debug info. + self:T(self.lid..string.format("After regrouping: Nunits=%d, weight=%.1f cargobaytot=%.1f kg", #asset.template.units, asset.weight, asset.cargobaytot)) + end + + -- Set takeoff type. + asset.takeoffType=cohort.takeoffType~=nil and cohort.takeoffType or self.takeoffType + + -- Set parking IDs. + asset.parkingIDs=cohort.parkingIDs + + -- Create callsign and modex (needs to be after grouping). + cohort:GetCallsign(asset) + cohort:GetModex(asset) + + -- Set spawn group name. This has to include "AID-" for warehouse. + asset.spawngroupname=string.format("%s_AID-%d", cohort.name, asset.uid) + + -- Add asset to cohort. + cohort:AddAsset(asset) + + else + + --- + -- Asset is returned to the COHORT + --- + + self:T(self.lid..string.format("Asset returned to legion ==> calling LegionAssetReturned event")) + + -- Set takeoff type in case it was overwritten for an ALERT5 mission. + asset.takeoffType=cohort.takeoffType + + -- Trigger event. + self:LegionAssetReturned(cohort, asset) + + end + + end +end + +--- On after "LegionAssetReturned" event. Triggered when an asset group returned to its legion. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned. +function LEGION:onafterLegionAssetReturned(From, Event, To, Cohort, Asset) + -- Debug message. + self:T(self.lid..string.format("Asset %s from Cohort %s returned! asset.assignment=\"%s\"", Asset.spawngroupname, Cohort.name, tostring(Asset.assignment))) + + -- Stop flightgroup. + if Asset.flightgroup and not Asset.flightgroup:IsStopped() then + Asset.flightgroup:Stop() + end + + -- Return payload. + if Asset.flightgroup:IsFlightgroup() then + self:ReturnPayloadFromAsset(Asset) + end + + -- Return tacan channel. + if Asset.tacan then + Cohort: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 #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group spawned. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset that was spawned. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset. +function LEGION:onafterAssetSpawned(From, Event, To, group, asset, request) + self:T({From, Event, To, group:GetName(), asset.assignment, request.assignment}) + + -- Call parent warehouse function first. + self:GetParent(self, LEGION).onafterAssetSpawned(self, From, Event, To, group, asset, request) + + -- Get the COHORT of the asset. + local cohort=self:_GetCohortOfAsset(asset) + + -- Check if we have a cohort or if this was some other request. + if cohort then + + -- Debug info. + self:T(self.lid..string.format("Cohort asset spawned %s", asset.spawngroupname)) + + -- 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 + + + --- + -- Cohort + --- + + -- Get TACAN channel. + local Tacan=cohort:FetchTacan() + if Tacan then + asset.tacan=Tacan + flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band) + end + + -- Set radio frequency and modulation + local radioFreq, radioModu=cohort:GetRadio() + if radioFreq then + flightgroup:SwitchRadio(radioFreq, radioModu) + end + + if cohort.fuellow then + flightgroup:SetFuelLowThreshold(cohort.fuellow) + end + + if cohort.fuellowRefuel then + flightgroup:SetFuelLowRefuel(cohort.fuellowRefuel) + end + + -- Assignment. + local assignment=request.assignment + + -- Set pathfinding for naval groups. + if self:IsFleet() then + flightgroup:SetPathfinding(self.pathfinding) + end + + if string.find(assignment, "Mission-") then + + --- + -- Mission + --- + + local uid=UTILS.Split(assignment, "-")[2] + + -- Get Mission (if any). + local mission=self:GetMissionByID(uid) + + local despawnLanding=cohort.despawnAfterLanding~=nil and cohort.despawnAfterLanding or self.despawnAfterLanding + if despawnLanding then + flightgroup:SetDespawnAfterLanding() + end + + local despawnHolding=cohort.despawnAfterHolding~=nil and cohort.despawnAfterHolding or self.despawnAfterHolding + if despawnHolding then + flightgroup:SetDespawnAfterHolding() + end + + -- Add mission to flightgroup queue. + if mission then + + if Tacan then + --mission:SetTACAN(Tacan, Morse, UnitName, Band) + end + + -- Add mission to flightgroup queue. If mission has an OPSTRANSPORT attached, all added OPSGROUPS are added as CARGO for a transport. + flightgroup:AddMission(mission) + + -- RTZ on out of ammo. + if self:IsBrigade() or self:IsFleet() then + flightgroup:SetReturnOnOutOfAmmo() + end + + -- Trigger event. + self:__OpsOnMission(5, flightgroup, mission) + + else + + if Tacan then + --flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band) + end + + end + + -- Add group to the detection set of the CHIEF (INTEL). + local chief=self.chief or (self.commander and self.commander.chief or nil) --Ops.Chief#CHIEF + if chief then + self:T(self.lid..string.format("Adding group %s to agents of CHIEF", group:GetName())) + chief.detectionset:AddGroup(asset.flightgroup.group) + end + + elseif string.find(assignment, "Transport-") then + + --- + -- Transport + --- + + local uid=UTILS.Split(assignment, "-")[2] + + -- Get Mission (if any). + local transport=self:GetTransportByID(uid) + + -- Add mission to flightgroup queue. + if transport then + flightgroup:AddOpsTransport(transport) + end + + end + + end + +end + +--- On after "AssetDead" event triggered when an asset group died. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset that is dead. +-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset. +function LEGION:onafterAssetDead(From, Event, To, asset, request) + + -- Call parent warehouse function first. + self:GetParent(self, LEGION).onafterAssetDead(self, From, Event, To, asset, request) + + -- Remove group from the detection set of the CHIEF (INTEL). + if self.commander and self.commander.chief then + self.commander.chief.detectionset:RemoveGroupsByName({asset.spawngroupname}) + end + + -- Remove asset from cohort and legion. + self:DelAsset(asset) + + -- 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 cohorts. Stop cohorts. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function LEGION:onafterDestroyed(From, Event, To) + + -- Debug message. + self:T(self.lid.."Legion warehouse destroyed!") + + -- Cancel all missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + mission:Cancel() + end + + -- Remove all cohort assets. + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + -- Stop Cohort. This also removes all assets. + cohort:Stop() + end + + -- Call parent warehouse function first. + self:GetParent(self, LEGION).onafterDestroyed(self, From, Event, To) + +end + + +--- On after "Request" event. +-- @param #LEGION 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 LEGION:onafterRequest(From, Event, To, Request) + + if Request.toself then + + -- 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 --Functional.Warehouse#WAREHOUSE.Assetitem + -- This would be the place to modify the asset table before the asset is spawned. + end + + end + + end + + -- Call parent warehouse function after assets have been adjusted. + self:GetParent(self, LEGION).onafterRequest(self, From, Event, To, Request) + +end + +--- On after "SelfRequest" event. +-- @param #LEGION 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 LEGION:onafterSelfRequest(From, Event, To, groupset, request) + + -- Call parent warehouse function first. + self:GetParent(self, LEGION).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 --Functional.Warehouse#WAREHOUSE.Assetitem + end + + for _,_group in pairs(groupset:GetSet()) do + local group=_group --Wrapper.Group#GROUP + end + +end + +--- On after "RequestSpawned" event. +-- @param #LEGION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Functional.Warehouse#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 LEGION:onafterRequestSpawned(From, Event, To, Request, CargoGroupSet, TransportGroupSet) + + -- Call parent warehouse function. + self:GetParent(self, LEGION).onafterRequestSpawned(self, From, Event, To, Request, CargoGroupSet, TransportGroupSet) + +end + +--- On after "Captured" event. +-- @param #LEGION 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 LEGION:onafterCaptured(From, Event, To, Coalition, Country) + + -- Call parent warehouse function. + self:GetParent(self, LEGION).onafterCaptured(self, From, Event, To, Coalition, Country) + + + if self.chief then + -- Trigger event for chief and commander. + self.chief.commander:LegionLost(self, Coalition, Country) + self.chief:LegionLost(self, Coalition, Country) + -- Remove legion from chief and commander. + self.chief:RemoveLegion(self) + elseif self.commander then + -- Trigger event. + self.commander:LegionLost(self, Coalition,Country) + -- Remove legion from commander. + self.commander:RemoveLegion(self) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mission Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new OPS group after an asset was spawned. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset. +-- @return Ops.FlightGroup#FLIGHTGROUP The created flightgroup object. +function LEGION:_CreateFlightGroup(asset) + + -- Create flightgroup. + local opsgroup=nil --Ops.OpsGroup#OPSGROUP + + if self:IsAirwing() then + + --- + -- FLIGHTGROUP + --- + + opsgroup=FLIGHTGROUP:New(asset.spawngroupname) + + elseif self:IsBrigade() then + + --- + -- ARMYGROUP + --- + + opsgroup=ARMYGROUP:New(asset.spawngroupname) + opsgroup:SetValidateAndRepositionGroundUnits(self.ValidateAndRepositionGroundUnits) + + elseif self:IsFleet() then + + --- + -- NAVYGROUP + --- + + opsgroup=NAVYGROUP:New(asset.spawngroupname) + + else + self:E(self.lid.."ERROR: not airwing or brigade!") + end + + -- Set legion. + opsgroup:_SetLegion(self) + + -- Set cohort. + opsgroup.cohort=self:_GetCohortOfAsset(asset) + + -- Set home base. + opsgroup.homebase=self.airbase + + -- Set destination base + opsgroup.destbase=self.airbase + + -- Set home zone. + opsgroup.homezone=self.spawnzone + + -- Set weapon data. + if opsgroup.cohort.weaponData then + local text="Weapon data for group:" + opsgroup.weaponData=opsgroup.weaponData or {} + for bittype,_weapondata in pairs(opsgroup.cohort.weaponData) do + local weapondata=_weapondata --Ops.OpsGroup#OPSGROUP.WeaponData + opsgroup.weaponData[bittype]=UTILS.DeepCopy(weapondata) -- Careful with the units. + text=text..string.format("\n- Bit=%s: Rmin=%.1f km, Rmax=%.1f km", bittype, weapondata.RangeMin/1000, weapondata.RangeMax/1000) + end + self:T3(self.lid..text) + end + + return opsgroup +end + +--- Display tactical overview. +-- @param #LEGION self +function LEGION:_TacticalOverview() + + if self.tacview then + + local NassetsTotal=self:CountAssets(nil) + local NassetsStock=self:CountAssets(true) + local NassetsActiv=self:CountAssets(false) + + local NmissionsTotal=#self.missionqueue + local NmissionsRunni=self:CountMissionsInQueue() + + -- Info message + local text=string.format("Tactical Overview %s\n", self.alias) + text=text..string.format("===================================\n") + + -- Asset info. + text=text..string.format("Assets: %d [Active=%d, Stock=%d]\n", NassetsTotal, NassetsActiv, NassetsStock) + + -- Mission info. + text=text..string.format("Missions: %d [Running=%d]\n", NmissionsTotal, NmissionsRunni) + for _,mtype in pairs(AUFTRAG.Type) do + local n=self:CountMissionsInQueue(mtype) + if n>0 then + local N=self:CountMissionsInQueue(mtype, true) + text=text..string.format(" - %s: %d [Running=%d]\n", mtype, n, N) + end + end + + + local Ntransports=#self.transportqueue + if Ntransports>0 then + text=text..string.format("Transports: %d\n", Ntransports) + for _,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + text=text..string.format(" - %s", transport:GetState()) + end + end + + -- Message to coalition. + MESSAGE:New(text, 60, nil, true):ToCoalition(self:GetCoalition()) + + + end + +end + +--- Check if an asset is currently on a mission (STARTED or EXECUTING). +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem 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 LEGION: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 AUFTRAG.CheckMissionType(mission.type, MissionTypes) then + return true + end + + end + + end + + end + + return false +end + +--- Get the current mission of the asset. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset. +-- @return Ops.Auftrag#AUFTRAG Current mission or *nil*. +function LEGION:GetAssetCurrentMission(asset) + + if asset.flightgroup then + return asset.flightgroup:GetMissionCurrent() + end + + return nil +end + +--- Count payloads in stock. +-- @param #LEGION 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 LEGION: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 or {}) do + local payload=_payload --Ops.Airwing#AIRWING.Payload + + for _,MissionType in pairs(MissionTypes) do + + local specialpayload=_checkPayloads(payload) + local compatible=AUFTRAG.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 #LEGION self +-- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`. +-- @param #boolean OnlyRunning If `true`, only count running missions. +-- @return #number Number of missions that are not over yet. +function LEGION:CountMissionsInQueue(MissionTypes, OnlyRunning) + + MissionTypes=MissionTypes or AUFTRAG.Type + + local N=0 + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + if (not OnlyRunning) or (mission.statusLegion~=AUFTRAG.Status.PLANNED) then + + -- Check if this mission type is requested. + if mission:IsNotOver() and AUFTRAG.CheckMissionType(mission.type, MissionTypes) then + N=N+1 + end + + end + + end + + return N +end + +--- Count total number of assets of the legion. +-- @param #LEGION self +-- @param #boolean InStock If `true`, only assets that are in the warehouse stock/inventory are counted. If `false`, only assets that are NOT in stock (i.e. spawned) are counted. If `nil`, all assets are counted. +-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. +-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `GROUP.Attribute.AIR_BOMBER`. +-- @return #number Amount of asset groups in stock. +function LEGION:CountAssets(InStock, MissionTypes, Attributes) + + local N=0 + + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + N=N+cohort:CountAssets(InStock, MissionTypes, Attributes) + end + + return N +end + +--- Get OPSGROUPs that are spawned and alive. +-- @param #LEGION self +-- @param #table MissionTypes (Optional) Get only assest that can perform certain mission type(s). Default is all types. +-- @param #table Attributes (Optional) Get only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. +-- @return Core.Set#SET_OPSGROUP The set of OPSGROUPs. Can be empty if no groups are spawned or alive! +function LEGION:GetOpsGroups(MissionTypes, Attributes) + + local setLegion=SET_OPSGROUP:New() + + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + -- Get cohort set. + local setCohort=cohort:GetOpsGroups(MissionTypes, Attributes) + + -- Debug info. + self:T2(self.lid..string.format("Found %d opsgroups of cohort %s", setCohort:Count(), cohort.name)) + + -- Add to legion set. + setLegion:AddSet(setCohort) + end + + return setLegion +end + +--- Count total number of assets in LEGION warehouse stock that also have a payload. +-- @param #LEGION self +-- @param #boolean Payloads (Optional) Specifc payloads to consider. Default all. +-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types. +-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`. +-- @return #number Amount of asset groups in stock. +function LEGION:CountAssetsWithPayloadsInStock(Payloads, MissionTypes, Attributes) + + -- Total number counted. + local N=0 + + -- Number of payloads in stock per aircraft type. + local Npayloads={} + + -- First get payloads for aircraft types of squadrons. + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + if Npayloads[cohort.aircrafttype]==nil then + Npayloads[cohort.aircrafttype]=self:CountPayloadsInStock(MissionTypes, cohort.aircrafttype, Payloads) + self:T3(self.lid..string.format("Got Npayloads=%d for type=%s",Npayloads[cohort.aircrafttype], cohort.aircrafttype)) + end + end + + for _,_cohort in pairs(self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + -- Number of assets in stock. + local n=cohort:CountAssets(true, MissionTypes, Attributes) + + -- Number of payloads. + local p=Npayloads[cohort.aircrafttype] or 0 + + -- Only the smaller number of assets or paylods is really available. + local m=math.min(n, p) + + -- Add up what we have. Could also be zero. + N=N+m + + -- Reduce number of available payloads. + Npayloads[cohort.aircrafttype]=Npayloads[cohort.aircrafttype]-m + end + + return N +end + +--- Count assets on mission. +-- @param #LEGION self +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @param Ops.Cohort#COHORT Cohort Only count assets of this cohort. Default count assets of all cohorts. +-- @return #number Number of pending and queued assets. +-- @return #number Number of pending assets. +-- @return #number Number of queued assets. +function LEGION:CountAssetsOnMission(MissionTypes, Cohort) + + 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 AUFTRAG.CheckMissionType(mission.type, MissionTypes or AUFTRAG.Type) then + + for _,_asset in pairs(mission.assets or {}) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Ensure asset belongs to this letion. + if asset.wid==self.uid then + + if Cohort==nil or Cohort.name==asset.squadname then + + local request, isqueued=self:GetRequestByID(mission.requestID[self.alias]) + + if isqueued then + Nq=Nq+1 + else + Np=Np+1 + end + + end + + end + end + end + end + + return Np+Nq, Np, Nq +end + +--- Get assets on mission. +-- @param #LEGION self +-- @param #table MissionTypes Types on mission to be checked. Default all. +-- @return #table Assets on pending requests. +function LEGION: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 AUFTRAG.CheckMissionType(mission.type, MissionTypes) then + + for _,_asset in pairs(mission.assets or {}) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Ensure asset belongs to this legion. + if asset.wid==self.uid then + + table.insert(assets, asset) + + end + + end + end + end + + return assets +end + +--- Get the unit types of this legion. These are the unit types of all assigned cohorts. +-- @param #LEGION self +-- @param #boolean onlyactive Count only the active ones. +-- @param #table cohorts Table of cohorts. Default all. +-- @return #table Table of unit types. +function LEGION:GetAircraftTypes(onlyactive, cohorts) + + -- Get all unit types that can do the job. + local unittypes={} + + -- Loop over all cohorts. + for _,_cohort in pairs(cohorts or self.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + if (not onlyactive) or cohort:IsOnDuty() then + + local gotit=false + for _,unittype in pairs(unittypes) do + if cohort.aircrafttype==unittype then + gotit=true + break + end + end + if not gotit then + table.insert(unittypes, cohort.aircrafttype) + end + + end + end + + return unittypes +end + +--- Count payloads of all cohorts for all unit types. +-- @param #LEGION self +-- @param #string MissionType Mission type. +-- @param #table Cohorts Cohorts included. +-- @param #table Payloads (Optional) Special payloads. +-- @return #table Table of payloads for each unit type. +function LEGION:_CountPayloads(MissionType, Cohorts, Payloads) + + -- Number of payloads in stock per aircraft type. + local Npayloads={} + + -- First get payloads for aircraft types of squadrons. + for _,_cohort in pairs(Cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + -- We only need that element once. + if Npayloads[cohort.aircrafttype]==nil then + + -- Count number of payloads in stock for the cohort aircraft type. + Npayloads[cohort.aircrafttype]=cohort.legion:IsAirwing() and self:CountPayloadsInStock(MissionType, cohort.aircrafttype, Payloads) or 999 + + -- Debug info. + self:T2(self.lid..string.format("Got N=%d payloads for mission type=%s and unit type=%s", Npayloads[cohort.aircrafttype], MissionType, cohort.aircrafttype)) + end + end + + return Npayloads +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Recruiting Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Recruit assets for a given mission. +-- @param #LEGION self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @return #boolean If `true` enough assets could be recruited. +-- @return #table Recruited assets. +-- @return #table Legions of recruited assets. +function LEGION:RecruitAssetsForMission(Mission) + + -- Get required assets. + local NreqMin, NreqMax=Mission:GetRequiredAssets() + + -- Target position vector. + local TargetVec2=Mission:GetTargetVec2() + + -- Payloads. + local Payloads=Mission.payloads + + -- Largest cargo bay available of available carrier assets if mission assets need to be transported. + local MaxWeight=nil + + if Mission.NcarriersMin then + + local legions={self} + local cohorts=self.cohorts + if Mission.transportLegions or Mission.transportCohorts then + legions=Mission.transportLegions + cohorts=Mission.transportCohorts + end + + -- Get transport cohorts. + local Cohorts=LEGION._GetCohorts(legions, cohorts) + + -- Filter cohorts that can actually perform transport missions. + local transportcohorts={} + for _,_cohort in pairs(Cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + -- Check if cohort can perform transport to target. + local can=LEGION._CohortCan(cohort, AUFTRAG.Type.OPSTRANSPORT, Mission.carrierCategories, Mission.carrierAttributes, Mission.carrierProperties, nil, TargetVec2) + + -- MaxWeight of cargo assets is limited by the largets available cargo bay. We don't want to select, e.g., tanks that cannot be transported by APCs or helos. + if can and (MaxWeight==nil or cohort.cargobayLimit>MaxWeight) then + MaxWeight=cohort.cargobayLimit + end + end + + self:T(self.lid..string.format("Largest cargo bay available=%.1f", MaxWeight or 0)) + end + + + local legions={self} + local cohorts=self.cohorts + if Mission.specialLegions or Mission.specialCohorts then + legions=Mission.specialLegions + cohorts=Mission.specialCohorts + end + + -- Get cohorts. + local Cohorts=LEGION._GetCohorts(legions, cohorts, Operation, OpsQueue) + + -- Recuit assets. + local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, Mission.type, Mission.alert5MissionType, NreqMin, NreqMax, TargetVec2, Payloads, + Mission.engageRange, Mission.refuelSystem, nil, nil, MaxWeight, nil, Mission.attributes, Mission.properties, {Mission.engageWeaponType}) + + return recruited, assets, legions +end + +--- Recruit assets for a given OPS transport. +-- @param #LEGION self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport. +-- @return #boolean If `true`, enough assets could be recruited. +-- @return #table assets Recruited assets. +-- @return #table legions Legions of recruited assets. +function LEGION:RecruitAssetsForTransport(Transport) + + -- Get all undelivered cargo ops groups. + local cargoOpsGroups=Transport:GetCargoOpsGroups(false) + + local weightGroup=0 + local TotalWeight=nil + + -- At least one group should be spawned. + if #cargoOpsGroups>0 then + + -- Calculate the max weight so we know which cohorts can provide carriers. + TotalWeight=0 + for _,_opsgroup in pairs(cargoOpsGroups) do + local opsgroup=_opsgroup --Ops.OpsGroup#OPSGROUP + local weight=opsgroup:GetWeightTotal() + if weight>weightGroup then + weightGroup=weight + end + TotalWeight=TotalWeight+weight + end + else + -- No cargo groups! + return false + end + + + -- TODO: Special transport cohorts/legions. + + -- Target is the deploy zone. + local TargetVec2=Transport:GetDeployZone():GetVec2() + + -- Number of required carriers. + local NreqMin,NreqMax=Transport:GetRequiredCarriers() + + + -- Recruit assets and legions. + local recruited, assets, legions=LEGION.RecruitCohortAssets(self.cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NreqMin, NreqMax, TargetVec2, nil, nil, nil, weightGroup, TotalWeight) + + return recruited, assets, legions +end + +--- Recruit assets performing an escort mission for a given asset. +-- @param #LEGION self +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +-- @param #table Assets Table of assets. +-- @return #boolean If `true`, enough assets could be recruited or no escort was required in the first place. +function LEGION:RecruitAssetsForEscort(Mission, Assets) + + -- Is an escort requested in the first place? + if Mission.NescortMin and Mission.NescortMax and (Mission.NescortMin>0 or Mission.NescortMax>0) then + + -- Debug info. + self:T(self.lid..string.format("Requested escort for mission %s [%s]. Required assets=%d-%d", Mission:GetName(), Mission:GetType(), Mission.NescortMin,Mission.NescortMax)) + + -- Get special escort legions and/or cohorts. + local Cohorts={} + for _,_legion in pairs(Mission.escortLegions or {}) do + local legion=_legion --Ops.Legion#LEGION + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + table.insert(Cohorts, cohort) + end + end + for _,_cohort in pairs(Mission.escortCohorts or {}) do + local cohort=_cohort --Ops.Cohort#COHORT + table.insert(Cohorts, cohort) + end + + -- No escort cohorts/legions given ==> take own cohorts. + if #Cohorts==0 then + Cohorts=self.cohorts + end + + -- Call LEGION function but provide COMMANDER as self. + local assigned=LEGION.AssignAssetsForEscort(self, Cohorts, Assets, Mission.NescortMin, Mission.NescortMax, Mission.escortMissionType, Mission.escortTargetTypes) + + return assigned + end + + return true +end + +--- Get cohorts. +-- @param #table Legions Special legions. +-- @param #table Cohorts Special cohorts. +-- @param Ops.Operation#OPERATION Operation Operation. +-- @param #table OpsQueue Queue of operations. +-- @return #table Cohorts. +function LEGION._GetCohorts(Legions, Cohorts, Operation, OpsQueue) + + OpsQueue=OpsQueue or {} + + --- Function that check if a legion or cohort is part of an operation. + local function CheckOperation(LegionOrCohort) + -- No operations ==> no problem! + if #OpsQueue==0 then + return true + end + + -- Cohort is not dedicated to a running(!) operation. We assume so. + local isAvail=true + + -- Only available... + if Operation then + isAvail=false + end + + for _,_operation in pairs(OpsQueue) do + local operation=_operation --Ops.Operation#OPERATION + + -- Legion is assigned to this operation. + local isOps=operation:IsAssignedCohortOrLegion(LegionOrCohort) + + if isOps and operation:IsRunning() then + + -- Is dedicated. + isAvail=false + + if Operation==nil then + -- No Operation given and this is dedicated to at least one operation. + return false + else + if Operation.uid==operation.uid then + -- Operation given and is part of it. + return true + end + end + end + end + + return isAvail + end + + -- Chosen cohorts. + local cohorts={} + + -- Check if there are any special legions and/or cohorts. + if (Legions and #Legions>0) or (Cohorts and #Cohorts>0) then + + -- Add cohorts of special legions. + for _,_legion in pairs(Legions or {}) do + local legion=_legion --Ops.Legion#LEGION + + -- Check that runway is operational. + local Runway=true + if legion:IsAirwing() then + Runway=legion:IsRunwayOperational() and legion.airbase and legion.airbase:GetCoalition() == legion:GetCoalition() + end + + -- Legion has to be running. + if legion:IsRunning() and Runway then + + -- Add cohorts of legion. + for _,_cohort in pairs(legion.cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + if (CheckOperation(cohort.legion) or CheckOperation(cohort)) and not UTILS.IsInTable(cohorts, cohort, "name") then + table.insert(cohorts, cohort) + end + end + + end + end + + -- Add special cohorts. + for _,_cohort in pairs(Cohorts or {}) do + local cohort=_cohort --Ops.Cohort#COHORT + if CheckOperation(cohort) and not UTILS.IsInTable(cohorts, cohort, "name") then + table.insert(cohorts, cohort) + end + end + + end + + return cohorts +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Recruiting and Optimization Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Recruit assets from Cohorts for the given parameters. **NOTE** that we set the `asset.isReserved=true` flag so it cant be recruited by anyone else. +-- @param Ops.Cohort#COHORT Cohort The Cohort. +-- @param #string MissionType Misson type(s). +-- @param #table Categories Group categories. +-- @param #table Attributes Group attributes. See `GROUP.Attribute.` +-- @param #table Properties DCS attributes. +-- @param #table WeaponTypes Bit of weapon types. +-- @param DCS#Vec2 TargetVec2 Target position. +-- @param RangeMax Max range in meters. +-- @param #number RefuelSystem Refueling system (boom or probe). +-- @param #number CargoWeight Cargo weight [kg]. This checks the cargo bay of the cohort assets and ensures that it is large enough to carry the given cargo weight. +-- @param #number MaxWeight Max weight [kg]. This checks whether the cohort asset group is not too heavy. +-- @return #boolean Returns `true` if given cohort can meet all requirements. +function LEGION._CohortCan(Cohort, MissionType, Categories, Attributes, Properties, WeaponTypes, TargetVec2, RangeMax, RefuelSystem, CargoWeight, MaxWeight) + + --- Function to check category. + local function CheckCategory(_cohort) + local cohort=_cohort --Ops.Cohort#COHORT + if Categories and #Categories>0 then + for _,category in pairs(Categories) do + if category==cohort.category then + return true + end + end + else + return true + end + end + + --- Function to check attribute. + local function CheckAttribute(_cohort) + local cohort=_cohort --Ops.Cohort#COHORT + if Attributes and #Attributes>0 then + for _,attribute in pairs(Attributes) do + if attribute==cohort.attribute then + return true + end + end + else + return true + end + end + + --- Function to check property. + local function CheckProperty(_cohort) + local cohort=_cohort --Ops.Cohort#COHORT + if Properties and #Properties>0 then + for _,Property in pairs(Properties) do + for property,value in pairs(cohort.properties) do + if Property==property then + return true + end + end + end + else + return true + end + end + + --- Function to check weapon type. + local function CheckWeapon(_cohort) + local cohort=_cohort --Ops.Cohort#COHORT + if WeaponTypes and #WeaponTypes>0 then + for _,WeaponType in pairs(WeaponTypes) do + if WeaponType==ENUMS.WeaponFlag.Auto then + return true + else + for _,_weaponData in pairs(cohort.weaponData or {}) do + local weaponData=_weaponData --Ops.OpsGroup#OPSGROUP.WeaponData + if weaponData.BitType==WeaponType then + return true + end + end + end + end + return false + else + return true + end + end + + --- Function to check range. + local function CheckRange(_cohort) + local cohort=_cohort --Ops.Cohort#COHORT + + -- Distance to target. + local TargetDistance=TargetVec2 and UTILS.VecDist2D(TargetVec2, cohort.legion:GetVec2()) or 0 + + -- Is in range? + local Rmax=cohort:GetMissionRange(WeaponTypes) + local RangeMax = RangeMax or 0 + local InRange=(RangeMax and math.max(RangeMax, Rmax) or Rmax) >= TargetDistance + + --env.info(string.format("Range TargetDist=%.1f Rmax=%.1f RangeMax=%.1f InRange=%s", TargetDistance, Rmax, RangeMax, tostring(InRange))) + + return InRange + end + + + --- Function to check weapon type. + local function CheckRefueling(_cohort) + local cohort=_cohort --Ops.Cohort#COHORT + + -- Has the requested refuelsystem? + --local Refuel=RefuelSystem~=nil and (RefuelSystem==cohort.tankerSystem) or true + + -- STRANGE: Why did the above line did not give the same result?! Above Refuel is always true! + if RefuelSystem then + if cohort.tankerSystem then + return RefuelSystem==cohort.tankerSystem + else + return false + end + else + return true + end + end + + --- Function to check cargo weight. + local function CheckCargoWeight(_cohort) + local cohort=_cohort --Ops.Cohort#COHORT + if CargoWeight~=nil then + return cohort.cargobayLimit>=CargoWeight + else + return true + end + end + + --- Function to check cargo weight. + local function CheckMaxWeight(_cohort) + local cohort=_cohort --Ops.Cohort#COHORT + if MaxWeight~=nil then + cohort:T(string.format("Cohort weight=%.1f | max weight=%.1f", cohort.weightAsset, MaxWeight)) + return cohort.weightAsset<=MaxWeight + else + return true + end + end + + + -- Is capable of the mission type? + local can=AUFTRAG.CheckMissionCapability(MissionType, Cohort.missiontypes) + + + if can then + can=CheckCategory(Cohort) + else + Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of mission types", Cohort.name)) + return false + end + + if can then + if MissionType==AUFTRAG.Type.RELOCATECOHORT then + can=Cohort:IsRelocating() + else + can=Cohort:IsOnDuty() + end + else + Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of category", Cohort.name)) + return false + end + + if can then + can=CheckAttribute(Cohort) + else + Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of readyiness", Cohort.name)) + return false + end + + if can then + can=CheckProperty(Cohort) + else + Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of attribute", Cohort.name)) + return false + end + + if can then + can=CheckWeapon(Cohort) + else + Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of property", Cohort.name)) + return false + end + + if can then + can=CheckRange(Cohort) + else + Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of weapon type", Cohort.name)) + return false + end + + if can then + can=CheckRefueling(Cohort) + else + Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of range", Cohort.name)) + return false + end + + if can then + can=CheckCargoWeight(Cohort) + else + Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of refueling system", Cohort.name)) + return false + end + + if can then + can=CheckMaxWeight(Cohort) + else + Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of cargo weight", Cohort.name)) + return false + end + + if can then + return true + else + Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of max weight", Cohort.name)) + return false + end + + return nil +end + +--- Recruit assets from Cohorts for the given parameters. **NOTE** that we set the `asset.isReserved=true` flag so it cannot be recruited by anyone else. +-- @param #table Cohorts Cohorts included. +-- @param #string MissionTypeRecruit Mission type for recruiting the cohort assets. +-- @param #string MissionTypeOpt Mission type for which the assets are optimized. Default is the same as `MissionTypeRecruit`. +-- @param #number NreqMin Minimum number of required assets. +-- @param #number NreqMax Maximum number of required assets. +-- @param DCS#Vec2 TargetVec2 Target position as 2D vector. +-- @param #table Payloads Special payloads. +-- @param #number RangeMax Max range in meters. +-- @param #number RefuelSystem Refuelsystem. +-- @param #number CargoWeight Cargo weight for recruiting transport carriers. +-- @param #number TotalWeight Total cargo weight in kg. +-- @param #number MaxWeight Max weight [kg] of the asset group. +-- @param #table Categories Group categories. +-- @param #table Attributes Group attributes. See `GROUP.Attribute.` +-- @param #table Properties DCS attributes. +-- @param #table WeaponTypes Bit of weapon types. +-- @return #boolean If `true` enough assets could be recruited. +-- @return #table Recruited assets. **NOTE** that we set the `asset.isReserved=true` flag so it cant be recruited by anyone else. +-- @return #table Legions of recruited assets. +function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt, NreqMin, NreqMax, TargetVec2, Payloads, RangeMax, RefuelSystem, CargoWeight, TotalWeight, MaxWeight, Categories, Attributes, Properties, WeaponTypes) + + -- The recruited assets. + local Assets={} + + -- Legions of recruited assets. + local Legions={} + + -- Set MissionTypeOpt to Recruit if nil. + if MissionTypeOpt==nil then + MissionTypeOpt=MissionTypeRecruit + end + + -- Loops over cohorts. + for _,_cohort in pairs(Cohorts) do + local cohort=_cohort --Ops.Cohort#COHORT + + -- Check if cohort can do the mission. + local can=LEGION._CohortCan(cohort, MissionTypeRecruit, Categories, Attributes, Properties, WeaponTypes, TargetVec2, RangeMax, RefuelSystem, CargoWeight, MaxWeight) + + --env.info(string.format("RecruitCohortAssets %s Cohort=%s can=%s", MissionTypeRecruit, cohort:GetName(), tostring(can))) + + -- Check OnDuty, capable, in range and refueling type (if TANKER). + if can then + + -- Recruit assets from cohort. + local assets, npayloads=cohort:RecruitAssets(MissionTypeRecruit, 999) + + -- Add assets to the list. + for _,asset in pairs(assets) do + table.insert(Assets, asset) + end + + end + + end + + -- Break if no assets could be found + if #Assets==0 then + --env.info(string.format("LEGION.RecruitCohortAssets: No assets could be recruited for mission type %s [Nmin=%s, Nmax=%s]", MissionTypeRecruit, tostring(NreqMin), tostring(NreqMax))) + return false, {}, {} + end + + -- Now we have a long list with assets. + LEGION._OptimizeAssetSelection(Assets, MissionTypeOpt, TargetVec2, false, TotalWeight) + + + -- Get payloads for air assets. + for _,_asset in pairs(Assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Only assets that have no payload. Should be only spawned assets! + if asset.legion:IsAirwing() and not asset.payload then + + -- Fetch payload for asset. This can be nil! + asset.payload=asset.legion:FetchPayloadFromStock(asset.unittype, MissionTypeOpt, Payloads) + + end + end + + -- Remove assets that dont have a payload. + for i=#Assets,1,-1 do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.legion:IsAirwing() and not asset.payload then + table.remove(Assets, i) + end + end + + -- Now find the best asset for the given payloads. + LEGION._OptimizeAssetSelection(Assets, MissionTypeOpt, TargetVec2, true, TotalWeight) + + -- Number of assets. At most NreqMax. + local Nassets=math.min(#Assets, NreqMax) + + if #Assets>=NreqMin then + + --- + -- Found enough assets + --- + + -- Total cargo bay of all carrier assets. + local cargobay=0 + + -- Add assets to mission. + for i=1,Nassets do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Asset is reserved and will not be picked for other missions. + asset.isReserved=true + + -- Add legion. + Legions[asset.legion.alias]=asset.legion + + -- Check if total cargo weight was given. + if TotalWeight then + + -- Number of + local N=math.floor(asset.cargobaytot/asset.nunits / CargoWeight)*asset.nunits + --env.info(string.format("cargobaytot=%d, cargoweight=%d ==> N=%d", asset.cargobaytot, CargoWeight, N)) + + -- Sum up total cargo bay of all carrier assets. + cargobay=cargobay + N*CargoWeight + + -- Check if enough carrier assets were found to transport all cargo. + if cargobay>=TotalWeight then + --env.info(string.format("FF found enough assets to transport all cargo! N=%d [%d], cargobay=%.1f >= %.1f kg total weight", i, Nassets, cargobay, TotalWeight)) + Nassets=i + break + end + + end + + end + + -- Return payloads of not needed assets. + for i=#Assets,Nassets+1,-1 do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.legion:IsAirwing() and not asset.spawned then + asset.legion:T2(asset.legion.lid..string.format("Returning payload from asset %s", asset.spawngroupname)) + asset.legion:ReturnPayloadFromAsset(asset) + end + table.remove(Assets, i) + end + + -- Found enough assets. + return true, Assets, Legions + else + + --- + -- NOT enough assets + --- + + -- Return payloads of assets. + for i=1,#Assets do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.legion:IsAirwing() and not asset.spawned then + asset.legion:T2(asset.legion.lid..string.format("Returning payload from asset %s", asset.spawngroupname)) + asset.legion:ReturnPayloadFromAsset(asset) + end + end + + -- Not enough assets found. + return false, {}, {} + end + + return false, {}, {} +end + +--- Unrecruit assets. Set `isReserved` to false, return payload to airwing and (optionally) remove from assigned mission. +-- @param #table Assets List of assets. +-- @param Ops.Auftrag#AUFTRAG Mission (Optional) The mission from which the assets will be deleted. +function LEGION.UnRecruitAssets(Assets, Mission) + + -- Return payloads of assets. + for i=1,#Assets do + local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem + -- Not reserved any more. + asset.isReserved=false + -- Return payload. + if asset.legion:IsAirwing() and not asset.spawned then + asset.legion:T2(asset.legion.lid..string.format("Returning payload from asset %s", asset.spawngroupname)) + asset.legion:ReturnPayloadFromAsset(asset) + end + -- Remove from mission. + if Mission then + Mission:DelAsset(asset) + end + end + +end + + +--- Recruit and assign assets performing an escort mission for a given asset list. Note that each asset gets an escort. +-- @param #LEGION self +-- @param #table Cohorts Cohorts for escorting assets. +-- @param #table Assets Table of assets to be escorted. +-- @param #number NescortMin Min number of escort groups required per escorted asset. +-- @param #number NescortMax Max number of escort groups required per escorted asset. +-- @param #string MissionType Mission type. +-- @param #string TargetTypes Types of targets that are engaged. +-- @param #number EngageRange EngageRange in Nautical Miles. +-- @return #boolean If `true`, enough assets could be recruited or no escort was required in the first place. +function LEGION:AssignAssetsForEscort(Cohorts, Assets, NescortMin, NescortMax, MissionType, TargetTypes, EngageRange) + + -- Is an escort requested in the first place? + if NescortMin and NescortMax and (NescortMin>0 or NescortMax>0) then + + -- Debug info. + self:T(self.lid..string.format("Requested escort for %d assets from %d cohorts. Required escort assets=%d-%d", #Assets, #Cohorts, NescortMin, NescortMax)) + + -- Escorts for each asset. + local Escorts={} + + local EscortAvail=true + for _,_asset in pairs(Assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Target vector is the legion of the asset. + local TargetVec2=asset.legion:GetVec2() + + -- We want airplanes for airplanes and helos for everything else. + local Categories={Group.Category.HELICOPTER} + local targetTypes={"Ground Units"} + if asset.category==Group.Category.AIRPLANE then + Categories={Group.Category.AIRPLANE} + targetTypes={"Air"} + end + + TargetTypes=TargetTypes or targetTypes + + -- Recruit escort asset for the mission asset. + local Erecruited, eassets, elegions=LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.ESCORT, MissionType, NescortMin, NescortMax, TargetVec2, nil, nil, nil, nil, nil, nil, Categories) + + if Erecruited then + Escorts[asset.spawngroupname]={EscortLegions=elegions, EscortAssets=eassets, ecategory=asset.category} + else + -- Could not find escort for this asset ==> Escort not possible ==> Break the loop. + EscortAvail=false + break + end + end + + -- ALL escorts could be recruited. + if EscortAvail then + + local N=0 + for groupname,value in pairs(Escorts) do + + local Elegions=value.EscortLegions + local Eassets=value.EscortAssets + local ecategory=value.ecategory + + for _,_legion in pairs(Elegions) do + local legion=_legion --Ops.Legion#LEGION + + local OffsetVector=nil --DCS#Vec3 + if ecategory==Group.Category.GROUND then + -- Overhead at 1000 ft. + OffsetVector={} + OffsetVector.x=0 + OffsetVector.y=UTILS.FeetToMeters(1000) + OffsetVector.z=0 + elseif MissionType==AUFTRAG.Type.SEAD then + -- Overhead slightly higher and right. + OffsetVector={} + OffsetVector.x=-100 + OffsetVector.y= 500 + OffsetVector.z= 500 + end + + -- Create and ESCORT mission for this asset. + local escort=AUFTRAG:NewESCORT(groupname, OffsetVector, EngageRange, TargetTypes) + + -- For a SEAD mission, we also adjust the mission task. + if MissionType==AUFTRAG.Type.SEAD then + escort.missionTask=ENUMS.MissionTask.SEAD + -- Add enroute task SEAD. + local DCStask=CONTROLLABLE.EnRouteTaskSEAD(nil) + table.insert(escort.enrouteTasks, DCStask) + end + + -- Reserve assts and add to mission. + for _,_asset in pairs(Eassets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + escort:AddAsset(asset) + N=N+1 + end + + -- Assign mission to legion. + self:MissionAssign(escort, {legion}) + end + end + + -- Debug info. + self:T(self.lid..string.format("Recruited %d escort assets", N)) + + -- Yup! + return true + else + + -- Debug info. + self:T(self.lid..string.format("Could not get at least one escort!")) + + -- Could not get at least one escort. Unrecruit all recruited ones. + for groupname,value in pairs(Escorts) do + local Eassets=value.EscortAssets + LEGION.UnRecruitAssets(Eassets) + end + + -- No,no! + return false + end + + else + -- No escort required. + self:T(self.lid..string.format("No escort required! NescortMin=%s, NescortMax=%s", tostring(NescortMin), tostring(NescortMax))) + return true + end + +end + +--- Recruit and assign assets performing an OPSTRANSPORT for a given asset list. +-- @param #LEGION self +-- @param #table Legions Transport legions. +-- @param #table CargoAssets Weight of the heaviest cargo group to be transported. +-- @param #number NcarriersMin Min number of carrier assets. +-- @param #number NcarriersMax Max number of carrier assets. +-- @param Core.Zone#ZONE DeployZone Deploy zone. +-- @param Core.Zone#ZONE DisembarkZone (Optional) Disembark zone. +-- @param #table Categories Group categories. +-- @param #table Attributes Generalizes group attributes. +-- @param #table Properties DCS attributes. +-- @return #boolean If `true`, enough assets could be recruited and an OPSTRANSPORT object was created. +-- @return Ops.OpsTransport#OPSTRANSPORT Transport The transport. +function LEGION:AssignAssetsForTransport(Legions, CargoAssets, NcarriersMin, NcarriersMax, DeployZone, DisembarkZone, Categories, Attributes, Properties) + + -- Is an escort requested in the first place? + if NcarriersMin and NcarriersMax and (NcarriersMin>0 or NcarriersMax>0) then + + -- Get cohorts. + local Cohorts=LEGION._GetCohorts(Legions) + + -- Get all legions and heaviest cargo group weight + local CargoLegions={} ; local CargoWeight=nil ; local TotalWeight=0 + for _,_asset in pairs(CargoAssets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + CargoLegions[asset.legion.alias]=asset.legion + if CargoWeight==nil or asset.weight>CargoWeight then + CargoWeight=asset.weight + end + TotalWeight=TotalWeight+asset.weight + end + + -- Debug info. + self:T(self.lid..string.format("Cargo weight=%.1f", CargoWeight)) + self:T(self.lid..string.format("Total weight=%.1f", TotalWeight)) + + -- Target is the deploy zone. + local TargetVec2=DeployZone:GetVec2() + + -- Recruit assets and legions. + local TransportAvail, CarrierAssets, CarrierLegions= + LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NcarriersMin, NcarriersMax, TargetVec2, nil, nil, nil, CargoWeight, TotalWeight, nil, Categories, Attributes, Properties) + + if TransportAvail then + + -- Create and OPSTRANSPORT assignment. + local Transport=OPSTRANSPORT:New(nil, nil, DeployZone) + if DisembarkZone then + Transport:SetDisembarkZone(DisembarkZone) + end + + -- Debug info. + self:T(self.lid..string.format("Transport available with %d carrier assets", #CarrierAssets)) + + -- Add cargo assets to transport. + for _,_legion in pairs(CargoLegions) do + local legion=_legion --Ops.Legion#LEGION + + -- Set pickup zone to spawn zone or airbase if the legion has one that is operational. + local pickupzone=legion.spawnzone + + -- Add TZC from legion spawn zone to deploy zone. + local tpz=Transport:AddTransportZoneCombo(nil, pickupzone, Transport:GetDeployZone()) + + -- Set pickup airbase if the legion has an airbase. Could also be the ship itself. + tpz.PickupAirbase=legion:IsRunwayOperational() and legion.airbase or nil + + -- Set embark zone to spawn zone. + Transport:SetEmbarkZone(legion.spawnzone, tpz) + + -- Add cargo assets to transport. + for _,_asset in pairs(CargoAssets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + if asset.legion.alias==legion.alias then + Transport:AddAssetCargo(asset, tpz) + end + end + end + + -- Add carrier assets. + for _,_asset in pairs(CarrierAssets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + Transport:AddAsset(asset) + end + + -- Assign TRANSPORT to legions. This also sends the request for the assets. + self:TransportAssign(Transport, CarrierLegions) + + -- Got transport. + return true, Transport + else + -- Debug info. + self:T(self.lid..string.format("Transport assets could not be allocated ==> Unrecruiting assets")) + + -- Uncrecruit transport assets. + LEGION.UnRecruitAssets(CarrierAssets) + return false, nil + end + + return nil, nil + end + + -- No transport requested in the first place. + return true, nil +end + + +--- Calculate the mission score of an asset. +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset Asset +-- @param #string MissionType Mission type for which the best assets are desired. +-- @param DCS#Vec2 TargetVec2 Target 2D vector. +-- @param #boolean IncludePayload If `true`, include the payload in the calulation if the asset has one attached. +-- @param #number TotalWeight The total weight of the cargo to be transported, if applicable. +-- @return #number Mission score. +function LEGION.CalculateAssetMissionScore(asset, MissionType, TargetVec2, IncludePayload, TotalWeight) + + -- Mission score. + 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. + score=score+asset.cohort:GetMissionPeformance(MissionType) + + -- Add payload performance to score. + local function scorePayload(Payload, MissionType) + for _,Capability in pairs(Payload.capabilities) do + local capability=Capability --Ops.Auftrag#AUFTRAG.Capability + if capability.MissionType==MissionType then + return capability.Performance + end + end + return 0 + end + + if IncludePayload and asset.payload then + score=score+scorePayload(asset.payload, MissionType) + end + + -- Origin: We take the OPSGROUP position or the one of the legion. + local OrigVec2=asset.flightgroup and asset.flightgroup:GetVec2() or asset.legion:GetVec2() + + -- Distance factor. + local distance=0 + if TargetVec2 and OrigVec2 then + + -- Distance in NM. + distance=UTILS.MetersToNM(UTILS.VecDist2D(OrigVec2, TargetVec2)) + + if asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then + -- Round: 55 NM ==> 5.5 ==> 6, 63 NM ==> 6.3 ==> 6 + distance=UTILS.Round(distance/10, 0) + else + -- For ground units the distance is a more important factor + distance=UTILS.Round(distance, 0) + end + + end + + -- Reduce score for legions that are futher away. + score=score-distance + + -- Check for spawned assets. + if asset.spawned and asset.flightgroup and asset.flightgroup:IsAlive() then + + -- Get current mission. + local currmission=asset.flightgroup:GetMissionCurrent() + + if currmission then + + if currmission.type==AUFTRAG.Type.ALERT5 and currmission.alert5MissionType==MissionType then + -- Prefer assets that are on ALERT5 for this mission type. + score=score+25 + elseif (currmission.type==AUFTRAG.Type.GCICAP or currmission.type==AUFTRAG.Type.PATROLRACETRACK) and MissionType==AUFTRAG.Type.INTERCEPT then + -- Prefer assets that are on GCICAP to perform INTERCEPTS. We set this even higher than alert5 because they are already in the air. + score=score+35 + elseif (currmission.type==AUFTRAG.Type.ONGUARD or currmission.type==AUFTRAG.Type.PATROLZONE) and (MissionType==AUFTRAG.Type.ARTY or MissionType==AUFTRAG.Type.GROUNDATTACK) then + score=score+25 + elseif currmission.type==AUFTRAG.Type.NOTHING then + score=score+30 + end + + end + + if MissionType==AUFTRAG.Type.OPSTRANSPORT or MissionType==AUFTRAG.Type.AMMOSUPPLY or MissionType==AUFTRAG.Type.AWACS or MissionType==AUFTRAG.Type.FUELSUPPLY or MissionType==AUFTRAG.Type.TANKER then + -- TODO: need to check for missions that do not require ammo like transport, recon, awacs, tanker etc. + -- We better take a fresh asset. Sometimes spawned assets do something else, which is difficult to check. + score=score-10 + else + -- Combat mission. + if asset.flightgroup:IsOutOfAmmo() then + -- Assets that are out of ammo are not considered. + score=score-1000 + end + end + end + + -- TRANSPORT specific. + if MissionType==AUFTRAG.Type.OPSTRANSPORT then + if TotalWeight then + -- Add 1 score point for each 10 kg of cargo bay capacity up to the total cargo weight, + -- and then subtract 1 score point for each excess 10kg of cargo bay capacity. + if asset.cargobaymax < TotalWeight then + score=score+UTILS.Round(asset.cargobaymax/10, 0) + else + score=score+UTILS.Round(TotalWeight/10, 0) + end + else + -- Add 1 score point for each 10 kg of cargo bay. + score=score+UTILS.Round(asset.cargobaymax/10, 0) + 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? + + if asset.legion and asset.legion.verbose>=2 then + asset.legion:I(asset.legion.lid..string.format("Asset %s [spawned=%s] score=%d", asset.spawngroupname, tostring(asset.spawned), score)) + end + + return score +end + +--- Optimize chosen assets for the mission at hand. +-- @param #table assets Table of (unoptimized) assets. +-- @param #string MissionType Mission type. +-- @param DCS#Vec2 TargetVec2 Target position as 2D vector. +-- @param #boolean IncludePayload If `true`, include the payload in the calulation if the asset has one attached. +-- @param #number TotalWeight The total weight of the cargo to be transported, if applicable. +function LEGION._OptimizeAssetSelection(assets, MissionType, TargetVec2, IncludePayload, TotalWeight) + + -- Calculate the mission score of all assets. + for _,_asset in pairs(assets) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + -- Calculate the asset score. + asset.score=LEGION.CalculateAssetMissionScore(asset, MissionType, TargetVec2, IncludePayload, TotalWeight) + + if IncludePayload then + + -- Max random asset score. + local RandomScoreMax=asset.legion and asset.legion.RandomAssetScore or LEGION.RandomAssetScore + + -- Random score. + local RandomScore=math.random(0, RandomScoreMax) + + -- Debug info. + --env.info(string.format("Asset %s: randomscore=%d, max=%d", asset.spawngroupname, RandomScore, RandomScoreMax)) + + -- Add a bit of randomness. + asset.score=asset.score+RandomScore + + end + end + + --- Sort assets wrt to their mission score. Higher is better. + local function optimize(a, b) + local assetA=a --Functional.Warehouse#WAREHOUSE.Assetitem + local assetB=b --Functional.Warehouse#WAREHOUSE.Assetitem + -- Higher score wins. If equal score ==> closer wins. + return (assetA.score>assetB.score) + end + table.sort(assets, optimize) + + -- Remove distance parameter. + if LEGION.verbose>0 then + local text=string.format("Optimized %d assets for %s mission/transport (payload=%s):", #assets, MissionType, tostring(IncludePayload)) + for i,Asset in pairs(assets) do + local asset=Asset --Functional.Warehouse#WAREHOUSE.Assetitem + text=text..string.format("\n%d. %s [%s]: score=%d", i, asset.spawngroupname, asset.squadname, asset.score or -1) + asset.score=nil + end + env.info(text) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Returns the mission for a given mission ID (Autragsnummer). +-- @param #LEGION self +-- @param #number mid Mission ID (Auftragsnummer). +-- @return Ops.Auftrag#AUFTRAG Mission table. +function LEGION: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 ID. +-- @param #LEGION self +-- @param #number uid Transport UID. +-- @return Ops.OpsTransport#OPSTRANSPORT Transport assignment. +function LEGION:GetTransportByID(uid) + + for _,_transport in pairs(self.transportqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + if transport.uid==tonumber(uid) then + return transport + end + + end + + return nil +end + +--- Returns the mission for a given request ID. +-- @param #LEGION self +-- @param #number RequestID Unique ID of the request. +-- @return Ops.Auftrag#AUFTRAG Mission table or *nil*. +function LEGION:GetMissionFromRequestID(RequestID) + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + local mid=mission.requestID[self.alias] + if mid and mid==RequestID then + return mission + end + end + return nil +end + +--- Returns the mission for a given request. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Queueitem Request The warehouse request. +-- @return Ops.Auftrag#AUFTRAG Mission table or *nil*. +function LEGION:GetMissionFromRequest(Request) + return self:GetMissionFromRequestID(Request.uid) +end + +--- Fetch a payload from the airwing resources for a given unit and mission type. +-- The payload with the highest priority is preferred. +-- @param #LEGION 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 Ops.Airwing#AIRWING.Payload Payload table or *nil*. +function LEGION:FetchPayloadFromStock(UnitType, MissionType, Payloads) + -- Polymorphic. Will return something when called by airwing. + return nil +end + +--- Return payload from asset back to stock. +-- @param #LEGION self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The squadron asset. +function LEGION:ReturnPayloadFromAsset(asset) + -- Polymorphic. + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/NavyGroup.lua b/Moose Development/Moose/Ops/NavyGroup.lua new file mode 100644 index 000000000..5f2869d03 --- /dev/null +++ b/Moose Development/Moose/Ops/NavyGroup.lua @@ -0,0 +1,2456 @@ +--- **Ops** - Enhanced Naval Group. +-- +-- ## Main Features: +-- +-- * Let the group steam into the wind +-- * Command a full stop +-- * Patrol waypoints *ad infinitum* +-- * Collision warning, if group is heading towards a land mass or another obstacle +-- * Automatic pathfinding, e.g. around islands +-- * Let a submarine dive and surface +-- * Manage TACAN and ICLS beacons +-- * 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/Navygroup) +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.NavyGroup +-- @image OPS_NavyGroup.png + + +--- NAVYGROUP class. +-- @type NAVYGROUP +-- @field #boolean turning If true, group is currently turning. +-- @field #NAVYGROUP.IntoWind intowind Into wind info. +-- @field #table Qintowind Queue of "into wind" turns. +-- @field #number intowindcounter Counter of into wind IDs. +-- @field #number depth Ordered depth in meters. +-- @field #boolean collisionwarning If true, collition warning. +-- @field #boolean pathfindingOn If true, enable pathfining. +-- @field #number pathCorridor Path corrdidor width in meters. +-- @field #boolean ispathfinding If true, group is currently path finding. +-- @field #NAVYGROUP.Target engage Engage target. +-- @field #boolean intowindold Use old calculation to determine heading into wind. +-- @extends Ops.OpsGroup#OPSGROUP + +--- *Something must be left to chance; nothing is sure in a sea fight above all.* -- Horatio Nelson +-- +-- === +-- +-- # The NAVYGROUP Concept +-- +-- This class enhances naval groups. +-- +-- @field #NAVYGROUP +NAVYGROUP = { + ClassName = "NAVYGROUP", + turning = false, + intowind = nil, + intowindcounter = 0, + Qintowind = {}, + pathCorridor = 400, + engage = {}, +} + +--- Turn into wind parameters. +-- @type NAVYGROUP.IntoWind +-- @field #number Tstart Time to start. +-- @field #number Tstop Time to stop. +-- @field #boolean Uturn U-turn. +-- @field #number Speed Speed in knots. +-- @field #number Offset Offset angle in degrees. +-- @field #number Id Unique ID of the turn. +-- @field Ops.OpsGroup#OPSGROUP.Waypoint waypoint Turn into wind waypoint. +-- @field Core.Point#COORDINATE Coordinate Coordinate where we left the route. +-- @field #number Heading Heading the boat will take in degrees. +-- @field #boolean Open Currently active. +-- @field #boolean Over This turn is over. +-- @field #boolean Recovery If `true` this is a recovery window. If `false`, this is a launch window. If `nil` this is just a turn into the wind. + +--- Engage Target. +-- @type NAVYGROUP.Target +-- @field Ops.Target#TARGET Target The target. +-- @field Core.Point#COORDINATE Coordinate Last known coordinate of the target. +-- @field Ops.OpsGroup#OPSGROUP.Waypoint Waypoint the waypoint created to go to the target. +-- @field #number Speed Speed in knots. +-- @field #number Depth Depth of the engagement (submarines). +-- @field #number roe ROE backup. +-- @field #number alarmstate Alarm state backup. + +--- NavyGroup version. +-- @field #string version +NAVYGROUP.version="1.0.4" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add Retreat. +-- TODO: Submaries. +-- TODO: Extend, shorten turn into wind windows. +-- TODO: Skipper menu. +-- DONE: Add EngageTarget. +-- DONE: Add RTZ. +-- DONE: Collision warning. +-- DONE: Detour, add temporary waypoint and resume route. +-- DONE: Stop and resume route. +-- DONE: Add waypoints. +-- DONE: Add tasks. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new NAVYGROUP class object. +-- @param #NAVYGROUP self +-- @param Wrapper.Group#GROUP group The group object. Can also be given by its group name as `#string`. +-- @return #NAVYGROUP self +function NAVYGROUP:New(group) + + -- First check if we already have an OPS group for this group. + local og=_DATABASE:GetOpsGroup(group) + if og then + og:I(og.lid..string.format("WARNING: OPS group already exists in data base!")) + return og + end + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, OPSGROUP:New(group)) -- #NAVYGROUP + + -- Set some string id for output to DCS.log file. + self.lid=string.format("NAVYGROUP %s | ", self.groupname) + + -- Defaults + self:SetDefaultROE() + self:SetDefaultAlarmstate() + self:SetDefaultEPLRS(self.isEPLRS) + self:SetDefaultEmission() + self:SetDetection() + self:SetPatrolAdInfinitum(true) + self:SetPathfinding(false) + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "FullStop", "Holding") -- Hold position. + self:AddTransition("*", "Cruise", "Cruising") -- Hold position. + + self:AddTransition("*", "RTZ", "Returning") -- Group is returning to (home) zone. + self:AddTransition("Returning", "Returned", "Returned") -- Group is returned to (home) zone. + + self:AddTransition("*", "Detour", "Cruising") -- Make a detour to a coordinate and resume route afterwards. + self:AddTransition("*", "DetourReached", "*") -- Group reached the detour coordinate. + + self:AddTransition("*", "Retreat", "Retreating") -- Order a retreat. + self:AddTransition("Retreating", "Retreated", "Retreated") -- Group retreated. + + self:AddTransition("Cruising", "EngageTarget", "Engaging") -- Engage a target from Cruising state + self:AddTransition("Holding", "EngageTarget", "Engaging") -- Engage a target from Holding state + self:AddTransition("OnDetour", "EngageTarget", "Engaging") -- Engage a target from OnDetour state + self:AddTransition("Engaging", "Disengage", "Cruising") -- Disengage and back to cruising. + + self:AddTransition("*", "TurnIntoWind", "Cruising") -- Command the group to turn into the wind. + self:AddTransition("*", "TurnedIntoWind", "*") -- Group turned into wind. + self:AddTransition("*", "TurnIntoWindStop", "*") -- Stop a turn into wind. + self:AddTransition("*", "TurnIntoWindOver", "*") -- Turn into wind is over. + + self:AddTransition("*", "TurningStarted", "*") -- Group started turning. + self:AddTransition("*", "TurningStopped", "*") -- Group stopped turning. + + self:AddTransition("*", "CollisionWarning", "*") -- Collision warning. + self:AddTransition("*", "ClearAhead", "*") -- Clear ahead. + + self:AddTransition("Cruising", "Dive", "Cruising") -- Command a submarine to dive. + self:AddTransition("Engaging", "Dive", "Engaging") -- Command a submarine to dive. + self:AddTransition("Cruising", "Surface", "Cruising") -- Command a submarine to go to the surface. + self:AddTransition("Engaging", "Surface", "Engaging") -- Command a submarine to go to the surface. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Cruise". + -- @function [parent=#NAVYGROUP] Cruise + -- @param #NAVYGROUP self + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- Triggers the FSM event "Cruise" after a delay. + -- @function [parent=#NAVYGROUP] __Cruise + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- On after "Cruise" event. + -- @function [parent=#NAVYGROUP] OnAfterCruise + -- @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. + + + --- Triggers the FSM event "TurnIntoWind". + -- @function [parent=#NAVYGROUP] TurnIntoWind + -- @param #NAVYGROUP self + -- @param #NAVYGROUP.IntoWind Into wind parameters. + + --- Triggers the FSM event "TurnIntoWind" after a delay. + -- @function [parent=#NAVYGROUP] __TurnIntoWind + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + -- @param #NAVYGROUP.IntoWind Into wind parameters. + + --- On after "TurnIntoWind" event. + -- @function [parent=#NAVYGROUP] OnAfterTurnIntoWind + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #NAVYGROUP.IntoWind Into wind parameters. + + + --- Triggers the FSM event "TurnedIntoWind". + -- @function [parent=#NAVYGROUP] TurnedIntoWind + -- @param #NAVYGROUP self + + --- Triggers the FSM event "TurnedIntoWind" after a delay. + -- @function [parent=#NAVYGROUP] __TurnedIntoWind + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "TurnedIntoWind" event. + -- @function [parent=#NAVYGROUP] OnAfterTurnedIntoWind + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "TurnIntoWindStop". + -- @function [parent=#NAVYGROUP] TurnIntoWindStop + -- @param #NAVYGROUP self + + --- Triggers the FSM event "TurnIntoWindStop" after a delay. + -- @function [parent=#NAVYGROUP] __TurnIntoWindStop + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "TurnIntoWindStop" event. + -- @function [parent=#NAVYGROUP] OnAfterTurnIntoWindStop + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "TurnIntoWindOver". + -- @function [parent=#NAVYGROUP] TurnIntoWindOver + -- @param #NAVYGROUP self + -- @param #NAVYGROUP.IntoWind IntoWindData Data table. + + --- Triggers the FSM event "TurnIntoWindOver" after a delay. + -- @function [parent=#NAVYGROUP] __TurnIntoWindOver + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + -- @param #NAVYGROUP.IntoWind IntoWindData Data table. + + --- On after "TurnIntoWindOver" event. + -- @function [parent=#NAVYGROUP] OnAfterTurnIntoWindOver + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #NAVYGROUP.IntoWind IntoWindData Data table. + + + --- Triggers the FSM event "TurningStarted". + -- @function [parent=#NAVYGROUP] TurningStarted + -- @param #NAVYGROUP self + + --- Triggers the FSM event "TurningStarted" after a delay. + -- @function [parent=#NAVYGROUP] __TurningStarted + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "TurningStarted" event. + -- @function [parent=#NAVYGROUP] OnAfterTurningStarted + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "TurningStopped". + -- @function [parent=#NAVYGROUP] TurningStopped + -- @param #NAVYGROUP self + + --- Triggers the FSM event "TurningStopped" after a delay. + -- @function [parent=#NAVYGROUP] __TurningStopped + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "TurningStopped" event. + -- @function [parent=#NAVYGROUP] OnAfterTurningStopped + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "CollisionWarning". + -- @function [parent=#NAVYGROUP] CollisionWarning + -- @param #NAVYGROUP self + + --- Triggers the FSM event "CollisionWarning" after a delay. + -- @function [parent=#NAVYGROUP] __CollisionWarning + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "CollisionWarning" event. + -- @function [parent=#NAVYGROUP] OnAfterCollisionWarning + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "ClearAhead". + -- @function [parent=#NAVYGROUP] ClearAhead + -- @param #NAVYGROUP self + + --- Triggers the FSM event "ClearAhead" after a delay. + -- @function [parent=#NAVYGROUP] __ClearAhead + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + + --- On after "ClearAhead" event. + -- @function [parent=#NAVYGROUP] OnAfterClearAhead + -- @param #NAVYGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Dive". + -- @function [parent=#NAVYGROUP] Dive + -- @param #NAVYGROUP self + -- @param #number Depth Dive depth in meters. Default 50 meters. + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- Triggers the FSM event "Dive" after a delay. + -- @function [parent=#NAVYGROUP] __Dive + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + -- @param #number Depth Dive depth in meters. Default 50 meters. + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- On after "Dive" event. + -- @function [parent=#NAVYGROUP] OnAfterDive + -- @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. + + + --- Triggers the FSM event "Surface". + -- @function [parent=#NAVYGROUP] Surface + -- @param #NAVYGROUP self + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- Triggers the FSM event "Surface" after a delay. + -- @function [parent=#NAVYGROUP] __Surface + -- @param #NAVYGROUP self + -- @param #number delay Delay in seconds. + -- @param #number Speed Speed in knots until next waypoint is reached. + + --- On after "Surface" event. + -- @function [parent=#NAVYGROUP] OnAfterSurface + -- @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. + + + -- 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.UnitLost, self.OnEventRemoveUnit) + + -- Start the status monitoring. + self.timerStatus=TIMER:New(self.Status, self):Start(1, 30) + + -- 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) + + -- Add OPSGROUP to _DATABASE. + _DATABASE:AddOpsGroup(self) + + 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(false, self.pathCorridor) + return self +end + +--- Set if old into wind calculation is used when carrier turns into the wind for a recovery. +-- @param #NAVYGROUP self +-- @param #boolean SwitchOn If `true` or `nil`, use old into wind calculation. +-- @return #NAVYGROUP self +function NAVYGROUP:SetIntoWindLegacy( SwitchOn ) + if SwitchOn==nil then + SwitchOn=true + end + self.intowindold=SwitchOn + 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) + + if uturn==nil then + uturn=true + end + + -- 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 Wind speed on deck in knots during turn into wind leg. Default 20 knots. +-- @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 clock-wise in degrees, *e.g.* to account for an angled runway. Default 0 deg. Use around -9.1° for US carriers. +-- @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 + +--- Get "Turn Into Wind" data. You can specify a certain ID. +-- @param #NAVYGROUP self +-- @param #number TID (Optional) Turn Into wind ID. If not given, the currently open "Turn into Wind" data is return (if there is any). +-- @return #NAVYGROUP.IntoWind Turn into window data table. +function NAVYGROUP:GetTurnIntoWind(TID) + + if TID then + + -- Look for a specific ID. + for _,_turn in pairs(self.Qintowind) do + local turn=_turn --#NAVYGROUP.IntoWind + if turn.Id==TID then + return turn + end + end + + else + + -- Return currently open window. + return self.intowind + + end + + return nil +end + +--- Extend duration of turn into wind. +-- @param #NAVYGROUP self +-- @param #number Duration Duration in seconds. Default 300 sec. +-- @param #NAVYGROUP.IntoWind TurnIntoWind (Optional) Turn into window data table. If not given, the currently open one is used (if there is any). +-- @return #NAVYGROUP self +function NAVYGROUP:ExtendTurnIntoWind(Duration, TurnIntoWind) + + Duration=Duration or 300 + + -- ID of turn or nil + local TID=TurnIntoWind and TurnIntoWind.Id or nil + + -- Get turn data. + local turn=self:GetTurnIntoWind(TID) + + if turn then + turn.Tstop=turn.Tstop+Duration + self:T(self.lid..string.format("Extending turn into wind by %d seconds. New stop time is %s", Duration, UTILS.SecondsToClock(turn.Tstop))) + else + self:E(self.lid.."Could not get turn into wind to extend!") + end + + return self +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 + 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 + +--- Check if the group is currently recovering aircraft. +-- @param #NAVYGROUP self +-- @return #boolean If true, group is currently recovering. +function NAVYGROUP:IsRecovering() + if self.intowind then + if self.intowind.Recovery==true then + return true + else + return false + end + else + return false + end +end + +--- Check if the group is currently launching aircraft. +-- @param #NAVYGROUP self +-- @return #boolean If true, group is currently launching. +function NAVYGROUP:IsLaunching() + if self.intowind then + if self.intowind.Recovery==false then + return true + else + return false + end + else + return false + end +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Update status. +-- @param #NAVYGROUP self +function NAVYGROUP:Status() + + -- FSM state. + local fsmstate=self:GetState() + + -- Is group alive? + local alive=self:IsAlive() + + -- Free path. + local freepath=0 + + -- Check if group is exists and is active. + if alive then + + -- Update last known position, orientation, velocity. + self:_UpdatePosition() + + -- Check if group has detected any units. + self:_CheckDetectedUnits() + + -- Check if group started or stopped turning. + self:_CheckTurning() + + -- Distance to next Waypoint. + local disttoWP=math.min(self:GetDistanceToWaypoint(), UTILS.NMToMeters(10)) + freepath=disttoWP + + -- Only check if not currently turning. + if not self:IsTurning() then + + -- Check free path ahead. + freepath=self:_CheckFreePath(freepath, 100) + + if disttoWP>1 and freepathself.Twaiting+self.dTwait then + self.Twaiting=nil + self.dTwait=nil + if self:_CountPausedMissions()>0 then + self:UnpauseMission() + else + self:Cruise() + end + end + end + end + + + -- Get current mission (if any). + local mission=self:GetMissionCurrent() + + -- If mission, check if DCS task needs to be updated. + if mission and mission.updateDCSTask then + + if mission.type==AUFTRAG.Type.CAPTUREZONE then + + -- Get task. + local Task=mission:GetGroupWaypointTask(self) + + -- Update task: Engage or get new zone. + if mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.EXECUTING or mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.STARTED then + self:_UpdateTask(Task, mission) + end + + end + + end + + + else + -- Check damage of elements and group. + self:_CheckDamage() + end + + -- Group exists but can also be inactive. + if alive~=nil then + + if self.verbose>=1 then + + -- Number of elements. + local nelem=self:CountElements() + local Nelem=#self.elements + + -- Get number of tasks and missions. + local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks() + local nMissions=self:CountRemainingMissison() + + -- ROE and Alarm State. + local roe=self:GetROE() or -1 + local als=self:GetAlarmstate() or -1 + + -- 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 wpN=#self.waypoints or 0 + local wpF=tostring(self.passedfinalwp) + + -- Speed. + local speed=UTILS.MpsToKnots(self.velocity or 0) + local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed()) + + -- Altitude. + local alt=self.position and self.position.y or 0 + + -- Heading in degrees. + local hdg=self.heading or 0 + + -- Life points. + local life=self.life or 0 + + -- Total ammo. + local ammo=self:GetAmmoTot().Total + + -- Detected units. + local ndetected=self.detectionOn and tostring(self.detectedunits:Count()) or "Off" + + -- Get cargo weight. + local cargo=0 + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + cargo=cargo+element.weightCargo + end + + -- Into wind and turning status. + local intowind=self:IsSteamingIntoWind() and UTILS.SecondsToClock(self.intowind.Tstop-timer.getAbsTime(), true) or "N/A" + local turning=tostring(self:IsTurning()) + + -- Info text. + local text=string.format("%s [%d/%d]: ROE/AS=%d/%d | T/M=%d/%d | Wp=%d[%d]-->%d[%d]/%d [%s] | Life=%.1f | v=%.1f (%d) | Hdg=%03d | Ammo=%d | Detect=%s | Cargo=%.1f | Turn=%s Collision=%d IntoWind=%s", + fsmstate, nelem, Nelem, roe, als, nTaskTot, nMissions, wpidxCurr, wpuidCurr, wpidxNext, wpuidNext, wpN, wpF, life, speed, speedEx, hdg, ammo, ndetected, cargo, turning, freepath, intowind) + self:I(self.lid..text) + + 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 alive and self.verbose>=2 and #self.Qintowind>0 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 + + --- + -- Elements + --- + + if self.verbose>=2 then + local text="Elements:" + for i,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + + local name=element.name + local status=element.status + local unit=element.unit + local life,life0=self:GetLifePoints(element) + + local life0=element.life0 + + -- Get ammo. + local ammo=self:GetAmmoElement(element) + + -- Output text for element. + text=text..string.format("\n[%d] %s: status=%s, life=%.1f/%.1f, guns=%d, rockets=%d, bombs=%d, missiles=%d, cargo=%d/%d kg", + i, name, status, life, life0, ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles, element.weightCargo, element.weightMaxCargo) + end + if #self.elements==0 then + text=text.." none!" + end + self:I(self.lid..text) + end + + --- + -- Engage Detected Targets + --- + if self:IsCruising() and self.detectionOn and self.engagedetectedOn then + + local targetgroup, targetdist=self:_GetDetectedTarget() + + -- If we found a group, we engage it. + if targetgroup then + self:I(self.lid..string.format("Engaging target group %s at distance %d meters", targetgroup:GetName(), targetdist)) + self:EngageTarget(targetgroup) + end + + end + + --- + -- Cargo + --- + + self:_CheckCargoTransport() + + --- + -- Tasks & Missions + --- + + self:_PrintTaskAndMissionStatus() + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Events ==> See OPSGROUP +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- See OPSGROUP! + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "ElementSpawned" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP.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!")) + + -- Debug info. + if self.verbose>=1 then + local text=string.format("Initialized Navy Group %s [GID=%d]:\n", self.groupname, self.group:GetID()) + 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("Weight = %.1f kg\n", self:GetWeightTotal()) + text=text..string.format("Cargo bay = %.1f kg\n", self:GetFreeCargobay()) + text=text..string.format("Has EPLRS = %s\n", tostring(self.isEPLRS)) + text=text..string.format("Is Submarine = %s\n", tostring(self.isSubmarine)) + 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/C=%d/R=%d/M=%d/T=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Cannons, 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 + + -- Update position. + self:_UpdatePosition() + + -- Not dead or destroyed yet. + self.isDead=false + self.isDestroyed=false + + if self.isAI then + + -- Set default ROE. + self:SwitchROE(self.option.ROE) + + -- Set default Alarm State. + self:SwitchAlarmstate(self.option.Alarm) + + -- Set emission. + self:SwitchEmission(self.option.Emission) + + -- Set default EPLRS. + self:SwitchEPLRS(self.option.EPLRS) + + -- Set default Invisible. + self:SwitchInvisible(self.option.Invisible) + + -- Set default Immortal. + self:SwitchImmortal(self.option.Immortal) + + -- Set TACAN beacon. + self:_SwitchTACAN() + + -- Turn ICLS on. + self:_SwitchICLS() + + -- Set radio. + if self.radioDefault then + -- CAREFUL: This makes DCS crash for some ships like speed boats or Higgins boats! (On a respawn for example). Looks like the command SetFrequency is causing this. + --self:SwitchRadio() + else + self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, false) + end + + -- Update route. + if #self.waypoints>1 then + self:__Cruise(-0.1) + else + self:FullStop() + end + + end + +end + +--- On before "UpdateRoute" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final waypoint. +-- @param #number Speed Speed in knots to the next waypoint. +-- @param #number Depth Depth in meters to the next waypoint. +function NAVYGROUP:onbeforeUpdateRoute(From, Event, To, n, Speed, Depth) + + -- Is transition allowed? We assume yes until proven otherwise. + local allowed=true + local trepeat=nil + + if self:IsWaiting() then + self:T(self.lid.."Update route denied. Group is WAITING!") + return false + elseif self:IsInUtero() then + self:T(self.lid.."Update route denied. Group is INUTERO!") + return false + elseif self:IsDead() then + self:T(self.lid.."Update route denied. Group is DEAD!") + return false + elseif self:IsStopped() then + self:T(self.lid.."Update route denied. Group is STOPPED!") + return false + elseif self:IsHolding() then + self:T(self.lid.."Update route denied. Group is holding position!") + return false + elseif self:IsEngaging() then + self:T(self.lid.."Update route allowed. Group is engaging!") + return true + end + + -- Check for a current task. + if self.taskcurrent>0 then + + -- Get the current task. Must not be executing already. + local task=self:GetTaskByID(self.taskcurrent) + + if task then + if task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then + -- For patrol zone, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: PatrolZone") + elseif task.dcstask.id==AUFTRAG.SpecialTask.RECON then + -- For recon missions, we need to allow the update as we insert new waypoints. + self:T2(self.lid.."Allowing update route for Task: ReconMission") + elseif task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then + -- For relocate + self:T2(self.lid.."Allowing update route for Task: Relocate Cohort") + elseif task.dcstask.id==AUFTRAG.SpecialTask.REARMING then + -- For rearming + self:T2(self.lid.."Allowing update route for Task: Rearming") + else + local taskname=task and task.description or "No description" + self:T(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. Therefore, also directly executed tasks should be added to the queue! + 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 in state %s: allowed=%s (repeat in %s)", self:GetState(), tostring(allowed), tostring(trepeat))) + + -- Try again? + if trepeat then + self:__UpdateRoute(trepeat, n) + end + + return allowed +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 Next waypoint index. Default is the one coming after that one that has been passed last. +-- @param #number N Waypoint Max waypoint index to be included in the route. Default is the final 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, N, Speed, Depth) + + -- Update route from this waypoint number onwards. + n=n or self:GetWaypointIndexNext() + + -- Max index. + N=N or #self.waypoints + N=math.min(N, #self.waypoints) + + + -- Waypoints. + local waypoints={} + + for i=n, N do + + -- Waypoint. + local wp=UTILS.DeepCopy(self.waypoints[i]) --Ops.OpsGroup#OPSGROUP.Waypoint + + --env.info(string.format("FF i=%d UID=%d n=%d, N=%d", i, wp.uid, n, N)) + + -- 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 wp.speed<0.1 then --self.adinfinitum and + wp.speed=UTILS.KmphToMps(self.speedCruise) + end + end + + -- Depth. + if Depth then + wp.alt=-Depth + elseif self.depth then + wp.alt=-self.depth + else + -- Take default waypoint alt. + wp.alt=wp.alt or 0 + end + + -- Current set speed in m/s. + if i==n then + self.speedWp=wp.speed + self.altWp=wp.alt + end + + -- Add waypoint. + table.insert(waypoints, wp) + + end + + -- Current waypoint. + local current=self:GetCoordinate():WaypointNaval(UTILS.MpsToKmph(self.speedWp), self.altWp) + table.insert(waypoints, 1, current) + + + if self:IsEngaging() or not self.passedfinalwp then + + if self.verbose>=10 then + for i=1,#waypoints do + local wp=waypoints[i] --Ops.OpsGroup#OPSGROUP.Waypoint + local text=string.format("%s Waypoint [%d] UID=%d speed=%d m/s", self.groupname, i-1, wp.uid or -1, wp.speed) + self:I(self.lid..text) + COORDINATE:NewFromWaypoint(wp):MarkToAll(text) + end + end + + -- 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), self.altWp)) + + -- 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) + + -- Calculate heading and speed of ship. + local heading, speed=self:GetHeadingIntoWind(IntoWind.Offset, IntoWind.Speed) + + IntoWind.Heading=heading + IntoWind.Open=true + + -- Get coordinate. + IntoWind.Coordinate=self:GetCoordinate(true) + + -- Set current into wind parameters. + self.intowind=IntoWind + + -- Debug info. + self:T(self.lid..string.format("Steaming into wind: Heading=%03d Speed=%.1f, Tstart=%d Tstop=%d", IntoWind.Heading, speed, 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 false 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!") + + -- ID of current waypoint. + local uid=self:GetWaypointCurrent().uid + + -- Add temp waypoint. + local wp=self:AddWaypoint(self.intowind.Coordinate, self:GetSpeedCruise(), uid) ; wp.temp=true + + else + + --- + -- Go directly to next waypoint. + --- + + -- Next waypoint index and speed. + local indx=self:GetWaypointIndexNext() + local speed=self:GetSpeedToWaypoint(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, nil, 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) + + -- Not waiting anymore. + self.Twaiting=nil + self.dTwait=nil + + -- No set depth. + self.depth=nil + + self:__UpdateRoute(-0.1, nil, 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:I(self.lid..string.format("Diving to %d meters", Depth)) + + self.depth=Depth + + self:__UpdateRoute(-1, nil, 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, 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 "EngageTarget" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Target#TARGET Target The target to be engaged. Can also be a GROUP or UNIT object. +-- @param #number Speed Attack speed in knots. +-- @param #number Depth The depth in meters. Only for submarins. +function NAVYGROUP:onafterEngageTarget(From, Event, To, Target, Speed, Depth) + self:T(self.lid.."Engaging 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()) + + -- Get a coordinate close to the target. + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.8) + + -- Backup ROE and alarm state. + self.engage.roe=self:GetROE() + self.engage.alarmstate=self:GetAlarmstate() + + -- Switch ROE and alarm state. + self:SwitchAlarmstate(ENUMS.AlarmState.Auto) + self:SwitchROE(ENUMS.ROE.OpenFire) + + -- ID of current waypoint. + local uid=self:GetWaypointCurrentUID() + + -- Set formation. + self.engage.Depth=Depth or 0 + + -- Set speed. + self.engage.Speed=Speed + + -- Add waypoint after current. + self.engage.Waypoint=self:AddWaypoint(intercoord, Speed, uid, Depth, true) + + -- Set if we want to resume route after reaching the detour waypoint. + self.engage.Waypoint.detour=1 + +end + +--- Update engage target. +-- @param #NAVYGROUP self +function NAVYGROUP:_UpdateEngageTarget() + + if self.engage.Target and self.engage.Target:IsAlive() then + + -- Get current position vector. + local vec3=self.engage.Target:GetVec3() + + if vec3 then + + -- Distance to last known position of target. + local dist=UTILS.VecDist3D(vec3, self.engage.Coordinate:GetVec3()) + + -- Check if target moved more than 100 meters. + if dist>100 then + + -- Update new position. + self.engage.Coordinate:UpdateFromVec3(vec3) + + -- ID of current waypoint. + local uid=self:GetWaypointCurrentUID() + + -- Remove current waypoint + self:RemoveWaypointByID(self.engage.Waypoint.uid) + + local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.8) + + -- Add waypoint after current. + self.engage.Waypoint=self:AddWaypoint(intercoord, self.engage.Speed, uid, self.engage.Depth, true) + + -- Set if we want to resume route after reaching the detour waypoint. + self.engage.Waypoint.detour=1 + + end + + else + + -- Could not get position of target (not alive any more?) ==> Disengage. + self:Disengage() + + end + + else + + -- Target not alive any more ==> Disengage. + self:Disengage() + + end + +end + +--- On after "Disengage" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterDisengage(From, Event, To) + self:T(self.lid.."Disengage Target") + + -- Restore previous ROE and alarm state. + self:SwitchROE(self.engage.roe) + self:SwitchAlarmstate(self.engage.alarmstate) + + -- Get current task + local task=self:GetTaskCurrent() + + -- Get if current task is ground attack. + if task and (task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK or task.dcstask.id==AUFTRAG.SpecialTask.NAVALENGAGEMENT) then + self:T(self.lid.."Disengage with current task GROUNDATTACK/NAVALENGAGEMENT ==> Task Done!") + self:TaskDone(task) + end + + -- Remove current waypoint + if self.engage.Waypoint then + self:RemoveWaypointByID(self.engage.Waypoint.uid) + end + + -- Check group is done + self:_CheckGroupDone(1) +end + +--- On after "OutOfAmmo" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterOutOfAmmo(From, Event, To) + self:T(self.lid..string.format("Group is out of ammo at t=%.3f", timer.getTime())) + + -- Check if we want to retreat once out of ammo. + if self.retreatOnOutOfAmmo then + self:__Retreat(-1) + return + end + + -- Third, check if we want to RTZ once out of ammo. + if self.rtzOnOutOfAmmo then + self:__RTZ(-1) + end + + -- Get current task. + local task=self:GetTaskCurrent() + + if task then + if task.dcstask.id=="FireAtPoint" or task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then + self:T(self.lid..string.format("Cancelling current %s task because out of ammo!", task.dcstask.id)) + self:TaskCancel(task) + end + end + +end + +--- On after "RTZ" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE Zone The zone to return to. +-- @param #number Formation Formation of the group. +function NAVYGROUP:onafterRTZ(From, Event, To, Zone, Formation) + + -- Zone. + local zone=Zone or self.homezone + + -- Cancel all missions in the queue. + self:CancelAllMissions() + + if zone then + + if self:IsInZone(zone) then + self:Returned() + else + + -- Debug info. + self:T(self.lid..string.format("RTZ to Zone %s", zone:GetName())) + + local Coordinate=zone:GetRandomCoordinate() + + -- ID of current waypoint. + local uid=self:GetWaypointCurrentUID() + + -- 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 + + else + self:T(self.lid.."ERROR: No RTZ zone given!") + end + +end + + +--- On after "Returned" event. +-- @param #NAVYGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function NAVYGROUP:onafterReturned(From, Event, To) + + -- Debug info. + self:T(self.lid..string.format("Group returned")) + + if self.legion then + -- Debug info. + self:T(self.lid..string.format("Adding group back to warehouse stock")) + + -- Add asset back in 10 seconds. + self.legion:__AddAsset(10, self.group, 1) + 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()` 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 feet. 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) + + -- Create coordinate. + local coordinate=self:_CoordinateFromObject(Coordinate) + + -- Set waypoint index. + local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID) + + -- 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) + + -- Set altitude. + if Depth then + waypoint.alt=UTILS.FeetToMeters(Depth) + end + + -- 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:__UpdateRoute(-0.01) + end + + return waypoint +end + +--- Initialize group parameters. Also initializes waypoints if self.waypoints is nil. +-- @param #NAVYGROUP self +-- @param #table Template Template used to init the group. Default is `self.template`. +-- @param #number Delay Delay in seconds before group is initialized. Default `nil`, *i.e.* instantaneous. +-- @return #NAVYGROUP self +function NAVYGROUP:_InitGroup(Template, Delay) + + if Delay and Delay>0 then + -- Delayed call + self:ScheduleOnce(Delay, NAVYGROUP._InitGroup, self, Template, 0) + else + + -- First check if group was already initialized. + if self.groupinitialized then + self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!") + return + end + + -- Get template of group. + local template=Template or self:_GetTemplate() + + -- Ships are always AI. + self.isAI=true + + -- Is (template) group late activated. + self.isLateActivated=template.lateActivation + + -- Naval groups cannot be uncontrolled. + self.isUncontrolled=false + + -- Max speed in km/h. + self.speedMax=self.group:GetSpeedMax() + + -- Is group mobile? + if self.speedMax and self.speedMax>3.6 then + self.isMobile=true + else + self.isMobile=false + self.speedMax = 0 + end + + -- 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(template.units[1].frequency)/1000000 + self.radio.Modu=tonumber(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 (we check if something is set already to keep those values in case of respawn) + if not self.tacanDefault then + self:SetDefaultTACAN(nil, nil, nil, nil, true) + end + if not self.tacan then + self.tacan=UTILS.DeepCopy(self.tacanDefault) + end + + -- Default ICLS off. + if not self.iclsDefault then + self:SetDefaultICLS(nil, nil, nil, true) + end + if not self.icls then + self.icls=UTILS.DeepCopy(self.iclsDefault) + end + + -- Get all units of the group. + local units=self.group:GetUnits() + + -- DCS group. + local dcsgroup=Group.getByName(self.groupname) + local size0=dcsgroup:getInitialSize() + + -- Quick check. + if #units~=size0 then + self:E(self.lid..string.format("ERROR: Got #units=%d but group consists of %d units!", #units, size0)) + end + + -- Add elemets. + for _,unit in pairs(units) do + self:_AddElementByName(unit:GetName()) + end + + -- Init done. + self.groupinitialized=true + end + + 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 + + local vec3=self:GetVec3() + vec3.y=offsetY + + -- Current heading. + local heading=self:GetHeading() + + local function LoS(dist) + 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:T(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 + + local _check=check() + + 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 time 0 and windSpeed < 3 then + degreesAdjustment = 30 + elseif windSpeed >= 3 and windSpeed < 5 then + degreesAdjustment = 20 + elseif windSpeed >= 5 and windSpeed < 8 then + degreesAdjustment = 8 + elseif windSpeed >= 8 and windSpeed < 13 then + degreesAdjustment = 4 + elseif windSpeed >= 13 then + degreesAdjustment = 0 + end + + return degreesAdjustment + end + + Offset=Offset or 0 + + -- Get direction the wind is blowing from. This is where we want to go. + local windfrom, vwind=self:GetWind() + + -- Actually, we want the runway in the wind. + local intowind = windfrom - Offset + adjustDegreesForWindSpeed(vwind) + + -- If no wind, take current heading. + if vwind<0.1 then + intowind=self:GetHeading() + end + + -- Adjust negative values. + if intowind<0 then + intowind=intowind+360 + end + + -- Speed of carrier in m/s but at least 4 knots. + local vtot = math.max(vdeck-UTILS.MpsToKnots(vwind), 4) + + return intowind, vtot +end + + +--- Get heading of group into the wind. This minimizes the cross wind for an angled runway. +-- Implementation based on [Mags & Bami](https://magwo.github.io/carrier-cruise/) work. +-- @param #NAVYGROUP self +-- @param #number Offset Offset angle in degrees, e.g. to account for an angled runway. +-- @param #number vdeck Desired wind speed on deck in Knots. +-- @return #number Carrier heading in degrees. +-- @return #number Carrier speed in knots. +function NAVYGROUP:GetHeadingIntoWind_new(Offset, vdeck) + + -- Default offset angle. + Offset=Offset or 0 + + -- Get direction the wind is blowing from. + local windfrom, vwind=self:GetWind(18) + + -- Convert wind speed to knots. + vwind=UTILS.MpsToKnots(vwind) + + -- Wind to in knots. + local windto=(windfrom+180)%360 + + -- Offset angle in rad. We also define the rotation to be clock-wise, which requires a minus sign. + local alpha=math.rad(-Offset) + + -- Ships min/max speed. + local Vmin=4 + local Vmax=UTILS.KmphToKnots(self.speedMax) + + -- Constant. + local C = math.sqrt(math.cos(alpha)^2 / math.sin(alpha)^2 + 1) + + + -- Upper limit of desired speed due to max boat speed. + local vdeckMax=vwind + math.cos(alpha) * Vmax + + -- Lower limit of desired speed due to min boat speed. + local vdeckMin=vwind + math.cos(alpha) * Vmin + + + -- Speed of ship so it matches the desired speed. + local v=0 + + -- Angle wrt. to wind TO-direction + local theta=0 + + if vdeck>vdeckMax then + -- Boat cannot go fast enough + + -- Set max speed. + v=Vmax + + -- Calculate theta. + theta = math.asin(v/(vwind*C)) - math.asin(-1/C) + + elseif vdeckvwind then + -- Too little wind + + -- Set theta to 90° + theta=math.pi/2 + + -- Set speed. + v = math.sqrt(vdeck^2 - vwind^2) + + else + -- Normal case + theta = math.asin(vdeck * math.sin(alpha) / vwind) + v = vdeck * math.cos(alpha) - vwind * math.cos(theta) + end + + + -- Ship heading so cross wind is min for the given wind. + local intowind = (540 + (windto + math.deg(theta) )) % 360 + + -- Debug info. + self:T(self.lid..string.format("Heading into Wind: vship=%.1f, vwind=%.1f, WindTo=%03d°, Theta=%03d°, Heading=%03d", v, vwind, windto, theta, intowind)) + + return intowind, v +end + +--- Get heading of group into the wind. This minimizes the cross wind for an angled runway. +-- Implementation based on [Mags & Bami](https://magwo.github.io/carrier-cruise/) work. +-- @param #NAVYGROUP self +-- @param #number Offset Offset angle in degrees, e.g. to account for an angled runway. +-- @param #number vdeck Desired wind speed on deck in Knots. +-- @return #number Carrier heading in degrees. +-- @return #number Carrier speed in knots. +function NAVYGROUP:GetHeadingIntoWind(Offset, vdeck) + + if self.intowindold then + --env.info("FF use OLD into wind") + return self:GetHeadingIntoWind_old(Offset, vdeck) + else + --env.info("FF use NEW into wind") + return self:GetHeadingIntoWind_new(Offset, vdeck) + end + +end + + +--- Find free path to next waypoint. +-- @param #NAVYGROUP self +-- @return #boolean If true, a path was found. +function NAVYGROUP:_FindPathToNextWaypoint() + self:T3(self.lid.."Path finding") + + --TODO: Do not create a new ASTAR object each time this function is called but make it self.astar and reuse. Should be better for performance. + + -- Pathfinding A* + local astar=ASTAR:New() + + -- Current positon of the group. + local position=self:GetCoordinate() + + -- Next waypoint. + local wpnext=self:GetWaypointNext() + + -- No next waypoint. + if wpnext==nil then + return + end + + -- Next waypoint coordinate. + local nextwp=wpnext.coordinate + + -- If we are currently turning into the wind... + if wpnext.intowind then + local hdg=self:GetHeading() + nextwp=position:Translate(UTILS.NMToMeters(20), hdg, true) + end + + local speed=UTILS.MpsToKnots(wpnext.speed) + + -- Set start coordinate. + astar:SetStartCoordinate(position) + + -- Set end coordinate. + astar:SetEndCoordinate(nextwp) + + -- Distance to next waypoint. + local dist=position:Get2DDistance(nextwp) + + -- Check distance >= 5 meters. + if dist<5 then + return + end + + local boxwidth=dist*2 + local spacex=dist*0.1 + local delta=dist/10 + + -- Create a grid of nodes. We only want nodes of surface type water. + astar:CreateGrid({land.SurfaceType.WATER}, boxwidth, spacex, delta, delta, self.verbose>10) + + -- Valid neighbour nodes need to have line of sight. + astar:SetValidNeighbourLoS(self.pathCorridor) + + --- Function to find a path and add waypoints to the group. + local function findpath() + + -- Calculate path from start to end node. + local path=astar:GetPath(true, true) + + if path then + + -- Loop over nodes in found path. + local uid=self:GetWaypointCurrent().uid -- ID of current waypoint. + + for i,_node in ipairs(path) do + local node=_node --Core.Astar#ASTAR.Node + + -- Add waypoints along detour path to next waypoint. + local wp=self:AddWaypoint(node.coordinate, speed, uid) + wp.astar=true + + -- Update id so the next wp is added after this one. + uid=wp.uid + + -- Debug: smoke and mark path. + if self.verbose>=10 then + node.coordinate:MarkToAll(string.format("Path node #%d", i)) + end + + end + + return #path>0 + else + return false + end + + end + + -- Return if path was found. + return findpath() +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Operation.lua b/Moose Development/Moose/Ops/Operation.lua new file mode 100644 index 000000000..34834439a --- /dev/null +++ b/Moose Development/Moose/Ops/Operation.lua @@ -0,0 +1,1377 @@ +--- **Ops** - Operation with multiple phases. +-- +-- ## Main Features: +-- +-- * Define operation phases +-- * Define conditions when phases are over +-- * Option to have branches in the phase tree +-- * Dedicate resources to operations +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/Operation). +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- @module Ops.Operation +-- @image OPS_Operation.png + + +--- OPERATION class. +-- @type OPERATION +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #number uid Unique ID of the operation. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string name Name of the operation. +-- @field #number Tstart Start time in seconds of abs mission time. +-- @field #number Tstop Stop time in seconds of abs mission time. +-- @field #number duration Duration of the operation in seconds. +-- @field Core.Condition#CONDITION conditionStart Start condition. +-- @field Core.Condition#CONDITION conditionOver Over condition. +-- @field #table branches Branches. +-- @field #OPERATION.Branch branchMaster Master branch. +-- @field #OPERATION.Branch branchActive Active branch. +-- @field #number counterPhase Running number counting the phases. +-- @field #number counterBranch Running number counting the branches. +-- @field #OPERATION.Phase phase Currently active phase (if any). +-- @field #OPERATION.Phase phaseLast The phase that was active before the current one. +-- @field #table cohorts Dedicated cohorts. +-- @field #table legions Dedicated legions. +-- @field #table targets Targets. +-- @field #table missions Missions. +-- @extends Core.Fsm#FSM + +--- *Before this time tomorrow I shall have gained a peerage, or Westminster Abbey.* -- Horatio Nelson +-- +-- === +-- +-- # The OPERATION Concept +-- +-- This class allows you to create complex operations, which consist of multiple phases. Conditions can be specified, when a phase is over. If a phase is over, the next phase is started. +-- FSM events can be used to customize code that is executed at each phase. Phases can also switched manually, of course. +-- +-- In the simplest case, adding phases leads to a linear chain. However, you can also create branches to contruct a more tree like structure of phases. You can switch between branches +-- manually or add "edges" with conditions when to switch branches. We are diving a bit into graph theory here. So don't feel embarrassed at all, if you stick to linear chains. +-- +-- # Constructor +-- +-- A new operation can be created with the @{#OPERATION.New}(*Name*) function, where the parameter `Name` is a free to choose string. +-- +-- ## Adding Phases +-- +-- You can add phases with the @{#OPERATION.AddPhase}(*Name*, *Branch*) function. The first parameter `Name` is the name of the phase. The second parameter `Branch` is the branch to which the phase is +-- added. If this is omitted (nil), the phase is added to the default, *i.e.* "master branch". More about adding branches later. +-- +-- +-- +-- +-- @field #OPERATION +OPERATION = { + ClassName = "OPERATION", + verbose = 0, + branches = {}, + counterPhase = 0, + counterBranch = 0, + counterEdge = 0, + cohorts = {}, + legions = {}, + targets = {}, + missions = {}, +} + +--- Global mission counter. +_OPERATIONID=0 + +--- Operation phase. +-- @type OPERATION.Phase +-- @field #number uid Unique ID of the phase. +-- @field #string name Name of the phase. +-- @field Core.Condition#CONDITION conditionOver Conditions when the phase is over. +-- @field #string status Phase status. +-- @field #number Tstart Abs. mission time when the phase was started. +-- @field #number nActive Number of times the phase was active. +-- @field #number duration Duration in seconds how long the phase should be active after it started. +-- @field #OPERATION.Branch branch The branch this phase belongs to. + +--- Operation branch. +-- @type OPERATION.Branch +-- @field #number uid Unique ID of the branch. +-- @field #string name Name of the branch. +-- @field #table phases Phases of this branch. +-- @field #table edges Edges of this branch. + +--- Operation edge. +-- @type OPERATION.Edge +-- @field #number uid Unique ID of the edge. +-- @field #OPERATION.Branch branchFrom The from branch. +-- @field #OPERATION.Phase phaseFrom The from phase after which to switch. +-- @field #OPERATION.Branch branchTo The branch to switch to. +-- @field #OPERATION.Phase phaseTo The phase to switch to. +-- @field Core.Condition#CONDITION conditionSwitch Conditions when to switch the branch. + +--- Operation phase. +-- @type OPERATION.PhaseStatus +-- @field #string PLANNED Planned. +-- @field #string ACTIVE Active phase. +-- @field #string OVER Phase is over. +OPERATION.PhaseStatus={ + PLANNED="Planned", + ACTIVE="Active", + OVER="Over", +} + +--- OPERATION class version. +-- @field #string version +OPERATION.version="0.2.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: "Over" conditions. +-- TODO: Repeat phases: after over ==> planned (not over) +-- DONE: Branches. +-- DONE: Phases. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new generic OPERATION object. +-- @param #OPERATION self +-- @param #string Name Name of the operation. Be creative! Default "Operation-01" where the last number is a running number. +-- @return #OPERATION self +function OPERATION:New(Name) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #OPERATION + + -- Increase global counter. + _OPERATIONID=_OPERATIONID+1 + + -- Unique ID of the operation. + self.uid=_OPERATIONID + + -- Set Name. + self.name=Name or string.format("Operation-%02d", _OPERATIONID) + + -- Set log ID. + self.lid=string.format("%s | ",self.name) + + -- FMS start state is PLANNED. + self:SetStartState("Planned") + + -- Master branch. + self.branchMaster=self:AddBranch("Master") + + self.conditionStart=CONDITION:New("Operation %s start", self.name) + self.conditionStart:SetNoneResult(false) --If no condition function is specified, the ops will NOT be over. + self.conditionStart:SetDefaultPersistence(false) + + self.conditionOver=CONDITION:New("Operation %s over", self.name) + self.conditionOver:SetNoneResult(false) + self.conditionOver:SetDefaultPersistence(false) + + + -- Set master as active branch. + self.branchActive=self.branchMaster + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("*", "Start", "Running") + + self:AddTransition("*", "StatusUpdate", "*") + + self:AddTransition("Running", "Pause", "Paused") + self:AddTransition("Paused", "Unpause", "Running") + + self:AddTransition("*", "PhaseOver", "*") + self:AddTransition("*", "PhaseNext", "*") + self:AddTransition("*", "PhaseChange", "*") + + self:AddTransition("*", "BranchSwitch", "*") + + self:AddTransition("*", "Over", "Over") + + self:AddTransition("*", "Stop", "Stopped") + + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". + -- @function [parent=#OPERATION] Start + -- @param #OPERATION self + + --- Triggers the FSM event "Start" after a delay. + -- @function [parent=#OPERATION] __Start + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + + --- On after "Start" event. + -- @function [parent=#OPERATION] OnAfterStart + -- @param #OPERATION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Triggers the FSM event "Stop". + -- @function [parent=#OPERATION] Stop + -- @param #OPERATION self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#OPERATION] __Stop + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "StatusUpdate". + -- @function [parent=#OPERATION] StatusUpdate + -- @param #OPERATION self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#OPERATION] __StatusUpdate + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "PhaseChange". + -- @function [parent=#OPERATION] PhaseChange + -- @param #OPERATION self + -- @param #OPERATION.Phase Phase The new phase. + + --- Triggers the FSM event "PhaseChange" after a delay. + -- @function [parent=#OPERATION] __PhaseChange + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + -- @param #OPERATION.Phase Phase The new phase. + + --- On after "PhaseChange" event. + -- @function [parent=#OPERATION] OnAfterPhaseChange + -- @param #OPERATION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #OPERATION.Phase Phase The new phase. + + + --- Triggers the FSM event "PhaseNext". + -- @function [parent=#OPERATION] PhaseNext + -- @param #OPERATION self + + --- Triggers the FSM event "PhaseNext" after a delay. + -- @function [parent=#OPERATION] __PhaseNext + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + + --- On after "PhaseNext" event. + -- @function [parent=#OPERATION] OnAfterPhaseNext + -- @param #OPERATION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "PhaseOver". + -- @function [parent=#OPERATION] PhaseOver + -- @param #OPERATION self + -- @param #OPERATION.Phase Phase The phase that is over. + + --- Triggers the FSM event "PhaseOver" after a delay. + -- @function [parent=#OPERATION] __PhaseOver + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + -- @param #OPERATION.Phase Phase The phase that is over. + + --- On after "PhaseOver" event. + -- @function [parent=#OPERATION] OnAfterPhaseOver + -- @param #OPERATION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #OPERATION.Phase Phase The phase that is over. + + + --- Triggers the FSM event "BranchSwitch". + -- @function [parent=#OPERATION] BranchSwitch + -- @param #OPERATION self + -- @param #OPERATION.Branch Branch The branch that is now active. + -- @param #OPERATION.Phase Phase The new phase. + + --- Triggers the FSM event "BranchSwitch" after a delay. + -- @function [parent=#OPERATION] __BranchSwitch + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + -- @param #OPERATION.Branch Branch The branch that is now active. + -- @param #OPERATION.Phase Phase The new phase. + + --- On after "BranchSwitch" event. + -- @function [parent=#OPERATION] OnAfterBranchSwitch + -- @param #OPERATION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #OPERATION.Branch Branch The branch that is now active. + -- @param #OPERATION.Phase Phase The new phase. + + --- Triggers the FSM event "Over". + -- @function [parent=#OPERATION] Over + -- @param #OPERATION self + + --- Triggers the FSM event "Over" after a delay. + -- @function [parent=#OPERATION] __Over + -- @param #OPERATION self + -- @param #number delay Delay in seconds. + + --- On after "Over" event. + -- @function [parent=#OPERATION] OnAfterOver + -- @param #OPERATION self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + -- Init status update. + self:__StatusUpdate(-1) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set verbosity level. +-- @param #OPERATION self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #OPERATION self +function OPERATION:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set start and stop time of the operation. +-- @param #OPERATION 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 #OPERATION self +function OPERATION: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 + +--- Add (all) condition function when the whole operation is over. Must return a `#boolean`. +-- @param #OPERATION self +-- @param #function Function Function that needs to be `true` before the operation is over. +-- @param ... Condition function arguments if any. +-- @return Core.Condition#CONDITION.Function Condition function table. +function OPERATION:AddConditonOverAll(Function, ...) + local cf=self.conditionOver:AddFunctionAll(Function, ...) + return cf +end + +--- Add (any) condition function when the whole operation is over. Must return a `#boolean`. +-- @param #OPERATION self +-- @param #function Function Function that needs to be `true` before the operation is over. +-- @param ... Condition function arguments if any. +-- @return Core.Condition#CONDITION.Function Condition function table. +function OPERATION:AddConditonOverAny(Phase, Function, ...) + local cf=self.conditionOver:AddFunctionAny(Function, ...) + return cf +end + + +--- Add a new phase to the operation. This is added add the end of all previously added phases (if any). +-- @param #OPERATION self +-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number. +-- @param #OPERATION.Branch Branch The branch to which this phase is added. Default is the master branch. +-- @param #number Duration Duration in seconds how long the phase will last. Default `nil`=forever. +-- @return #OPERATION.Phase Phase table object. +function OPERATION:AddPhase(Name, Branch, Duration) + + -- Branch. + Branch=Branch or self.branchMaster + + -- Create a new phase. + local phase=self:_CreatePhase(Name) + + -- Branch of phase + phase.branch=Branch + + -- Set duraction of pahse (if any). + phase.duration=Duration + + -- Debug output. + self:T(self.lid..string.format("Adding phase %s to branch %s", phase.name, Branch.name)) + + -- Add phase. + table.insert(Branch.phases, phase) + + return phase +end + +---Insert a new phase after an already defined phase of the operation. +-- @param #OPERATION self +-- @param #OPERATION.Phase PhaseAfter The phase after which the new phase is inserted. +-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number. +-- @return #OPERATION.Phase Phase table object. +function OPERATION:InsertPhaseAfter(PhaseAfter, Name) + + for i=1,#self.phases do + local phase=self.phases[i] --#OPERATION.Phase + if PhaseAfter.uid==phase.uid then + + -- Create a new phase. + local phase=self:_CreatePhase(Name) + + + end + end + + return nil +end + +--- Get a name of this operation. +-- @param #OPERATION self +-- @return #string Name of this operation or "Unknown". +function OPERATION:GetName() + return self.name or "Unknown" +end + +--- Get a phase by its name. +-- @param #OPERATION self +-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number. +-- @return #OPERATION.Phase Phase table object or nil if phase could not be found. +function OPERATION:GetPhaseByName(Name) + + for _,_branch in pairs(self.branches) do + local branch=_branch --#OPERATION.Branch + for _,_phase in pairs(branch.phases or {}) do + local phase=_phase --#OPERATION.Phase + if phase.name==Name then + return phase + end + end + end + + return nil +end + +--- Set status of a phase. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @param #string Status New status, *e.g.* `OPERATION.PhaseStatus.OVER`. +-- @return #OPERATION self +function OPERATION:SetPhaseStatus(Phase, Status) + + if Phase then + + -- Debug message. + self:T(self.lid..string.format("Phase %s status: %s-->%s", tostring(Phase.name), tostring(Phase.status), tostring(Status))) + + -- Set status. + Phase.status=Status + + -- Set time stamp when phase becase active. + if Phase.status==OPERATION.PhaseStatus.ACTIVE then + Phase.Tstart=timer.getAbsTime() + Phase.nActive=Phase.nActive+1 + elseif Phase.status==OPERATION.PhaseStatus.OVER then + -- Trigger PhaseOver event. + self:PhaseOver(Phase) + end + + end + + return self +end + +--- Get status of a phase. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @return #string Phase status, *e.g.* `OPERATION.PhaseStatus.OVER`. +function OPERATION:GetPhaseStatus(Phase) + return Phase.status +end + +--- Set condition when the given phase is over. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @param Core.Condition#CONDITION Condition Condition when the phase is over. +-- @return #OPERATION self +function OPERATION:SetPhaseConditonOver(Phase, Condition) + if Phase then + self:T(self.lid..string.format("Setting phase %s conditon over %s", self:GetPhaseName(Phase), Condition and Condition.name or "None")) + Phase.conditionOver=Condition + end + return self +end + +--- Add condition function when the given phase is over. Must return a `#boolean`. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @param #function Function Function that needs to be `true` before the phase is over. +-- @param ... Condition function arguments if any. +-- @return Core.Condition#CONDITION.Function Condition function table. +function OPERATION:AddPhaseConditonOverAll(Phase, Function, ...) + if Phase then + local cf=Phase.conditionOver:AddFunctionAll(Function, ...) + return cf + end + return nil +end + +--- Add condition function when the given phase is over. Must return a `#boolean`. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @param #function Function Function that needs to be `true` before the phase is over. +-- @param ... Condition function arguments if any. +-- @return Core.Condition#CONDITION.Function Condition function table. +function OPERATION:AddPhaseConditonOverAny(Phase, Function, ...) + if Phase then + local cf=Phase.conditionOver:AddFunctionAny(Function, ...) + return cf + end + return nil +end + +--- Set persistence of condition function. By default, condition functions are removed after a phase is over. +-- @param #OPERATION self +-- @param Core.Condition#CONDITION.Function ConditionFunction Condition function table. +-- @param #boolean IsPersistent If `true` or `nil`, condition function is persistent. +-- @return #OPERATION self +function OPERATION:SetConditionFunctionPersistence(ConditionFunction, IsPersistent) + ConditionFunction.persistence=IsPersistent + return self +end + +--- Add condition function when the given phase is to be repeated. The provided function must return a `#boolean`. +-- If the condition evaluation returns `true`, the phase is set to state `Planned` instead of `Over` and can be repeated. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @param #function Function Function that needs to be `true` before the phase is over. +-- @param ... Condition function arguments if any. +-- @return #OPERATION self +function OPERATION:AddPhaseConditonRepeatAll(Phase, Function, ...) + if Phase then + Phase.conditionRepeat:AddFunctionAll(Function, ...) + end + return self +end + + +--- Get condition when the given phase is over. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @return Core.Condition#CONDITION Condition when the phase is over (if any). +function OPERATION:GetPhaseConditonOver(Phase, Condition) + return Phase.conditionOver +end + +--- Get how many times a phase has been active. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @return #number Number of times the phase has been active. +function OPERATION:GetPhaseNactive(Phase) + return Phase.nActive +end + +--- Get name of a phase. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase of which the name is returned. Default is the currently active phase. +-- @return #string The name of the phase or "None" if no phase is given or active. +function OPERATION:GetPhaseName(Phase) + + Phase=Phase or self.phase + + if Phase then + return Phase.name + end + + return "None" +end + +--- Get currrently active phase. +-- @param #OPERATION self +-- @return #OPERATION.Phase Current phase or `nil` if no current phase is active. +function OPERATION:GetPhaseActive() + return self.phase +end + +--- Get index of phase. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @return #number The index. +-- @return #OPERATION.Branch The branch. +function OPERATION:GetPhaseIndex(Phase) + + local branch=Phase.branch + + for i,_phase in pairs(branch.phases) do + local phase=_phase --#OPERATION.Phase + if phase.uid==Phase.uid then + return i, branch + end + end + + return nil +end + +--- Get next phase. +-- @param #OPERATION self +-- @param #OPERATION.Branch Branch (Optional) The branch from which the next phase is retrieved. Default is the currently active branch. +-- @param #string PhaseStatus (Optional) Only return a phase, which is in this status. For example, `OPERATION.PhaseStatus.PLANNED` to make sure, the next phase is planned. +-- @return #OPERATION.Phase Next phase or `nil` if no next phase exists. +function OPERATION:GetPhaseNext(Branch, PhaseStatus) + + -- Branch. + Branch=Branch or self:GetBranchActive() + + -- The phases of the branch. + local phases=Branch.phases or {} + + local phase=nil + if self.phase and self.phase.branch.uid==Branch.uid then + phase=self.phase + end + + -- Number of phases. + local N=#phases + + -- Debug message. + self:T(self.lid..string.format("Getting next phase! Branch=%s, Phases=%d, Status=%s", Branch.name, N, tostring(PhaseStatus))) + + if N>0 then + + -- Check if there there is an active phase already. + if phase==nil and PhaseStatus==nil then + return phases[1] + end + + local n=1 + + if phase then + n=self:GetPhaseIndex(phase)+1 + end + + for i=n,N do + local phase=phases[i] --#OPERATION.Phase + + if PhaseStatus==nil or PhaseStatus==phase.status then + return phase + end + + end + + end + + return nil +end + +--- Count phases. +-- @param #OPERATION self +-- @param #string Status (Optional) Only count phases in a certain status, e.g. `OPERATION.PhaseStatus.PLANNED`. +-- @param #OPERATION.Branch Branch (Optional) Branch. +-- @return #number Number of phases +function OPERATION:CountPhases(Status, Branch) + + Branch=Branch or self.branchActive + + local N=0 + for _,_phase in pairs(Branch.phases) do + local phase=_phase --#OPERATION.Phase + if Status==nil or Status==phase.status then + N=N+1 + end + end + + return N +end + + +--- Add a new branch to the operation. +-- @param #OPERATION self +-- @param #string Name +-- @return #OPERATION.Branch Branch table object. +function OPERATION:AddBranch(Name) + + -- Create a new branch. + local branch=self:_CreateBranch(Name) + + -- Add phase. + table.insert(self.branches, branch) + + return branch +end + +--- Get the master branch. This is the default branch and should always exist (if it was not explicitly deleted). +-- @param #OPERATION self +-- @return #OPERATION.Branch The master branch. +function OPERATION:GetBranchMaster() + return self.branchMaster +end + +--- Get the currently active branch. +-- @param #OPERATION self +-- @return #OPERATION.Branch The active branch. If no branch is active, the master branch is returned. +function OPERATION:GetBranchActive() + return self.branchActive or self.branchMaster +end + +--- Get name of the branch. +-- @param #OPERATION self +-- @param #OPERATION.Branch Branch The branch of which the name is requested. Default is the currently active or master branch. +-- @return #string Name Name or "None" +function OPERATION:GetBranchName(Branch) + Branch=Branch or self:GetBranchActive() + if Branch then + return Branch.name + end + return "None" +end + +--- Add an edge between two branches. +-- @param #OPERATION self +-- @param #OPERATION.Phase PhaseFrom The phase of the *from* branch *after* which to switch. +-- @param #OPERATION.Phase PhaseTo The phase of the *to* branch *to* which to switch. +-- @param Core.Condition#CONDITION ConditionSwitch (Optional) Condition(s) when to switch the branches. +-- @return #OPERATION.Edge Edge table object. +function OPERATION:AddEdge(PhaseFrom, PhaseTo, ConditionSwitch) + + local edge={} --#OPERATION.Edge + + edge.phaseFrom=PhaseFrom + edge.phaseTo=PhaseTo + + edge.branchFrom=PhaseFrom.branch + edge.branchTo=PhaseTo.branch + + if ConditionSwitch then + edge.conditionSwitch=ConditionSwitch + else + edge.conditionSwitch=CONDITION:New("Edge") + edge.conditionSwitch:SetNoneResult(true) + end + + table.insert(edge.branchFrom.edges, edge) + + return edge +end + +--- Add condition function to an edge when branches are switched. The function must return a `#boolean`. +-- If multiple condition functions are added, all of these must return true for the branch switch to occur. +-- @param #OPERATION self +-- @param #OPERATION.Edge Edge The edge connecting the two branches. +-- @param #function Function Function that needs to be `true` for switching between the branches. +-- @param ... Condition function arguments if any. +-- @return Core.Condition#CONDITION.Function Condition function table. +function OPERATION:AddEdgeConditonSwitchAll(Edge, Function, ...) + if Edge then + local cf=Edge.conditionSwitch:AddFunctionAll(Function, ...) + return cf + end + return nil +end + +--- Add mission to operation. +-- @param #OPERATION self +-- @param Ops.Auftrag#AUFTRAG Mission The mission to add. +-- @param #OPERATION.Phase Phase (Optional) The phase in which the mission should be executed. If no phase is given, it will be exectuted ASAP. +function OPERATION:AddMission(Mission, Phase) + + Mission.phase=Phase + Mission.operation=self + + table.insert(self.missions, Mission) + + return self +end + +--- Add Target to operation. +-- @param #OPERATION self +-- @param Ops.Target#TARGET Target The target to add. +-- @param #OPERATION.Phase Phase (Optional) The phase in which the target should be attacked. If no phase is given, it will be attacked ASAP. +function OPERATION:AddTarget(Target, Phase) + + Target.phase=Phase + Target.operation=self + + table.insert(self.targets, Target) + + return self +end + +--- Get targets of operation. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase (Optional) Only return targets set for this phase. Default is targets of all phases. +-- @return #table Targets Table of #TARGET objects +function OPERATION:GetTargets(Phase) + local N = {} + for _,_target in pairs(self.targets) do + local target=_target --Ops.Target#TARGET + if target:IsAlive() and (Phase==nil or target.phase==Phase) then + table.insert(N,target) + end + end + return N +end + +--- Count targets alive. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase (Optional) Only count targets set for this phase. +-- @return #number Number of phases +function OPERATION:CountTargets(Phase) + + local N=0 + for _,_target in pairs(self.targets) do + local target=_target --Ops.Target#TARGET + + if target:IsAlive() and (Phase==nil or target.phase==Phase) then + N=N+1 + end + end + + return N +end + +--- Assign cohort to operation. +-- @param #OPERATION self +-- @param Ops.Cohort#COHORT Cohort The cohort +-- @return #OPERATION self +function OPERATION:AssignCohort(Cohort) + + self:T(self.lid..string.format("Assiging Cohort %s to operation", Cohort.name)) + self.cohorts[Cohort.name]=Cohort + +end + +--- Assign legion to operation. All cohorts of this legion will be assigned and are only available. +-- @param #OPERATION self +-- @param Ops.Legion#LEGION Legion The legion to be assigned. +-- @return #OPERATION self +function OPERATION:AssignLegion(Legion) + + self.legions[Legion.alias]=Legion + +end + +--- Check if a given legion is assigned to this operation. All cohorts of this legion will be checked. +-- @param #OPERATION self +-- @param Ops.Legion#LEGION Legion The legion to be assigned. +-- @return #boolean If `true`, legion is assigned to this operation. +function OPERATION:IsAssignedLegion(Legion) + + local legion=self.legions[Legion.alias] + + if legion then + self:T(self.lid..string.format("Legion %s is assigned to this operation", Legion.alias)) + return true + else + self:T(self.lid..string.format("Legion %s is NOT assigned to this operation", Legion.alias)) + return false + end + +end + +--- Check if a given cohort is assigned to this operation. +-- @param #OPERATION self +-- @param Ops.Cohort#COHORT Cohort The Cohort. +-- @return #boolean If `true`, cohort is assigned to this operation. +function OPERATION:IsAssignedCohort(Cohort) + + local cohort=self.cohorts[Cohort.name] + + if cohort then + self:T(self.lid..string.format("Cohort %s is assigned to this operation", Cohort.name)) + return true + else + + -- Check if legion of this cohort was assigned. + local Legion=Cohort.legion + if Legion and self:IsAssignedLegion(Legion) then + self:T(self.lid..string.format("Legion %s of Cohort %s is assigned to this operation", Legion.alias, Cohort.name)) + return true + end + + self:T(self.lid..string.format("Cohort %s is NOT assigned to this operation", Cohort.name)) + return false + end + + return nil +end + +--- Check if a given cohort or legion is assigned to this operation. +-- @param #OPERATION self +-- @param Wrapper.Object#OBJECT Object The cohort or legion object. +-- @return #boolean If `true`, cohort is assigned to this operation. +function OPERATION:IsAssignedCohortOrLegion(Object) + + local isAssigned=nil + if Object:IsInstanceOf("COHORT") then + isAssigned=self:IsAssignedCohort(Object) + elseif Object:IsInstanceOf("LEGION") then + isAssigned=self:IsAssignedLegion(Object) + else + self:E(self.lid.."ERROR: Unknown Object!") + end + + return isAssigned +end + +--- Check if operation is in FSM state "Planned". +-- @param #OPERATION self +-- @return #boolean If `true`, operation is "Planned". +function OPERATION:IsPlanned() + local is=self:is("Planned") + return is +end + +--- Check if operation is in FSM state "Running". +-- @param #OPERATION self +-- @return #boolean If `true`, operation is "Running". +function OPERATION:IsRunning() + local is=self:is("Running") + return is +end + +--- Check if operation is in FSM state "Paused". +-- @param #OPERATION self +-- @return #boolean If `true`, operation is "Paused". +function OPERATION:IsPaused() + local is=self:is("Paused") + return is +end + +--- Check if operation is in FSM state "Over". +-- @param #OPERATION self +-- @return #boolean If `true`, operation is "Over". +function OPERATION:IsOver() + local is=self:is("Over") + return is +end + +--- Check if operation is in FSM state "Stopped". +-- @param #OPERATION self +-- @return #boolean If `true`, operation is "Stopped". +function OPERATION:IsStopped() + local is=self:is("Stopped") + return is +end + +--- Check if operation is **not** "Over" or "Stopped". +-- @param #OPERATION self +-- @return #boolean If `true`, operation is not "Over" or "Stopped". +function OPERATION:IsNotOver() + local is=not (self:IsOver() or self:IsStopped()) + return is +end + +--- Check if phase is in status "Active". +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @return #boolean If `true`, phase is active. +function OPERATION:IsPhaseActive(Phase) + if Phase and Phase.status and Phase.status==OPERATION.PhaseStatus.ACTIVE then + return true + end + return false +end + +--- Check if a phase is the currently active one. +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase to check. +-- @return #boolean If `true`, this phase is currently active. +function OPERATION:IsPhaseActive(Phase) + local phase=self:GetPhaseActive() + if phase and phase.uid==Phase.uid then + return true + else + return false + end + return nil +end + +--- Check if phase is in status "Planned". +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @return #boolean If `true`, phase is Planned. +function OPERATION:IsPhasePlanned(Phase) + if Phase and Phase.status and Phase.status==OPERATION.PhaseStatus.PLANNED then + return true + end + return false +end + +--- Check if phase is in status "Over". +-- @param #OPERATION self +-- @param #OPERATION.Phase Phase The phase. +-- @return #boolean If `true`, phase is over. +function OPERATION:IsPhaseOver(Phase) + if Phase and Phase.status and Phase.status==OPERATION.PhaseStatus.OVER then + return true + end + return false +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Update +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Start" event. +-- @param #OPERATION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPERATION:onafterStart(From, Event, To) + + -- Debug message. + self:T(self.lid..string.format("Starting Operation!")) + return self +end + + +--- On after "StatusUpdate" event. +-- @param #OPERATION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPERATION:onafterStatusUpdate(From, Event, To) + + -- Current abs. mission time. + local Tnow=timer.getAbsTime() + + -- Current FSM state. + local fsmstate=self:GetState() + + -- Start operation. + if self:IsPlanned() then + + -- Start operation if start time has passed (if any) and start condition(s) are met (if any). + if (self.Tstart and Tnow>self.Tstart or self.Tstart==nil) and (self.conditionStart==nil or self.conditionStart:Evaluate()) then + self:Start() + end + + elseif self:IsNotOver() then + + -- Operation is over if stop time has passed (if any) and over condition(s) are met (if any). + if (self.Tstop and Tnow>self.Tstop or self.Tstop==nil) and (self.conditionOver==nil or self.conditionOver:Evaluate()) then + self:Over() + end + + end + + + -- Check phases. + if self:IsRunning() then + self:_CheckPhases() + end + + + -- Debug output. + if self.verbose>=1 then + + -- Current phase. + local phaseName=self:GetPhaseName() + local branchName=self:GetBranchName() + local NphaseTot=self:CountPhases() + local NphaseAct=self:CountPhases(OPERATION.PhaseStatus.ACTIVE) + local NphasePla=self:CountPhases(OPERATION.PhaseStatus.PLANNED) + local NphaseOvr=self:CountPhases(OPERATION.PhaseStatus.OVER) + + -- General info. + local text=string.format("State=%s: Phase=%s [%s], Phases=%d [Active=%d, Planned=%d, Over=%d]", fsmstate, phaseName, branchName, NphaseTot, NphaseAct, NphasePla, NphaseOvr) + self:I(self.lid..text) + + end + + -- Debug output. + if self.verbose>=2 then + + -- Info on phases. + local text="Phases:" + for i,_phase in pairs(self.branchActive.phases) do + local phase=_phase --#OPERATION.Phase + text=text..string.format("\n[%d] %s [uid=%d]: status=%s Nact=%d", i, phase.name, phase.uid, tostring(phase.status), phase.nActive) + end + if text=="Phases:" then text=text.." None" end + self:I(self.lid..text) + + end + + -- Next status update. + self:__StatusUpdate(-30) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "PhaseNext" event. +-- @param #OPERATION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPERATION:onafterPhaseNext(From, Event, To) + + -- Get next phase. + local Phase=self:GetPhaseNext() + + if Phase then + + -- Change phase to next one. + self:PhaseChange(Phase) + + else + + -- No further phases defined ==> Operation is over. + self:Over() + + end + + return self +end + + +--- On after "PhaseChange" event. +-- @param #OPERATION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPERATION.Phase Phase The new phase. +function OPERATION:onafterPhaseChange(From, Event, To, Phase) + + -- Previous phase (if any). + local oldphase="None" + if self.phase then + if self.phase.status~=OPERATION.PhaseStatus.OVER then + self:SetPhaseStatus(self.phase, OPERATION.PhaseStatus.OVER) + end + oldphase=self.phase.name + end + + -- Debug message. + self:I(self.lid..string.format("Phase change: %s --> %s", oldphase, Phase.name)) + + -- Set currently active phase. + self.phase=Phase + + -- Phase is active. + self:SetPhaseStatus(Phase, OPERATION.PhaseStatus.ACTIVE) + + return self +end + +--- On after "PhaseOver" event. +-- @param #OPERATION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPERATION.Phase Phase The phase that is over. +function OPERATION:onafterPhaseOver(From, Event, To, Phase) + + -- Remove all non-persistant condition functions. + Phase.conditionOver:RemoveNonPersistant() + +end + +--- On after "BranchSwitch" event. +-- @param #OPERATION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPERATION.Branch Branch The new branch. +-- @param #OPERATION.Phase Phase The phase. +function OPERATION:onafterBranchSwitch(From, Event, To, Branch, Phase) + + -- Debug info. + self:T(self.lid..string.format("Switching to branch %s", Branch.name)) + + -- Set active branch. + self.branchActive=Branch + + -- Change phase. + self:PhaseChange(Phase) + + return self +end + +--- On after "Over" event. +-- @param #OPERATION self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPERATION:onafterOver(From, Event, To) + + -- Debug message. + self:T(self.lid..string.format("Operation is over!")) + + -- No active phase. + self.phase=nil + + -- Set all phases to OVER. + for _,_branch in pairs(self.branches) do + local branch=_branch --#OPERATION.Branch + for _,_phase in pairs(branch.phases) do + local phase=_phase --#OPERATION.Phase + if not self:IsPhaseOver(phase) then + self:SetPhaseStatus(phase, OPERATION.PhaseStatus.OVER) + end + end + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc (private) Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check phases. +-- @param #OPERATION self +function OPERATION:_CheckPhases() + + -- Currently active phase. + local phase=self:GetPhaseActive() + + -- Check if active phase is over if conditon over is defined. + if phase and phase.conditionOver then + + -- Evaluate if phase is over. + local isOver=phase.conditionOver:Evaluate() + + local Tnow=timer.getAbsTime() + + -- Check if duration of phase if over. + if phase.duration and phase.Tstart and Tnow-phase.Tstart>phase.duration then + isOver=true + end + + -- Set phase status to over. This also triggers the PhaseOver() event. + if isOver then + self:SetPhaseStatus(phase, OPERATION.PhaseStatus.OVER) + end + end + + -- If no current phase or current phase is over, get next phase. + if phase==nil or phase.status==OPERATION.PhaseStatus.OVER then + + for _,_edge in pairs(self.branchActive.edges) do + local edge=_edge --#OPERATION.Edge + + if phase then + --env.info(string.format("phase active uid=%d", phase.uid)) + --env.info(string.format("Phase from uid=%d", edge.phaseFrom.uid)) + end + + if (edge.phaseFrom==nil) or (phase and edge.phaseFrom.uid==phase.uid) then + + -- Evaluate switch condition. + local switch=edge.conditionSwitch:Evaluate() + + if switch then + + -- Get next phase of the branch + local phaseTo=edge.phaseTo or self:GetPhaseNext(edge.branchTo, nil) + + if phaseTo then + + -- Switch to new branch. + self:BranchSwitch(edge.branchTo, phaseTo) + + else + + -- No next phase ==> Ops is over! + self:Over() + + end + + -- Done here! + return + end + end + + end + + -- Next phase. + self:PhaseNext() + + end + +end + +--- Create a new phase object. +-- @param #OPERATION self +-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number. +-- @return #OPERATION.Phase Phase table object. +function OPERATION:_CreatePhase(Name) + + -- Increase phase counter. + self.counterPhase=self.counterPhase+1 + + local phase={} --#OPERATION.Phase + phase.uid=self.counterPhase + phase.name=Name or string.format("Phase-%02d", self.counterPhase) + phase.conditionOver=CONDITION:New(Name.." Over") + phase.conditionOver:SetDefaultPersistence(false) + phase.status=OPERATION.PhaseStatus.PLANNED + phase.nActive=0 + + return phase +end + +--- Create a new branch object. +-- @param #OPERATION self +-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number. +-- @return #OPERATION.Branch Branch table object. +function OPERATION:_CreateBranch(Name) + + -- Increase phase counter. + self.counterBranch=self.counterBranch+1 + + local branch={} --#OPERATION.Branch + branch.uid=self.counterBranch + branch.name=Name or string.format("Branch-%02d", self.counterBranch) + branch.phases={} + branch.edges={} + + return branch +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/OpsGroup.lua b/Moose Development/Moose/Ops/OpsGroup.lua new file mode 100644 index 000000000..13b4597f4 --- /dev/null +++ b/Moose Development/Moose/Ops/OpsGroup.lua @@ -0,0 +1,14047 @@ +--- **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 #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 DCS#Group dcsgroup The DCS group object. +-- @field DCS#Controller controller The DCS controller of the group. +-- @field DCS#Template template Template table of the group. +-- @field #table elements Table of elements, i.e. units 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 #boolean isHelo If true, this is a helicopter group. +-- @field #boolean isVTOL If true, this is capable of Vertical TakeOff and Landing (VTOL). +-- @field #boolean isSubmarine If true, this is a submarine group. +-- @field #boolean isAI If true, group is purely AI. +-- @field #boolean isDestroyed If true, the whole group was destroyed. +-- @field #boolean isDead If true, the whole group is dead. +-- @field #table waypoints Table of waypoints. +-- @field #table waypoints0 Table of initial waypoints. +-- @field #boolean useMEtasks If `true`, use tasks set in the ME. Default `false`. +-- @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 Wrapper.Airbase#AIRBASE currbase The current airbase of the flight group, i.e. where it is currently located or landing at. +-- @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 #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 #number Twaiting Abs. mission time stamp when the group was ordered to wait. +-- @field #number dTwait Time to wait in seconds. Default `nil` (for ever). +-- @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 isMobile If `true`, group is mobile (speed > 1 m/s) +-- @field #boolean passedfinalwp Group has passed the final waypoint. +-- @field #number wpcounter Running number counting waypoints. +-- @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 timerStatus Timer for status update. +-- @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 #table pausedmissions Paused missions. +-- @field #number Ndestroyed Number of destroyed units. +-- @field #number Nkills Number kills of this groups. +-- @field #number Nhit Number of hits taken. +-- +-- @field #boolean rearmOnOutOfAmmo If `true`, group will go to rearm once it runs out of ammo. +-- +-- @field Ops.Legion#LEGION legion Legion the group belongs to. +-- @field Ops.Cohort#COHORT cohort Cohort the group belongs to. +-- +-- @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 #boolean engagedetectedOn If `true`, auto engage detected targets. +-- @field #number engagedetectedRmax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM. +-- @field #table engagedetectedTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default "All". +-- @field Core.Set#SET_ZONE engagedetectedEngageZones Set of zones in which targets are engaged. Default is anywhere. +-- @field Core.Set#SET_ZONE engagedetectedNoEngageZones Set of zones in which targets are *not* engaged. Default is nowhere. +-- +-- @field #OPSGROUP.Radio radio Current radio settings. +-- @field #OPSGROUP.Radio radioDefault Default radio settings. +-- @field Sound.Radio#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 #string callsignName Callsign name. +-- @field #string callsignAlias Callsign alias. +-- +-- @field #OPSGROUP.Spot spot Laser and IR spot. +-- +-- @field DCS#Vec3 stuckVec3 Position where the group got stuck. +-- @field #number stuckTimestamp Time stamp [sec], when the group got stuck. +-- @field #boolean stuckDespawn If `true`, group gets despawned after beeing stuck for a certain time. +-- +-- @field #OPSGROUP.Ammo ammo Initial ammount of ammo. +-- @field #OPSGROUP.WeaponData weaponData Weapon data table with key=BitType. +-- +-- @field #OPSGROUP.Element carrier Carrier the group is loaded into as cargo. +-- @field #OPSGROUP carrierGroup Carrier group transporting this group as cargo. +-- @field #OPSGROUP.MyCarrier mycarrier Carrier group for this group. +-- @field #table cargoqueue Table containing cargo groups to be transported. +-- @field #table cargoBay Table containing OPSGROUP loaded into this group. +-- @field Ops.OpsTransport#OPSTRANSPORT cargoTransport Current cargo transport assignment. +-- @field Ops.OpsTransport#OPSTRANSPORT.TransportZoneCombo cargoTZC Transport zone combo (pickup, deploy etc.) currently used. +-- @field #string cargoStatus Cargo status of this group acting as cargo. +-- @field #number cargoTransportUID Unique ID of the transport assignment this cargo group is associated with. +-- @field #string carrierStatus Carrier status of this group acting as cargo carrier. +-- @field #OPSGROUP.CarrierLoader carrierLoader Carrier loader parameters. +-- @field #OPSGROUP.CarrierLoader carrierUnloader Carrier unloader parameters. +-- +-- @field #boolean useSRS Use SRS for transmissions. +-- @field Sound.SRS#MSRS msrs MOOSE SRS wrapper. +-- +-- @extends Core.Fsm#FSM + +--- *A small group of determined and like-minded people can change the course of history.* -- Mahatma Gandhi +-- +-- === +-- +-- # The OPSGROUP Concept +-- +-- The OPSGROUP class contains common functions used by other classes such as FLIGHTGROUP, 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", + 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, + wpcounter = 1, + radio = {}, + option = {}, + optionDefault = {}, + tacan = {}, + icls = {}, + callsign = {}, + Ndestroyed = 0, + Nkills = 0, + Nhit = 0, + weaponData = {}, + cargoqueue = {}, + cargoBay = {}, + mycarrier = {}, + carrierLoader = {}, + carrierUnloader = {}, + useMEtasks = false, + pausedmissions = {}, +} + + +--- OPS group element. +-- @type OPSGROUP.Element +-- @field #string name Name of the element, i.e. the unit. +-- @field #string status The element status. See @{#OPSGROUP.ElementStatus}. +-- @field Wrapper.Unit#UNIT unit The UNIT object. +-- @field Wrapper.Group#GROUP group The GROUP object. +-- @field DCS#Unit DCSunit The DCS unit object. +-- @field DCS#Controller controller The DCS controller of the unit. +-- @field #boolean ai If true, element is AI. +-- @field #string skill Skill level. +-- @field #string playerName Name of player if this is a client. +-- @field #number Nhit Number of times the element was hit. +-- @field #boolean engineOn If `true`, engines were started. +-- +-- @field Core.Zone#ZONE_POLYGON_BASE zoneBoundingbox Bounding box zone of the element unit. +-- @field Core.Zone#ZONE_POLYGON_BASE zoneLoad Loading zone. +-- @field Core.Zone#ZONE_POLYGON_BASE zoneUnload Unloading zone. +-- +-- @field #string typename Type name. +-- @field #number category Aircraft category. +-- @field #string categoryname Aircraft category name. +-- +-- @field #number size Size (max of length, width, height) in meters. +-- @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 DCS#Vec3 vec3 Last known 3D position vector. +-- @field DCS#Vec3 orientX Last known ordientation vector in the direction of the nose X. +-- @field #number heading Last known heading in degrees. +-- +-- @field #number life0 Initial life points. +-- @field #number life Life points when last updated. +-- @field #number damage Damage of element in percent. +-- +-- @field DCS#Object.Desc descriptors Descriptors table. +-- @field #number weightEmpty Empty weight in kg. +-- @field #number weightMaxTotal Max. total weight in kg. +-- @field #number weightMaxCargo Max. cargo weight in kg. +-- @field #number weightCargo Current cargo weight in kg. +-- @field #number weight Current weight including cargo in kg. +-- @field #table cargoBay Cargo bay. +-- +-- @field #string modex Tail number. +-- @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 #string callsign Call sign, e.g. "Uzi 1-1". +-- @field Wrapper.Airbase#AIRBASE.ParkingSpot parking The parking spot table the element is parking on. + + +--- 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="Engine On", + TAXIING="Taxiing", + TAKEOFF="Takeoff", + AIRBORNE="Airborne", + LANDING="Landing", + LANDED="Landed", + ARRIVED="Arrived", + DEAD="Dead", +} + +--- Status of group. +-- @type OPSGROUP.GroupStatus +-- @field #string INUTERO Not spawned yet or its status is unknown so far. +-- @field #string PARKING Parking after spawned on ramp. +-- @field #string TAXIING Taxiing after engine startup. +-- @field #string AIRBORNE Element is airborne. Either after takeoff or after air start. +-- @field #string LANDING Landing. +-- @field #string LANDED Landed and is taxiing to its parking spot. +-- @field #string ARRIVED 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.GroupStatus={ + INUTERO="InUtero", + PARKING="Parking", + TAXIING="Taxiing", + AIRBORNE="Airborne", + INBOUND="Inbound", + 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 #boolean ismission This is an AUFTRAG task. +-- @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. +-- @field Ops.Target#TARGET target Target object. + +--- 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. +-- @field #boolean Emission Emission on/off. +-- @field #boolean Invisible Invisible on/off. +-- @field #boolean Immortal Immortal on/off. + +--- 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 #string NameSquad Name of the squad, e.g. "Uzi". + +--- 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 Shells Amount of shells (guns + cannons). +-- @field #number Guns Amount of gun shells (caliber < 25). +-- @field #number Cannons Amount of cannon shells (caliber >= 25). +-- @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. +-- @field #number MissilesSA Amount of surfe-to-air missiles. + +--- Spawn point data. +-- @type OPSGROUP.Spawnpoint +-- @field Core.Point#COORDINATE Coordinate Coordinate where to spawn +-- @field Wrapper.Airbase#AIRBASE Airport Airport where to spawn. +-- @field #table TerminalIDs Terminal IDs, where to spawn the group. It is a table of `#number`s because a group can consist of multiple units. + +--- 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 #number detour Signifies that this waypoint is not part of the normal route: 0=Hold, 1=Resume 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 #boolean temp If true, this is a temporary waypoint and will be deleted when passed. Also the passing waypoint FSM event is not triggered. +-- @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. +-- @field #number missionUID Mission UID (Auftragsnr) this waypoint belongs to. + +--- Cargo Carrier status. +-- @type OPSGROUP.CarrierStatus +-- @field #string NOTCARRIER This group is not a carrier yet. +-- @field #string PICKUP Carrier is on its way to pickup cargo. +-- @field #string LOADING Carrier is loading cargo. +-- @field #string LOADED Carrier has loaded cargo. +-- @field #string TRANSPORTING Carrier is transporting cargo. +-- @field #string UNLOADING Carrier is unloading cargo. +OPSGROUP.CarrierStatus={ + NOTCARRIER="not carrier", + PICKUP="pickup", + LOADING="loading", + LOADED="loaded", + TRANSPORTING="transporting", + UNLOADING="unloading", +} + +--- Cargo status. +-- @type OPSGROUP.CargoStatus +-- @field #string AWAITING Group is awaiting carrier. +-- @field #string NOTCARGO This group is no cargo yet. +-- @field #string ASSIGNED Cargo is assigned to a carrier. (Not used!) +-- @field #string BOARDING Cargo is boarding a carrier. +-- @field #string LOADED Cargo is loaded into a carrier. +OPSGROUP.CargoStatus={ + AWAITING="Awaiting carrier", + NOTCARGO="not cargo", + ASSIGNED="assigned to carrier", + BOARDING="boarding", + LOADED="loaded", +} + +--- Cargo carrier loader parameters. +-- @type OPSGROUP.CarrierLoader +-- @field #string type Loader type "Front", "Back", "Left", "Right", "All". +-- @field #number length Length of (un-)loading zone in meters. +-- @field #number width Width of (un-)loading zone in meters. + +--- Data of the carrier that has loaded this group. +-- @type OPSGROUP.MyCarrier +-- @field #OPSGROUP group The carrier group. +-- @field #OPSGROUP.Element element The carrier element. +-- @field #boolean reserved If `true`, the carrier has caro space reserved for me. + +--- Element cargo bay data. +-- @type OPSGROUP.MyCargo +-- @field #OPSGROUP group The cargo group. +-- @field #number storageType Type of storage. +-- @field #number storageAmount Amount of storage. +-- @field #number storageWeight Weight of storage item. +-- @field #boolean reserved If `true`, the cargo bay space is reserved but cargo has not actually been loaded yet. + +--- Cargo group data. +-- @type OPSGROUP.CargoGroup +-- @field #number uid Unique ID of this cargo data. +-- @field #string type Type of cargo: "OPSGROUP" or "STORAGE". +-- @field #OPSGROUP opsgroup The cargo opsgroup. +-- @field Ops.OpsTransport#OPSTRANSPORT.Storage storage Storage data. +-- @field #boolean delivered If `true`, group was delivered. +-- @field #boolean disembarkActivation If `true`, group is activated. If `false`, group is late activated. +-- @field Core.Zone#ZONE disembarkZone Zone where this group is disembarked to. +-- @field Core.Set#SET_OPSGROUP disembarkCarriers Carriers where this group is directly disembared to. +-- @field #string status Status of the cargo group. Not used yet. + +--- OpsGroup version. +-- @field #string version +OPSGROUP.version="1.0.4" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: AI on/off. +-- TODO: F10 menu. +-- TODO: Add pseudo function. +-- TODO: Afterburner restrict. +-- TODO: What more options? +-- TODO: Shot events? +-- TODO: Marks to add waypoints/tasks on-the-fly. +-- DONE: Invisible/immortal. +-- DONE: Emission on/off +-- DONE: Damage? +-- DONE: Options EPLRS + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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)) + + -- Check if group exists. + if self.group then + if not self:IsExist() then + self:E(self.lid.."ERROR: GROUP does not exist! Returning nil") + return nil + end + end + + if UTILS.IsInstanceOf(group,"OPSGROUP") then + self:E(self.lid.."ERROR: GROUP is already an OPSGROUP: "..tostring(self.groupname).."!") + return group + end + + -- Set the template. + self:_SetTemplate() + + -- Set DCS group and controller. + self.dcsgroup=self:GetDCSGroup() + self.controller=self.dcsgroup:getController() + + -- Category. + self.category=self.dcsgroup:getCategory() + if self.category==Group.Category.GROUND then + self.isArmygroup=true + elseif self.category==Group.Category.TRAIN then + self.isArmygroup=true + self.isTrain=true + elseif self.category==Group.Category.SHIP then + self.isNavygroup=true + elseif self.category==Group.Category.AIRPLANE then + self.isFlightgroup=true + elseif self.category==Group.Category.HELICOPTER then + self.isFlightgroup=true + self.isHelo=true + else + + end + + -- Set gen attribute. + self.attribute=self.group:GetAttribute() + + local units=self.group:GetUnits() + + if units then + local masterunit=units[1] --Wrapper.Unit#UNIT + + if masterunit then + -- Get Descriptors. + self.descriptors=masterunit:GetDesc() + + -- Set type name. + self.actype=masterunit:GetTypeName() + + -- Is this a submarine. + self.isSubmarine=masterunit:HasAttribute("Submarines") + + -- Has this a datalink? + self.isEPLRS=masterunit:HasAttribute("Datalink") + + if self:IsFlightgroup() then + + self.rangemax=self.descriptors.range and self.descriptors.range*1000 or 500*1000 + + self.ceiling=self.descriptors.Hmax + + self.tankertype=select(2, masterunit:IsTanker()) + self.refueltype=select(2, masterunit:IsRefuelable()) + + --env.info("DCS Unit BOOM_AND_RECEPTACLE="..tostring(Unit.RefuelingSystem.BOOM_AND_RECEPTACLE)) + --env.info("DCS Unit PROBE_AND_DROGUE="..tostring(Unit.RefuelingSystem.PROBE_AND_DROGUE)) + + end + 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() + + -- Set Default altitude. + self:SetDefaultAltitude() + + -- Group will return to its legion when done. + self:SetReturnToLegion() + + -- 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) + + -- Cargo. + self.cargoStatus=OPSGROUP.CargoStatus.NOTCARGO + self.carrierStatus=OPSGROUP.CarrierStatus.NOTCARRIER + self:SetCarrierLoaderAllAspect() + self:SetCarrierUnloaderAllAspect() + + -- 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("*", "Respawn", "InUtero") -- Respawn group. + self:AddTransition("*", "Dead", "InUtero") -- The whole group is dead and goes back to mummy. + self:AddTransition("*", "InUtero", "InUtero") -- Deactivated group goes back to mummy. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self:AddTransition("*", "Hit", "*") -- Someone in the group was hit. + self:AddTransition("*", "Damaged", "*") -- Someone in the group took damage. + self:AddTransition("*", "Destroyed", "*") -- The whole group is dead. + + self:AddTransition("*", "UpdateRoute", "*") -- Update route of group. + + self:AddTransition("*", "PassingWaypoint", "*") -- Group passed a waypoint. + self:AddTransition("*", "PassedFinalWaypoint", "*") -- Group passed the waypoint. + self:AddTransition("*", "GotoWaypoint", "*") -- Group switches to a specific waypoint. + + self:AddTransition("*", "Wait", "*") -- Group will wait for further orders. + self:AddTransition("*", "Stuck", "*") -- Group got stuck. + + 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", "*") -- Group was detected (again) in this detection cycle. + self:AddTransition("*", "DetectedGroupNew", "*") -- Add a newly detected Group to the detected Groups set. + self:AddTransition("*", "DetectedGroupKnown", "*") -- A known Group is still detected. + self:AddTransition("*", "DetectedGroupLost", "*") -- Group lost a detected target group. + + 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("*", "OutOfTorpedos", "*") -- Group is out of torpedos. + + self:AddTransition("*", "OutOfMissilesAA", "*") -- Group is out of A2A (air) missiles. + self:AddTransition("*", "OutOfMissilesAG", "*") -- Group is out of A2G (ground) missiles. + self:AddTransition("*", "OutOfMissilesAS", "*") -- Group is out of A2S (ship) 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("*", "ElementInUtero", "*") -- An element is in utero again. + 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. + self:AddTransition("*", "ElementHit", "*") -- An element was hit. + + self:AddTransition("*", "Board", "*") -- Group is ordered to board the carrier. + self:AddTransition("*", "Embarked", "*") -- Group was loaded into a cargo carrier. + self:AddTransition("*", "Disembarked", "*") -- Group was unloaded from a cargo carrier. + + self:AddTransition("*", "Pickup", "*") -- Carrier and is on route to pick up cargo. + self:AddTransition("*", "Loading", "*") -- Carrier is loading cargo. + self:AddTransition("*", "Load", "*") -- Carrier loads cargo into carrier. + self:AddTransition("*", "Loaded", "*") -- Carrier loaded cargo into carrier. + self:AddTransition("*", "LoadingDone", "*") -- Carrier loaded all assigned/possible cargo into carrier. + self:AddTransition("*", "Transport", "*") -- Carrier is transporting cargo. + self:AddTransition("*", "Unloading", "*") -- Carrier is unloading the cargo. + self:AddTransition("*", "Unload", "*") -- Carrier unloads a cargo group. + self:AddTransition("*", "Unloaded", "*") -- Carrier unloaded a cargo group. + self:AddTransition("*", "UnloadingDone", "*") -- Carrier unloaded all its current cargo. + self:AddTransition("*", "Delivered", "*") -- Carrier delivered ALL cargo of the transport assignment. + + self:AddTransition("*", "TransportCancel", "*") -- Cancel (current) transport. + + self:AddTransition("*", "HoverStart", "*") -- Helo group is hovering + self:AddTransition("*", "HoverEnd", "*") -- Helo group is flying on + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Stop". Stops the OPSGROUP and all its event handlers. + -- @function [parent=#OPSGROUP] Stop + -- @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. + + + --- Triggers the FSM event "MissionStart". + -- @function [parent=#OPSGROUP] MissionStart + -- @param #OPSGROUP self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionStart" after a delay. + -- @function [parent=#OPSGROUP] __MissionStart + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionStart" event. + -- @function [parent=#OPSGROUP] OnAfterMissionStart + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "MissionExecute". + -- @function [parent=#OPSGROUP] MissionExecute + -- @param #OPSGROUP self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionExecute" after a delay. + -- @function [parent=#OPSGROUP] __MissionExecute + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionExecute" event. + -- @function [parent=#OPSGROUP] OnAfterMissionExecute + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "MissionCancel". + -- @function [parent=#OPSGROUP] MissionCancel + -- @param #OPSGROUP self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionCancel" after a delay. + -- @function [parent=#OPSGROUP] __MissionCancel + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionCancel" event. + -- @function [parent=#OPSGROUP] OnAfterMissionCancel + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + + --- Triggers the FSM event "MissionDone". + -- @function [parent=#OPSGROUP] MissionDone + -- @param #OPSGROUP self + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- Triggers the FSM event "MissionDone" after a delay. + -- @function [parent=#OPSGROUP] __MissionDone + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "MissionDone" event. + -- @function [parent=#OPSGROUP] OnAfterMissionDone + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.Auftrag#AUFTRAG Mission The mission. + + --- On after "HoverStart" event. + -- @function [parent=#OPSGROUP] OnAfterHoverStart + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On after "HoverEnd" event. + -- @function [parent=#OPSGROUP] OnAfterHoverEnd + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Triggers the FSM event "TransportCancel". + -- @function [parent=#OPSGROUP] TransportCancel + -- @param #OPSGROUP self + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- Triggers the FSM event "TransportCancel" after a delay. + -- @function [parent=#OPSGROUP] __TransportCancel + -- @param #OPSGROUP self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On after "TransportCancel" event. + -- @function [parent=#OPSGROUP] OnAfterTransportCancel + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. + + --- On After "DetectedGroup" event. + -- @function [parent=#OPSGROUP] OnAfterDetectedGroup + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#Group Group Detected Group. + + --- On After "DetectedGroupNew" event. + -- @function [parent=#OPSGROUP] OnAfterDetectedGroupNew + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#Group Group Newly detected group. + + --- On After "DetectedGroupKnown" event. + -- @function [parent=#OPSGROUP] OnAfterDetectedGroupKnown + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#Group Group Known detected group. + + --- On After "DetectedGroupLost" event. + -- @function [parent=#OPSGROUP] OnAfterDetectedGroupLost + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#Group Group Lost detected group. + + --- On After "DetectedUnit" event. + -- @function [parent=#OPSGROUP] OnAfterDetectedUnit + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#Unit Unit Detected Unit. + + --- On After "DetectedUnitNew" event. + -- @function [parent=#OPSGROUP] OnAfterDetectedUnitNew + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#Unit Unit Newly detected unit. + + --- On After "DetectedUnitKnown" event. + -- @function [parent=#OPSGROUP] OnAfterDetectedUnitKnown + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#Unit Unit Known detected unit. + + --- On After "DetectedUnitLost" event. + -- @function [parent=#OPSGROUP] OnAfterDetectedUnitLost + -- @param #OPSGROUP self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#Unit Unit Lost detected unit. + + -- 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 total life points of the group. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element Element (Optional) Only get life points of this element. +-- @return #number Life points, *i.e.* the sum of life points over all units in the group (unless a specific element was passed). +-- @return #number Initial life points. +function OPSGROUP:GetLifePoints(Element) + + local life=0 + local life0=0 + + if Element then + + local unit=Element.unit + + if unit then + life=unit:GetLife() + life0=unit:GetLife0() + life=math.min(life, life0) -- Some units have more life than life0 returns! + end + + else + + for _,element in pairs(self.elements) do + local l,l0=self:GetLifePoints(element) + life=life+l + life0=life0+l0 + end + + end + + return life, life0 +end + +--- Get generalized attribute. +-- @param #OPSGROUP self +-- @return #string Generalized attribute. +function OPSGROUP:GetAttribute() + return self.attribute +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 legion this ops group belongs to. +-- @param #OPSGROUP self +-- @param Ops.Legion#LEGION Legion The Legion. +-- @return #OPSGROUP self +function OPSGROUP:_SetLegion(Legion) + self:T2(self.lid..string.format("Adding opsgroup to legion %s", Legion.alias)) + self.legion=Legion + return self +end + +--- **[GROUND, NAVAL]** Set whether this group should return to its legion once all mission etc are finished. Only for ground and naval groups. Aircraft will +-- @param #OPSGROUP self +-- @param #boolean Switch If `true` or `nil`, group will return. If `false`, group will not return and stay where it finishes its last mission. +-- @return #OPSGROUP self +function OPSGROUP:SetReturnToLegion(Switch) + if Switch==false then + self.legionReturn=false + else + self.legionReturn=true + end + self:T(self.lid..string.format("Setting ReturnToLegion=%s", tostring(self.legionReturn))) + 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() + local speed=UTILS.KmphToKnots(self.speedCruise or self.speedMax*0.7) + return speed +end + +--- Set default cruise altitude. +-- @param #OPSGROUP self +-- @param #number Altitude Altitude in feet. Default is 10,000 ft for airplanes and 1,500 feet for helicopters. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultAltitude(Altitude) + if Altitude then + self.altitudeCruise=UTILS.FeetToMeters(Altitude) + else + if self:IsFlightgroup() then + if self.isHelo then + self.altitudeCruise=UTILS.FeetToMeters(1500) + else + self.altitudeCruise=UTILS.FeetToMeters(10000) + end + else + self.altitudeCruise=0 + end + end + return self +end + +--- Get default cruise speed. +-- @param #OPSGROUP self +-- @return #number Cruise altitude in feet. +function OPSGROUP:GetCruiseAltitude() + local alt=UTILS.MetersToFeet(self.altitudeCruise) + return alt +end + +--- Set current altitude. +-- @param #OPSGROUP self +-- @param #number Altitude Altitude in feet. Default is 10,000 ft for airplanes and 1,500 feet for helicopters. +-- @param #boolean Keep If `true` the group will maintain that speed on passing waypoints. If `nil` or `false` the group will return to the speed as defined by their route. +-- @return #OPSGROUP self +function OPSGROUP:SetAltitude(Altitude, Keep, RadarAlt) + if Altitude then + Altitude=UTILS.FeetToMeters(Altitude) + else + if self:IsFlightgroup() then + if self.isHelo then + Altitude=UTILS.FeetToMeters(1500) + else + Altitude=UTILS.FeetToMeters(10000) + end + else + Altitude=0 + end + end + + local AltType="BARO" + if RadarAlt then + AltType="RADIO" + end + + if self.controller then + self.controller:setAltitude(Altitude, Keep, AltType) + end + + return self +end + +--- Set current altitude. +-- @param #OPSGROUP self +-- @return #number Altitude in feet. +function OPSGROUP:GetAltitude() + + local alt=0 + + if self.group then + + alt=self.group:GetAltitude() + + alt=UTILS.MetersToFeet(alt) + + end + + return alt +end + +--- Set current speed. +-- @param #OPSGROUP self +-- @param #number Speed Speed in knots. Default is 70% of max speed. +-- @param #boolean Keep If `true` the group will maintain that speed on passing waypoints. If `nil` or `false` the group will return to the speed as defined by their route. +-- @param #boolean AltCorrected If `true`, use altitude corrected indicated air speed. +-- @return #OPSGROUP self +function OPSGROUP:SetSpeed(Speed, Keep, AltCorrected) + if Speed then + + else + Speed=UTILS.KmphToKnots(self.speedMax) + end + + + if AltCorrected then + local altitude=self:GetAltitude() + Speed=UTILS.KnotsToAltKIAS(Speed, altitude) + end + + Speed=UTILS.KnotsToMps(Speed) + + if self.controller then + self.controller:setSpeed(Speed, Keep) + end + + return self +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:T(self.lid..string.format("Detection is %s", tostring(Switch))) + self.detectionOn=Switch + return self +end + +--- Get DCS group object. +-- @param #OPSGROUP self +-- @return DCS#Group DCS group object. +function OPSGROUP:GetDCSObject() + return self.dcsgroup +end + +--- Make a target (unit, group, opsgroup) known to this group. +-- This is useing the DCS function `controller.knowTarget`. +-- @param #OPSGROUP self +-- @param Wrapper.Positionable#POSITIONABLE TargetObject The target object. +-- @param #boolean KnowType Make type known. +-- @param #boolean KnowDist Make distance known. +-- @param #number Delay Delay in seconds before the target is known. +-- @return #OPSGROUP self +function OPSGROUP:KnowTarget(TargetObject, KnowType, KnowDist, Delay) + + if Delay and Delay>0 then + -- Delayed call. + self:ScheduleOnce(Delay, OPSGROUP.KnowTarget, self, TargetObject, KnowType, KnowDist, 0) + else + + if TargetObject:IsInstanceOf("GROUP") then + TargetObject=TargetObject:GetUnit(1) + elseif TargetObject:IsInstanceOf("OPSGROUP") then + TargetObject=TargetObject.group:GetUnit(1) + end + + -- Get the DCS object. + local object=TargetObject:GetDCSObject() + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.controller then + element.controller:knowTarget(object, true, true) + --self:T(self.lid..string.format("Element %s should now know target %s", element.name, TargetObject:GetName())) + end + end + + -- Debug info. + self:T(self.lid..string.format("We should now know target %s", TargetObject:GetName())) + + end + + return self +end + +--- Check if target is detected. +-- @param #OPSGROUP self +-- @param Wrapper.Positionable#POSITIONABLE TargetObject The target object. +-- @return #boolean If `true`, target was detected. +function OPSGROUP:IsTargetDetected(TargetObject) + + local objects={} + + if TargetObject:IsInstanceOf("GROUP") then + for _,unit in pairs(TargetObject:GetUnits()) do + table.insert(objects, unit:GetDCSObject()) + end + elseif TargetObject:IsInstanceOf("OPSGROUP") then + for _,unit in pairs(TargetObject.group:GetUnits()) do + table.insert(objects, unit:GetDCSObject()) + end + elseif TargetObject:IsInstanceOf("UNIT") or TargetObject:IsInstanceOf("STATIC") then + table.insert(objects, TargetObject:GetDCSObject()) + end + + for _,object in pairs(objects or {}) do + + -- Check group controller. + local detected, visible, lastTime, type, distance, lastPos, lastVel = self.controller:isTargetDetected(object, 1, 2, 4, 8, 16, 32) + + --env.info(self.lid..string.format("Detected target %s: %s", TargetObject:GetName(), tostring(detected))) + + if detected then + return true + end + + + -- Check all elements. + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.controller then + + -- Check. + local detected, visible, lastTime, type, distance, lastPos, lastVel= + element.controller:isTargetDetected(object, 1, 2, 4, 8, 16, 32) + + --env.info(self.lid..string.format("Element %s detected target %s: %s", element.name, TargetObject:GetName(), tostring(detected))) + + if detected then + return true + end + + end + end + + end + + return false +end + +--- Check if a given coordinate is in weapon range. +-- @param #OPSGROUP self +-- @param Core.Point#COORDINATE TargetCoord Coordinate of the target. +-- @param #number WeaponBitType Weapon type. +-- @param Core.Point#COORDINATE RefCoord Reference coordinate. +-- @return #boolean If `true`, coordinate is in range. +function OPSGROUP:InWeaponRange(TargetCoord, WeaponBitType, RefCoord) + + RefCoord=RefCoord or self:GetCoordinate() + + local dist=TargetCoord:Get2DDistance(RefCoord) + + if WeaponBitType then + + local weapondata=self:GetWeaponData(WeaponBitType) + + if weapondata then + + if dist>=weapondata.RangeMin and dist<=weapondata.RangeMax then + return true + else + return false + end + + end + + else + + for _,_weapondata in pairs(self.weaponData or {}) do + local weapondata=_weapondata --#OPSGROUP.WeaponData + + if dist>=weapondata.RangeMin and dist<=weapondata.RangeMax then + return true + end + + end + + return false + end + + + return nil +end + +--- Get a coordinate, which is in weapon range. +-- @param #OPSGROUP self +-- @param Core.Point#COORDINATE TargetCoord Coordinate of the target. +-- @param #number WeaponBitType Weapon type. +-- @param Core.Point#COORDINATE RefCoord Reference coordinate. +-- @param #table SurfaceTypes Valid surfaces types of the coordinate. Default any (nil). +-- @return Core.Point#COORDINATE Coordinate in weapon range +function OPSGROUP:GetCoordinateInRange(TargetCoord, WeaponBitType, RefCoord, SurfaceTypes) + + local coordInRange=nil --Core.Point#COORDINATE + + RefCoord=RefCoord or self:GetCoordinate() + + -- Get weapon range. + local weapondata=self:GetWeaponData(WeaponBitType) + + -- Heading intervals to search for a possible new coordinate in range. + local dh={0, -5, 5, -10, 10, -15, 15, -20, 20, -25, 25, -30, 30, -35, 35, -40, 40, -45, 45, -50, 50, -55, 55, -60, 60, -65, 65, -70, 70, -75, 75, -80, 80} + + -- Function that checks if the given surface type is valid + local function _checkSurface(point) + if SurfaceTypes then + local stype=point:GetSurfaceType() + for _,sf in pairs(SurfaceTypes) do + if sf==stype then + return true + end + end + return false + else + return true + end + end + + if weapondata then + + -- Heading to target. + local heading=TargetCoord:HeadingTo(RefCoord) + + -- Distance to target. + local dist=RefCoord:Get2DDistance(TargetCoord) + + local range=nil + if dist>weapondata.RangeMax then + range=weapondata.RangeMax + self:T(self.lid..string.format("Out of max range = %.1f km by %.1f km for weapon %s", weapondata.RangeMax/1000, (weapondata.RangeMax-dist)/1000, tostring(WeaponBitType))) + elseif dist=ThreatLevelMin and threatlevel<=ThreatLevelMax then + + if threatlevellevelmax then + threat=unit + levelmax=threatlevel + end + + end + + return threat, levelmax +end + +--- Enable to automatically engage detected targets. +-- @param #OPSGROUP 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 #OPSGROUP self +function OPSGROUP: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 + + -- Debug info. + self:T(self.lid..string.format("Engage detected ON: Rmax=%d NM", UTILS.MetersToNM(self.engagedetectedRmax))) + + -- Ensure detection is ON or it does not make any sense. + self:SetDetection(true) + + return self +end + +--- Disable to automatically engage detected targets. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:SetEngageDetectedOff() + self:T(self.lid..string.format("Engage detected OFF")) + self.engagedetectedOn=false + return self +end + +--- Set that group is going to rearm once it runs out of ammo. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:SetRearmOnOutOfAmmo() + self.rearmOnOutOfAmmo=true + return self +end + +--- Set that group is retreating once it runs out of ammo. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:SetRetreatOnOutOfAmmo() + self.retreatOnOutOfAmmo=true + return self +end + +--- Set that group is return to legion once it runs out of ammo. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:SetReturnOnOutOfAmmo() + self.rtzOnOutOfAmmo=true + return self +end + +--- Set max weight that each unit of the group can handle. +-- @param #OPSGROUP self +-- @param #number Weight Max weight of cargo in kg the unit can carry. +-- @param #string UnitName Name of the Unit. If not given, weight is set for all units of the group. +-- @return #OPSGROUP self +function OPSGROUP:SetCargoBayLimit(Weight, UnitName) + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if UnitName==nil or UnitName==element.name then + + element.weightMaxCargo=Weight + + if element.unit then + element.unit:SetCargoBayWeightLimit(Weight) + end + + end + + end + + return self +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. Can also be a DCS#Vec3. +-- @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) + + if Coordinate then + + -- Target vector. + local Vec3={x=Coordinate.x, y=Coordinate.y, z=Coordinate.z} --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(vec3) + if vec3 then + 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 + return nil + end + + if Element then + -- Check los for the given element. + if Element.unit and Element.unit:IsAlive() then + local vec3=Element.unit:GetVec3() + local los=checklos(vec3) + return los + end + else + + -- Check if any element has los. + local gotit=false + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element and element.unit and element.unit:IsAlive() then + gotit=true + local vec3=element.unit:GetVec3() + -- Get LoS of this element. + local los=checklos(vec3) + if los then + return true + end + end + end + + if gotit then + return false + end + end + + 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 + else + self:E(self.lid..string.format("ERROR: DCS group does not exist! Cannot get 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 + + +--- Get current 2D position vector of the group. +-- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get position of a specifc unit of the group. Default is the first existing unit in the group. +-- @return DCS#Vec2 Vector with x,y components. +function OPSGROUP:GetVec2(UnitName) + + local vec3=self:GetVec3(UnitName) + + 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 +-- @param #string UnitName (Optional) Get position of a specifc unit of the group. Default is the first existing unit in the group. +-- @return DCS#Vec3 Vector with x,y,z components. +function OPSGROUP:GetVec3(UnitName) + + local vec3=nil --DCS#Vec3 + + -- First check if this group is loaded into a carrier + local carrier=self:_GetMyCarrierElement() + if carrier and carrier.status~=OPSGROUP.ElementStatus.DEAD and self:IsLoaded() then + local unit=carrier.unit + if unit and unit:IsExist() then + vec3=unit:GetVec3() + return vec3 + end + end + + if self:IsExist() then + + local unit=nil --DCS#Unit + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() + end + + + if unit then + local vec3=unit:getPoint() + return vec3 + end + + end + + -- Return last known position. + if self.position then + return self.position + end + + return nil +end + +--- Get current coordinate of the group. If the current position cannot be determined, the last known position is returned. +-- @param #OPSGROUP self +-- @param #boolean NewObject Create a new coordiante object. +-- @param #string UnitName (Optional) Get position of a specifc unit of the group. Default is the first existing unit in the group. +-- @return Core.Point#COORDINATE The coordinate (of the first unit) of the group. +function OPSGROUP:GetCoordinate(NewObject, UnitName) + + local vec3=self:GetVec3(UnitName) or self.position --DCS#Vec3 + + 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:T(self.lid.."WARNING: Cannot get coordinate!") + end + + return nil +end + +--- Get current velocity of the group. +-- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get velocity of a specific unit of the group. Default is from the first existing unit in the group. +-- @return #number Velocity in m/s. +function OPSGROUP:GetVelocity(UnitName) + + if self:IsExist() then + + local unit=nil --DCS#Unit + + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() + end + + if unit then + + local velvec3=unit:getVelocity() + + local vel=UTILS.VecNorm(velvec3) + + return vel + + else + self:T(self.lid.."WARNING: Unit does not exist. Cannot get velocity!") + end + + else + self:T(self.lid.."WARNING: Group does not exist. Cannot get velocity!") + end + + return nil +end + +--- Get current heading of the group or (optionally) of a specific unit of the group. +-- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get heading of a specific unit of the group. Default is from the first existing unit in the group. +-- @return #number Current heading of the group in degrees. +function OPSGROUP:GetHeading(UnitName) + + if self:IsExist() then + + local unit=nil --DCS#Unit + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() + end + + 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:T(self.lid.."WARNING: Group does not exist. Cannot get heading!") + end + + return nil +end + +--- Get current orientation of the group. +-- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get orientation of a specific unit of the group. Default is the first existing unit of the group. +-- @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(UnitName) + + if self:IsExist() then + + local unit=nil --DCS#Unit + + if UnitName then + unit=Unit.getByName(UnitName) + else + unit=self:GetDCSUnit() + end + + if unit then + + local pos=unit:getPosition() + + return pos.x, pos.y, pos.z + end + + else + self:T(self.lid.."WARNING: Group does not exist. Cannot get orientation!") + end + + return nil +end + +--- Get current "X" orientation of the first unit in the group. +-- @param #OPSGROUP self +-- @param #string UnitName (Optional) Get orientation of a specific unit of the group. Default is the first existing unit of the group. +-- @return DCS#Vec3 Orientation X parallel to where the "nose" is pointing. +function OPSGROUP:GetOrientationX(UnitName) + + local X,Y,Z=self:GetOrientation(UnitName) + + 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 + + +--- Despawn a unit of the group. A "Remove Unit" event is generated by default. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit +-- @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:DespawnUnit(UnitName, Delay, NoEventRemoveUnit) + + -- Debug info. + self:T(self.lid.."Despawn element "..tostring(UnitName)) + + -- Get element. + local element=self:GetElementByName(UnitName) + + if element then + + -- Get DCS unit object. + local DCSunit=Unit.getByName(UnitName) + + if DCSunit then + + -- Despawn unit. + DCSunit:destroy() + + -- Element goes back in utero. + self:ElementInUtero(element) + + if not NoEventRemoveUnit then + self:CreateEventRemoveUnit(timer.getTime(), DCSunit) + end + + end + + end + +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 + +--- Despawn the group. The whole group is despawned and a "`Remove Unit`" event is generated for all current units of the group. +-- If no `Remove Unit` event should be generated, the second optional parameter needs to be set to `true`. +-- If this group belongs to an AIRWING, BRIGADE or FLEET, it will be added to the warehouse stock if the `NoEventRemoveUnit` parameter is `false` or `nil`. +-- @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.scheduleIDDespawn=self:ScheduleOnce(Delay, OPSGROUP.Despawn, self, 0, NoEventRemoveUnit) + else + + -- Debug info. + self:T(self.lid..string.format("Despawning Group!")) + + -- DCS group obejct. + local DCSGroup=self:GetDCSGroup() + + if DCSGroup then + + -- Clear any task ==> makes DCS crash! + --self.group:ClearTasks() + + -- Get all units. + local units=self:GetDCSUnits() + + for i=1,#units do + local unit=units[i] + if unit then + local name=unit:getName() + if name then + -- Despawn the unit. + self:DespawnUnit(name, 0, NoEventRemoveUnit) + end + end + end + + end + end + + return self +end + +--- Return group back to the legion it belongs to. +-- Group is despawned and added back to the stock. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before the group will be despawned. Default immediately +-- @return #OPSGROUP self +function OPSGROUP:ReturnToLegion(Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.ReturnToLegion, self) + else + + if self.legion then + -- Add asset back. + self:T(self.lid..string.format("Adding asset back to LEGION")) + self.legion:AddAsset(self.group, 1) + else + self:E(self.lid..string.format("ERROR: Group does not belong to a LEGION!")) + end + + end + + return self +end + +--- Destroy a unit of the group. A *Unit Lost* for aircraft or *Dead* event for ground/naval units is generated. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit which should be destroyed. +-- @param #number Delay Delay in seconds before the group will be destroyed. Default immediately. +-- @return #OPSGROUP self +function OPSGROUP:DestroyUnit(UnitName, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.DestroyUnit, self, UnitName, 0) + else + + local unit=Unit.getByName(UnitName) + + if unit then + + -- Create a "Unit Lost" event. + local EventTime=timer.getTime() + + if self:IsFlightgroup() then + self:CreateEventUnitLost(EventTime, unit) + else + self:CreateEventDead(EventTime, unit) + end + + -- Despawn unit. + unit:destroy() + + end + + end + +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, 0) + else + + self:T(self.lid.."Destroying group!") + + -- Get all units. + local units=self:GetDCSUnits() + + if units then + + -- Create a "Unit Lost" event. + for _,unit in pairs(units) do + if unit then + self:DestroyUnit(unit:getName()) + end + end + + end + + end + + return self +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:T(self.lid.."WARNING: Activating group that is already activated") + else + self:T(self.lid.."ERROR: Activating group that is does not exist!") + end + + end + + return self +end + +--- Deactivate the group. Group will be respawned in late activated state. +-- @param #OPSGROUP self +-- @param #number delay (Optional) Delay in seconds before the group is deactivated. Default is immediately. +-- @return #OPSGROUP self +function OPSGROUP:Deactivate(delay) + + if delay and delay>0 then + self:ScheduleOnce(delay, OPSGROUP.Deactivate, self) + else + + if self:IsAlive()==true then + + self.template.lateActivation=true + + local template=UTILS.DeepCopy(self.template) + + self:_Respawn(0, template) + + 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 100 kg. +-- @param #string ElementName Name of the element that should be destroyed. Default is all elements. +-- @return #OPSGROUP self +function OPSGROUP:SelfDestruction(Delay, ExplosionPower, ElementName) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.SelfDestruction, self, 0, ExplosionPower, ElementName) + else + + -- Loop over all elements. + for i,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if ElementName==nil or ElementName==element.name then + + local unit=element.unit + + if unit and unit:IsAlive() then + unit:Explode(ExplosionPower or 100) + end + + end + end + end + + return self +end + +--- Use SRS Simple-Text-To-Speech for transmissions. +-- @param #OPSGROUP 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. +-- @param #string PathToGoogleKey Full path to the google credentials JSON file, e.g. `"C:\Users\myUsername\Downloads\key.json"`. +-- @param #string Label Label of the SRS comms for the SRS Radio overlay. Defaults to "ROBOT". No spaces allowed! +-- @param #number Volume Volume to be set, 0.0 = silent, 1.0 = loudest. Defaults to 1.0 +-- @return #OPSGROUP self +function OPSGROUP:SetSRS(PathToSRS, Gender, Culture, Voice, Port, PathToGoogleKey, Label, Volume) + self.useSRS=true + local path = PathToSRS or MSRS.path + local port = Port or MSRS.port + self.msrs=MSRS:New(path, self.frequency, self.modulation) + self.msrs:SetGender(Gender) + self.msrs:SetCulture(Culture) + self.msrs:SetVoice(Voice) + self.msrs:SetPort(port) + self.msrs:SetLabel(Label) + if PathToGoogleKey then + self.msrs:SetProviderOptionsGoogle(PathToGoogleKey,PathToGoogleKey) + self.msrs:SetProvider(MSRS.Provider.GOOGLE) + end + self.msrs:SetCoalition(self:GetCoalition()) + self.msrs:SetVolume(Volume) + return self +end + +--- Send a radio transmission via SRS Text-To-Speech. +-- @param #OPSGROUP self +-- @param #string Text Text of transmission. +-- @param #number Delay Delay in seconds before the transmission is started. +-- @param #boolean SayCallsign If `true`, the callsign is prepended to the given text. Default `false`. +-- @param #number Frequency Override sender frequency, helpful when you need multiple radios from the same sender. Default is the frequency set for the OpsGroup. +-- @return #OPSGROUP self +function OPSGROUP:RadioTransmission(Text, Delay, SayCallsign, Frequency) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.RadioTransmission, self, Text, 0, SayCallsign) + else + + if self.useSRS and self.msrs then + + local freq, modu, radioon=self:GetRadio() + + local coord = self:GetCoordinate() + + self.msrs:SetCoordinate(coord) + + if Frequency then + self.msrs:SetFrequencies(Frequency) + else + self.msrs:SetFrequencies(freq) + end + self.msrs:SetModulations(modu) + + if SayCallsign then + local callsign=self:GetCallsignName() + Text=string.format("%s, %s", callsign, Text) + end + + -- Debug info. + self:T(self.lid..string.format("Radio transmission on %.3f MHz %s: %s", freq, UTILS.GetModulationName(modu), Text)) + + self.msrs:PlayText(Text) + end + + end + + return self +end + +--- Set that this carrier is an all aspect loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderAllAspect(Length, Width) + self.carrierLoader.type="front" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a front loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderFront(Length, Width) + self.carrierLoader.type="front" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a back loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderBack(Length, Width) + self.carrierLoader.type="back" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a starboard (right side) loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderStarboard(Length, Width) + self.carrierLoader.type="right" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + +--- Set that this carrier is a port (left side) loader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierLoaderPort(Length, Width) + self.carrierLoader.type="left" + self.carrierLoader.length=Length or 50 + self.carrierLoader.width=Width or 20 + return self +end + + +--- Set that this carrier is an all aspect unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderAllAspect(Length, Width) + self.carrierUnloader.type="front" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a front unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderFront(Length, Width) + self.carrierUnloader.type="front" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a back unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderBack(Length, Width) + self.carrierUnloader.type="back" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a starboard (right side) unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderStarboard(Length, Width) + self.carrierUnloader.type="right" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Set that this carrier is a port (left side) unloader. +-- @param #OPSGROUP self +-- @param #number Length Length of loading zone in meters. Default 50 m. +-- @param #number Width Width of loading zone in meters. Default 20 m. +-- @return #OPSGROUP self +function OPSGROUP:SetCarrierUnloaderPort(Length, Width) + self.carrierUnloader.type="left" + self.carrierUnloader.length=Length or 50 + self.carrierUnloader.width=Width or 20 + return self +end + +--- Check if group is currently inside a zone. +-- @param #OPSGROUP self +-- @param Core.Zone#ZONE Zone The zone. +-- @return #boolean If true, group is in this zone +function OPSGROUP:IsInZone(Zone) + local vec2=self:GetVec2() + local is=false + if vec2 then + is=Zone:IsVec2InZone(vec2) + else + self:T3(self.lid.."WARNING: Cannot get vec2 at IsInZone()!") + end + return is +end + +--- Get 2D distance to a coordinate. +-- @param #OPSGROUP self +-- @param Core.Point#COORDINATE Coordinate. Can also be a DCS#Vec2 or DCS#Vec3. +-- @return #number Distance in meters. +function OPSGROUP:Get2DDistance(Coordinate) + + local a=self:GetVec2() + local b={} + if Coordinate.z then + b.x=Coordinate.x + b.y=Coordinate.z + else + b.x=Coordinate.x + b.y=Coordinate.y + end + + local dist=UTILS.VecDist2D(a, b) + + return dist +end + +--- Check if this is a FLIGHTGROUP. +-- @param #OPSGROUP self +-- @return #boolean If true, this is an airplane or helo group. +function OPSGROUP:IsFlightgroup() + return self.isFlightgroup +end + +--- Check if this is a ARMYGROUP. +-- @param #OPSGROUP self +-- @return #boolean If true, this is a ground group. +function OPSGROUP:IsArmygroup() + return self.isArmygroup +end + +--- Check if this is a NAVYGROUP. +-- @param #OPSGROUP self +-- @return #boolean If true, this is a ship group. +function OPSGROUP:IsNavygroup() + return self.isNavygroup +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() + + if self.group then + local active=self.group:IsActive() + return active + end + + return nil +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. Note that dead groups are also in utero but will return `false` here. +-- @param #OPSGROUP self +-- @return #boolean If true, group is not spawned yet. +function OPSGROUP:IsInUtero() + local is=self:Is("InUtero") and not self:IsDead() + return is +end + +--- Check if group is in state spawned. +-- @param #OPSGROUP self +-- @return #boolean If true, group is spawned. +function OPSGROUP:IsSpawned() + local is=self:Is("Spawned") + return is +end + +--- Check if group is dead. Could be destroyed or despawned. FSM state of dead group is `InUtero` though. +-- @param #OPSGROUP self +-- @return #boolean If true, all units/elements of the group are dead. +function OPSGROUP:IsDead() + return self.isDead +end + +--- Check if group was destroyed. +-- @param #OPSGROUP self +-- @return #boolean If true, all units/elements of the group were destroyed. +function OPSGROUP:IsDestroyed() + return self.isDestroyed +end + +--- Check if FSM is stopped. +-- @param #OPSGROUP self +-- @return #boolean If true, FSM state is stopped. +function OPSGROUP:IsStopped() + local is=self:Is("Stopped") + return is +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 or on its way to the rearming place. +-- @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 is completely out of ammo. +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is out-of-ammo. +function OPSGROUP:IsOutOfAmmo() + return self.outofAmmo +end + +--- Check if the group is out of bombs. +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is out of bombs. +function OPSGROUP:IsOutOfBombs() + return self.outofBombs +end + +--- Check if the group is out of guns. +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is out of guns. +function OPSGROUP:IsOutOfGuns() + return self.outofGuns +end + +--- Check if the group is out of missiles. +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is out of missiles. +function OPSGROUP:IsOutOfMissiles() + return self.outofMissiles +end + +--- Check if the group is out of torpedos. +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is out of torpedos. +function OPSGROUP:IsOutOfTorpedos() + return self.outofTorpedos +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 or retreated. +-- @param #OPSGROUP self +-- @return #boolean If true, group is retreating or retreated. +function OPSGROUP:IsRetreating() + local is=self:is("Retreating") or self:is("Retreated") + return is +end + +--- Check if the group is retreated (has reached its retreat zone). +-- @param #OPSGROUP self +-- @return #boolean If true, group is retreated. +function OPSGROUP:IsRetreated() + local is=self:is("Retreated") + return is +end + + +--- Check if the group is currently returning to a zone. +-- @param #OPSGROUP self +-- @return #boolean If true, group is returning. +function OPSGROUP:IsReturning() + local is=self:is("Returning") + return is +end + +--- Check if the group is engaging another unit or group. +-- @param #OPSGROUP self +-- @return #boolean If true, group is engaging. +function OPSGROUP:IsEngaging() + local is=self:is("Engaging") + return is +end + +--- Check if group is currently waiting. +-- @param #OPSGROUP self +-- @return #boolean If true, group is currently waiting. +function OPSGROUP:IsWaiting() + if self.Twaiting then + return true + end + return false +end + +--- Check if the group is not a carrier yet. +-- @param #OPSGROUP self +-- @return #boolean If true, group is not a carrier. +function OPSGROUP:IsNotCarrier() + return self.carrierStatus==OPSGROUP.CarrierStatus.NOTCARRIER +end + +--- Check if the group is a carrier. +-- @param #OPSGROUP self +-- @return #boolean If true, group is a carrier. +function OPSGROUP:IsCarrier() + return not self:IsNotCarrier() +end + +--- Check if the group is picking up cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is picking up. +function OPSGROUP:IsPickingup() + return self.carrierStatus==OPSGROUP.CarrierStatus.PICKUP +end + +--- Check if the group is loading cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is loading. +function OPSGROUP:IsLoading() + return self.carrierStatus==OPSGROUP.CarrierStatus.LOADING +end + +--- Check if the group is transporting cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is transporting. +function OPSGROUP:IsTransporting() + return self.carrierStatus==OPSGROUP.CarrierStatus.TRANSPORTING +end + +--- Check if the group is unloading cargo. +-- @param #OPSGROUP self +-- @return #boolean If true, group is unloading. +function OPSGROUP:IsUnloading() + return self.carrierStatus==OPSGROUP.CarrierStatus.UNLOADING +end + + +--- Check if the group is assigned as cargo. +-- @param #OPSGROUP self +-- @param #boolean CheckTransport If `true` or `nil`, also check if cargo is associated with a transport assignment. If not, we consider it not cargo. +-- @return #boolean If true, group is cargo. +function OPSGROUP:IsCargo(CheckTransport) + return not self:IsNotCargo(CheckTransport) +end + +--- Check if the group is **not** cargo. +-- @param #OPSGROUP self +-- @param #boolean CheckTransport If `true` or `nil`, also check if cargo is associated with a transport assignment. If not, we consider it not cargo. +-- @return #boolean If true, group is *not* cargo. +function OPSGROUP:IsNotCargo(CheckTransport) + local notcargo=self.cargoStatus==OPSGROUP.CargoStatus.NOTCARGO + + if notcargo then + -- Not cargo. + return true + else + -- Is cargo (e.g. loaded or boarding) + + if CheckTransport then + -- Check if transport UID was set. + if self.cargoTransportUID==nil then + return true + else + -- Some transport UID was assigned. + return false + end + else + -- Is cargo. + return false + end + + end + + + return notcargo +end + +--- Check if awaiting a transport. +-- @param #OPSGROUP self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. +-- @return #OPSGROUP self +function OPSGROUP:_AddMyLift(Transport) + self.mylifts=self.mylifts or {} + self.mylifts[Transport.uid]=true + return self +end + +--- Remove my lift. +-- @param #OPSGROUP self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport. +-- @return #OPSGROUP self +function OPSGROUP:_DelMyLift(Transport) + if self.mylifts then + self.mylifts[Transport.uid]=nil + end + return self +end + + +--- Check if awaiting a transport lift. +-- @param #OPSGROUP self +-- @param Ops.OpsTransport#OPSTRANSPORT Transport (Optional) The transport. +-- @return #boolean If true, group is awaiting transport lift.. +function OPSGROUP:IsAwaitingLift(Transport) + + if self.mylifts then + + for uid,iswaiting in pairs(self.mylifts) do + if Transport==nil or Transport.uid==uid then + if iswaiting==true then + return true + end + end + end + + end + + return false +end + +--- Get paused mission. +-- @param #OPSGROUP self +-- @return Ops.Auftrag#AUFTRAG Paused mission or nil. +function OPSGROUP:_GetPausedMission() + + if self.pausedmissions and #self.pausedmissions>0 then + for _,mid in pairs(self.pausedmissions) do + if mid then + local mission=self:GetMissionByID(mid) + if mission and mission:IsNotOver() then + return mission + end + end + end + end + + return nil +end + +--- Count paused mission. +-- @param #OPSGROUP self +-- @return #number Number of paused missions. +function OPSGROUP:_CountPausedMissions() + local N=0 + if self.pausedmissions and #self.pausedmissions>0 then + for _,mid in pairs(self.pausedmissions) do + local mission=self:GetMissionByID(mid) + if mission and mission:IsNotOver() then + N=N+1 + end + end + end + + return N +end + +--- Remove paused mission from the table. +-- @param #OPSGROUP self +-- @param #number AuftragsNummer Mission ID of the paused mission to remove. +-- @return #OPSGROUP self +function OPSGROUP:_RemovePausedMission(AuftragsNummer) + + if self.pausedmissions and #self.pausedmissions>0 then + for i=#self.pausedmissions,1,-1 do + local mid=self.pausedmissions[i] + if mid==AuftragsNummer then + table.remove(self.pausedmissions, i) + return self + end + end + end + + return self +end + +--- Check if the group is currently boarding a carrier. +-- @param #OPSGROUP self +-- @param #string CarrierGroupName (Optional) Additionally check if group is boarding this particular carrier group. +-- @return #boolean If true, group is boarding. +function OPSGROUP:IsBoarding(CarrierGroupName) + if CarrierGroupName then + local carrierGroup=self:_GetMyCarrierGroup() + if carrierGroup and carrierGroup.groupname~=CarrierGroupName then + return false + end + end + return self.cargoStatus==OPSGROUP.CargoStatus.BOARDING +end + +--- Check if the group is currently loaded into a carrier. +-- @param #OPSGROUP self +-- @param #string CarrierGroupName (Optional) Additionally check if group is loaded into a particular carrier group(s). +-- @return #boolean If true, group is loaded. +function OPSGROUP:IsLoaded(CarrierGroupName) + + local isloaded=self.cargoStatus==OPSGROUP.CargoStatus.LOADED + + -- If not loaded, we can return false + if not isloaded then + return false + end + + if CarrierGroupName then + if type(CarrierGroupName)~="table" then + CarrierGroupName={CarrierGroupName} + end + for _,CarrierName in pairs(CarrierGroupName) do + local carrierGroup=self:_GetMyCarrierGroup() + if carrierGroup and carrierGroup.groupname==CarrierName then + return isloaded + end + end + -- Not in any specified carrier. + return false + end + + return isloaded +end + +--- Check if the group is currently busy doing something. +-- +-- * Boarding +-- * Rearming +-- * Returning +-- * Pickingup, Loading, Transporting, Unloading +-- * Engageing +-- +-- @param #OPSGROUP self +-- @return #boolean If `true`, group is busy. +function OPSGROUP:IsBusy() + + if self:IsBoarding() then + return true + end + + if self:IsRearming() then + return true + end + + if self:IsReturning() then + return true + end + + -- Busy as carrier? + if self:IsPickingup() or self:IsLoading() or self:IsTransporting() or self:IsUnloading() then + return true + end + + if self:IsEngaging() then + return true + end + + + return false +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 or 0), "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 not specified, we take the adinititum value. + if cyclic==nil then + cyclic=self.adinfinitum + end + + -- Total number of waypoints. + local N=#self.waypoints + + -- Default is currentwp. + i=i or self.currentwp + + -- If no next waypoint exists, because the final waypoint was reached, we return the last waypoint. + local n=math.min(i+1, N) + + -- If last waypoint was reached, the first waypoint is the next in line. + 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 current waypoint UID. +-- @param #OPSGROUP self +-- @return #number Current waypoint UID. +function OPSGROUP:GetWaypointCurrentUID() + local wp=self:GetWaypointCurrent() + if wp then + return wp.uid + end + return nil +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.01 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() or self:Is("Rearming") or self:IsWaiting() or self:IsRetreated() then + --env.info("GetExpectedSpeed - returning ZERO") + return 0 + else + --env.info("GetExpectedSpeed - returning self.speedWP = "..self.speedWp) + 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 + + -- The waypoitn to be removed. + local wp=self:GetWaypoint(wpindex) + + -- Is this a temporary waypoint. + local istemp=wp.temp or wp.detour or wp.astar or wp.missionUID + + -- Number of waypoints before delete. + local N=#self.waypoints + + -- Always keep at least one waypoint. + if N==1 then + self:T(self.lid..string.format("ERROR: Cannot remove waypoint with index=%d! It is the only waypoint and a group needs at least ONE waypoint", wpindex)) + return self + end + + -- Check that wpindex is not larger than the number of waypoints in the table. + if wpindex>N then + self:T(self.lid..string.format("ERROR: Cannot remove waypoint with index=%d as there are only N=%d waypoints!", wpindex, N)) + return self + end + + -- Remove waypoint marker. + 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 UID=%d [temp=%s]: index=%d [currentwp=%d]. N %d-->%d", wp.uid, tostring(istemp), wpindex, self.currentwp, N, n)) + + -- Waypoint was not reached yet. + if wpindex > self.currentwp then + + --- + -- Removed a FUTURE waypoint + --- + + -- TODO: patrol adinfinitum. Not sure this is handled correctly. If patrol adinfinitum and we have now only one WP left, we should at least go back. + + -- Could be that the waypoint we are currently moving to was the LAST waypoint. Then we now passed the final waypoint. + if self.currentwp>=n and not (self.adinfinitum or istemp) then + self:_PassedFinalWaypoint(true, "Removed FUTURE waypoint we are currently moving to and that was the LAST waypoint") + end + + -- Check if group is done. + 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 + + -- Could be that the waypoint we are currently moving to was the LAST waypoint. Then we now passed the final waypoint. + if (self.adinfinitum or istemp) then + self:_PassedFinalWaypoint(false, "Removed PASSED temporary waypoint") + end + + end + + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event function handling the birth of a unit. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP: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 + + -- Set homebase if not already set. + if self.isFlightgroup then + + if EventData.Place then + self.homebase=self.homebase or EventData.Place + self.currbase=EventData.Place + else + self.currbase=nil + end + + if self.homebase and not self.destbase then + self.destbase=self.homebase + end + + self:T(self.lid..string.format("EVENT: Element %s born at airbase %s ==> spawned", unitname, self.currbase and self.currbase:GetName() or "unknown")) + else + self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", unitname)) + end + + -- Get element. + local element=self:GetElementByName(unitname) + + if element and element.status~=OPSGROUP.ElementStatus.SPAWNED then + + -- Debug info. + self:T(self.lid..string.format("EVENT: Element %s born ==> spawned", unitname)) + + self:T2(self.lid..string.format("DCS unit=%s isExist=%s", tostring(EventData.IniDCSUnit:getName()), tostring(EventData.IniDCSUnit:isExist()) )) + + -- Set element to spawned state. + self:ElementSpawned(element) + + end + + end + +end + +--- Event function handling the hit of a unit. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP:OnEventHit(EventData) + + -- Check that this is the right group. Here the hit group is stored as target. + if EventData and EventData.TgtGroup and EventData.TgtUnit and EventData.TgtGroupName and EventData.TgtGroupName==self.groupname then + self:T2(self.lid..string.format("EVENT: Unit %s hit!", EventData.TgtUnitName)) + + local unit=EventData.TgtUnit + local group=EventData.TgtGroup + local unitname=EventData.TgtUnitName + + -- Get element. + local element=self:GetElementByName(unitname) + + -- Increase group hit counter. + self.Nhit=self.Nhit or 0 + self.Nhit=self.Nhit + 1 + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + -- Trigger Element Hit Event. + self:ElementHit(element, EventData.IniUnit) + end + + end + +end + +--- Event function handling the dead of a unit. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP: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:T2(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 and element.status~=OPSGROUP.ElementStatus.DEAD 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 #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP: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 + self:T2(self.lid..string.format("EVENT: Unit %s removed!", EventData.IniUnitName)) + + 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 removed ==> dead", element.name)) + self:ElementDead(element) + end + + end + +end + +--- Event function handling when a unit is removed from the game. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP:OnEventPlayerLeaveUnit(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: Player left Unit %s!", EventData.IniUnitName)) + + 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: Player left Element %s ==> dead", element.name)) + self:ElementDead(element) + end + + end + +end + +--- Event function handling the event that a unit achieved a kill. +-- @param #OPSGROUP self +-- @param Core.Event#EVENTDATA EventData Event data. +function OPSGROUP:OnEventKill(EventData) + --self:I("FF event kill") + --self:I(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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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 + + -- 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.controller: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 + + -- 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 + + -- Push task. + self.controller: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 + +--- Returns true if the DCS controller currently has a task. +-- @param #OPSGROUP self +-- @return #boolean True or false if the controller has a task. Nil if no controller. +function OPSGROUP:HasTaskController() + local hastask=nil + if self.controller then + hastask=self.controller:hasTask() + end + self:T3(self.lid..string.format("Controller hasTask=%s", tostring(hastask))) + return hastask +end + +--- Clear DCS tasks. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:ClearTasks() + local hastask=self:HasTaskController() + if self:IsAlive() and self.controller and self:HasTaskController() then + self:T(self.lid..string.format("CLEARING Tasks")) + self.controller:resetTask() + 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 + self:T(self.lid..string.format("Adding enroute task")) + 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 before "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:onbeforeTaskExecute(From, Event, To, Task) + + -- Get mission of this task (if any). + local Mission=self:GetMissionByTaskID(Task.id) + + if Mission and (Mission.Tpush or #Mission.conditionPush>0) then + + if Mission:IsReadyToPush() then + + --- + -- READY to push yet + --- + + -- Group is currently waiting. + if self:IsWaiting() then + + -- Not waiting any more. + self.Twaiting=nil + self.dTwait=nil + + -- For a flight group, we must cancel the wait/orbit task. + if self:IsFlightgroup() then + + -- Set hold flag to 1. This is a condition in the wait/orbit task. + self.flaghold:Set(1) + + -- Reexecute task in 1 sec to allow to flag to take effect. + --self:__TaskExecute(-1, Task) + + -- Deny transition for now. + --return false + end + end + + else + + --- + -- NOT READY to push yet + --- + + if self:IsWaiting() then + -- Group is already waiting + else + -- Wait indefinitely. + local alt=Mission.missionAltitude and UTILS.MetersToFeet(Mission.missionAltitude) or nil + self:Wait(nil, alt) + end + + -- Time to for the next try. Best guess is when push time is reached or 20 sec when push conditions are not true yet. + local dt=Mission.Tpush and Mission.Tpush-timer.getAbsTime() or 20 + + -- Debug info. + self:T(self.lid..string.format("Mission %s task execute suspended for %d seconds", Mission.name, dt)) + + -- Reexecute task. + self:__TaskExecute(-dt, Task) + + -- Deny transition. + return false + end + + end + + if Mission and Mission.opstransport then + + local delivered=Mission.opstransport:IsCargoDelivered(self.groupname) + + if not delivered then + + local dt=30 + + -- Debug info. + self:T(self.lid..string.format("Mission %s task execute suspended for %d seconds because we were not delivered", Mission.name, dt)) + + -- Reexecute task. + self:__TaskExecute(-dt, Task) + + if (self:IsArmygroup() or self:IsNavygroup()) and self:IsCruising() then + self:FullStop() + end + + -- Deny transition. + return false + end + end + + return true +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) + + -- Debug info. + self:T(self.lid..text) + + -- Debug info. + self:T2({Task}) + + -- 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 + + -- Insert into task queue. Not sure any more, why I added this. But probably if a task is just executed without having been put into the queue. + if self:GetTaskCurrent()==nil then + table.insert(self.taskqueue, Task) + end + + -- Get mission of this task (if any). + local Mission=self:GetMissionByTaskID(self.taskcurrent) + + -- Update push DCS task. + self:_UpdateTask(Task, Mission) + + -- Set AUFTRAG status. + if Mission then + self:MissionExecute(Mission) + end + +end + +--- Update (DCS) task. +-- @param #OPSGROUP self +-- @param Ops.OpsGroup#OPSGROUP.Task Task The task. +-- @param Ops.Auftrag#AUFTRAG Mission The mission. +function OPSGROUP:_UpdateTask(Task, Mission) + + Mission=Mission or self:GetMissionByTaskID(self.taskcurrent) + + if Task.dcstask.id==AUFTRAG.SpecialTask.FORMATION then + + if Mission.type == AUFTRAG.Type.RESCUEHELO then + self:T("**********") + self:T("** RESCUEHELO USED") + self:T("**********") + local param=Task.dcstask.params + local followUnit=UNIT:FindByName(param.unitname) + local helogroupname = self:GetGroup():GetName() + Task.formation = RESCUEHELO:New(followUnit,helogroupname) + Task.formation:SetRespawnOnOff(false) + Task.formation.respawninair=false + Task.formation:SetTakeoffCold() + Task.formation:SetHomeBase(followUnit) + Task.formation.helo = self:GetGroup() + -- Start formation FSM. + Task.formation:Start() + if self:IsFlightgroup() then + self:SetDespawnAfterLanding() + end + else + + -- 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=FORMATION:New(followUnit, followSet, AUFTRAG.SpecialTask.FORMATION) + + -- 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() + + end + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then + + --- + -- Task patrol zone. + --- + + -- Parameters. + local zone=Task.dcstask.params.zone --Core.Zone#ZONE + + local surfacetypes=nil + if self:IsArmygroup() then + surfacetypes={land.SurfaceType.LAND, land.SurfaceType.ROAD} + elseif self:IsNavygroup() then + surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER} + end + + -- Random coordinate in zone. + local Coordinate=zone:GetRandomCoordinate(nil, nil, surfacetypes) + + --Coordinate:MarkToAll("Random Patrol Zone Coordinate") + + -- Speed and altitude. + local Speed=Task.dcstask.params.speed and UTILS.MpsToKnots(Task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) + local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil + + local currUID=self:GetWaypointCurrent().uid + + -- New waypoint. + local wp=nil --#OPSGROUP.Waypoint + if self.isFlightgroup then + wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + elseif self.isArmygroup then + wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Task.dcstask.params.formation) + elseif self.isNavygroup then + wp=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + end + + -- Set mission UID. + wp.missionUID=Mission and Mission.auftragsnummer or nil + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.RECON then + + --- + -- Task recon. + --- + + -- Target + local target=Task.dcstask.params.target --Ops.Target#TARGET + + -- Init a table. + self.reconindecies={} + for i=1,#target.targets do + table.insert(self.reconindecies, i) + end + + local n=1 + if Task.dcstask.params.randomly then + n=UTILS.GetRandomTableElement(self.reconindecies) + else + table.remove(self.reconindecies, n) + end + + -- Target object and zone. + local object=target.targets[n] --Ops.Target#TARGET.Object + local zone=object.Object --Core.Zone#ZONE + + -- Random coordinate in zone. + local Coordinate=zone:GetRandomCoordinate() + + -- Speed and altitude. + local Speed=Task.dcstask.params.speed and UTILS.MpsToKnots(Task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) + local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil + + --Coordinate:MarkToAll("Recon Waypoint Execute") + + local currUID=self:GetWaypointCurrent().uid + + -- New waypoint. + local wp=nil --#OPSGROUP.Waypoint + if self.isFlightgroup then + wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + elseif self.isArmygroup then + wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Task.dcstask.params.formation) + elseif self.isNavygroup then + wp=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + end + + -- Set mission UID. + wp.missionUID=Mission and Mission.auftragsnummer or nil + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.AMMOSUPPLY or Task.dcstask.id==AUFTRAG.SpecialTask.FUELSUPPLY then + + --- + -- Task "Ammo Supply" or "Fuel Supply" mission. + --- + + -- Just stay put and wait until something happens. + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.REARMING then + + --- + -- Task "Rearming" + --- + + -- Check if ammo is full. + + local rearmed=self:_CheckAmmoFull() + + if rearmed then + self:T2(self.lid.."Ammo already full ==> reaming task done!") + self:TaskDone(Task) + else + self:T2(self.lid.."Ammo not full ==> Rearm()") + self:Rearm() + end + + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.ALERT5 then + + --- + -- Task "Alert 5" mission. + --- + + -- Just stay put on the airfield and wait until something happens. + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.ONGUARD or Task.dcstask.id==AUFTRAG.SpecialTask.ARMOREDGUARD then + + --- + -- Task "On Guard" Mission. + --- + + -- Just stay put. + --TODO: Change ALARM STATE + + if self:IsArmygroup() or self:IsNavygroup() then + -- Especially NAVYGROUP needs a full stop as patrol ad infinitum + self:FullStop() + else + -- FLIGHTGROUP not implemented (intended!) for this AUFTRAG type. + end + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.NOTHING then + + --- + -- Task "Nothing" Mission. + --- + + -- Just stay put. + --TODO: Change ALARM STATE + + if self:IsArmygroup() or self:IsNavygroup() then + -- Especially NAVYGROUP needs a full stop as patrol ad infinitum + self:__FullStop(0.1) + else + -- FLIGHTGROUP not implemented (intended!) for this AUFTRAG type. + end + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.AIRDEFENSE or Task.dcstask.id==AUFTRAG.SpecialTask.EWR then + + --- + -- Task "AIRDEFENSE" or "EWR" Mission. + --- + + -- Just stay put. + --TODO: Change ALARM STATE + + if self:IsArmygroup() or self:IsNavygroup() then + self:FullStop() + else + -- FLIGHTGROUP not implemented (intended!) for this AUFTRAG type. + end + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK or Task.dcstask.id==AUFTRAG.SpecialTask.ARMORATTACK then + + --- + -- Task "Ground Attack" Mission. + --- + + -- Engage target. + local target=Task.dcstask.params.target --Ops.Target#TARGET + + -- Set speed. Default max. + local speed=self.speedMax and UTILS.KmphToKnots(self.speedMax) or nil + if Task.dcstask.params.speed then + speed=UTILS.MpsToKnots(Task.dcstask.params.speed) + end + + if target then + self:EngageTarget(target, speed, Task.dcstask.params.formation) + end + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.NAVALENGAGEMENT then + + --- + -- Task "Naval Engagement" Mission. + --- + + -- Engage target. + local target=Task.dcstask.params.target --Ops.Target#TARGET + + -- Set speed. Default max. + local speed=self.speedMax and UTILS.KmphToKnots(self.speedMax) or nil + if Task.dcstask.params.speed then + speed=UTILS.MpsToKnots(Task.dcstask.params.speed) + end + + if target then + self:EngageTarget(target, speed, Task.dcstask.params.altitude) + end + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.PATROLRACETRACK then + + --- + -- Task "Patrol Race Track" Mission. + --- + + if self.isFlightgroup then + self:T("We are Special Auftrag Patrol Race Track, starting now ...") + --self:I({Task.dcstask.params}) + --[[ + Task.dcstask.params.TrackAltitude = self.TrackAltitude + Task.dcstask.params.TrackSpeed = self.TrackSpeed + Task.dcstask.params.TrackPoint1 = self.TrackPoint1 + Task.dcstask.params.TrackPoint2 = self.TrackPoint2 + Task.dcstask.params.TrackFormation = self.TrackFormation + --]] + local aircraft = self:GetGroup() + aircraft:PatrolRaceTrack(Task.dcstask.params.TrackPoint1,Task.dcstask.params.TrackPoint2,Task.dcstask.params.TrackAltitude,Task.dcstask.params.TrackSpeed,Task.dcstask.params.TrackFormation,false,1) + end + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.HOVER then + + --- + -- Task "Hover" Mission. + --- + + if self.isFlightgroup then + self:T("We are Special Auftrag HOVER, hovering now ...") + --self:I({Task.dcstask.params}) + local alt = Task.dcstask.params.hoverAltitude + local time =Task.dcstask.params.hoverTime + local mSpeed = Task.dcstask.params.missionSpeed or self.speedCruise or 150 + local Speed = UTILS.KmphToKnots(mSpeed) + local CruiseAlt = UTILS.FeetToMeters(Task.dcstask.params.missionAltitude or 1000) + local helo = self:GetGroup() + helo:SetSpeed(0.01,true) + helo:SetAltitude(alt,true,"BARO") + self:HoverStart() + local function FlyOn(Helo,Speed,CruiseAlt,Task) + if Helo then + Helo:SetSpeed(Speed,true) + Helo:SetAltitude(CruiseAlt,true,"BARO") + self:T("We are Special Auftrag HOVER, end of hovering now ...") + self:TaskDone(Task) + self:HoverEnd() + end + end + local timer = TIMER:New(FlyOn,helo,Speed,CruiseAlt,Task) + timer:Start(time) + end + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then + + --- + -- Task "RelocateCohort" Mission. + --- + + -- Debug mission. + self:T(self.lid.."Executing task for relocation mission") + + -- The new legion. + local legion=Task.dcstask.params.legion --Ops.Legion#LEGION + + -- Get random coordinate in spawn zone of new legion. + local Coordinate=legion.spawnzone:GetRandomCoordinate() + + -- Get current waypoint ID. + local currUID=self:GetWaypointCurrent().uid + + local wp=nil --#OPSGROUP.Waypoint + if self.isArmygroup then + self:T2(self.lid.."Routing group to spawn zone of new legion") + wp=ARMYGROUP.AddWaypoint(self, Coordinate, UTILS.KmphToKnots(self.speedCruise), currUID, Mission.optionFormation) + elseif self.isFlightgroup then + self:T2(self.lid.."Routing group to intermediate point near new legion") + Coordinate=self:GetCoordinate():GetIntermediateCoordinate(Coordinate, 0.8) + wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, UTILS.KmphToKnots(self.speedCruise), currUID, UTILS.MetersToFeet(self.altitudeCruise)) + elseif self.isNavygroup then + self:T2(self.lid.."Routing group to spawn zone of new legion") + wp=NAVYGROUP.AddWaypoint(self, Coordinate, UTILS.KmphToKnots(self.speedCruise), currUID) + else + + end + + wp.missionUID=Mission and Mission.auftragsnummer or nil + + elseif Task.dcstask.id==AUFTRAG.SpecialTask.CAPTUREZONE then + + --- + -- Task "CaptureZone" Mission. + -- Check if zone was captured or find new target to engage. + --- + + -- Not enganging already. + if self:IsEngaging() then + + -- Group is currently engaging an enemy unit to capture the zone. + self:T2(self.lid..string.format("CaptureZone: Engaging currently!")) + else + + -- Get enemy coalitions. We do not include neutrals. + local Coalitions=UTILS.GetCoalitionEnemy(self:GetCoalition(), false) + + -- Current target object. + local zoneCurr=Task.target --Ops.OpsZone#OPSZONE + + if zoneCurr then + + self:T(self.lid..string.format("Current target zone=%s owner=%s", zoneCurr:GetName(), zoneCurr:GetOwnerName())) + + if zoneCurr:GetOwner()==self:GetCoalition() then + -- Current zone captured ==> Find next zone or call it a day! + + -- Debug info. + self:T(self.lid..string.format("Zone %s captured ==> Task DONE!", zoneCurr:GetName())) + + -- Task done. + if Task.StayInZoneTime then + local stay = Task.StayInZoneTime + self:__TaskDone(stay,Task) + else + self:TaskDone(Task) + end + + else + -- Current zone NOT captured yet ==> Find Target + + -- Debug info. + self:T(self.lid..string.format("Zone %s NOT captured!", zoneCurr:GetName())) + + if Mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.EXECUTING then + + -- Debug info. + self:T(self.lid..string.format("Zone %s NOT captured and EXECUTING ==> Find target", zoneCurr:GetName())) + + + -- Get closest target. + local targetgroup=zoneCurr:GetScannedGroupSet():GetClosestGroup(self.coordinate, Coalitions) + + if targetgroup then + + -- Debug info. + self:T(self.lid..string.format("Zone %s NOT captured: engaging target %s", zoneCurr:GetName(), targetgroup:GetName())) + + -- Engage target group. + self:EngageTarget(targetgroup) + + else + + if self:IsFlightgroup() then + -- Debug info. + self:T(self.lid..string.format("Zone %s not captured but no target group could be found ==> TaskDone as FLIGHTGROUPS cannot capture zones", zoneCurr:GetName())) + + -- Task done. + self:TaskDone(Task) + else + -- Debug info. + self:T(self.lid..string.format("Zone %s not captured but no target group could be found. Should be captured in the next zone evaluation.", zoneCurr:GetName())) + end + + end + + else + self:T(self.lid..string.format("Zone %s NOT captured and NOT EXECUTING", zoneCurr:GetName())) + end + + end + + else + self:T(self.lid..string.format("NO Current target zone=%s")) + end + + end + + else + + -- If task is scheduled (not waypoint) set task. + if Task.type==OPSGROUP.TaskType.SCHEDULED or Task.ismission then + + -- DCS task. + local DCSTask=nil + + -- BARRAGE is special! + if Task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then + --- + -- BARRAGE + + -- Current vec2. + local vec2=self:GetVec2() + + -- Task parameters. + local param=Task.dcstask.params + + -- Set heading and altitude. + local heading=param.heading or math.random(1, 360) + local Altitude=param.altitude or 500 + local Alpha=param.angle or math.random(45, 85) + local distance=Altitude/math.tan(math.rad(Alpha)) + local tvec2=UTILS.Vec2Translate(vec2, distance, heading) + + -- Debug info. + self:T(self.lid..string.format("Barrage: Shots=%s, Altitude=%d m, Angle=%d°, heading=%03d°, distance=%d m", tostring(param.shots), Altitude, Alpha, heading, distance)) + + -- Set fire at point task. + DCSTask=CONTROLLABLE.TaskFireAtPoint(nil, tvec2, param.radius, param.shots, param.weaponType, Altitude) + + elseif Task.ismission and Task.dcstask.id=='FireAtPoint' then + + -- Copy DCS task. + DCSTask=UTILS.DeepCopy(Task.dcstask) + + -- Get current ammo. + local ammo=self:GetAmmoTot() + + -- Number of ammo avail. + local nAmmo=ammo.Total + + local weaponType=DCSTask.params.weaponType or -1 + + -- Adjust max number of ammo for specific weapon types requested. + if weaponType==ENUMS.WeaponFlag.CruiseMissile then + nAmmo=ammo.MissilesCR + elseif weaponType==ENUMS.WeaponFlag.AnyRocket then + nAmmo=ammo.Rockets + elseif weaponType==ENUMS.WeaponFlag.Cannons then + nAmmo=ammo.Cannons + end + + --TODO: Update target location while we're at it anyway. + --TODO: Adjust mission result evaluation time? E.g. cruise missiles can fly a long time depending on target distance. + + -- Number of shots to be fired. + local nShots=DCSTask.params.expendQty or 1 + + -- Debug info. + self:T(self.lid..string.format("Fire at point with nshots=%d of %d", nShots, nAmmo)) + + if nShots==-1 then + -- The -1 is for using all available ammo. + nShots=nAmmo + self:T(self.lid..string.format("Fire at point taking max amount of ammo = %d", nShots)) + elseif nShots<1 then + local p=nShots + nShots=UTILS.Round(p*nAmmo, 0) + self:T(self.lid..string.format("Fire at point taking %.1f percent amount of ammo = %d", p, nShots)) + else + -- Fire nShots but at most nAmmo. + nShots=math.min(nShots, nAmmo) + end + + -- Set quantity of task. + DCSTask.params.expendQty=nShots + + else + --- + -- Take DCS task + --- + DCSTask=Task.dcstask + end + + self:_SandwitchDCSTask(DCSTask, Task) + + elseif Task.type==OPSGROUP.TaskType.WAYPOINT then + -- Waypoint tasks are executed elsewhere! + else + self:T(self.lid.."ERROR: Unknown task type: ") + end + + end + +end + +--- Sandwitch DCS task in stop condition and push the task to the group. +-- @param #OPSGROUP self +-- @param DCS#Task DCSTask The DCS task. +-- @param Ops.OpsGroup#OPSGROUP.Task Task +-- @param #boolean SetTask Set task instead of pushing it. +-- @param #number Delay Delay in seconds. Default nil. +function OPSGROUP:_SandwitchDCSTask(DCSTask, Task, SetTask, Delay) + + if Delay and Delay>0 then + -- Delayed call. + self:ScheduleOnce(Delay, OPSGROUP._SandwitchDCSTask, self, DCSTask, Task, SetTask) + else + + local DCStasks={} + if DCSTask.id=='ComboTask' then + -- Loop over all combo tasks. + for TaskID, Task in ipairs(DCSTask.params.tasks) do + table.insert(DCStasks, Task) + end + else + table.insert(DCStasks, 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. + -- NOTE: I am pushing the task instead of setting it as it seems to keep the mission task alive. + -- There were issues that flights did not proceed to a later waypoint because the task did not finish until the fired missiles + -- impacted (took rather long). Then the flight flew to the nearest airbase and one lost completely the control over the group. + if SetTask then + self:SetTask(TaskFinal) + else + self:PushTask(TaskFinal) + end + + 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==AUFTRAG.SpecialTask.FORMATION then + Task.formation:Stop() + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.RECON then + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.AMMOSUPPLY then + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.FUELSUPPLY then + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.REARMING then + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.ALERT5 then + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.ONGUARD or Task.dcstask.id==AUFTRAG.SpecialTask.ARMOREDGUARD then + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK or Task.dcstask.id==AUFTRAG.SpecialTask.ARMORATTACK or Task.dcstask.id==AUFTRAG.SpecialTask.NAVALENGAGEMENT then + done=true + elseif Task.dcstask.id==AUFTRAG.SpecialTask.NOTHING 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:T(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 + + -- Get mission status of this group. + local status=Mission:GetGroupStatus(self) + + -- Check if mission is paused. + if status~=AUFTRAG.GroupStatus.PAUSED then + --- + -- Mission is NOT over ==> trigger DONE + --- + + if Mission.type==AUFTRAG.Type.CAPTUREZONE and Mission:CountMissionTargets()>0 then + + -- Remove mission waypoints. + self:T(self.lid.."Remove mission waypoints") + self:_RemoveMissionWaypoints(Mission, false) + + if self:IsFlightgroup() then + + -- A flight cannot capture so we assume done. + +-- local opszone=Mission:GetTargetData() --Ops.OpsZone#OPSZONE +-- +-- if opszone then +-- +-- local mycoalition=self:GetCoalition() +-- +-- if mycoalition~=opszone:GetOwner() then +-- local nenemy=0 +-- if mycoalition==coalition.side.BLUE then +-- nenemy=opszone.Nred +-- else +-- nenemy=opszone.Nblu +-- end +-- +-- end +-- +-- end + + else + + self:T(self.lid.."Task done ==> Route to mission for next opszone") + self:MissionStart(Mission) + + return + end + end + + -- Get egress waypoint uid. + local EgressUID=Mission:GetGroupEgressWaypointUID(self) + + if EgressUID then + -- Egress coordinate given ==> wait until we pass that waypoint. + self:T(self.lid..string.format("Task Done but Egress waypoint defined ==> Will call Mission Done once group passed waypoint UID=%d!", EgressUID)) + else + -- Mission done! + self:T(self.lid.."Task Done ==> Mission Done!") + self:MissionDone(Mission) + end + else + --- + -- Mission Paused: Do nothing! Just set the current mission to nil so we can launch a new one. + --- + if self:IsOnMission(Mission.auftragsnummer) then + self.currentmission=nil + end + -- Remove mission waypoints. + self:T(self.lid.."Remove mission waypoints") + self:_RemoveMissionWaypoints(Mission, false) + end + + else + + if Task.description=="Engage_Target" then + self:T(self.lid.."Task DONE Engage_Target ==> Cruise") + self:Disengage() + end + + if Task.description==AUFTRAG.SpecialTask.ONGUARD or Task.description==AUFTRAG.SpecialTask.ARMOREDGUARD or Task.description==AUFTRAG.SpecialTask.NOTHING then + self:T(self.lid.."Task DONE OnGuard ==> Cruise") + self:Cruise() + end + + if Task.description=="Task_Land_At" then + self:T(self.lid.."Taske DONE Task_Land_At ==> Wait") + self:Cruise() + self:Wait(20, 100) + else + self:T(self.lid.."Task Done but NO mission found ==> _CheckGroupDone in 1 sec") + self:_CheckGroupDone(1) + end + + 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 + + -- Increase number of groups. + Mission.Ngroups=Mission.Ngroups+1 + + -- Add mission to queue. + table.insert(self.missionqueue, Mission) + + -- ad infinitum? + self.adinfinitum = Mission.DCStask.params.adinfinitum and Mission.DCStask.params.adinfinitum or false + + -- 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 + for i=#self.missionqueue,1,-1 do + + -- Mission. + local mission=self.missionqueue[i] --Ops.Auftrag#AUFTRAG + + -- Check mission ID. + if mission.auftragsnummer==Mission.auftragsnummer then + + -- Remove mission waypoint task. + local Task=Mission:GetGroupWaypointTask(self) + + if Task then + self:RemoveTask(Task) + end + + -- Take care of a paused mission. + for j=#self.pausedmissions,1,-1 do + local mid=self.pausedmissions[j] + if Mission.auftragsnummer==mid then + table.remove(self.pausedmissions, j) + end + end + + -- Remove mission from queue. + table.remove(self.missionqueue, i) + + return self + end + + end + + return self +end + +--- Cancel all missions in mission queue that are not already done or cancelled. +-- @param #OPSGROUP self +function OPSGROUP:CancelAllMissions() + self:T(self.lid.."Cancelling ALL missions!") + + -- Cancel all missions. + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + + -- Current group status. + local mystatus=mission:GetGroupStatus(self) + + -- Check if mission is already over! + if not (mystatus==AUFTRAG.GroupStatus.DONE or mystatus==AUFTRAG.GroupStatus.CANCELLED) then + --if mission:IsNotOver() then + self:T(self.lid.."Cancelling mission "..tostring(mission:GetName())) + self:MissionCancel(mission) + end + end + +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 + +--- Count remaining cargo transport assignments. +-- @param #OPSGROUP self +-- @return #number Number of unfinished transports in the queue. +function OPSGROUP:CountRemainingTransports() + + local N=0 + + -- Loop over mission queue. + for _,_transport in pairs(self.cargoqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + + local mystatus=transport:GetCarrierTransportStatus(self) + local status=transport:GetState() + + -- Debug info. + self:T(self.lid..string.format("Transport my status=%s [%s]", mystatus, status)) + + -- Count not delivered (executing or scheduled) assignments. + if transport and mystatus==OPSTRANSPORT.Status.SCHEDULED and status~=OPSTRANSPORT.Status.DELIVERED and status~=OPSTRANSPORT.Status.CANCELLED then + N=N+1 + end + end + + -- In case we directly set the cargo transport (not in queue). + if N==0 and self.cargoTransport and + self.cargoTransport:GetState()~=OPSTRANSPORT.Status.DELIVERED and self.cargoTransport:GetCarrierTransportStatus(self)~=OPSTRANSPORT.Status.DELIVERED and + self.cargoTransport:GetState()~=OPSTRANSPORT.Status.CANCELLED and self.cargoTransport:GetCarrierTransportStatus(self)~=OPSTRANSPORT.Status.CANCELLED then + N=1 + end + + return N +end + +--- Get next mission. +-- @param #OPSGROUP self +-- @return Ops.Auftrag#AUFTRAG Next mission or *nil*. +function OPSGROUP:_GetNextMission() + + -- Check if group is acting as carrier or cargo at the moment. + if self:IsPickingup() or self:IsLoading() or self:IsTransporting() or self:IsUnloading() or self:IsLoaded() then + return nil + end + + -- 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.prio3.6 or true then + + self:RouteToMission(Mission, 3) + + else + --- + -- IMMOBILE Group + --- + + -- Debug info. + self:T(self.lid.."Immobile GROUP!") + + -- Add waypoint task. UpdateRoute is called inside. + local Clock=Mission.Tpush and UTILS.SecondsToClock(Mission.Tpush) or 5 + + -- Add mission task. + local Task=self:AddTask(Mission.DCStask, Clock, Mission.name, Mission.prio, Mission.duration) + Task.ismission=true + + -- Set waypoint task. + Mission:SetGroupWaypointTask(self, Task) + + -- Execute task. This calls mission execute. + self:__TaskExecute(3, Task) + end + +end + +--- On after "MissionExecute" event. Mission execution began. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission table. +function OPSGROUP:onafterMissionExecute(From, Event, To, Mission) + + local text=string.format("Executing %s Mission %s, target %s", Mission.type, tostring(Mission.name), Mission:GetTargetName()) + self:T(self.lid..text) + + -- Set group mission status to EXECUTING. + Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.EXECUTING) + + -- Set mission status to EXECUTING. + Mission:Executing() + + -- Group is holding but has waypoints ==> Cruise. + if self:IsHolding() and not self:HasPassedFinalWaypoint() then + self:Cruise() + end + + -- Set auto engage detected targets. + if Mission.engagedetectedOn then + self:SetEngageDetectedOn(UTILS.MetersToNM(Mission.engagedetectedRmax), Mission.engagedetectedTypes, Mission.engagedetectedEngageZones, Mission.engagedetectedNoEngageZones) + end + + -- Set AB usage for mission execution based on Mission entry, if the option was set in the mission + if self.isFlightgroup then + if Mission.prohibitABExecute == true then + self:SetProhibitAfterburner() + self:T(self.lid.."Set prohibit AB") + elseif Mission.prohibitABExecute == false then + self:SetAllowAfterburner() + self:T2(self.lid.."Set allow AB") + end + end + +end + +--- On after "PauseMission" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterPauseMission(From, Event, To) + + local Mission=self:GetMissionCurrent() + + if Mission then + + -- Set group mission status to PAUSED. + Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.PAUSED) + + -- Get mission waypoint task. + local Task=Mission:GetGroupWaypointTask(self) + + -- Debug message. + self:T(self.lid..string.format("Pausing current mission %s. Task=%s", tostring(Mission.name), tostring(Task and Task.description or "WTF"))) + + -- Cancelling the mission is actually cancelling the current task. + self:TaskCancel(Task) + + self:_RemoveMissionWaypoints(Mission) + + -- Set mission to pause so we can unpause it later. + table.insert(self.pausedmissions, 1, Mission.auftragsnummer) + + end + +end + +--- On after "UnpauseMission" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterUnpauseMission(From, Event, To) + + -- Get paused mission. + local mission=self:_GetPausedMission() + + if mission then + + -- Debug info. + self:T(self.lid..string.format("Unpausing mission %s [%s]", mission:GetName(), mission:GetType())) + + -- Set state of mission, e.g. for not teleporting again + mission.unpaused=true + + -- Start mission. + self:MissionStart(mission) + + -- Remove mission from pausedmissions queue + for i,mid in pairs(self.pausedmissions) do + --self:T(self.lid..string.format("Checking paused mission", mid)) + if mid==mission.auftragsnummer then + self:T(self.lid..string.format("Removing paused mission id=%d", mid)) + table.remove(self.pausedmissions, i) + break + end + end + + else + self:T(self.lid.."ERROR: No mission to unpause!") + end + +end + + +--- On after "MissionCancel" event. Cancels the mission. +-- @param #OPSGROUP 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 OPSGROUP:onafterMissionCancel(From, Event, To, Mission) + + if self:IsOnMission(Mission.auftragsnummer) then + + --- + -- Current Mission + --- + + -- Some missions dont have a task set, which could be cancelled. + --[[ + if Mission.type==AUFTRAG.Type.ALERT5 or + Mission.type==AUFTRAG.Type.ONGUARD or + Mission.type==AUFTRAG.Type.ARMOREDGUARD or + --Mission.type==AUFTRAG.Type.NOTHING or + Mission.type==AUFTRAG.Type.AIRDEFENSE or + Mission.type==AUFTRAG.Type.EWR then + + -- Trigger mission don task. + self:MissionDone(Mission) + + return + end + ]] + + -- Get mission waypoint task. + local Task=Mission:GetGroupWaypointTask(self) + + if Task then + + -- Debug info. + self:T(self.lid..string.format("Cancel current mission %s. Task=%s", tostring(Mission.name), tostring(Task and Task.description or "WTF"))) + + -- Cancelling the mission is actually cancelling the current task. + -- Note that two things can happen. + -- 1.) Group is still on the way to the waypoint (status should be STARTED). In this case there would not be a current task! + -- 2.) Group already passed the mission waypoint (status should be EXECUTING). + + self:TaskCancel(Task) + + else + + -- Some missions dont have a task set, which could be cancelled. + + -- Trigger mission don task. + self:MissionDone(Mission) + + end + + else + + --- + -- NOT the current mission + --- + + -- Set mission group status. + Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.CANCELLED) + + -- Remove mission from queue + self:RemoveMission(Mission) + + -- Send group RTB or WAIT if nothing left to do. + self:_CheckGroupDone(1) + + end + +end + +--- On after "MissionDone" event. +-- @param #OPSGROUP self +-- @param Ops.Auftrag#AUFTRAG Mission +-- @param #boolean Silently Remove waypoints by `table.remove()` and do not update the route. +function OPSGROUP:_RemoveMissionWaypoints(Mission, Silently) + + for i=#self.waypoints,1,-1 do + local wp=self.waypoints[i] --#OPSGROUP.Waypoint + if wp.missionUID==Mission.auftragsnummer then + if Silently then + table.remove(self.waypoints, i) + else + self:RemoveWaypoint(i) + end + end + end + +end + +--- On after "MissionDone" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.Auftrag#AUFTRAG Mission The mission that is done. +function OPSGROUP:onafterMissionDone(From, Event, To, Mission) + + -- Debug info. + local text=string.format("Mission DONE %s!", Mission.name) + self:T(self.lid..text) + + -- Set group status. + Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.DONE) + + -- Set current mission to nil. + if self:IsOnMission(Mission.auftragsnummer) then + self.currentmission=nil + end + + -- Remove mission waypoints. + self:_RemoveMissionWaypoints(Mission) + + -- Decrease patrol data. + if Mission.patroldata then + Mission.patroldata.noccupied=Mission.patroldata.noccupied-1 + AIRWING.UpdatePatrolPointMarker(self,Mission.patroldata) + end + + -- Switch auto engage detected off. This IGNORES that engage detected had been activated for the group! + if Mission.engagedetectedOn then + self:SetEngageDetectedOff() + end + + -- ROE to default. + if Mission.optionROE then + self:SwitchROE() + end + -- ROT to default + if self:IsFlightgroup() and Mission.optionROT then + self:SwitchROT() + end + -- Alarm state to default. + if Mission.optionAlarm then + self:SwitchAlarmstate() + end + -- EPLRS to default. + if Mission.optionEPLRS then + self:SwitchEPLRS() + end + -- Emission to default. + if Mission.optionEmission then + self:SwitchEmission() + end + -- Invisible to default. + if Mission.optionInvisible then + self:SwitchInvisible() + end + -- Immortal to default. + if Mission.optionImmortal then + self:SwitchImmortal() + end + -- Formation to default. + if Mission.optionFormation and self:IsFlightgroup() then + self:SwitchFormation() + end + -- Radio freq and modu to default. + if Mission.radio then + self:SwitchRadio() + end + + -- TACAN beacon. + if Mission.tacan then + + -- Switch to default. + self:_SwitchTACAN() + + -- Return Cohort's TACAN channel. + local cohort=self.cohort --Ops.Cohort#COHORT + if cohort then + cohort:ReturnTacan(Mission.tacan.Channel) + end + + -- Set asset TACAN to nil. + local asset=Mission:GetAssetByName(self.groupname) + if asset then + asset.tacan=nil + end + end + + -- ICLS beacon to default. + if Mission.icls then + self:_SwitchICLS() + end + + -- Return to legion? + if self.legion and Mission.legionReturn~=nil then + self:SetReturnToLegion(Mission.legionReturn) + end + + -- Delay before check if group is done. + local delay=1 + + -- Special mission cases. + if Mission.type==AUFTRAG.Type.ARTY then + -- We add a 10 sec delay for ARTY. Found that they need some time to readjust the barrel of their gun. Not sure if necessary for all. Needs some more testing! + delay=60 + elseif Mission.type==AUFTRAG.Type.RELOCATECOHORT then + + -- New legion. + local legion=Mission.DCStask.params.legion --Ops.Legion#LEGION + + -- Debug message. + self:T(self.lid..string.format("Asset relocated to new legion=%s",tostring(legion.alias))) + + -- Get asset and change its warehouse id. + local asset=Mission:GetAssetByName(self.groupname) + if asset then + asset.wid=legion.uid + end + + -- Set new legion. + self.legion=legion + + if self.isArmygroup then + self:T2(self.lid.."Adding asset via ReturnToLegion()") + self:ReturnToLegion() + elseif self.isFlightgroup then + self:T2(self.lid.."Adding asset via RTB to new legion airbase") + self:RTB(self.legion.airbase) + end + + return + end + + -- Set AB usage based on Mission entry, if the option was set in the mission + if self.isFlightgroup then + if Mission.prohibitAB == true then + self:T2("Setting prohibit AB") + self:SetProhibitAfterburner() + elseif Mission.prohibitAB == false then + self:T2("Setting allow AB") + self:SetAllowAfterburner() + end + end + + if self.legion and self.legionReturn==false and self.waypoints and #self.waypoints==1 then + --- + -- This is the case where a group was send on a mission (which is over now), has no addional + -- waypoints or tasks and should NOT return to its legion. + -- We create a new waypoint at the current position and let it hold here. + --- + + local Coordinate=self:GetCoordinate() + + if self.isArmygroup then + ARMYGROUP.AddWaypoint(self, Coordinate, 0, nil, nil, false) + elseif self.isNavygroup then + NAVYGROUP.AddWaypoint(self,Coordinate, 0, nil, nil, false) + end + + -- Remove original waypoint. + self:RemoveWaypoint(1) + + self:_PassedFinalWaypoint(true, "Passed final waypoint as group is done with mission but should NOT return to its legion") + end + + -- Check if group is done. + self:_CheckGroupDone(delay) + +end + +--- Route group to mission. +-- @param #OPSGROUP self +-- @param Ops.Auftrag#AUFTRAG mission The mission table. +-- @param #number delay Delay in seconds. +function OPSGROUP:RouteToMission(mission, delay) + + if delay and delay>0 then + -- Delayed call. + self:ScheduleOnce(delay, OPSGROUP.RouteToMission, self, mission) + else + + -- Debug info. + self:T(self.lid..string.format("Route To Mission")) + + -- Catch dead or stopped groups. + if self:IsDead() or self:IsStopped() then + self:T(self.lid..string.format("Route To Mission: I am DEAD or STOPPED! Ooops...")) + return + end + + -- Check if this group is cargo. + if self:IsCargo() then + self:T(self.lid..string.format("Route To Mission: I am CARGO! You cannot route me...")) + return + end + + -- OPSTRANSPORT: Just add the ops transport to the queue. + if mission.type==AUFTRAG.Type.OPSTRANSPORT then + self:T(self.lid..string.format("Route To Mission: I am OPSTRANSPORT! Add transport and return...")) + self:AddOpsTransport(mission.opstransport) + return + end + + -- ALERT5: Just set the mission to executing. + if mission.type==AUFTRAG.Type.ALERT5 then + self:T(self.lid..string.format("Route To Mission: I am ALERT5! Go right to MissionExecute()...")) + self:MissionExecute(mission) + return + end + + -- ID of current waypoint. + local uid=self:GetWaypointCurrentUID() + + -- Ingress waypoint coordinate where the mission is executed. + local waypointcoord=nil --Core.Point#COORDINATE + + -- Current coordinate of the group. + local currentcoord=self:GetCoordinate() + + -- Road connection. + local roadcoord=currentcoord:GetClosestPointToRoad() + + local roaddist=nil + if roadcoord then + roaddist=currentcoord:Get2DDistance(roadcoord) + end + + -- Target zone. + local targetzone=nil --Core.Zone#ZONE + + -- Random radius of 1000 meters. + local randomradius=mission.missionWaypointRadius or 1000 + + -- Surface types. + local surfacetypes=nil + if self:IsArmygroup() then + surfacetypes={land.SurfaceType.LAND, land.SurfaceType.ROAD} + elseif self:IsNavygroup() then + surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER} + end + + -- Get target object. + local targetobject=mission:GetObjective(currentcoord, UTILS.GetCoalitionEnemy(self:GetCoalition(), true)) + + if targetobject then + self:T(self.lid..string.format("Route to mission target object %s", targetobject:GetName())) + end + + -- Get ingress waypoint. + if mission.opstransport and not mission.opstransport:IsCargoDelivered(self.groupname) then + + -- Get transport zone combo. + local tzc=mission.opstransport:GetTZCofCargo(self.groupname) + + local pickupzone=tzc.PickupZone + + if self:IsInZone(pickupzone) then + -- We are already in the pickup zone. + self:PauseMission() + self:FullStop() + return + else + -- Get a random coordinate inside the pickup zone. + waypointcoord=pickupzone:GetRandomCoordinate() + end + + elseif mission.type==AUFTRAG.Type.PATROLZONE or + mission.type==AUFTRAG.Type.BARRAGE or + mission.type==AUFTRAG.Type.AMMOSUPPLY or + mission.type==AUFTRAG.Type.FUELSUPPLY or + mission.type==AUFTRAG.Type.REARMING or + mission.type==AUFTRAG.Type.AIRDEFENSE or + mission.type==AUFTRAG.Type.EWR then + --- + -- Missions with ZONE as target + --- + + -- Get the zone. + targetzone=targetobject --Core.Zone#ZONE + + -- Random coordinate. + waypointcoord=targetzone:GetRandomCoordinate(nil , nil, surfacetypes) + + elseif mission.type==AUFTRAG.Type.ONGUARD or mission.type==AUFTRAG.Type.ARMOREDGUARD then + --- + -- Guard + --- + + -- Mission waypoint + waypointcoord=mission:GetMissionWaypointCoord(self.group, nil, surfacetypes) + + elseif mission.type==AUFTRAG.Type.NOTHING then + --- + -- Nothing + --- + + -- Get the zone. + targetzone=targetobject --Core.Zone#ZONE + + -- Random coordinate. + waypointcoord=targetzone:GetRandomCoordinate(nil , nil, surfacetypes) + + elseif mission.type==AUFTRAG.Type.HOVER then + --- + -- Hover + --- + + local zone=targetobject --Core.Zone#ZONE + + waypointcoord=zone:GetCoordinate() + + elseif mission.type==AUFTRAG.Type.RELOCATECOHORT then + --- + -- Relocation + --- + + -- Roughly go to the new legion. + local ToCoordinate=mission.DCStask.params.legion:GetCoordinate() + + if self.isFlightgroup then + -- Get mission waypoint coord in direction of the + waypointcoord=currentcoord:GetIntermediateCoordinate(ToCoordinate, 0.2):SetAltitude(self.altitudeCruise) + elseif self.isArmygroup then + -- Army group: check for road connection. + if roadcoord then + waypointcoord=roadcoord + else + waypointcoord=currentcoord:GetIntermediateCoordinate(ToCoordinate, 100) + end + else + -- Navy group: Route into direction of the target. + waypointcoord=currentcoord:GetIntermediateCoordinate(ToCoordinate, 0.05) + end + + elseif mission.type==AUFTRAG.Type.CAPTUREZONE then + + -- Get the zone. + targetzone=targetobject:GetZone() + + -- Random coordinate. + waypointcoord=targetzone:GetRandomCoordinate(nil , nil, surfacetypes) + + else + --- + -- Default case + --- + + waypointcoord=mission:GetMissionWaypointCoord(self.group, randomradius, surfacetypes) + end + + -- Add enroute tasks. + for _,task in pairs(mission.enrouteTasks) do + self:AddTaskEnroute(task) + end + + -- Speed to mission waypoint. + local SpeedToMission=mission.missionSpeed and UTILS.KmphToKnots(mission.missionSpeed) or self:GetSpeedCruise() + + -- Special for Troop transport. + if mission.type==AUFTRAG.Type.TROOPTRANSPORT then + + --- + -- TROOP TRANSPORT + --- + + -- Refresh DCS task with the known controllable. + mission.DCStask=mission:GetDCSMissionTask(self.group) + + -- Create a pickup zone around the pickup coordinate. The troops will go to a random point inside the zone. + -- This is necessary so the helos do not try to land at the exact same location where the troops wait. + local pradius=mission.transportPickupRadius + local pickupZone=ZONE_RADIUS:New("Pickup Zone", mission.transportPickup:GetVec2(), pradius) + + -- 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 + -- Get random coordinate inside the zone. + local pcoord=pickupZone:GetRandomCoordinate(20, pradius, {land.SurfaceType.LAND, land.SurfaceType.ROAD}) + + -- Let the troops embark the transport. + local DCSTask=group:TaskEmbarkToTransport(pcoord, pradius) + group:SetTask(DCSTask, 5) + end + + end + + elseif mission.type==AUFTRAG.Type.ARTY then + + --- + -- ARTY + --- + + -- Target Coord. + local targetcoord=mission:GetTargetCoordinate() + + -- In range already? + local inRange=self:InWeaponRange(targetcoord, mission.engageWeaponType, waypointcoord) + + if inRange then + + --waypointcoord=self:GetCoordinate(true) + + else + + local coordInRange=self:GetCoordinateInRange(targetcoord, mission.engageWeaponType, waypointcoord, surfacetypes) + + if coordInRange then + + -- Add waypoint at + local waypoint=nil --#OPSGROUP.Waypoint + if self:IsFlightgroup() then + waypoint=FLIGHTGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise), false) + elseif self:IsArmygroup() then + waypoint=ARMYGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, mission.optionFormation, false) + elseif self:IsNavygroup() then + waypoint=NAVYGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise), false) + end + waypoint.missionUID=mission.auftragsnummer + + -- Set waypoint coord to be the one in range. Take care of proper waypoint uid. + waypointcoord=coordInRange + uid=waypoint.uid + + end + + end + + end + + + -- Distance to waypoint coordinate. + local d=currentcoord:Get2DDistance(waypointcoord) + + -- Debug info. + self:T(self.lid..string.format("Distance to ingress waypoint=%.1f m", d)) + + -- Add mission execution (ingress) waypoint. + local waypoint=nil --#OPSGROUP.Waypoint + if self:IsFlightgroup() then + + + local ingresscoord = mission:GetMissionIngressCoord() + local holdingcoord = mission:GetMissionHoldingCoord() + + if holdingcoord then + waypoint=FLIGHTGROUP.AddWaypoint(self, holdingcoord, mission.missionHoldingCoordSpeed or SpeedToMission, uid, UTILS.MetersToFeet(mission.missionHoldingCoordAlt or self.altitudeCruise), false) + uid=waypoint.uid + -- Orbit until flaghold=1 (true) but max 5 min + self.flaghold:Set(0) + local TaskOrbit = self.group:TaskOrbit(holdingcoord, mission.missionHoldingCoordAlt, UTILS.KnotsToMps(mission.missionHoldingCoordSpeed or SpeedToMission)) + local TaskStop = self.group:TaskCondition(nil, self.flaghold.UserFlagName, 1, nil, mission.missionHoldingDuration or 900) + local TaskCntr = self.group:TaskControlled(TaskOrbit, TaskStop) + local TaskOver = self.group:TaskFunction("FLIGHTGROUP._FinishedWaiting", self) + local DCSTasks=self.group:TaskCombo({TaskCntr, TaskOver}) + -- Add waypoint task. UpdateRoute is called inside. + local waypointtask=self:AddTaskWaypoint(DCSTasks, waypoint, "Holding") + waypointtask.ismission=false + self.isHoldingAtHoldingPoint = true + end + + if ingresscoord then + waypoint=FLIGHTGROUP.AddWaypoint(self, ingresscoord, mission.missionIngressCoordSpeed or SpeedToMission, uid, UTILS.MetersToFeet(mission.missionIngressCoordAlt or self.altitudeCruise), false) + uid=waypoint.uid + end + + waypoint=FLIGHTGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise), false) + + elseif self:IsArmygroup() then + + -- Set formation. + local formation=mission.optionFormation + + -- If distance is < 1 km or RELOCATECOHORT mission, go off-road. + if d<1000 or mission.type==AUFTRAG.Type.RELOCATECOHORT then + formation=ENUMS.Formation.Vehicle.OffRoad + end + + waypoint=ARMYGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, formation, false) + + elseif self:IsNavygroup() then + + waypoint=NAVYGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise), false) + + end + waypoint.missionUID=mission.auftragsnummer + + -- Add waypoint task. UpdateRoute is called inside. + local waypointtask=self:AddTaskWaypoint(mission.DCStask, waypoint, mission.name, mission.prio, mission.duration) + waypointtask.ismission=true + + waypointtask.target=targetobject + + -- Set waypoint task. + mission:SetGroupWaypointTask(self, waypointtask) + + -- Set waypoint index. + mission:SetGroupWaypointIndex(self, waypoint.uid) + + -- Add egress waypoint. + local egresscoord=mission:GetMissionEgressCoord() + if egresscoord then + local Ewaypoint=nil --#OPSGROUP.Waypoint + if self:IsFlightgroup() then + Ewaypoint=FLIGHTGROUP.AddWaypoint(self, egresscoord, mission.missionEgressCoordSpeed or SpeedToMission, waypoint.uid, UTILS.MetersToFeet(mission.missionEgressCoordAlt or self.altitudeCruise), false) + elseif self:IsArmygroup() then + Ewaypoint=ARMYGROUP.AddWaypoint(self, egresscoord, SpeedToMission, waypoint.uid, mission.optionFormation, false) + elseif self:IsNavygroup() then + Ewaypoint=NAVYGROUP.AddWaypoint(self, egresscoord, SpeedToMission, waypoint.uid, UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise), false) + end + Ewaypoint.missionUID=mission.auftragsnummer + mission:SetGroupEgressWaypointUID(self, Ewaypoint.uid) + end + + -- Check if we are already where we want to be. + if targetzone and self:IsInZone(targetzone) then + self:T(self.lid.."Already in mission zone ==> TaskExecute()") + self:TaskExecute(waypointtask) + -- TODO: Calling PassingWaypoint here is probably better as it marks the mission waypoint as passed! + self:PassingWaypoint(waypoint) + return + elseif d<25 then + self:T(self.lid.."Already within 25 meters of mission waypoint ==> TaskExecute()") + self:TaskExecute(waypointtask) + self:PassingWaypoint(waypoint) + return + end + + -- Check if group is mobile. Note that some immobile units report a speed of 1 m/s = 3.6 km/h. + if (self.speedMax<=3.6 or mission.teleport) and not mission.unpaused then + + -- Teleport to waypoint coordinate. Mission will not be paused. + self:Teleport(waypointcoord, nil, true) + + -- Execute task in one second. + self:__TaskExecute(-1, waypointtask) + + else + + -- Give cruise command/update route. + if self:IsArmygroup() then + self:Cruise(SpeedToMission) + elseif self:IsNavygroup() then + self:Cruise(SpeedToMission) + elseif self:IsFlightgroup() then + self:UpdateRoute() + end + + end + + --- + -- Mission Specific Settings + --- + self:_SetMissionOptions(mission) + + end +end + +--- Set mission specific options for ROE, Alarm state, etc. +-- @param #OPSGROUP self +-- @param Ops.Auftrag#AUFTRAG mission The mission table. +function OPSGROUP:_SetMissionOptions(mission) + + -- ROE + if mission.optionROE then + self:SwitchROE(mission.optionROE) + end + -- ROT + if mission.optionROT then + self:SwitchROT(mission.optionROT) + end + -- Alarm state + if mission.optionAlarm then + self:SwitchAlarmstate(mission.optionAlarm) + end + -- EPLRS + if mission.optionEPLRS then + self:SwitchEPLRS(mission.optionEPLRS) + end + -- Emission + if mission.optionEmission then + self:SwitchEmission(mission.optionEmission) + end + -- Invisible + if mission.optionInvisible then + self:SwitchInvisible(mission.optionInvisible) + end + -- Immortal + if mission.optionImmortal then + self:SwitchImmortal(mission.optionImmortal) + end + -- Formation + if mission.optionFormation and self:IsFlightgroup() then + self:SwitchFormation(mission.optionFormation) + end + -- Radio frequency and modulation. + if mission.radio then + self:SwitchRadio(mission.radio.Freq, mission.radio.Modu) + end + -- TACAN settings. + if mission.tacan then + self:SwitchTACAN(mission.tacan.Channel, mission.tacan.Morse, mission.tacan.BeaconName, mission.tacan.Band) + end + -- ICLS settings. + if mission.icls then + self:SwitchICLS(mission.icls.Channel, mission.icls.Morse, mission.icls.UnitName) + end + + -- Set AB usage based on Mission entry, if the option was set in the mission + if self.isFlightgroup then + if mission.prohibitAB == true then + self:SetProhibitAfterburner() + self:T2("Set prohibit AB") + elseif mission.prohibitAB == false then + self:SetAllowAfterburner() + self:T2("Set allow AB") + end + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Queue Update: Missions & Tasks +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "QueueUpdate" event. +-- @param #OPSGROUP self +function OPSGROUP:_QueueUpdate() + + --- + -- Mission + --- + + -- First check if group is alive? Late activated groups are activated and uncontrolled units are started automatically. + if self:IsExist() then + + local mission=self:_GetNextMission() + + if mission then + + local currentmission=self:GetMissionCurrent() + + if currentmission then + + -- Current mission but new mission is urgent with higher prio. + if mission.urgent and mission.prio0 then + self:T(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 30 sec!")) + Tsuspend=-30 + allowed=false + end + + -- Check for a current transport assignment. + if self.cargoTransport then + self:T(self.lid..string.format("WARNING: Got current TRANSPORT assignment ==> WAIT event is suspended for 30 sec!")) + Tsuspend=-30 + allowed=false + end + + -- Call wait again. + if Tsuspend and not allowed then + self:__Wait(Tsuspend, Duration) + end + + return allowed +end + +--- On after "Wait" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Duration Duration in seconds how long the group will be waiting. Default `nil` (for ever). +function OPSGROUP:onafterWait(From, Event, To, Duration) + + -- Order Group to hold. + self:FullStop() + + -- Set time stamp. + self.Twaiting=timer.getAbsTime() + + -- Max waiting + self.dTwait=Duration + +end + + +--- On after "PassingWaypoint" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP.Waypoint Waypoint Waypoint data passed. +function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint) + + -- Get the current task. + local task=self:GetTaskCurrent() + + -- Get the corresponding mission. + local mission=nil --Ops.Auftrag#AUFTRAG + if task then + mission=self:GetMissionByTaskID(task.id) + end + + if task and task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then + + --- + -- SPECIAL TASK: Patrol Zone + --- + + -- Remove old waypoint. + self:RemoveWaypointByID(Waypoint.uid) + + -- Zone object. + local zone=task.dcstask.params.zone --Core.Zone#ZONE + + -- Surface types. + local surfacetypes=nil + if self:IsArmygroup() then + surfacetypes={land.SurfaceType.LAND, land.SurfaceType.ROAD} + elseif self:IsNavygroup() then + surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER} + end + + -- Random coordinate in zone. + local Coordinate=zone:GetRandomCoordinate(nil, nil, surfacetypes) + + -- Speed and altitude. + local Speed=task.dcstask.params.speed and UTILS.MpsToKnots(task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) + local Altitude=UTILS.MetersToFeet(task.dcstask.params.altitude or self.altitudeCruise) + + local currUID=self:GetWaypointCurrent().uid + + local wp=nil --#OPSGROUP.Waypoint + if self.isFlightgroup then + wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + elseif self.isArmygroup then + wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, task.dcstask.params.formation) + elseif self.isNavygroup then + wp=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + end + wp.missionUID=mission and mission.auftragsnummer or nil + + elseif task and task.dcstask.id==AUFTRAG.SpecialTask.RECON then + + --- + -- SPECIAL TASK: Recon Mission + --- + + -- TARGET. + local target=task.dcstask.params.target --Ops.Target#TARGET + + -- Init a table. + if self.adinfinitum and #self.reconindecies==0 then -- all targets done once + self.reconindecies={} + for i=1,#target.targets do + table.insert(self.reconindecies, i) + end + end + + if #self.reconindecies>0 then + + local n=1 + if task.dcstask.params.randomly then + n=UTILS.GetRandomTableElement(self.reconindecies) + else + n=self.reconindecies[1] + table.remove(self.reconindecies, 1) + end + + -- Zone object. + local object=target.targets[n] --Ops.Target#TARGET.Object + local zone=object.Object --Core.Zone#ZONE + + -- Random coordinate in zone. + local Coordinate=zone:GetRandomCoordinate() + + -- Speed and altitude. + local Speed=task.dcstask.params.speed and UTILS.MpsToKnots(task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise) + local Altitude=task.dcstask.params.altitude and UTILS.MetersToFeet(task.dcstask.params.altitude) or nil + + -- Debug. + --Coordinate:MarkToAll("Recon Waypoint n="..tostring(n)) + + local currUID=self:GetWaypointCurrent().uid + + local wp=nil --#OPSGROUP.Waypoint + if self.isFlightgroup then + wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + elseif self.isArmygroup then + wp=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, task.dcstask.params.formation) + elseif self.isNavygroup then + wp=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude) + end + wp.missionUID=mission and mission.auftragsnummer or nil + + else + + -- Get waypoint index. + local wpindex=self:GetWaypointIndex(Waypoint.uid) + + -- Final waypoint reached? + if wpindex==nil or wpindex==#self.waypoints then + + -- Set switch to true. + if not self.adinfinitum or #self.waypoints<=1 then + self:_PassedFinalWaypoint(true, "Passing waypoint and NOT adinfinitum and #self.waypoints<=1") + end + + end + + -- Final zone reached ==> task done. + self:TaskDone(task) + + end + + elseif task and task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then + + --- + -- SPECIAL TASK: Relocate Mission + --- + + -- TARGET. + local legion=task.dcstask.params.legion --Ops.Legion#LEGION + + self:T(self.lid..string.format("Asset arrived at relocation task waypoint ==> Task Done!")) + + -- Final zone reached ==> task done. + self:TaskDone(task) + + elseif task and task.dcstask.id==AUFTRAG.SpecialTask.REARMING then + + --- + -- SPECIAL TASK: Rearming Mission + --- + + -- Debug info. + self:T(self.lid..string.format("FF Rearming Mission ==> Rearm()")) + + -- Call rearm event. + self:Rearm() + + else + + --- + -- No special task active + --- + + -- Apply tasks of this waypoint. + local ntasks=self:_SetWaypointTasks(Waypoint) + + -- Get waypoint index. + local wpindex=self:GetWaypointIndex(Waypoint.uid) + + -- Final waypoint reached? + if wpindex==nil or wpindex==#self.waypoints then + + -- Ad infinitum and not mission waypoint? + if self.adinfinitum then + --- + -- Ad Infinitum + --- + + if Waypoint.missionUID then + --- + -- Last waypoint was a mission waypoint ==> Do nothing (when mission is over, it should take care of this) + --- + else + + --- + -- Last waypoint reached. + --- + + if #self.waypoints<=1 then + -- Only one waypoint. Ad infinitum does not really make sense. However, another waypoint could be added later... + self:_PassedFinalWaypoint(true, "PassingWaypoint: adinfinitum but only ONE WAYPOINT left") + else + + --[[ Solved now! + + -- Looks like the passing waypoint function is triggered over and over again if the group is near the final waypoint. + -- So the only good solution is to guide the group away from that waypoint and then update the route. + + -- Get first waypoint. + local wp1=self:GetWaypointByIndex(1) + + -- Get a waypoint + local Coordinate=Waypoint.coordinate:GetIntermediateCoordinate(wp1.coordinate, 0.1) + + local formation=nil + if self.isArmygroup then + formation=ENUMS.Formation.Vehicle.OffRoad + end + + self:Detour(Coordinate, self.speedCruise, formation, true) + + ]] + + + -- Send + self:__UpdateRoute(-0.01, 1, 1) + + end + end + else + --- + -- NOT Ad Infinitum + --- + + -- Final waypoint reached. + self:_PassedFinalWaypoint(true, "PassingWaypoint: wpindex=#self.waypoints (or wpindex=nil)") + end + + elseif wpindex==1 then + + -- Ad infinitum and not mission waypoint? + if self.adinfinitum then + --- + -- Ad Infinitum + --- + + if #self.waypoints<=1 then + -- Only one waypoint. Ad infinitum does not really make sense. However, another waypoint could be added later... + self:_PassedFinalWaypoint(true, "PassingWaypoint: adinfinitum but only ONE WAYPOINT left") + + else + + if not Waypoint.missionUID then + -- Redo the route until the end. + self:__UpdateRoute(-0.01, 2) + end + end + end + + end + + -- Passing mission waypoint? + local isEgress=false + if Waypoint.missionUID then + + -- Debug info. + self:T2(self.lid..string.format("Passing mission waypoint UID=%s", tostring(Waypoint.uid))) + + -- Get the mission. + local mission=self:GetMissionByID(Waypoint.missionUID) + + -- Check if this was an Egress waypoint of the mission. If so, call Mission Done! This will call CheckGroupDone. + local EgressUID=mission and mission:GetGroupEgressWaypointUID(self) or nil + isEgress=EgressUID and Waypoint.uid==EgressUID + if isEgress and mission:GetGroupStatus(self)~=AUFTRAG.GroupStatus.DONE then + self:MissionDone(mission) + end + end + + -- Check if all tasks/mission are done? + -- Note, we delay it for a second to let the OnAfterPassingwaypoint function to be executed in case someone wants to add another waypoint there. + if ntasks==0 and self:HasPassedFinalWaypoint() and not isEgress then + self:_CheckGroupDone(0.01) + end + + -- Debug info. + local text=string.format("Group passed waypoint %s/%d ID=%d: final=%s detour=%s astar=%s", + tostring(wpindex), #self.waypoints, Waypoint.uid, tostring(self.passedfinalwp), tostring(Waypoint.detour), tostring(Waypoint.astar)) + self:T(self.lid..text) + + end + + -- Set expected speed. + local wpnext=self:GetWaypointNext() + if wpnext then + self.speedWp=wpnext.speed + self:T(self.lid..string.format("Expected/waypoint speed=%.1f m/s", self.speedWp)) + end + +end + +--- Set tasks at this waypoint +-- @param #OPSGROUP self +-- @param #OPSGROUP.Waypoint Waypoint The waypoint. +-- @return #number Number of tasks. +function OPSGROUP:_SetWaypointTasks(Waypoint) + + -- Get all waypoint tasks. + local tasks=self:GetTasksWaypoint(Waypoint.uid) + + -- Debug info. + local text=string.format("WP uid=%d tasks:", Waypoint.uid) + local missiontask=nil --Ops.OpsGroup#OPSGROUP.Task + if #tasks>0 then + for i,_task in pairs(tasks) do + local task=_task --#OPSGROUP.Task + text=text..string.format("\n[%d] %s", i, task.description) + if task.ismission then + missiontask=task + end + end + else + text=text.." None" + end + self:T(self.lid..text) + + -- Check if there is mission task + if missiontask then + self:T(self.lid.."Executing mission task") + local mission=self:GetMissionByTaskID(missiontask.id) + if mission then + if mission.opstransport and not mission.opstransport:IsCargoDelivered(self.groupname) then + self:PauseMission() + return + end + end + self:TaskExecute(missiontask) + return 1 + end + + -- TODO: maybe set waypoint enroute tasks? + + -- Tasks at this waypoints. + local taskswp={} + + 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 "PassedFinalWaypoint" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterPassedFinalWaypoint(From, Event, To) + self:T(self.lid..string.format("Group passed final waypoint")) + + -- Check if group is done? No tasks mission running. + --self:_CheckGroupDone() + +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. +-- @param #number Speed (Optional) Speed to waypoint in knots. +function OPSGROUP:onafterGotoWaypoint(From, Event, To, UID, Speed) + + local n=self:GetWaypointIndex(UID) + + if n then + + -- Speed to waypoint. + Speed=Speed or self:GetSpeedToWaypoint(n) + + -- Debug message + self:T(self.lid..string.format("Goto Waypoint UID=%d index=%d from %d at speed %.1f knots", UID, n, self.currentwp, Speed)) + + -- Update the route. + self:__UpdateRoute(0.1, n, nil, 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:T(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=2 --2m for ARMYGROUP, else there might be no LOS + if self.isFlightgroup or self.isNavygroup 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:T({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:T(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:T(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) + + -- No of sight. + self.spot.LOS=false + + -- Lost line of sight. + self.spot.lostLOS=true + + if self.spot.On then + + -- 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 + + if self.spot.lostLOS then + + -- Did not loose LOS anymore. + self.spot.lostLOS=false + + -- Resume laser if currently paused. + if self.spot.Paused then + 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=3, 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 + + else + self:T("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:T(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) + + --self.spot.Coordinate:MarkToAll("Target Laser",ReadOnly,Text) + 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) + + if los then + -- Got LOS + if self.spot.lostLOS then + --self:T({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 before "ElementSpawned" event. Check that element is not in status spawned already. +-- @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:onbeforeElementSpawned(From, Event, To, Element) + + if Element and Element.status==OPSGROUP.ElementStatus.SPAWNED then + self:T2(self.lid..string.format("Element %s is already spawned ==> Transition denied!", Element.name)) + return false + end + + return true +end + +--- On after "ElementInUtero" 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:onafterElementInUtero(From, Event, To, Element) + self:T(self.lid..string.format("Element in utero %s", Element.name)) + + -- Set element status. + self:_UpdateStatus(Element, OPSGROUP.ElementStatus.INUTERO) + +end + +--- On after "ElementDamaged" 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:onafterElementDamaged(From, Event, To, Element) + self:T(self.lid..string.format("Element damaged %s", Element.name)) + + if Element and (Element.status~=OPSGROUP.ElementStatus.DEAD and Element.status~=OPSGROUP.ElementStatus.INUTERO) then + + local lifepoints=0 + + if Element.DCSunit and Element.DCSunit:isExist() then + + -- Get life of unit + lifepoints=Element.DCSunit:getLife() + + -- Debug output. + self:T(self.lid..string.format("Element life %s: %.2f/%.2f", Element.name, lifepoints, Element.life0)) + + else + + self:T(self.lid..string.format("Element.DCSunit %s does not exist!", Element.name)) + + end + + if lifepoints<=1.0 then + self:T(self.lid..string.format("Element %s life %.2f <= 1.0 ==> Destroyed!", Element.name, lifepoints)) + self:ElementDestroyed(Element) + end + + end + +end + +--- On after "ElementHit" 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. +-- @param Wrapper.Unit#UNIT Enemy Unit that hit the element or `nil`. +function OPSGROUP:onafterElementHit(From, Event, To, Element, Enemy) + + -- Increase element hit counter. + Element.Nhit=Element.Nhit+1 + + -- Debug message. + self:T(self.lid..string.format("Element hit %s by %s [n=%d, N=%d]", Element.name, Enemy and Enemy:GetName() or "unknown", Element.Nhit, self.Nhit)) + + -- Group was hit. + self:__Hit(-3, Enemy) + +end + +--- On after "Hit" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT Enemy Unit that hit the element or `nil`. +function OPSGROUP:onafterHit(From, Event, To, Enemy) + self:T(self.lid..string.format("Group hit by %s", Enemy and Enemy:GetName() or "unknown")) +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 + + -- Element is dead. + self:ElementDead(Element) + +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) + + -- Debug info. + 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 + + + -- Clear cargo bay of element. + for i=#Element.cargoBay,1,-1 do + local mycargo=Element.cargoBay[i] --#OPSGROUP.MyCargo + + if mycargo.group then + + -- Remove from cargo bay. + self:_DelCargobay(mycargo.group) + + if mycargo.group and not (mycargo.group:IsDead() or mycargo.group:IsStopped()) then + + -- Remove my carrier + mycargo.group:_RemoveMyCarrier() + + if mycargo.reserved then + + -- This group was not loaded yet ==> Not cargo any more. + mycargo.group:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + + else + + -- Carrier dead ==> cargo dead. + for _,cargoelement in pairs(mycargo.group.elements) do + + -- Debug info. + self:T2(self.lid.."Cargo element dead "..cargoelement.name) + + -- Trigger dead event. + mycargo.group:ElementDead(cargoelement) + + end + end + + end + + else + + -- Add cargo to lost. + if self.cargoTZC then + for _,_cargo in pairs(self.cargoTZC.Cargos) do + local cargo=_cargo --#OPSGROUP.CargoGroup + if cargo.uid==mycargo.cargoUID then + cargo.storage.cargoLost=cargo.storage.cargoLost+mycargo.storageAmount + end + end + end + + -- Remove cargo from cargo bay. + self:_DelCargobayElement(Element, mycargo) + + end + end + +end + +--- On after "Respawn" event. +-- @param #OPSGROUP 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. Default is the inital template of the group. +function OPSGROUP:onafterRespawn(From, Event, To, Template) + + -- Debug info. + self:T(self.lid.."Respawning group!") + + -- Copy template. + local template=UTILS.DeepCopy(Template or self.template) + + -- Late activation off. + template.lateActivation=false + + self:_Respawn(0, template) + +end + +--- Teleport the group to a different location. +-- @param #OPSGROUP self +-- @param Core.Point#COORDINATE Coordinate Coordinate where the group is teleported to. +-- @param #number Delay Delay in seconds before respawn happens. Default 0. +-- @param #boolean NoPauseMission If `true`, dont pause a running mission. +-- @return #OPSGROUP self +function OPSGROUP:Teleport(Coordinate, Delay, NoPauseMission) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP.Teleport, self, Coordinate, 0, NoPauseMission) + else + + -- Debug message. + self:T(self.lid.."FF Teleporting...") + --Coordinate:MarkToAll("Teleport "..self.groupname) + + -- Check if we have a mission running. + if self:IsOnMission() and not NoPauseMission then + self:T(self.lid.."Pausing current mission for telport") + self:PauseMission() + end + + -- Get copy of template. + local Template=UTILS.DeepCopy(self.template) --DCS#Template + + -- Set late activation of template to current state. + Template.lateActivation=self:IsLateActivated() + + -- Not uncontrolled. + Template.uncontrolled=false + + -- Set waypoint in air for flighgroups. + if self:IsFlightgroup() then + Template.route.points[1]=Coordinate:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, 300, true, nil, nil, "Spawnpoint") + elseif self:IsArmygroup() then + Template.route.points[1]=Coordinate:WaypointGround(0) + elseif self:IsNavygroup() then + Template.route.points[1]=Coordinate:WaypointNaval(0) + end + + -- Template units. + local units=Template.units + + -- Table with teleported vectors. + local d={} + for i=1,#units do + local unit=units[i] + d[i]={x=Coordinate.x+(units[i].x-units[1].x), y=Coordinate.z+units[i].y-units[1].y} + end + + for i=#units,1,-1 do + local unit=units[i] + + -- Get element. + local element=self:GetElementByName(unit.name) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + -- No parking. + unit.parking=nil + unit.parking_id=nil + + -- Current position. + local vec3=element.unit:GetVec3() + + -- Current heading. + local heading=element.unit:GetHeading() + + -- Set new x,y. + unit.x=d[i].x + unit.y=d[i].y + + -- Set altitude. + unit.alt=Coordinate.y + + -- Set heading. + unit.heading=math.rad(heading) + unit.psi=-unit.heading + else + -- Remove unit from spawn template because it is already dead + table.remove(units, i) + end + end + + -- Respawn from new template. + self:_Respawn(0, Template, true) + + end +end + +--- Respawn the group. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before respawn happens. Default 0. +-- @param DCS#Template 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 waypoints and reinit group if `true`. +-- @return #OPSGROUP self +function OPSGROUP:_Respawn(Delay, Template, Reset) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP._Respawn, self, 0, Template, Reset) + else + + -- Debug message. + self:T2(self.lid.."FF _Respawn") + + -- Given template or get copy of old. + Template=Template or self:_GetTemplate(true) + + -- Number of destroyed units. + self.Ndestroyed=0 + self.Nhit=0 + + -- Check if group is currently alive. + if self:IsAlive() then + + --- + -- Group is ALIVE + --- + + -- Template units. + local units=Template.units + + for i=#units,1,-1 do + local unit=units[i] + + -- Get the element. + local element=self:GetElementByName(unit.name) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + if not Reset then + + -- Parking ID. + unit.parking=element.parking and element.parking.TerminalID or unit.parking + unit.parking_id=nil + + -- Get current position vector. + local vec3=element.unit:GetVec3() + + -- Get heading. + local heading=element.unit:GetHeading() + + -- Set unit position. + unit.x=vec3.x + unit.y=vec3.z + unit.alt=vec3.y + + -- Set heading in rad. + unit.heading=math.rad(heading) + unit.psi=-unit.heading + + end + + else + + -- Element is dead. Remove from template. + table.remove(units, i) + + self.Ndestroyed=self.Ndestroyed+1 + + end + end + + + -- Despawn old group. Dont trigger any remove unit event since this is a respawn. + self:Despawn(0, true) + + end + + -- Ensure elements in utero. + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + self:ElementInUtero(element) + end + end + + -- Spawn with a little delay (especially Navy groups caused problems if they were instantly respawned) + self:_Spawn(0.01, Template) + + end + + return self +end + +--- Spawn group from a given template. +-- @param #OPSGROUP self +-- @param #number Delay Delay in seconds before respawn happens. Default 0. +-- @param DCS#Template 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. +-- @return #OPSGROUP self +function OPSGROUP:_Spawn(Delay, Template) + if Delay and Delay>0 then + self:ScheduleOnce(Delay, OPSGROUP._Spawn, self, 0, Template) + else + -- Debug output. + --self:T2({Template=Template}) + + if self:IsArmygroup() and self.ValidateAndRepositionGroundUnits then + UTILS.ValidateAndRepositionGroundUnits(Template.units) + end + + -- Spawn new group. + self.group=_DATABASE:Spawn(Template) + self.group:SetValidateAndRepositionGroundUnits(self.ValidateAndRepositionGroundUnits) + --local countryID=self.group:GetCountry() + --local categoryID=self.group:GetCategory() + --local dcsgroup=coalition.addGroup(countryID, categoryID, Template) + + -- Set DCS group and controller. + self.dcsgroup=self:GetDCSGroup() + self.controller=self.dcsgroup:getController() + + -- Set activation and controlled state. + self.isLateActivated=Template.lateActivation + self.isUncontrolled=Template.uncontrolled + + -- Not dead or destroyed any more. + self.isDead=false + self.isDestroyed=false + + self.groupinitialized=false + self.wpcounter=1 + self.currentwp=1 + + -- Init waypoints. + self:_InitWaypoints() + + -- Init Group. This call is delayed because NAVY groups did not like to be initialized just yet (group did not contain any units). + self:_InitGroup(Template, 0.001) + + -- Reset events. + --self:ResetEvents() + end +end + +--- On after "InUtero" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterInUtero(From, Event, To) + self:T(self.lid..string.format("Group inutero at t=%.3f", timer.getTime())) + --TODO: set element status to inutero +end + +--- On after "Damaged" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterDamaged(From, Event, To) + self:T(self.lid..string.format("Group damaged at t=%.3f", timer.getTime())) +end + +--- On after "Destroyed" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterDestroyed(From, Event, To) + self:T(self.lid..string.format("Group destroyed at t=%.3f", timer.getTime())) + self.isDestroyed=true +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) + + -- Debug info. + self:T(self.lid..string.format("Group dead at t=%.3f", timer.getTime())) + + -- Is dead now. + self.isDead=true + + -- 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 + + -- Delete waypoints so they are re-initialized at the next spawn. + self:ClearWaypoints() + self.groupinitialized=false + + -- Set cargo status to NOTCARGO. + self.cargoStatus=OPSGROUP.CargoStatus.NOTCARGO + self.carrierStatus=OPSGROUP.CarrierStatus.NOTCARRIER + + -- Remove from cargo bay of carrier. + local mycarrier=self:_GetMyCarrierGroup() + if mycarrier and not mycarrier:IsDead() then + mycarrier:_DelCargobay(self) + self:_RemoveMyCarrier() + end + + -- Inform all transports in the queue that this carrier group is dead now. + for i,_transport in pairs(self.cargoqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + transport:__DeadCarrierGroup(1, self) + end + + -- Cargo queue empty + self.cargoqueue={} + + -- No current cargo transport. + self.cargoTransport=nil + self.cargoTZC=nil + + if self.Ndestroyed==#self.elements then + if self.cohort then + -- All elements were destroyed ==> Asset group is gone. + self.cohort:DelGroup(self.groupname) + end + else + -- Not all assets were destroyed (despawn) ==> Add asset back to legion? + end + + + if self.legion then + if not self:IsInUtero() then + + -- Get asset. + local asset=self.legion:GetAssetByName(self.groupname) + + if asset then + + -- Get request. + local request=self.legion:GetRequestByID(asset.rid) + + -- Trigger asset dead event. + self.legion:AssetDead(asset, request) + + end + + end + + -- Stop in 5 sec to give possible respawn attempts a chance. + self:__Stop(-5) + + elseif not self.isAI then + -- Stop player flights. + self:__Stop(-1) + end + +end + +--- On before "Stop" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onbeforeStop(From, Event, To) + + -- We check if + if self:IsAlive() then + self:T(self.lid..string.format("WARNING: Group is still alive! Will not stop the FSM. Use :Despawn() instead")) + return false + end + + return true +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) + + -- Handle events: + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.RemoveUnit) + + -- Handle events: + if self.isFlightgroup then + 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.currbase=nil + elseif self.isArmygroup then + self:UnHandleEvent(EVENTS.Hit) + end + + for _,_mission in pairs(self.missionqueue) do + local mission=_mission --Ops.Auftrag#AUFTRAG + self:MissionCancel(mission) + end + + -- Stop check timers. + self.timerCheckZone:Stop() + self.timerQueueUpdate:Stop() + self.timerStatus:Stop() + + -- Stop FSM scheduler. + self.CallScheduler:Clear() + if self.Scheduler then + self.Scheduler:Clear() + end + + -- Flightcontrol. + if self.flightcontrol then + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.parking then + self.flightcontrol:SetParkingFree(element.parking) + end + end + self.flightcontrol:_RemoveFlight(self) + end + + 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:T(self.lid..text) + end + + -- Remove flight from data base. + _DATABASE.FLIGHTGROUPS[self.groupname]=nil + + -- Debug output. + self:T(self.lid.."STOPPED! Unhandled events, cleared scheduler and removed from _DATABASE") +end + +--- On after "OutOfAmmo" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterOutOfAmmo(From, Event, To) + self:T(self.lid..string.format("Group is out of ammo at t=%.3f", timer.getTime())) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Cargo Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check cargo transport assignments. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:_CheckCargoTransport() + + -- Abs. missin time in seconds. + local Time=timer.getAbsTime() + + -- Cargo bay debug info. + if self.verbose>=1 then + local text="" + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if cargo.group then + text=text..string.format("\n- %s in carrier %s, reserved=%s", tostring(cargo.group:GetName()), tostring(element.name), tostring(cargo.reserved)) + else + text=text..string.format("\n- storage %s=%d kg in carrier %s [UID=%s]", + tostring(cargo.storageType), tostring(cargo.storageAmount*cargo.storageWeight), tostring(element.name), tostring(cargo.cargoUID)) + end + end + end + if text=="" then + text=" empty" + end + self:T(self.lid.."Cargo bay:"..text) + end + + -- Cargo queue debug info. + if self.verbose>=3 then + local text="" + for i,_transport in pairs(self.cargoqueue) do + local transport=_transport --Ops.OpsTransport#OPSTRANSPORT + local pickupzone=transport:GetPickupZone() + local deployzone=transport:GetDeployZone() + local pickupname=pickupzone and pickupzone:GetName() or "unknown" + local deployname=deployzone and deployzone:GetName() or "unknown" + text=text..string.format("\n[%d] UID=%d Status=%s: %s --> %s", i, transport.uid, transport:GetState(), pickupname, deployname) + for j,_cargo in pairs(transport:GetCargos()) do + local cargo=_cargo --#OPSGROUP.CargoGroup + if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then + local state=cargo.opsgroup:GetState() + local status=cargo.opsgroup.cargoStatus + local name=cargo.opsgroup.groupname + local carriergroup, carrierelement, reserved=cargo.opsgroup:_GetMyCarrier() + local carrierGroupname=carriergroup and carriergroup.groupname or "none" + local carrierElementname=carrierelement and carrierelement.name or "none" + text=text..string.format("\n (%d) %s [%s]: %s, carrier=%s(%s), delivered=%s", j, name, state, status, carrierGroupname, carrierElementname, tostring(cargo.delivered)) + else + --TODO: STORAGE + end + end + end + if text~="" then + self:T(self.lid.."Cargo queue:"..text) + end + end + + if self.cargoTransport and self.cargoTransport:GetCarrierTransportStatus(self)==OPSTRANSPORT.Status.DELIVERED then + -- Remove transport from queue. + self:DelOpsTransport(self.cargoTransport) + -- No current transport any more. + self.cargoTransport=nil + self.cargoTZC=nil + end + + -- Get current mission (if any). + local mission=self:GetMissionCurrent() + + -- Check if there is anything in the queue. + if (not self.cargoTransport) and (mission==nil or mission.type==AUFTRAG.Type.NOTHING) then + self.cargoTransport=self:_GetNextCargoTransport() + if self.cargoTransport and mission then + self:MissionCancel(mission) + end + if self.cargoTransport and not self:IsActive() then + self:Activate() + end + end + + -- Now handle the transport. + if self.cargoTransport then + + if self:IsNotCarrier() then + + -- Unset time stamps. + self.Tpickingup=nil + self.Tloading=nil + self.Ttransporting=nil + self.Tunloading=nil + + -- Get transport zone combo (TZC). + self.cargoTZC=self.cargoTransport:_GetTransportZoneCombo(self) + + if self.cargoTZC then + + -- Found TZC + self:T(self.lid..string.format("Not carrier ==> pickup at %s [TZC UID=%d]", self.cargoTZC.PickupZone and self.cargoTZC.PickupZone:GetName() or "unknown", self.cargoTZC.uid)) + + -- Initiate the cargo transport process. + self:__Pickup(-1) + + else + self:T2(self.lid.."Not carrier ==> No TZC found") + end + + elseif self:IsPickingup() then + + -- Set time stamp. + self.Tpickingup=self.Tpickingup or Time + + -- Current pickup time. + local tpickingup=Time-self.Tpickingup + + -- Debug Info. + self:T(self.lid..string.format("Picking up at %s [TZC UID=%d] for %s sec...", self.cargoTZC.PickupZone and self.cargoTZC.PickupZone:GetName() or "unknown", self.cargoTZC.uid, tpickingup)) + + elseif self:IsLoading() then + + -- Set loading time stamp. + self.Tloading=self.Tloading or Time + + -- Current pickup time. + local tloading=Time-self.Tloading + + --TODO: Check max loading time. If exceeded ==> abort transport. Time might depend on required cargos, because we need to give them time to arrive. + + -- Debug info. + self:T(self.lid..string.format("Loading at %s [TZC UID=%d] for %.1f sec...", self.cargoTZC.PickupZone and self.cargoTZC.PickupZone:GetName() or "unknown", self.cargoTZC.uid, tloading)) + + local boarding=false + local gotcargo=false + for _,_cargo in pairs(self.cargoTZC.Cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then + + -- Check if anyone is still boarding. + if cargo.opsgroup and cargo.opsgroup:IsBoarding(self.groupname) then + boarding=true + end + + -- Check if we have any cargo to transport. + if cargo.opsgroup and cargo.opsgroup:IsLoaded(self.groupname) then + gotcargo=true + end + + else + + -- Get cargo if it is in the cargo bay of any carrier element. + local mycargo=self:_GetMyCargoBayFromUID(cargo.uid) + + if mycargo and mycargo.storageAmount>0 then + gotcargo=true + end + + end + + end + + -- Boarding finished ==> Transport cargo. + if gotcargo and self.cargoTransport:_CheckRequiredCargos(self.cargoTZC, self) and not boarding then + self:T(self.lid.."Boarding/loading finished ==> Loaded") + self.Tloading=nil + self:LoadingDone() + else + -- No cargo and no one is boarding ==> check again if we can make anyone board. + self:Loading() + end + + elseif self:IsTransporting() then + + -- Set time stamp. + self.Ttransporting=self.Ttransporting or Time + + -- Current pickup time. + local ttransporting=Time-self.Ttransporting + + -- Debug info. + self:T(self.lid.."Transporting (nothing to do)") + + elseif self:IsUnloading() then + + -- Set time stamp. + self.Tunloading=self.Tunloading or Time + + -- Current pickup time. + local tunloading=Time-self.Tunloading + + -- Debug info. + self:T(self.lid.."Unloading ==> Checking if all cargo was delivered") + + local delivered=true + for _,_cargo in pairs(self.cargoTZC.Cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then + + local carrierGroup=cargo.opsgroup:_GetMyCarrierGroup() + + -- Check that this group is + if (carrierGroup and carrierGroup:GetName()==self:GetName()) and not cargo.delivered then + delivered=false + break + end + + else + --- + -- STORAGE + --- + + -- Get cargo if it is in the cargo bay of any carrier element. + local mycargo=self:_GetMyCargoBayFromUID(cargo.uid) + + if mycargo and not cargo.delivered then + delivered=false + break + end + + end + + end + + -- Unloading finished ==> pickup next batch or call it a day. + if delivered then + self:T(self.lid.."Unloading finished ==> UnloadingDone") + self:UnloadingDone() + else + self:Unloading() + end + + end + + -- Debug info. (At this point, we might not have a current cargo transport ==> hence the check) + if self.verbose>=2 and self.cargoTransport then + local pickupzone=self.cargoTransport:GetPickupZone(self.cargoTZC) + local deployzone=self.cargoTransport:GetDeployZone(self.cargoTZC) + local pickupname=pickupzone and pickupzone:GetName() or "unknown" + local deployname=deployzone and deployzone:GetName() or "unknown" + local text=string.format("Carrier [%s]: %s --> %s", self.carrierStatus, pickupname, deployname) + for _,_cargo in pairs(self.cargoTransport:GetCargos(self.cargoTZC)) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then + local name=cargo.opsgroup:GetName() + local gstatus=cargo.opsgroup:GetState() + local cstatus=cargo.opsgroup.cargoStatus + local weight=cargo.opsgroup:GetWeightTotal() + local carriergroup, carrierelement, reserved=cargo.opsgroup:_GetMyCarrier() + local carrierGroupname=carriergroup and carriergroup.groupname or "none" + local carrierElementname=carrierelement and carrierelement.name or "none" + text=text..string.format("\n- %s (%.1f kg) [%s]: %s, carrier=%s (%s), delivered=%s", name, weight, gstatus, cstatus, carrierElementname, carrierGroupname, tostring(cargo.delivered)) + else + --TODO: Storage + end + end + self:I(self.lid..text) + end + + end + + return self +end + + +--- Check if a group is in the cargo bay. +-- @param #OPSGROUP self +-- @param #OPSGROUP OpsGroup Group to check. +-- @return #boolean If `true`, group is in the cargo bay. +function OPSGROUP:_IsInCargobay(OpsGroup) + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if cargo.group.groupname==OpsGroup.groupname then + return true + end + end + end + + return false +end + +--- Add OPSGROUP to cargo bay of a carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group. +-- @param #OPSGROUP.Element CarrierElement The element of the carrier. +-- @param #boolean Reserved Only reserve the cargo bay space. +function OPSGROUP:_AddCargobay(CargoGroup, CarrierElement, Reserved) + + --TODO: Check group is not already in cargobay of this carrier or any other carrier. + + local cargo=self:_GetCargobay(CargoGroup) + + if cargo then + cargo.reserved=Reserved + else + + --cargo=self:_CreateMyCargo(CargoUID, CargoGroup) + + cargo={} --#OPSGROUP.MyCargo + cargo.group=CargoGroup + cargo.reserved=Reserved + + table.insert(CarrierElement.cargoBay, cargo) + end + + + -- Set my carrier. + CargoGroup:_SetMyCarrier(self, CarrierElement, Reserved) + + -- Fill cargo bay (obsolete). + self.cargoBay[CargoGroup.groupname]=CarrierElement.name + + if not Reserved then + + -- Cargo weight. + local weight=CargoGroup:GetWeightTotal() + + -- Add weight to carrier. + self:AddWeightCargo(CarrierElement.name, weight) + + end + + return self +end + +--- Add warehouse storage to cargo bay of a carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element CarrierElement The element of the carrier. +-- @param #number CargoUID UID of the cargo data. +-- @param #string StorageType Storage type. +-- @param #number StorageAmount Storage amount. +-- @param #number StorageWeight Weight of a single storage item in kg. +function OPSGROUP:_AddCargobayStorage(CarrierElement, CargoUID, StorageType, StorageAmount, StorageWeight) + + local MyCargo=self:_CreateMyCargo(CargoUID, nil, StorageType, StorageAmount, StorageWeight) + + self:_AddMyCargoBay(MyCargo, CarrierElement) + + +end + +--- Add OPSGROUP to cargo bay of a carrier. +-- @param #OPSGROUP self +-- @param #number CargoUID UID of the cargo data. +-- @param #OPSGROUP OpsGroup Cargo group. +-- @param #string StorageType Storage type. +-- @param #number StorageAmount Storage amount. +-- @param #number StorageWeight Weight of a single storage item in kg. +-- @return #OPSGROUP.MyCargo My cargo object. +function OPSGROUP:_CreateMyCargo(CargoUID, OpsGroup, StorageType, StorageAmount, StorageWeight) + + local cargo={} --#OPSGROUP.MyCargo + + cargo.cargoUID=CargoUID + cargo.group=OpsGroup + cargo.storageType=StorageType + cargo.storageAmount=StorageAmount + cargo.storageWeight=StorageWeight + cargo.reserved=false + + return cargo +end + + +--- Add storage to cargo bay of a carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP.MyCargo MyCargo My cargo. +-- @param #OPSGROUP.Element CarrierElement The element of the carrier. +function OPSGROUP:_AddMyCargoBay(MyCargo, CarrierElement) + + table.insert(CarrierElement.cargoBay, MyCargo) + + if not MyCargo.reserved then + + -- Cargo weight. + local weight=0 + + if MyCargo.group then + weight=MyCargo.group:GetWeightTotal() + else + weight=MyCargo.storageAmount*MyCargo.storageWeight + end + + -- Add weight to carrier. + self:AddWeightCargo(CarrierElement.name, weight) + + end + + +end + +--- Get cargo bay data from a cargo data id. +-- @param #OPSGROUP self +-- @param #number uid Unique ID of cargo data. +-- @return #OPSGROUP.MyCargo Cargo My cargo. +-- @return #OPSGROUP.Element Element that has loaded the cargo. +function OPSGROUP:_GetMyCargoBayFromUID(uid) + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + for i,_mycargo in pairs(element.cargoBay) do + local mycargo=_mycargo --#OPSGROUP.MyCargo + + if mycargo.cargoUID and mycargo.cargoUID==uid then + return mycargo, element, i + end + end + end + + return nil, nil, nil +end + + +--- Get all groups currently loaded as cargo. +-- @param #OPSGROUP self +-- @param #string CarrierName (Optional) Only return cargo groups loaded into a particular carrier unit. +-- @return #table Cargo ops groups. +function OPSGROUP:GetCargoGroups(CarrierName) + local cargos={} + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if CarrierName==nil or element.name==CarrierName then + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if not cargo.reserved then + table.insert(cargos, cargo.group) + end + end + end + end + + return cargos +end + +--- Get cargo bay item. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group. +-- @return #OPSGROUP.MyCargo Cargo bay item or `nil` if the group is not in the carrier. +-- @return #number CargoBayIndex Index of item in the cargo bay table. +-- @return #OPSGROUP.Element Carrier element. +function OPSGROUP:_GetCargobay(CargoGroup) + + -- Loop over elements and their cargo bay items. + local CarrierElement=nil --#OPSGROUP.Element + local cargobayIndex=nil + local reserved=nil + for i,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for j,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if cargo.group and cargo.group.groupname==CargoGroup.groupname then + return cargo, j, element + end + end + end + + return nil, nil, nil +end + +--- Remove OPSGROUP from cargo bay of a carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element Element Cargo group. +-- @param #number CargoUID Cargo UID. +-- @return #OPSGROUP.MyCargo MyCargo My cargo data. +function OPSGROUP:_GetCargobayElement(Element, CargoUID) + self:T3({Element=Element, CargoUID=CargoUID}) + + for i,_mycargo in pairs(Element.cargoBay) do + local mycargo=_mycargo --#OPSGROUP.MyCargo + + if mycargo.cargoUID and mycargo.cargoUID==CargoUID then + return mycargo + end + + end + + return nil +end + +--- Remove OPSGROUP from cargo bay of a carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element Element Cargo group. +-- @param #OPSGROUP.MyCargo MyCargo My cargo data. +-- @return #boolean If `true`, cargo could be removed. +function OPSGROUP:_DelCargobayElement(Element, MyCargo) + + for i,_mycargo in pairs(Element.cargoBay) do + local mycargo=_mycargo --#OPSGROUP.MyCargo + + if mycargo.cargoUID and MyCargo.cargoUID and mycargo.cargoUID==MyCargo.cargoUID then + if MyCargo.group then + self:RedWeightCargo(Element.name, MyCargo.group:GetWeightTotal()) + else + self:RedWeightCargo(Element.name, MyCargo.storageAmount*MyCargo.storageWeight) + end + table.remove(Element.cargoBay, i) + return true + end + + end + + return false +end + +--- Remove OPSGROUP from cargo bay of a carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup Cargo group. +-- @return #boolean If `true`, cargo could be removed. +function OPSGROUP:_DelCargobay(CargoGroup) + + if self.cargoBay[CargoGroup.groupname] then + + -- Not in cargo bay any more. + self.cargoBay[CargoGroup.groupname]=nil + + end + + -- Get cargo bay info. + local cargoBayItem, cargoBayIndex, CarrierElement=self:_GetCargobay(CargoGroup) + + if cargoBayItem and cargoBayIndex then + + -- Debug info. + self:T(self.lid..string.format("Removing cargo group %s from cargo bay (index=%d) of carrier %s", CargoGroup:GetName(), cargoBayIndex, CarrierElement.name)) + + -- Remove + table.remove(CarrierElement.cargoBay, cargoBayIndex) + + -- Reduce weight (if cargo space was not just reserved). + if not cargoBayItem.reserved then + local weight=CargoGroup:GetWeightTotal() + self:RedWeightCargo(CarrierElement.name, weight) + end + + return true + end + + self:T(self.lid.."ERROR: Group is not in cargo bay. Cannot remove it!") + return false +end + +--- Get cargo transport from cargo queue. +-- @param #OPSGROUP self +-- @return Ops.OpsTransport#OPSTRANSPORT The next due cargo transport or `nil`. +function OPSGROUP:_GetNextCargoTransport() + + -- Current position. + local coord=self:GetCoordinate() + + -- Sort results table wrt prio and distance to pickup zone. + local function _sort(a, b) + local transportA=a --Ops.OpsTransport#OPSTRANSPORT + local transportB=b --Ops.OpsTransport#OPSTRANSPORT + --TODO: Include distance + --local distA=transportA.pickupzone:GetCoordinate():Get2DDistance(coord) + --local distB=transportB.pickupzone:GetCoordinate():Get2DDistance(coord) + return (transportA.priomaxweight then + maxweight=weight + end + + end + end + + return maxweight +end + + +--- Get weight of the internal cargo the group is carriing right now. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. Default is of the whole group. +-- @param #boolean IncludeReserved If `false`, cargo weight that is only *reserved* is **not** counted. By default (`true` or `nil`), the reserved cargo is included. +-- @return #number Cargo weight in kg. +function OPSGROUP:GetWeightCargo(UnitName, IncludeReserved) + + -- Calculate weight based on actual cargo weight. + local weight=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if (UnitName==nil or UnitName==element.name) and element.status~=OPSGROUP.ElementStatus.DEAD then + + weight=weight+element.weightCargo or 0 + + end + + end + + -- Calculate weight from stuff in cargo bay. By default this includes the reserved weight if a cargo group was assigned and is currently boarding. + local gewicht=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if (UnitName==nil or UnitName==element.name) and (element and element.status~=OPSGROUP.ElementStatus.DEAD) then + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + if (not cargo.reserved) or (cargo.reserved==true and (IncludeReserved==true or IncludeReserved==nil)) then + if cargo.group then + gewicht=gewicht+cargo.group:GetWeightTotal() + else + gewicht=gewicht+cargo.storageAmount*cargo.storageWeight + end + --self:I(self.lid..string.format("unit=%s (reserved=%s): cargo=%s weight=%d, total weight=%d", tostring(UnitName), tostring(IncludeReserved), cargo.group:GetName(), cargoweight, weight)) + end + end + end + end + + -- Debug info. + self:T3(self.lid..string.format("Unit=%s (reserved=%s): weight=%d, gewicht=%d", tostring(UnitName), tostring(IncludeReserved), weight, gewicht)) + + -- Quick check. + if IncludeReserved==false and gewicht~=weight then + self:T(self.lid..string.format("ERROR: FF weight!=gewicht: weight=%.1f, gewicht=%.1f", weight, gewicht)) + end + + return gewicht +end + +--- Get max weight of the internal cargo the group can carry. Optionally, the max cargo weight of a specific unit can be requested. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. Default is of the whole group. +-- @return #number Max cargo weight in kg. This does **not** include any cargo loaded or reserved currently. +function OPSGROUP:GetWeightCargoMax(UnitName) + + local weight=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if (UnitName==nil or UnitName==element.name) and element.status~=OPSGROUP.ElementStatus.DEAD then + + weight=weight+element.weightMaxCargo + + end + + end + + return weight +end + +--- Get OPSGROUPs in the cargo bay. +-- @param #OPSGROUP self +-- @return #table Cargo OPSGROUPs. +function OPSGROUP:GetCargoOpsGroups() + + local opsgroups={} + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + for _,_cargo in pairs(element.cargoBay) do + local cargo=_cargo --#OPSGROUP.MyCargo + table.insert(opsgroups, cargo.group) + end + end + + return opsgroups +end + +--- Add weight to the internal cargo of an element of the group. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. Default is of the whole group. +-- @param #number Weight Cargo weight to be added in kg. +function OPSGROUP:AddWeightCargo(UnitName, Weight) + + local element=self:GetElementByName(UnitName) + + if element then --we do not check if the element is actually alive because we need to remove cargo from dead units + + -- Add weight. + element.weightCargo=element.weightCargo+Weight + + -- Debug info. + self:T(self.lid..string.format("%s: Adding %.1f kg cargo weight. New cargo weight=%.1f kg", UnitName, Weight, element.weightCargo)) + + -- For airborne units, we set the weight in game. + if self.isFlightgroup and element.unit and element.unit:IsAlive() then -- #2272 trying to deduct cargo weight from possibly dead units + trigger.action.setUnitInternalCargo(element.name, element.weightCargo) --https://wiki.hoggitworld.com/view/DCS_func_setUnitInternalCargo + end + + end + + return self +end + +--- Reduce weight to the internal cargo of an element of the group. +-- @param #OPSGROUP self +-- @param #string UnitName Name of the unit. +-- @param #number Weight Cargo weight to be reduced in kg. +function OPSGROUP:RedWeightCargo(UnitName, Weight) + + -- Reduce weight by adding negative weight. + self:AddWeightCargo(UnitName, -Weight) + + return self +end + +--- Get weight of warehouse storage to transport. +-- @param #OPSGROUP self +-- @param Ops.OpsTransport#OPSTRANSPORT.Storage Storage +-- @param #boolean Total Get total weight. Otherweise the amount left to deliver (total-loaded-lost-delivered). +-- @param #boolean Reserved Reduce weight that is reserved. +-- @param #boolean Amount Return amount not weight. +-- @return #number Weight of cargo in kg or amount in number of items, if `Amount=true`. +function OPSGROUP:_GetWeightStorage(Storage, Total, Reserved, Amount) + + local weight=Storage.cargoAmount + + if not Total then + weight=weight-Storage.cargoLost-Storage.cargoLoaded-Storage.cargoDelivered + end + + if Reserved then + weight=weight-Storage.cargoReserved + end + + if not Amount then + weight=weight*Storage.cargoWeight + end + + return weight +end + +--- Check if the group can *in principle* be carrier of a cargo group. This checks the max cargo capacity of the group but *not* how much cargo is already loaded (if any). +-- **Note** that the cargo group *cannot* be split into units, i.e. the largest cargo bay of any element of the group must be able to load the whole cargo group in one piece. +-- @param #OPSGROUP self +-- @param Ops.OpsGroup#OPSGROUP.CargoGroup Cargo Cargo data, which needs a carrier. +-- @return #boolean If `true`, there is an element of the group that can load the whole cargo group. +function OPSGROUP:CanCargo(Cargo) + + if Cargo then + + local weight=math.huge + + if Cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then + + local weight=Cargo.opsgroup:GetWeightTotal() + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + -- Check that element is not dead and has + if element and element.status~=OPSGROUP.ElementStatus.DEAD and element.weightMaxCargo>=weight then + return true + end + end + + + else + + --- + -- STORAGE + --- + + -- Since storage cargo can be devided onto multiple carriers, we take the weight of a single cargo item (even 1 kg of fuel). + weight=Cargo.storage.cargoWeight + + end + + -- Calculate cargo bay space. + local bay=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + -- Check that element is not dead and has + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + bay=bay+element.weightMaxCargo + end + + end + + -- Check if cargo fits into cargo bay(s) of carrier group. + if bay>=weight then + return true + end + + + end + + return false +end + +--- Find carrier for cargo by evaluating the free cargo bay storage. +-- @param #OPSGROUP self +-- @param #number Weight Weight of cargo in kg. +-- @return #OPSGROUP.Element Carrier able to transport the cargo. +function OPSGROUP:FindCarrierForCargo(Weight) + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + local free=self:GetFreeCargobay(element.name) + + if free>=Weight then + return element + else + self:T3(self.lid..string.format("%s: Weight %d>%d free cargo bay", element.name, Weight, free)) + end + + end + + return nil +end + +--- Set my carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CarrierGroup Carrier group. +-- @param #OPSGROUP.Element CarrierElement Carrier element. +-- @param #boolean Reserved If `true`, reserve space for me. +function OPSGROUP:_SetMyCarrier(CarrierGroup, CarrierElement, Reserved) + + -- Debug info. + self:T(self.lid..string.format("Setting My Carrier: %s (%s), reserved=%s", CarrierGroup:GetName(), tostring(CarrierElement.name), tostring(Reserved))) + + self.mycarrier.group=CarrierGroup + self.mycarrier.element=CarrierElement + self.mycarrier.reserved=Reserved + + self.cargoTransportUID=CarrierGroup.cargoTransport and CarrierGroup.cargoTransport.uid or nil + +end + +--- Get my carrier group. +-- @param #OPSGROUP self +-- @return #OPSGROUP Carrier group. +function OPSGROUP:_GetMyCarrierGroup() + if self.mycarrier and self.mycarrier.group then + return self.mycarrier.group + end + return nil +end + +--- Get my carrier element. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Element Carrier element. +function OPSGROUP:_GetMyCarrierElement() + if self.mycarrier and self.mycarrier.element then + return self.mycarrier.element + end + return nil +end + +--- Is my carrier reserved. +-- @param #OPSGROUP self +-- @return #boolean If `true`, space for me was reserved. +function OPSGROUP:_IsMyCarrierReserved() + if self.mycarrier then + return self.mycarrier.reserved + end + return nil +end + + + +--- Get my carrier. +-- @param #OPSGROUP self +-- @return #OPSGROUP Carrier group. +-- @return #OPSGROUP.Element Carrier element. +-- @return #boolean If `true`, space is reserved for me +function OPSGROUP:_GetMyCarrier() + return self.mycarrier.group, self.mycarrier.element, self.mycarrier.reserved +end + + +--- Remove my carrier. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:_RemoveMyCarrier() + self:T(self.lid..string.format("Removing my carrier!")) + self.mycarrier.group=nil + self.mycarrier.element=nil + self.mycarrier.reserved=nil + self.mycarrier={} + self.cargoTransportUID=nil + return self +end + +--- On after "Pickup" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterPickup(From, Event, To) + + -- Old status. + local oldstatus=self.carrierStatus + + -- Set carrier status. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.PICKUP) + + local TZC=self.cargoTZC + + -- Pickup zone. + local Zone=TZC.PickupZone + + -- Check if already in the pickup zone. + local inzone=self:IsInZone(Zone) + + -- Pickup at an airbase. + local airbasePickup=TZC.PickupAirbase --Wrapper.Airbase#AIRBASE + + -- Check if group is already ready for loading. + local ready4loading=false + if self:IsArmygroup() or self:IsNavygroup() then + + -- Army and Navy groups just need to be inside the zone. + ready4loading=inzone + + else + + -- Aircraft is already parking at the pickup airbase. + ready4loading=self.currbase and airbasePickup and self.currbase:GetName()==airbasePickup:GetName() and self:IsParking() + + -- If a helo is landed in the zone, we also are ready for loading. + if ready4loading==false and self.isHelo and self:IsLandedAt() and inzone then + ready4loading=true + end + end + + -- Ready for loading? + if ready4loading then + + -- We are already in the pickup zone ==> wait and initiate loading. + if (self:IsArmygroup() or self:IsNavygroup()) and not self:IsHolding() then + self:FullStop() + end + + -- Start loading. + self:__Loading(-5) + + else + + -- Set surface type of random coordinate. + local surfacetypes=nil + if self:IsArmygroup() or self:IsFlightgroup() then + surfacetypes={land.SurfaceType.LAND} + elseif self:IsNavygroup() then + surfacetypes={land.SurfaceType.WATER} + end + + -- Get a random coordinate in the pickup zone and let the carrier go there. + local Coordinate=Zone:GetRandomCoordinate(nil, nil, surfacetypes) + + -- Current waypoint ID. + local uid=self:GetWaypointCurrentUID() + + -- Add waypoint. + if self:IsFlightgroup() then + + --- + -- Flight Group + --- + + -- Activate uncontrolled group. + if self:IsParking() and self:IsUncontrolled() then + self:StartUncontrolled() + end + + if airbasePickup then + + --- + -- Pickup at airbase + --- + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Get transport path. + if path and oldstatus~=OPSGROUP.CarrierStatus.NOTCARRIER then + + for i=#path.waypoints,1,-1 do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true + uid=waypoint.uid + if i==1 then + waypoint.temp=false + waypoint.detour=1 --Needs to trigger the landatairbase function. + end + end + + else + + local coordinate=self:GetCoordinate():GetIntermediateCoordinate(Coordinate, 0.5) + + -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone. + local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, UTILS.MetersToFeet(self.altitudeCruise), true) ; waypoint.detour=1 + + end + + elseif self.isHelo then + + --- + -- Helo can also land in a zone (NOTE: currently VTOL cannot!) + --- + + -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone. + local waypoint=FLIGHTGROUP.AddWaypoint(self, Coordinate, nil, uid, UTILS.MetersToFeet(self.altitudeCruise), false) ; waypoint.detour=1 + + else + self:T(self.lid.."ERROR: Transportcarrier aircraft cannot land in Pickup zone! Specify a ZONE_AIRBASE as pickup zone") + end + + -- Cancel landedAt task. This should trigger Cruise once airborne. + if self.isHelo and self:IsLandedAt() then + local Task=self:GetTaskCurrent() + if Task then + self:TaskCancel(Task) + else + self:T(self.lid.."ERROR: No current task but landed at?!") + end + end + + if self:IsWaiting() then + self:__Cruise(-2) + end + + elseif self:IsNavygroup() then + + --- + -- Navy Group + --- + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Get transport path. + if path then --and oldstatus~=OPSGROUP.CarrierStatus.NOTCARRIER then + for i=#path.waypoints,1,-1 do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=NAVYGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true + uid=waypoint.uid + end + end + + -- NAVYGROUP + local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate, nil, uid, self.altitudeCruise, false) ; waypoint.detour=1 + + -- Give cruise command. + self:__Cruise(-2) + + + elseif self:IsArmygroup() then + + --- + -- Army Group + --- + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Formation used to go to the pickup zone.. + local Formation=self.cargoTransport:_GetFormationPickup(self.cargoTZC, self) + + -- Get transport path. + if path and oldstatus~=OPSGROUP.CarrierStatus.NOTCARRIER then + for i=#path.waypoints,1,-1 do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=ARMYGROUP.AddWaypoint(self, coordinate, nil, uid, wp.action, false) ; waypoint.temp=true + uid=waypoint.uid + end + end + + -- ARMYGROUP + local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, nil, uid, Formation, false) ; waypoint.detour=1 + + -- Give cruise command. + self:__Cruise(-2, nil, Formation) + + end + + end + +end + +--- On after "Loading" event. +-- @param #OPSGROUP self +-- @param #table Cargos Table of cargos. +-- @return #table Table of sorted cargos. +function OPSGROUP:_SortCargo(Cargos) + + -- Sort results table wrt descending weight. + local function _sort(a, b) + local cargoA=a --Ops.OpsGroup#OPSGROUP.CargoGroup + local cargoB=b --Ops.OpsGroup#OPSGROUP.CargoGroup + local weightA=0 + local weightB=0 + if cargoA.opsgroup then + weightA=cargoA.opsgroup:GetWeightTotal() + else + weightA=self:_GetWeightStorage(cargoA.storage) + end + if cargoB.opsgroup then + weightB=cargoB.opsgroup:GetWeightTotal() + else + weightB=self:_GetWeightStorage(cargoB.storage) + end + return weightA>weightB + end + + table.sort(Cargos, _sort) + + return Cargos +end + + +--- On after "Loading" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLoading(From, Event, To) + + -- Set carrier status. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.LOADING) + + -- Get valid cargos of the TZC. + local cargos={} + for _,_cargo in pairs(self.cargoTZC.Cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + -- Check if this group can carry the cargo. + local canCargo=self:CanCargo(cargo) + + -- Check if this group is currently acting as carrier. + local isCarrier=false + + + -- Check if cargo is not already cargo. + local isNotCargo=true + + -- Check if cargo is holding or loaded + local isHolding=cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and (cargo.opsgroup:IsHolding() or cargo.opsgroup:IsLoaded()) or true + + -- Check if cargo is in embark/pickup zone. + -- Added InUtero here, if embark zone is moving (ship) and cargo has been spawned late activated and its position is not updated. Not sure if that breaks something else! + local inZone=cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and (cargo.opsgroup:IsInZone(self.cargoTZC.EmbarkZone) or cargo.opsgroup:IsInUtero()) or true + + -- Check if cargo is currently on a mission. + local isOnMission=cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup:IsOnMission() or false + + -- Check if current mission is using this ops transport. + if isOnMission then + local mission=cargo.opsgroup:GetMissionCurrent() + if mission and ((mission.opstransport and mission.opstransport.uid==self.cargoTransport.uid) or mission.type==AUFTRAG.Type.NOTHING) then + isOnMission=not isHolding + end + end + + local isAvail=true + if cargo.type==OPSTRANSPORT.CargoType.STORAGE then + local nAvail=cargo.storage.storageFrom:GetAmount(cargo.storage.cargoType) + if nAvail>0 then + isAvail=true + else + isAvail=false + end + else + isCarrier=cargo.opsgroup:IsPickingup() or cargo.opsgroup:IsLoading() or cargo.opsgroup:IsTransporting() or cargo.opsgroup:IsUnloading() + isNotCargo=cargo.opsgroup:IsNotCargo(true) + end + + local isDead=cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup:IsDead() or false + + -- Debug message. + self:T(self.lid..string.format("Loading: canCargo=%s, isCarrier=%s, isNotCargo=%s, isHolding=%s, isOnMission=%s", + tostring(canCargo), tostring(isCarrier), tostring(isNotCargo), tostring(isHolding), tostring(isOnMission))) + + -- TODO: Need a better :IsBusy() function or :IsReadyForMission() :IsReadyForBoarding() :IsReadyForTransport() + if canCargo and inZone and isNotCargo and isHolding and isAvail and (not (cargo.delivered or isDead or isCarrier or isOnMission)) then + table.insert(cargos, cargo) + end + end + + -- Sort cargos. + self:_SortCargo(cargos) + + -- Loop over all cargos. + for _,_cargo in pairs(cargos) do + local cargo=_cargo --#OPSGROUP.CargoGroup + + local weight=nil + if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then + + -- Get total weight of group. + weight=cargo.opsgroup:GetWeightTotal() + + -- Find a carrier for this cargo. + local carrier=self:FindCarrierForCargo(weight) + + -- Order cargo group to board the carrier. + if carrier then + cargo.opsgroup:Board(self, carrier) + end + + else + + --- + -- STORAGE + --- + + -- Get weight of cargo that needs to be transported. + weight=self:_GetWeightStorage(cargo.storage, false) + + -- Get amount that the warehouse currently has. + local Amount=cargo.storage.storageFrom:GetAmount(cargo.storage.cargoType) + local Weight=Amount*cargo.storage.cargoWeight + + -- Make sure, we do not take more than the warehouse can provide. + weight=math.min(weight, Weight) + + -- Debug info. + self:T(self.lid..string.format("Loading storage weight=%d kg (warehouse has %d kg)!", weight, Weight)) + + -- Loop over all elements of the carrier group. + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + -- Get the free cargo space of the carrier. + local free=self:GetFreeCargobay(element.name) + + -- Min of weight or bay. + local w=math.min(weight, free) + + -- Check that weight is >0 and also greater that at least one item. We cannot transport half a missile. + if w>=cargo.storage.cargoWeight then + + -- Calculate item amount. + local amount=math.floor(w/cargo.storage.cargoWeight) + + -- Remove items from warehouse. + cargo.storage.storageFrom:RemoveAmount(cargo.storage.cargoType, amount) + + -- Add amount to loaded cargo. + cargo.storage.cargoLoaded=cargo.storage.cargoLoaded+amount + + -- Add cargo to cargo by of element. + self:_AddCargobayStorage(element, cargo.uid, cargo.storage.cargoType, amount, cargo.storage.cargoWeight) + + -- Reduce weight for the next element (if any). + weight=weight-amount*cargo.storage.cargoWeight + + -- Debug info. + local text=string.format("Element %s: loaded amount=%d (weight=%d) ==> left=%d kg", element.name, amount, amount*cargo.storage.cargoWeight, weight) + self:T(self.lid..text) + + -- If no cargo left, break the loop. + if weight<=0 then + break + end + + end + + end + + end + end + +end + +--- Set (new) cargo status. +-- @param #OPSGROUP self +-- @param #string Status New status. +function OPSGROUP:_NewCargoStatus(Status) + + -- Debug info. + if self.verbose>=2 then + self:I(self.lid..string.format("New cargo status: %s --> %s", tostring(self.cargoStatus), tostring(Status))) + end + + -- Set cargo status. + self.cargoStatus=Status + +end + +--- Set (new) carrier status. +-- @param #OPSGROUP self +-- @param #string Status New status. +function OPSGROUP:_NewCarrierStatus(Status) + + -- Debug info. + if self.verbose>=2 then + self:I(self.lid..string.format("New carrier status: %s --> %s", tostring(self.carrierStatus), tostring(Status))) + end + + -- Set cargo status. + self.carrierStatus=Status + +end + +--- Transfer cargo from to another carrier. +-- @param #OPSGROUP self +-- @param #OPSGROUP CargoGroup The cargo group to be transferred. +-- @param #OPSGROUP CarrierGroup The new carrier group. +-- @param #OPSGROUP.Element CarrierElement The new carrier element. +function OPSGROUP:_TransferCargo(CargoGroup, CarrierGroup, CarrierElement) + + -- Debug info. + self:T(self.lid..string.format("Transferring cargo %s to new carrier group %s", CargoGroup:GetName(), CarrierGroup:GetName())) + + -- Unload from this and directly load into the other carrier. + self:Unload(CargoGroup) + CarrierGroup:Load(CargoGroup, CarrierElement) + +end + +--- On after "Load" event. Carrier loads a cargo group into ints cargo bay. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP CargoGroup The OPSGROUP loaded as cargo. +-- @param #OPSGROUP.Element Carrier The carrier element/unit. +function OPSGROUP:onafterLoad(From, Event, To, CargoGroup, Carrier) + + -- Debug info. + self:T(self.lid..string.format("Loading group %s", tostring(CargoGroup.groupname))) + + -- Carrier element. + local carrier=Carrier or CargoGroup:_GetMyCarrierElement() --#OPSGROUP.Element + + -- No carrier provided. + if not carrier then + -- Get total weight of group. + local weight=CargoGroup:GetWeightTotal() + -- Try to find a carrier manually. + carrier=self:FindCarrierForCargo(weight) + end + + if carrier then + + --- + -- Embark Cargo + --- + + -- New cargo status. + CargoGroup:_NewCargoStatus(OPSGROUP.CargoStatus.LOADED) + + -- Clear all waypoints. + CargoGroup:ClearWaypoints() + + -- Add into carrier bay. + self:_AddCargobay(CargoGroup, carrier, false) + + -- Despawn this group. + if CargoGroup:IsAlive() then + CargoGroup:Despawn(0, true) + end + + -- Trigger embarked event for cargo group. + CargoGroup:Embarked(self, carrier) + + -- Trigger Loaded event. + self:Loaded(CargoGroup) + + -- Trigger "Loaded" event for current cargo transport. + if self.cargoTransport then + CargoGroup:_DelMyLift(self.cargoTransport) + self.cargoTransport:Loaded(CargoGroup, self, carrier) + else + self:T(self.lid..string.format("WARNING: Loaded cargo but no current OPSTRANSPORT assignment!")) + end + + else + self:T(self.lid.."ERROR: Cargo has no carrier on Load event!") + end + +end + +--- On after "LoadingDone" event. Carrier has loaded all (possible) cargo at the pickup zone. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterLoadingDone(From, Event, To) + + -- Debug info. + self:T(self.lid.."Carrier Loading Done ==> Transport") + + -- Order group to transport. + self:__Transport(1) + +end + +--- On before "Transport" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onbeforeTransport(From, Event, To) + + if self.cargoTransport==nil then + return false + elseif self.cargoTransport:IsDelivered() then --could be if all cargo was dead on boarding + return false + end + + return true +end + + +--- On after "Transport" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterTransport(From, Event, To) + + -- Set carrier status. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.TRANSPORTING) + + --TODO: This is all very similar to the onafterPickup() function. Could make it general. + + -- Deploy zone. + local Zone=self.cargoTZC.DeployZone + + -- Check if already in deploy zone. + local inzone=self:IsInZone(Zone) + + -- Deploy airbase (if any). + local airbaseDeploy=self.cargoTZC.DeployAirbase --Wrapper.Airbase#AIRBASE + + -- Check if group is already ready for loading. + local ready2deploy=false + if self:IsArmygroup() or self:IsNavygroup() then + ready2deploy=inzone + else + -- Aircraft is already parking at the pickup airbase. + ready2deploy=self.currbase and airbaseDeploy and self.currbase:GetName()==airbaseDeploy:GetName() and self:IsParking() + + -- If a helo is landed in the zone, we also are ready for loading. + if ready2deploy==false and (self.isHelo or self.isVTOL) and self:IsLandedAt() and inzone then + ready2deploy=true + end + end + + --env.info(string.format("FF Transport: Zone=%s inzone=%s, ready2deploy=%s", Zone:GetName(), tostring(inzone), tostring(ready2deploy))) + + if inzone then + + -- We are already in the deploy zone ==> wait and initiate unloading. + if (self:IsArmygroup() or self:IsNavygroup()) and not self:IsHolding() then + self:FullStop() + end + + -- Start unloading. + self:__Unloading(-5) + + else + + local surfacetypes=nil + if self:IsArmygroup() or self:IsFlightgroup() then + surfacetypes={land.SurfaceType.LAND} + elseif self:IsNavygroup() then + surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER} + end + + -- Coord where the carrier goes to unload. + local Coordinate=Zone:GetRandomCoordinate(nil, nil, surfacetypes) --Core.Point#COORDINATE + + -- Current waypoint UID. + local uid=self:GetWaypointCurrentUID() + + -- Add waypoint. + if self:IsFlightgroup() then + + -- Activate uncontrolled group. + if self:IsParking() and self:IsUncontrolled() then + self:StartUncontrolled() + end + + if airbaseDeploy then + + --- + -- Deploy at airbase + --- + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Get transport path. + if path then + + for i=1, #path.waypoints do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true + uid=waypoint.uid + if i==#path.waypoints then + waypoint.temp=false + waypoint.detour=1 --Needs to trigger the landatairbase function. + end + end + + else + + local coordinate=self:GetCoordinate():GetIntermediateCoordinate(Coordinate, 0.5) + + -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone. + local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, UTILS.MetersToFeet(self.altitudeCruise), true) ; waypoint.detour=1 + + end + + elseif self.isHelo then + + --- + -- Helo can also land in a zone + --- + + -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone. + local waypoint=FLIGHTGROUP.AddWaypoint(self, Coordinate, nil, uid, UTILS.MetersToFeet(self.altitudeCruise), false) ; waypoint.detour=1 + + else + self:T(self.lid.."ERROR: Aircraft (cargo carrier) cannot land in Deploy zone! Specify a ZONE_AIRBASE as deploy zone") + end + + -- Cancel landedAt task. This should trigger Cruise once airborne. + if self.isHelo and self:IsLandedAt() then + local Task=self:GetTaskCurrent() + if Task then + self:TaskCancel(Task) + else + self:T(self.lid.."ERROR: No current task but landed at?!") + end + end + + elseif self:IsArmygroup() then + + -- Get transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Formation used for transporting. + local Formation=self.cargoTransport:_GetFormationTransport(self.cargoTZC, self) + + -- Get transport path. + if path then + for i=1,#path.waypoints do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=ARMYGROUP.AddWaypoint(self, coordinate, nil, uid, wp.action, false) ; waypoint.temp=true + uid=waypoint.uid + end + end + + -- ARMYGROUP + local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, nil, uid, Formation, false) ; waypoint.detour=1 + + -- Give cruise command. + self:Cruise(nil, Formation) + + elseif self:IsNavygroup() then + + -- Get a (random) pre-defined transport path. + local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC) + + -- Get transport path. + if path then + for i=1,#path.waypoints do + local wp=path.waypoints[i] + local coordinate=COORDINATE:NewFromWaypoint(wp) + local waypoint=NAVYGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true + uid=waypoint.uid + end + end + + -- NAVYGROUP + local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate, nil, uid, self.altitudeCruise, false) ; waypoint.detour=1 + + -- Give cruise command. + self:Cruise() + + end + + end + +end + +--- On after "Unloading" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterUnloading(From, Event, To) + + -- Set carrier status to UNLOADING. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.UNLOADING) + + self:T(self.lid.."Unloading..") + + -- Deploy zone. + local zone=self.cargoTZC.DisembarkZone or self.cargoTZC.DeployZone --Core.Zone#ZONE + + for _,_cargo in pairs(self.cargoTZC.Cargos) do + local cargo=_cargo --#OPSGROUP.CargoGroup + + if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then + + -- Check that cargo is loaded into this group. + -- NOTE: Could be that the element carriing this cargo group is DEAD, which would mean that the cargo group is also DEAD. + if cargo.opsgroup:IsLoaded(self.groupname) and not cargo.opsgroup:IsDead() then + + -- Disembark to carrier. + local carrier=nil --Ops.OpsGroup#OPSGROUP.Element + local carrierGroup=nil --Ops.OpsGroup#OPSGROUP + local disembarkToCarriers=cargo.disembarkCarriers~=nil or self.cargoTZC.disembarkToCarriers + + -- Set specifc zone for this cargo. + if cargo.disembarkZone then + zone=cargo.disembarkZone + end + + self:T(self.lid..string.format("Unloading cargo %s to zone %s", cargo.opsgroup:GetName(), zone and zone:GetName() or "No Zone Found!")) + + -- Try to get the OPSGROUP if deploy zone is a ship. + if zone and zone:IsInstanceOf("ZONE_AIRBASE") and zone:GetAirbase():IsShip() then + local shipname=zone:GetAirbase():GetName() + local ship=UNIT:FindByName(shipname) + local group=ship:GetGroup() + carrierGroup=_DATABASE:GetOpsGroup(group:GetName()) + carrier=carrierGroup:GetElementByName(shipname) + end + + if disembarkToCarriers then + + -- Debug info. + self:T(self.lid..string.format("Trying to find disembark carriers in zone %s", zone:GetName())) + + -- Disembarkcarriers. + local disembarkCarriers=cargo.disembarkCarriers or self.cargoTZC.DisembarkCarriers + + -- Try to find a carrier that can take the cargo. + carrier, carrierGroup=self.cargoTransport:FindTransferCarrierForCargo(cargo.opsgroup, zone, disembarkCarriers, self.cargoTZC.DeployAirbase) + + --TODO: max unloading time if transfer carrier does not arrive in the zone. + + end + + if (disembarkToCarriers and carrier and carrierGroup) or (not disembarkToCarriers) then + + -- Cargo was delivered (somehow). + cargo.delivered=true + + -- Increase number of delivered cargos. + self.cargoTransport.Ndelivered=self.cargoTransport.Ndelivered+1 + + if carrier and carrierGroup then + + --- + -- Delivered to another carrier group. + --- + + self:_TransferCargo(cargo.opsgroup, carrierGroup, carrier) + + elseif zone and zone:IsInstanceOf("ZONE_AIRBASE") and zone:GetAirbase():IsShip() then + + --- + -- Delivered to a ship via helo that landed on its platform + --- + + -- Issue warning. + self:T(self.lid.."ERROR: Deploy/disembark zone is a ZONE_AIRBASE of a ship! Where to put the cargo? Dumping into the sea, sorry!") + + -- Unload but keep "in utero" (no coordinate provided). + self:Unload(cargo.opsgroup) + + else + + --- + -- Delivered to deploy zone + --- + + if self.cargoTransport:GetDisembarkInUtero(self.cargoTZC) then + + -- Unload but keep "in utero" (no coordinate provided). + self:Unload(cargo.opsgroup) + + else + + -- Get disembark zone of this TZC. + local DisembarkZone=cargo.disembarkZone or self.cargoTransport:GetDisembarkZone(self.cargoTZC) + + local Coordinate=nil + + + if DisembarkZone then + + -- Random coordinate in disembark zone. + Coordinate=DisembarkZone:GetRandomCoordinate() + + else + + local element=cargo.opsgroup:_GetMyCarrierElement() + + if element then + + -- Get random point in disembark zone. + local zoneCarrier=self:GetElementZoneUnload(element.name) + + -- Random coordinate/heading in the zone. + Coordinate=zoneCarrier:GetRandomCoordinate() + + else + self:E(self.lid..string.format("ERROR carrier element nil!")) + end + + end + + -- Random heading of the group. + local Heading=math.random(0,359) + + -- Activation on/off. + local activation=self.cargoTransport:GetDisembarkActivation(self.cargoTZC) + if cargo.disembarkActivation~=nil then + activation=cargo.disembarkActivation + end + + -- Unload to Coordinate. + self:Unload(cargo.opsgroup, Coordinate, activation, Heading) + + end + + -- Trigger "Unloaded" event for current cargo transport + self.cargoTransport:Unloaded(cargo.opsgroup, self) + + end + + else + self:T(self.lid.."Cargo needs carrier but no carrier is avaiable (yet)!") + end + + else + -- Not loaded or dead + end + + else + + --- + -- STORAGE + --- + + -- TODO: should proabaly move this check to the top to include OPSGROUPS as well?! + if not cargo.delivered then + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + -- Get my cargo from cargo bay of element. + local mycargo=self:_GetCargobayElement(element, cargo.uid) + + if mycargo then + -- Add cargo to warehouse storage. + cargo.storage.storageTo:AddAmount(mycargo.storageType, mycargo.storageAmount) + + -- Add amount to delivered. + cargo.storage.cargoDelivered=cargo.storage.cargoDelivered+mycargo.storageAmount + + -- Reduce loaded amount. + cargo.storage.cargoLoaded=cargo.storage.cargoLoaded-mycargo.storageAmount + + -- Remove cargo from bay. + self:_DelCargobayElement(element, mycargo) + + -- Debug info + self:T2(self.lid..string.format("Cargo loaded=%d, delivered=%d, lost=%d", cargo.storage.cargoLoaded, cargo.storage.cargoDelivered, cargo.storage.cargoLost)) + + end + end + + -- Get amount that was delivered. + local amountToDeliver=self:_GetWeightStorage(cargo.storage, false, false, true) + + -- Get total amount to be delivered. + local amountTotal=self:_GetWeightStorage(cargo.storage, true, false, true) + + -- Debug info. + local text=string.format("Amount delivered=%d, total=%d", amountToDeliver, amountTotal) + self:T(self.lid..text) + + if amountToDeliver<=0 then + + -- Cargo was delivered (somehow). + cargo.delivered=true + + -- Increase number of delivered cargos. + self.cargoTransport.Ndelivered=self.cargoTransport.Ndelivered+1 + + -- Debug info. + local text=string.format("Ndelivered=%d delivered=%s", self.cargoTransport.Ndelivered, tostring(cargo.delivered)) + self:T(self.lid..text) + + end + end + + end + + end -- loop over cargos + +end + +--- On before "Unload" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP OpsGroup The OPSGROUP loaded as cargo. +-- @param Core.Point#COORDINATE Coordinate Coordinate were the group is unloaded to. +-- @param #number Heading Heading of group. +function OPSGROUP:onbeforeUnload(From, Event, To, OpsGroup, Coordinate, Heading) + + -- Remove group from carrier bay. If group is not in cargo bay, function will return false and transition is denied. + local removed=self:_DelCargobay(OpsGroup) + + return removed +end + +--- On after "Unload" event. Carrier unloads a cargo group from its cargo bay. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP OpsGroup The OPSGROUP loaded as cargo. +-- @param Core.Point#COORDINATE Coordinate Coordinate were the group is unloaded to. +-- @param #boolean Activated If `true`, group is active. If `false`, group is spawned in late activated state. +-- @param #number Heading (Optional) Heading of group in degrees. Default is random heading for each unit. +function OPSGROUP:onafterUnload(From, Event, To, OpsGroup, Coordinate, Activated, Heading) + + -- New cargo status. + OpsGroup:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + + --TODO: Unload flightgroup. Find parking spot etc. + + if Coordinate then + + --- + -- Respawn at a coordinate. + --- + + -- Template for the respawned group. + local Template=UTILS.DeepCopy(OpsGroup.template) --DCS#Template + + -- No late activation. + if Activated==false then + Template.lateActivation=true + else + Template.lateActivation=false + end + + -- Loop over template units. + for _,Unit in pairs(Template.units) do + + local element=OpsGroup:GetElementByName(Unit.name) + + if element then + + local vec3=element.vec3 + + -- Relative pos vector. + local rvec2={x=Unit.x-Template.x, y=Unit.y-Template.y} --DCS#Vec2 + + local cvec2={x=Coordinate.x, y=Coordinate.z} --DCS#Vec2 + + -- Position. + Unit.x=cvec2.x+rvec2.x + Unit.y=cvec2.y+rvec2.y + Unit.alt=land.getHeight({x=Unit.x, y=Unit.y}) + + -- Heading. + Unit.heading=Heading and math.rad(Heading) or Unit.heading + Unit.psi=-Unit.heading + + end + + end + + -- Respawn group. + OpsGroup:_Respawn(0, Template) + + -- Add current waypoint. These have been cleard on loading. + if OpsGroup:IsNavygroup() then + OpsGroup:ClearWaypoints() + OpsGroup.currentwp=1 + OpsGroup.passedfinalwp=true + NAVYGROUP.AddWaypoint(OpsGroup, Coordinate, nil, nil, nil, false) + elseif OpsGroup:IsArmygroup() then + OpsGroup:ClearWaypoints() + OpsGroup.currentwp=1 + OpsGroup.passedfinalwp=true + ARMYGROUP.AddWaypoint(OpsGroup, Coordinate, nil, nil, nil, false) + end + + else + + --- + -- Just remove from this carrier. + --- + + -- Nothing to do. + + OpsGroup.position=self:GetVec3() + + end + + -- Trigger "Disembarked" event. + OpsGroup:Disembarked(OpsGroup:_GetMyCarrierGroup(), OpsGroup:_GetMyCarrierElement()) + + -- Trigger "Unloaded" event. + self:Unloaded(OpsGroup) + + -- Remove my carrier. + OpsGroup:_RemoveMyCarrier() + +end + +--- On after "Unloaded" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. +function OPSGROUP:onafterUnloaded(From, Event, To, OpsGroupCargo) + self:T(self.lid..string.format("Unloaded OPSGROUP %s", OpsGroupCargo:GetName())) + + if OpsGroupCargo.legion and OpsGroupCargo:IsInZone(OpsGroupCargo.legion.spawnzone) then + self:T(self.lid..string.format("Unloaded group %s returned to legion", OpsGroupCargo:GetName())) + OpsGroupCargo:Returned() + end + + -- Check if there is a paused mission. + local paused=OpsGroupCargo:_CountPausedMissions()>0 + + if paused then + OpsGroupCargo:UnpauseMission() + end + +end + + +--- On after "UnloadingDone" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSGROUP:onafterUnloadingDone(From, Event, To) + + -- Debug info + self:T(self.lid.."Cargo unloading done..") + + -- Cancel landedAt task. + if self:IsFlightgroup() and self:IsLandedAt() then + local Task=self:GetTaskCurrent() + self:__TaskCancel(5, Task) + end + + -- Check everything was delivered (or is dead). + local delivered=self:_CheckGoPickup(self.cargoTransport) + + if not delivered then + + -- Get new TZC. + self.cargoTZC=self.cargoTransport:_GetTransportZoneCombo(self) + + if self.cargoTZC then + + -- Pickup the next batch. + self:T(self.lid.."Unloaded: Still cargo left ==> Pickup") + self:Pickup() + + else + + -- Debug info. + self:T(self.lid..string.format("WARNING: Not all cargo was delivered but could not get a transport zone combo ==> setting carrier state to NOT CARRIER")) + + -- This is not a carrier anymore. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.NOTCARRIER) + + end + + else + + -- Everything delivered. + self:T(self.lid.."Unloaded: ALL cargo unloaded ==> Delivered (current)") + self:Delivered(self.cargoTransport) + + end + +end + +--- On after "Delivered" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT CargoTransport The cargo transport assignment. +function OPSGROUP:onafterDelivered(From, Event, To, CargoTransport) + + -- Check if this was the current transport. + if self.cargoTransport and self.cargoTransport.uid==CargoTransport.uid then + + -- Checks + if self:IsPickingup() then + -- Delete pickup waypoint? + local wpindex=self:GetWaypointIndexNext(false) + if wpindex then + self:RemoveWaypoint(wpindex) + end + -- Remove landing airbase. + self.isLandingAtAirbase=nil + elseif self:IsLoading() then + -- Nothing to do? + elseif self:IsTransporting() then + -- This should not happen. Carrier is transporting, how can the cargo be delivered? + elseif self:IsUnloading() then + -- Nothing to do? + end + + -- This is not a carrier anymore. + self:_NewCarrierStatus(OPSGROUP.CarrierStatus.NOTCARRIER) + + -- Startup uncontrolled aircraft to allow it to go back. + if self:IsFlightgroup() then + + local function atbase(_airbase) + local airbase=_airbase --Wrapper.Airbase#AIRBASE + if airbase and self.currbase then + if airbase.AirbaseName==self.currbase.AirbaseName then + return true + end + end + return false + end + + -- Check if uncontrolled and NOT at destination. If so, start up uncontrolled and let flight return to whereever it wants to go. + if self:IsUncontrolled() and not atbase(self.destbase) then + self:StartUncontrolled() + end + if self:IsLandedAt() then + local Task=self:GetTaskCurrent() + self:TaskCancel(Task) + end + else + -- Army & Navy: give Cruise command to "wake up" from waiting status. + self:__Cruise(-0.1) + end + + -- Set carrier transport status. + self.cargoTransport:SetCarrierTransportStatus(self, OPSTRANSPORT.Status.DELIVERED) + + -- Check group done. + self:T(self.lid..string.format("All cargo of transport UID=%d delivered ==> check group done in 0.2 sec", self.cargoTransport.uid)) + self:_CheckGroupDone(0.2) + + + end + + -- Remove cargo transport from cargo queue. + --self:DelOpsTransport(CargoTransport) + +end + +--- On after "TransportCancel" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsTransport#OPSTRANSPORT The transport to be cancelled. +function OPSGROUP:onafterTransportCancel(From, Event, To, Transport) + + if self.cargoTransport and self.cargoTransport.uid==Transport.uid then + + --- + -- Current Transport + --- + + -- Debug info. + self:T(self.lid..string.format("Cancel current transport %d", Transport.uid)) + + -- Call delivered= + local calldelivered=false + + if self:IsPickingup() then + + -- On its way to the pickup zone. Remove waypoint. Will be done in delivered. + calldelivered=true + + elseif self:IsLoading() then + + -- Handle cargo groups. + local cargos=Transport:GetCargoOpsGroups(false) + + for _,_opsgroup in pairs(cargos) do + local opsgroup=_opsgroup --#OPSGROUP + + if opsgroup:IsBoarding(self.groupname) then + + -- Remove boarding waypoint. + opsgroup:RemoveWaypoint(self.currentwp+1) + + -- Remove from cargo bay (reserved), remove mycarrier, set cargo status. + self:_DelCargobay(opsgroup) + opsgroup:_RemoveMyCarrier() + opsgroup:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + + elseif opsgroup:IsLoaded(self.groupname) then + + -- Get random point in disembark zone. + local zoneCarrier=self:GetElementZoneUnload(opsgroup:_GetMyCarrierElement().name) + + -- Random coordinate/heading in the zone. + local Coordinate=zoneCarrier and zoneCarrier:GetRandomCoordinate() or self.cargoTransport:GetEmbarkZone(self.cargoTZC):GetRandomCoordinate() + + -- Random heading of the group. + local Heading=math.random(0,359) + + -- Unload to Coordinate. + self:Unload(opsgroup, Coordinate, self.cargoTransport:GetDisembarkActivation(self.cargoTZC), Heading) + + -- Trigger "Unloaded" event for current cargo transport + self.cargoTransport:Unloaded(opsgroup, self) + + end + + end + + -- Call delivered. + calldelivered=true + + elseif self:IsTransporting() then + + -- Well, we cannot just unload the cargo anywhere. + + -- TODO: Best would be to bring the cargo back to the pickup zone! + + elseif self:IsUnloading() then + -- Unloading anyway... delivered will be called when done. + else + + end + + -- Transport delivered. + if calldelivered then + self:__Delivered(-2, Transport) + end + + else + + --- + -- NOT the current transport + --- + + -- Set mission group status. + Transport:SetCarrierTransportStatus(self, AUFTRAG.GroupStatus.CANCELLED) + + -- Remove transport from queue. This also removes the carrier from the transport. + self:DelOpsTransport(Transport) + + -- Remove carrier. + --Transport:_DelCarrier(self) + + -- Send group RTB or WAIT if nothing left to do. + self:_CheckGroupDone(1) + + end + +end + + +--- +-- Cargo Group Functions +--- + +--- On before "Board" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP CarrierGroup The carrier group. +-- @param #OPSGROUP.Element Carrier The OPSGROUP element +function OPSGROUP:onbeforeBoard(From, Event, To, CarrierGroup, Carrier) + + if self:IsDead() then + self:T(self.lid.."Group DEAD ==> Deny Board transition!") + return false + elseif CarrierGroup:IsDead() then + self:T(self.lid.."Carrier Group DEAD ==> Deny Board transition!") + self:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + return false + elseif Carrier.status==OPSGROUP.ElementStatus.DEAD then + self:T(self.lid.."Carrier Element DEAD ==> Deny Board transition!") + self:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO) + return false + end + + return true +end + +--- On after "Board" event. +-- @param #OPSGROUP self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #OPSGROUP CarrierGroup The carrier group. +-- @param #OPSGROUP.Element Carrier The OPSGROUP element +function OPSGROUP:onafterBoard(From, Event, To, CarrierGroup, Carrier) + + -- Army or Navy group. + local CarrierIsArmyOrNavy=CarrierGroup:IsArmygroup() or CarrierGroup:IsNavygroup() + local CargoIsArmyOrNavy=self:IsArmygroup() or self:IsNavygroup() + + -- Check that carrier is standing still. + --if (CarrierIsArmyOrNavy and (CarrierGroup:IsHolding() and CarrierGroup:GetVelocity(Carrier.name)<=1)) or (CarrierGroup:IsFlightgroup() and (CarrierGroup:IsParking() or CarrierGroup:IsLandedAt())) then + if (CarrierIsArmyOrNavy and (CarrierGroup:GetVelocity(Carrier.name)<=1)) or (CarrierGroup:IsFlightgroup() and (CarrierGroup:IsParking() or CarrierGroup:IsLandedAt())) then + + -- Board if group is mobile, not late activated and army or navy. Everything else is loaded directly. + local board=self.speedMax>0 and CargoIsArmyOrNavy and self:IsAlive() and CarrierGroup:IsAlive() + + -- Armygroup cannot board ship ==> Load directly. + if self:IsArmygroup() and CarrierGroup:IsNavygroup() then + board=false + end + + if self:IsLoaded() then + + -- Debug info. + self:T(self.lid..string.format("Group is loaded currently ==> Moving directly to new carrier - No Unload(), Disembart() events triggered!")) + + -- Remove old/current carrier. + self:_RemoveMyCarrier() + + -- Trigger Load event. + CarrierGroup:Load(self) + + elseif board then + + -- Set cargo status. + self:_NewCargoStatus(OPSGROUP.CargoStatus.BOARDING) + + -- Debug info. + self:T(self.lid..string.format("Boarding group=%s [%s], carrier=%s", CarrierGroup:GetName(), CarrierGroup:GetState(), tostring(Carrier.name))) + + -- TODO: Implement embarkzone. + local Coordinate=Carrier.unit:GetCoordinate() + + -- Clear all waypoints. + self:ClearWaypoints(self.currentwp+1) + + if self.isArmygroup then + local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, nil, nil, ENUMS.Formation.Vehicle.Diamond) ; waypoint.detour=1 + self:Cruise() + else + local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate) ; waypoint.detour=1 + self:Cruise() + end + + -- Set carrier. As long as the group is not loaded, we only reserve the cargo space. + CarrierGroup:_AddCargobay(self, Carrier, true) + + else + + --- + -- Direct load into carrier. + --- + + -- Debug info. + self:T(self.lid..string.format("Board [loaded=%s] with direct load to carrier group=%s, element=%s", tostring(self:IsLoaded()), CarrierGroup:GetName(), tostring(Carrier.name))) + + -- Get current carrier group. + local mycarriergroup=self:_GetMyCarrierGroup() + if mycarriergroup then + self:T(self.lid..string.format("Current carrier group %s", mycarriergroup:GetName())) + end + + -- Unload cargo first. + if mycarriergroup and mycarriergroup:GetName()~=CarrierGroup:GetName() then + -- TODO: Unload triggers other stuff like Disembarked. This can be a problem! + self:T(self.lid.."Unloading from mycarrier") + mycarriergroup:Unload(self) + end + + -- Trigger Load event. + CarrierGroup:Load(self) + end + + else + + -- Redo boarding call. + self:T(self.lid.."Carrier not ready for boarding yet ==> repeating boarding call in 10 sec") + self:__Board(-10, CarrierGroup, Carrier) + + -- Set carrier. As long as the group is not loaded, we only reserve the cargo space.� + CarrierGroup:_AddCargobay(self, Carrier, true) + end + + +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.detectionOn and 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) + + -- FSM state. + local fsmstate=self:GetState() + + if self:IsAlive() and self.isAI then + + if delay and delay>0 then + -- Debug info. + self:T(self.lid..string.format("Check OPSGROUP done? [state=%s] in %.3f seconds...", fsmstate, delay)) + + -- Delayed call. + self:ScheduleOnce(delay, self._CheckGroupDone, self) + else + + -- Debug info. + self:T(self.lid..string.format("Check OSGROUP done? [state=%s]", fsmstate)) + + -- Group is engaging something. + if self:IsEngaging() then + self:T(self.lid.."Engaging! Group NOT done ==> UpdateRoute()") + self:UpdateRoute() + return + end + + -- Group is returning. + if self:IsReturning() then + self:T(self.lid.."Returning! Group NOT done...") + return + end + + -- Group is rearming. + if self:IsRearming() then + self:T(self.lid.."Rearming! Group NOT done...") + return + end + + -- Group is retreating. + if self:IsRetreating() then + self:T(self.lid.."Retreating! Group NOT done...") + return + end + + if self:IsBoarding() then + self:T(self.lid.."Boarding! Group NOT done...") + return + end + + -- Group is waiting. We deny all updates. + if self:IsWaiting() then + -- If group is waiting, we assume that is the way it is meant to be. + self:T(self.lid.."Waiting! Group NOT done...") + return + end + + -- Number of tasks remaining. + local nTasks=self:CountRemainingTasks() + + -- Number of mission remaining. + local nMissions=self:CountRemainingMissison() + + -- Number of cargo transports remaining. + local nTransports=self:CountRemainingTransports() + + -- Number of paused missions. + local nPaused=self:_CountPausedMissions() + + -- First check if there is a paused mission and that all remaining missions are paused. If there are other missions in the queue, we will run those. + if nPaused>0 and nPaused==nMissions then + local missionpaused=self:_GetPausedMission() + self:T(self.lid..string.format("Found paused mission %s [%s]. Unpausing mission...", missionpaused.name, missionpaused.type)) + self:UnpauseMission() + return + end + + -- Number of remaining tasks/missions? + if nTasks>0 or nMissions>0 or nTransports>0 then + self:T(self.lid..string.format("Group still has tasks, missions or transports ==> NOT DONE")) + return + end + + -- Get current waypoint. + local waypoint=self:GetWaypoint(self.currentwp) + + 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) + + -- Cruise. + self:Cruise(speed) + + -- Debug info. + self:T(self.lid..string.format("Adinfinitum=TRUE ==> Goto WP index=%d at speed=%d knots", i, speed)) + + else + self:T(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop")) + self:__FullStop(-1) + end + + else + + --- + -- Finite Patrol + --- + + if self:HasPassedFinalWaypoint() then + + --- + -- Passed FINAL waypoint + --- + + if self.legion and self.legionReturn then + + self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE, LEGION set ==> RTZ")) + if self.isArmygroup then + self:T2(self.lid.."RTZ to legion spawn zone") + self:RTZ(self.legion.spawnzone) + elseif self.isNavygroup then + self:T2(self.lid.."RTZ to legion port zone") + self:RTZ(self.legion.portzone) + end + + else + + -- No further waypoints. Command a full stop. + self:__FullStop(-1) + + self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE ==> Full Stop")) + + end + + 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:Cruise() + else + self:T(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() + + -- Cases we are not stuck. + if self:IsHolding() or self:Is("Rearming") or self:IsWaiting() or self:HasPassedFinalWaypoint() 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.1 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>=5*60 and holdtime<10*60 then + + -- Debug warning. + self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) + + -- Check what is happening. + if self:IsEngaging() then + self:__Disengage(1) + elseif self:IsReturning() then + self:T2(self.lid.."RTZ because of stuck") + self:__RTZ(1) + else + self:__Cruise(1) + end + + elseif holdtime>=10*60 and holdtime<30*60 then + + -- Debug warning. + self:T(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! + -- Look for a current mission and cancel it as we do not seem to be able to perform it. + local mission=self:GetMissionCurrent() + if mission then + self:T(self.lid..string.format("WARNING: Cancelling mission %s [%s] due to being stuck", mission:GetName(), mission:GetType())) + self:MissionCancel(mission) + else + -- Give cruise command again. + if self:IsReturning() then + self:T2(self.lid.."RTZ because of stuck") + self:__RTZ(1) + else + self:__Cruise(1) + end + end + + elseif holdtime>=30*60 then + + -- Debug warning. + self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime)) + + if self.legion then + self:T(self.lid..string.format("Asset is returned to its legion after being stuck!")) + self:ReturnToLegion() + end + + end + + end + +end + + +--- Check damage. +-- @param #OPSGROUP self +-- @return #OPSGROUP self +function OPSGROUP:_CheckDamage() + + self:T(self.lid..string.format("Checking damage...")) + + self.life=0 + local damaged=false + + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + + if element.status~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.ElementStatus.INUTERO then + + -- 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 (changed to shells) + if self.outofGuns and ammo.Shells>0 then + self.outofGuns=false + end + if ammo.Shells==0 and self.ammo.Shells>0 and not self.outofGuns then + self.outofGuns=true + self:OutOfGuns() + end + + -- Rockets. + if self.outofRockets and ammo.Rockets>0 then + self.outofRockets=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.outofBombs=false + end + if ammo.Bombs==0 and self.ammo.Bombs>0 and not self.outofBombs then + self.outofBombs=true + self:OutOfBombs() + end + + -- Missiles (All). + if self.outofMissiles and ammo.Missiles>0 then + self.outofMissiles=false + end + if ammo.Missiles==0 and self.ammo.Missiles>0 and not self.outofMissiles then + self.outofMissiles=true + self:OutOfMissiles() + end + + -- Missiles AA. + if self.outofMissilesAA and ammo.MissilesAA>0 then + self.outofMissilesAA=false + end + if ammo.MissilesAA==0 and self.ammo.MissilesAA>0 and not self.outofMissilesAA then + self.outofMissilesAA=true + self:OutOfMissilesAA() + end + + -- Missiles AG. + if self.outofMissilesAG and ammo.MissilesAG>0 then + self.outofMissilesAG=false + end + if ammo.MissilesAG==0 and self.ammo.MissilesAG>0 and not self.outofMissilesAG then + self.outofMissilesAG=true + self:OutOfMissilesAG() + end + + -- Missiles AS. + if self.outofMissilesAS and ammo.MissilesAS>0 then + self.outofMissilesAS=false + end + if ammo.MissilesAS==0 and self.ammo.MissilesAS>0 and not self.outofMissilesAS then + self.outofMissilesAS=true + self:OutOfMissilesAS() + end + + -- Torpedos. + if self.outofTorpedos and ammo.Torpedos>0 then + self.outofTorpedos=false + end + if ammo.Torpedos==0 and self.ammo.Torpedos>0 and not self.outofTorpedos then + self.outofTorpedos=true + self:OutOfTorpedos() + 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 +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Simple task function. Can be used to call a function which has the warehouse and the executing group as parameters. +-- @param #OPSGROUP self +-- @param #string Function The name of the function to call passed as string. +-- @param #number uid Waypoint UID. +function OPSGROUP:_SimpleTaskFunction(Function, uid) + + -- Task script. + local DCSScript = {} + + --_DATABASE:FindOpsGroup(groupname) + + DCSScript[#DCSScript+1] = string.format('local mygroup = _DATABASE:FindOpsGroup(\"%s\") ', self.groupname) -- The group that executes the task function. Very handy with the "...". + DCSScript[#DCSScript+1] = string.format('%s(mygroup, %d)', Function, uid) -- Call the function, e.g. myfunction.(warehouse,mygroup) + + -- Create task. + local DCSTask=CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) + + return DCSTask +end + +--- 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 + waypoint.temp=false + + -- Tasks of this waypoint + local taskswp={} + + -- At each waypoint report passing. + local TaskPassingWaypoint=self:_SimpleTaskFunction("OPSGROUP._PassingWaypoint", waypoint.uid) + table.insert(taskswp, TaskPassingWaypoint) + + -- Waypoint task combo. + waypoint.task=self.group:TaskCombo(taskswp) + + -- 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 with UID=%d", wpnumber, waypoint.uid)) + + -- Now we obviously did not pass the final waypoint. + if self.currentwp and wpnumber>self.currentwp then + self:_PassedFinalWaypoint(false, string.format("_AddWaypoint: wpnumber/index %d>%d self.currentwp", wpnumber, self.currentwp)) + end + +end + +--- Initialize Mission Editor waypoints. +-- @param #OPSGROUP self +-- @param #number WpIndexMin +-- @param #number WpIndexMax +-- @return #OPSGROUP self +function OPSGROUP:_InitWaypoints(WpIndexMin, WpIndexMax) + + -- Waypoints empty! + self.waypoints={} + self.waypoints0={} + + -- Get group template + local template=_DATABASE:GetGroupTemplate(self.groupname) + + if template==nil then + return self + end + + -- Template waypoints. + self.waypoints0=UTILS.DeepCopy(template.route.points) --self.group:GetTemplateRoutePoints() + + WpIndexMin=WpIndexMin or 1 + WpIndexMax=WpIndexMax or #self.waypoints0 + WpIndexMax=math.min(WpIndexMax, #self.waypoints0) --Ensure max is not out of bounce. + + --for index,wp in pairs(self.waypoints0) do + + for i=WpIndexMin,WpIndexMax do + + local wp=self.waypoints0[i] --DCS#Waypoint + + -- Coordinate of the waypoint. + local Coordinate=COORDINATE:NewFromWaypoint(wp) + + -- Strange! + wp.speed=wp.speed or 0 + + -- Speed at the waypoint. + local speedknots=UTILS.MpsToKnots(wp.speed) + + -- Expected speed to the first waypoint. + if i<=2 then + self.speedWp=wp.speed + self:T(self.lid..string.format("Expected/waypoint speed=%.1f m/s", self.speedWp)) + end + + -- Speed in knots. + local Speed=UTILS.MpsToKnots(wp.speed) + + -- Add waypoint. + local Waypoint=nil + if self:IsFlightgroup() then + Waypoint=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, nil, Altitude, false) + elseif self:IsArmygroup() then + Waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, Speed, nil, wp.action, false) + elseif self:IsNavygroup() then + Waypoint=NAVYGROUP.AddWaypoint(self, Coordinate, Speed, nil, Depth, false) + end + + -- Get DCS waypoint tasks set in the ME. EXPERIMENTAL! + local DCStasks=wp.task and wp.task.params.tasks or nil + if DCStasks and self.useMEtasks then + for _,DCStask in pairs(DCStasks) do + -- Wrapped Actions are commands. We do not take those. + if DCStask.id and DCStask.id~="WrappedAction" then + self:AddTaskWaypoint(DCStask,Waypoint, "ME Task") + end + end + end + + end + + -- Debug info. + self:T(self.lid..string.format("Initializing %d waypoints", #self.waypoints)) + + -- Flight group specific. + if self:IsFlightgroup() then + + -- Get home and destination airbases from waypoints. + self.homebase=self.homebase or self:GetHomebaseFromWaypoints() -- GetHomebaseFromWaypoints() returns carriers or destroyers if no airbase is found. + local destbase=self:GetDestinationFromWaypoints() + self.destbase=self.destbase or destbase + self.currbase=self:GetHomebaseFromWaypoints() -- Skipped To fix RTB issue + + --env.info("FF home base "..(self.homebase and self.homebase:GetName() or "unknown")) + --env.info("FF dest base "..(self.destbase and self.destbase:GetName() or "unknown")) + + -- Remove the landing waypoint. We use RTB for that. It makes adding new waypoints easier as we do not have to check if the last waypoint is the landing waypoint. + if destbase and #self.waypoints>1 then + table.remove(self.waypoints, #self.waypoints) + end + + -- Set destination to homebase. + if self.destbase==nil then -- Skipped To fix RTB issue + self.destbase=self.homebase + end + + end + + -- Update route. + if #self.waypoints>0 then + + -- Check if only 1 wp? + if #self.waypoints==1 then + self:_PassedFinalWaypoint(true, "_InitWaypoints: #self.waypoints==1") + end + + else + self:T(self.lid.."WARNING: No waypoints initialized. Number of waypoints is 0!") + 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 + + -- Clear all DCS tasks. NOTE: This can make DCS crash! + --self:ClearTasks() + + -- DCS mission task. + local DCSTask = { + id = 'Mission', + params = { + airborne = self:IsFlightgroup(), + route={points=waypoints}, + }, + } + + -- Set mission task. + self:SetTask(DCSTask) + + else + self:T(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 #OPSGROUP opsgroup Ops group object. +--@param #number uid Waypoint UID. +function OPSGROUP._PassingWaypoint(opsgroup, uid) + + -- Debug message. + local text=string.format("Group passing waypoint uid=%d", uid) + opsgroup:T(opsgroup.lid..text) + + -- Get waypoint data. + local waypoint=opsgroup:GetWaypointByID(uid) + + if waypoint then + + -- Increase passing counter. + waypoint.npassed=waypoint.npassed+1 + + -- Current wp. + local currentwp=opsgroup.currentwp + + -- Get the current waypoint index. + opsgroup.currentwp=opsgroup:GetWaypointIndex(uid) + + local wpistemp=waypoint.temp or waypoint.detour or waypoint.astar + + -- Remove temp waypoints. + if wpistemp then + opsgroup:RemoveWaypointByID(uid) + end + + -- Get next waypoint. Tricky part is that if + local wpnext=opsgroup:GetWaypointNext() + + if wpnext then --and (opsgroup.currentwp<#opsgroup.waypoints or opsgroup.adinfinitum or wpistemp) + + -- Debug info. + opsgroup:T(opsgroup.lid..string.format("Next waypoint UID=%d index=%d", wpnext.uid, opsgroup:GetWaypointIndex(wpnext.uid))) + + -- Set formation. + if opsgroup.isArmygroup then + opsgroup.option.Formation=wpnext.action + end + + -- Set speed to next wp. + opsgroup.speed=wpnext.speed + + if opsgroup.speed<0.01 then + opsgroup.speed=UTILS.KmphToMps(opsgroup.speedCruise) + end + + else + + -- Set passed final waypoint. + opsgroup:_PassedFinalWaypoint(true, "_PassingWaypoint No next Waypoint found") + + end + + -- Check if final waypoint was reached. + if opsgroup.currentwp==#opsgroup.waypoints and not (opsgroup.adinfinitum or wpistemp) then + -- Set passed final waypoint. + opsgroup:_PassedFinalWaypoint(true, "_PassingWaypoint currentwp==#waypoints and NOT adinfinitum and NOT a temporary waypoint") + end + + -- Trigger PassingWaypoint event. + if waypoint.temp then + + --- + -- Temporary Waypoint + --- + + if (opsgroup:IsNavygroup() or opsgroup:IsArmygroup()) and opsgroup.currentwp==#opsgroup.waypoints then + --TODO: not sure if this works with FLIGHTGROUPS + + -- Removing this for now. + opsgroup:Cruise() + end + + elseif waypoint.astar then + + --- + -- Pathfinding Waypoint + --- + + -- Cruise. + opsgroup:Cruise() + + elseif waypoint.detour then + + --- + -- Detour Waypoint + --- + + if opsgroup:IsRearming() then + + -- Trigger Rearming event. + opsgroup:Rearming() + + elseif opsgroup:IsRetreating() then + + -- Trigger Retreated event. + opsgroup:Retreated() + + elseif opsgroup:IsReturning() then + + -- Trigger Returned event. + opsgroup:Returned() + + elseif opsgroup:IsPickingup() then + + if opsgroup:IsFlightgroup() then + + -- Land at current pos and wait for 60 min max. + if opsgroup.cargoTZC then + + if opsgroup.cargoTZC.PickupAirbase then + -- Pickup airbase specified. Land there. + opsgroup:LandAtAirbase(opsgroup.cargoTZC.PickupAirbase) + else + -- Land somewhere in the pickup zone. Only helos can do that. + local coordinate=opsgroup.cargoTZC.PickupZone:GetRandomCoordinate(nil, nil, {land.SurfaceType.LAND}) + opsgroup:LandAt(coordinate, 60*60) + end + + else + local coordinate=opsgroup:GetCoordinate() + opsgroup:LandAt(coordinate, 60*60) + end + + + else + + -- Wait and load cargo. + opsgroup:FullStop() + opsgroup:__Loading(-5) + + end + + elseif opsgroup:IsTransporting() then + + if opsgroup:IsFlightgroup() then + + -- Land at current pos and wait for 60 min max. + if opsgroup.cargoTZC then + + if opsgroup.cargoTZC.DeployAirbase then + -- Deploy airbase specified. Land there. + opsgroup:LandAtAirbase(opsgroup.cargoTZC.DeployAirbase) + else + -- Land somewhere in the pickup zone. Only helos can do that. + local coordinate=opsgroup.cargoTZC.DeployZone:GetRandomCoordinate(nil, nil, {land.SurfaceType.LAND}) + opsgroup:LandAt(coordinate, 60*60) + end + + else + local coordinate=opsgroup:GetCoordinate() + opsgroup:LandAt(coordinate, 60*60) + end + + + else + -- Stop and unload. + opsgroup:FullStop() + opsgroup:Unloading() + end + + elseif opsgroup:IsBoarding() then + + local carrierGroup=opsgroup:_GetMyCarrierGroup() + local carrier=opsgroup:_GetMyCarrierElement() + + if carrierGroup and carrierGroup:IsAlive() then + + if carrier and carrier.unit and carrier.unit:IsAlive() then + + -- Load group into the carrier. + carrierGroup:Load(opsgroup) + + else + opsgroup:E(opsgroup.lid.."ERROR: Group cannot board assigned carrier UNIT as it is NOT alive!") + end + + else + opsgroup:E(opsgroup.lid.."ERROR: Group cannot board assigned carrier GROUP as it is NOT alive!") + end + + elseif opsgroup:IsEngaging() then + + -- Nothing to do really. + opsgroup:T(opsgroup.lid.."Passing engaging waypoint") + + 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") + opsgroup:FullStop() + end + + end + + else + + --- + -- Normal Route Waypoint + --- + + -- Check if the group is still pathfinding. + if opsgroup.ispathfinding then + opsgroup.ispathfinding=false + end + + -- 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:T(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:T(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:IsFlightgroup() then + + 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) + + -- Debug info. + 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:T(self.lid.."WARNING: Cannot switch ROT! Group is not alive") + end + + 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:T("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:T(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 the default EPLRS for the group. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true`, EPLRS is on by default. If `false` default EPLRS setting is off. If `nil`, default is on if group has EPLRS and off if it does not have a datalink. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultEPLRS(OnOffSwitch) + + if OnOffSwitch==nil then + self.optionDefault.EPLRS=self.isEPLRS + else + self.optionDefault.EPLRS=OnOffSwitch + end + + return self +end + +--- Switch EPLRS datalink on or off. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true` or `nil`, switch EPLRS on. If `false` EPLRS switched off. +-- @return #OPSGROUP self +function OPSGROUP:SwitchEPLRS(OnOffSwitch) + + if self:IsAlive() or self:IsInUtero() then + + if OnOffSwitch==nil then + + self.option.EPLRS=self.optionDefault.EPLRS + + else + + self.option.EPLRS=OnOffSwitch + + end + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current EPLRS=%s when GROUP is SPAWNED", tostring(self.option.EPLRS))) + else + + self.group:CommandEPLRS(self.option.EPLRS) + self:T(self.lid..string.format("Setting current EPLRS=%s", tostring(self.option.EPLRS))) + + end + else + self:E(self.lid.."WARNING: Cannot switch EPLRS! Group is not alive") + end + + return self +end + +--- Get current EPLRS state. +-- @param #OPSGROUP self +-- @return #boolean If `true`, EPLRS is on. +function OPSGROUP:GetEPLRS() + return self.option.EPLRS or self.optionDefault.EPLRS +end + +--- Set the default emission state for the group. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true`, EPLRS is on by default. If `false` default EPLRS setting is off. If `nil`, default is on if group has EPLRS and off if it does not have a datalink. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultEmission(OnOffSwitch) + + if OnOffSwitch==nil then + self.optionDefault.Emission=true + else + self.optionDefault.Emission=OnOffSwitch + end + + return self +end + +--- Switch emission on or off. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true` or `nil`, switch emission on. If `false` emission switched off. +-- @return #OPSGROUP self +function OPSGROUP:SwitchEmission(OnOffSwitch) + + if self:IsAlive() or self:IsInUtero() then + + if OnOffSwitch==nil then + + self.option.Emission=self.optionDefault.Emission + + else + + self.option.Emission=OnOffSwitch + + end + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current EMISSION=%s when GROUP is SPAWNED", tostring(self.option.Emission))) + else + + self.group:EnableEmission(self.option.Emission) + self:T(self.lid..string.format("Setting current EMISSION=%s", tostring(self.option.Emission))) + + end + else + self:E(self.lid.."WARNING: Cannot switch Emission! Group is not alive") + end + + return self +end + +--- Get current emission state. +-- @param #OPSGROUP self +-- @return #boolean If `true`, emission is on. +function OPSGROUP:GetEmission() + return self.option.Emission or self.optionDefault.Emission +end + +--- Set the default invisible for the group. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true`, group is ivisible by default. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultInvisible(OnOffSwitch) + + if OnOffSwitch==nil then + self.optionDefault.Invisible=true + else + self.optionDefault.Invisible=OnOffSwitch + end + + return self +end + +--- Switch invisibility on or off. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true` or `nil`, switch invisibliity on. If `false` invisibility switched off. +-- @return #OPSGROUP self +function OPSGROUP:SwitchInvisible(OnOffSwitch) + + if self:IsAlive() or self:IsInUtero() then + + if OnOffSwitch==nil then + + self.option.Invisible=self.optionDefault.Invisible + + else + + self.option.Invisible=OnOffSwitch + + end + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current INVISIBLE=%s when GROUP is SPAWNED", tostring(self.option.Invisible))) + else + + self.group:SetCommandInvisible(self.option.Invisible) + self:T(self.lid..string.format("Setting current INVISIBLE=%s", tostring(self.option.Invisible))) + + end + else + self:E(self.lid.."WARNING: Cannot switch Invisible! Group is not alive") + end + + return self +end + + +--- Set the default immortal for the group. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true`, group is immortal by default. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultImmortal(OnOffSwitch) + + if OnOffSwitch==nil then + self.optionDefault.Immortal=true + else + self.optionDefault.Immortal=OnOffSwitch + end + + return self +end + +--- Switch immortality on or off. +-- @param #OPSGROUP self +-- @param #boolean OnOffSwitch If `true` or `nil`, switch immortality on. If `false` immortality switched off. +-- @return #OPSGROUP self +function OPSGROUP:SwitchImmortal(OnOffSwitch) + + if self:IsAlive() or self:IsInUtero() then + + if OnOffSwitch==nil then + + self.option.Immortal=self.optionDefault.Immortal + + else + + self.option.Immortal=OnOffSwitch + + end + + if self:IsInUtero() then + self:T2(self.lid..string.format("Setting current IMMORTAL=%s when GROUP is SPAWNED", tostring(self.option.Immortal))) + else + + self.group:SetCommandImmortal(self.option.Immortal) + self:T(self.lid..string.format("Setting current IMMORTAL=%s", tostring(self.option.Immortal))) + + end + else + self:E(self.lid.."WARNING: Cannot switch Immortal! Group is not alive") + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- SETTINGS FUNCTIONS +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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:IsFlightgroup() 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:IsFlightgroup() 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:T(self.lid.."ERROR: Cound not set TACAN! Unit is not alive") + end + + else + self:T(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 + +--- Get current TACAN parameters. +-- @param #OPSGROUP self +-- @return #OPSGROUP.Beacon TACAN beacon. +function OPSGROUP:GetBeaconTACAN() + return self.tacan +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:T(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:IsFlightgroup() 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:T(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:IsFlightgroup() 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:T(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:IsFlightgroup() then + + self.group:SetOption(AI.Option.Air.id.FORMATION, Formation) + + elseif self.isArmygroup then + + -- Polymorphic and overwritten in ARMYGROUP. + + else + self:T(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 %s", tostring(self.option.Formation))) + + end + + return self +end + + + +--- Set default callsign. +-- @param #OPSGROUP self +-- @param #number CallsignName Callsign name. +-- @param #number CallsignNumber Callsign number. Default 1. +-- @return #OPSGROUP self +function OPSGROUP:SetDefaultCallsign(CallsignName, CallsignNumber) + + self:T(self.lid..string.format("Setting Default callsign %s-%s", tostring(CallsignName), tostring(CallsignNumber))) + + self.callsignDefault={} --#OPSGROUP.Callsign + self.callsignDefault.NumberSquad=CallsignName + self.callsignDefault.NumberGroup=CallsignNumber or 1 + self.callsignDefault.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) + + --self:I(self.lid..string.format("Default callsign=%s", self.callsignDefault.NameSquad)) + + 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) + --self.callsign=UTILS.DeepCopy(self.callsignDefault) + + 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) + + -- Callsign of the group, e.g. Colt-1 + self.callsignName=UTILS.GetCallsignName(self.callsign.NumberSquad).."-"..self.callsign.NumberGroup + self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad) + + -- Set callsign of elements. + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element.status~=OPSGROUP.ElementStatus.DEAD then + element.callsign=element.unit:GetCallsign() + end + end + + else + self:T(self.lid.."ERROR: Group is not alive and not in utero! Cannot switch callsign") + end + + return self +end + +--- Get callsign of the first element alive. +-- @param #OPSGROUP self +-- @param #boolean ShortCallsign If true, append major flight number only +-- @param #boolean Keepnumber (Player only) If true, and using a customized callsign in the #GROUP name after an #-sign, use all of that information. +-- @param #table CallsignTranslations (optional) Translation table between callsigns +-- @return #string Callsign name, e.g. Uzi11, or "Ghostrider11". +function OPSGROUP:GetCallsignName(ShortCallsign,Keepnumber,CallsignTranslations) + + local element=self:GetElementAlive() + + if element then + self:T2(self.lid..string.format("Callsign %s", tostring(element.callsign))) + local name=element.callsign or "Ghostrider11" + name=name:gsub("-", "") + if self.group:IsPlayer() or CallsignTranslations then + name=self.group:GetCustomCallSign(ShortCallsign,Keepnumber,CallsignTranslations) + end + return name + end + + return "Ghostrider11" +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:IsExist() 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() + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + element.vec3=self:GetVec3(element.name) + end + + -- 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 + + -- Travel distance since last check. + self.travelds=UTILS.VecNorm(UTILS.VecSubstract(self.position, self.positionLast)) + + -- Add up travelled distance. + self.traveldist=self.traveldist+self.travelds + + 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.INUTERO then + + -- Element INUTERO: Check that ALL others are also INUTERO + if element.status~=status then + return false + end + + + elseif 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.INUTERO then + --- + -- INUTERO + --- + + if self:_AllSimilarStatus(newstatus) then + self:InUtero() + end + + elseif newstatus==OPSGROUP.ElementStatus.SPAWNED then + --- + -- SPAWNED + --- + + if self:_AllSimilarStatus(newstatus) then + self:Spawned() + end + + elseif newstatus==OPSGROUP.ElementStatus.PARKING then + --- + -- PARKING + --- + + if self:_AllSimilarStatus(newstatus) then + self:Parking() + 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() + end + + elseif newstatus==OPSGROUP.ElementStatus.TAKEOFF then + --- + -- TAKEOFF + --- + + if self:_AllSimilarStatus(newstatus) then + -- Trigger takeoff event. Also triggers airborne event. + self:Takeoff(airbase) + end + + elseif newstatus==OPSGROUP.ElementStatus.AIRBORNE then + --- + -- AIRBORNE + --- + + if self:_AllSimilarStatus(newstatus) then + self:Airborne() + 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() + 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) + + if unitname and type(unitname)=="string" then + + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + + if element.name==unitname then + return element + end + + end + + end + + return nil +end + +--- Get the bounding box of the element. +-- @param #OPSGROUP self +-- @param #string UnitName Name of unit. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:GetElementZoneBoundingBox(UnitName) + + local element=self:GetElementByName(UnitName) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + -- Create a new zone if necessary. + element.zoneBoundingbox=element.zoneBoundingbox or ZONE_POLYGON_BASE:New(element.name.." Zone Bounding Box", {}) + + -- Length in meters. + local l=element.length + -- Width in meters. + local w=element.width + + -- Orientation vector. + local X=self:GetOrientationX(element.name) + + -- Heading in degrees. + local heading=math.deg(math.atan2(X.z, X.x)) + + -- Debug info. + self:T(self.lid..string.format("Element %s bouding box: l=%d w=%d heading=%d", element.name, l, w, heading)) + + -- Set of edges facing "North" at the origin of the map. + local b={} + b[1]={x=l/2, y=-w/2} --DCS#Vec2 + b[2]={x=l/2, y=w/2} --DCS#Vec2 + b[3]={x=-l/2, y=w/2} --DCS#Vec2 + b[4]={x=-l/2, y=-w/2} --DCS#Vec2 + + -- Rotate box to match current heading of the unit. + for i,p in pairs(b) do + b[i]=UTILS.Vec2Rotate2D(p, heading) + end + + -- Translate the zone to the positon of the unit. + local vec2=self:GetVec2(element.name) + local d=UTILS.Vec2Norm(vec2) + local h=UTILS.Vec2Hdg(vec2) + for i,p in pairs(b) do + b[i]=UTILS.Vec2Translate(p, d, h) + end + + -- Update existing zone. + element.zoneBoundingbox:UpdateFromVec2(b) + + return element.zoneBoundingbox + end + + return nil +end + +--- Get the loading zone of the element. +-- @param #OPSGROUP self +-- @param #string UnitName Name of unit. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:GetElementZoneLoad(UnitName) + + local element=self:GetElementByName(UnitName) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + element.zoneLoad=element.zoneLoad or ZONE_POLYGON_BASE:New(element.name.." Zone Load", {}) + + self:_GetElementZoneLoader(element, element.zoneLoad, self.carrierLoader) + + return element.zoneLoad + end + + return nil +end + +--- Get the unloading zone of the element. +-- @param #OPSGROUP self +-- @param #string UnitName Name of unit. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:GetElementZoneUnload(UnitName) + + local element=self:GetElementByName(UnitName) + + if element and element.status~=OPSGROUP.ElementStatus.DEAD then + + element.zoneUnload=element.zoneUnload or ZONE_POLYGON_BASE:New(element.name.." Zone Unload", {}) + + self:_GetElementZoneLoader(element, element.zoneUnload, self.carrierUnloader) + + return element.zoneUnload + end + + return nil +end + +--- Get/update the (un-)loading zone of the element. +-- @param #OPSGROUP self +-- @param #OPSGROUP.Element Element Element. +-- @param Core.Zone#ZONE_POLYGON Zone The zone. +-- @param #OPSGROUP.CarrierLoader Loader Loader parameters. +-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone. +function OPSGROUP:_GetElementZoneLoader(Element, Zone, Loader) + + if Element.status~=OPSGROUP.ElementStatus.DEAD then + + local l=Element.length + local w=Element.width + + -- Orientation 3D vector where the "nose" is pointing. + local X=self:GetOrientationX(Element.name) + + -- Heading in deg. + local heading=math.deg(math.atan2(X.z, X.x)) + + -- Bounding box at the origin of the map facing "North". + local b={} + + -- Create polygon rectangles. + if Loader.type:lower()=="front" then + table.insert(b, {x= l/2, y=-Loader.width/2}) -- left, low + table.insert(b, {x= l/2+Loader.length, y=-Loader.width/2}) -- left, up + table.insert(b, {x= l/2+Loader.length, y= Loader.width/2}) -- right, up + table.insert(b, {x= l/2, y= Loader.width/2}) -- right, low + elseif Loader.type:lower()=="back" then + table.insert(b, {x=-l/2, y=-Loader.width/2}) -- left, low + table.insert(b, {x=-l/2-Loader.length, y=-Loader.width/2}) -- left, up + table.insert(b, {x=-l/2-Loader.length, y= Loader.width/2}) -- right, up + table.insert(b, {x=-l/2, y= Loader.width/2}) -- right, low + elseif Loader.type:lower()=="left" then + table.insert(b, {x= Loader.length/2, y= -w/2}) -- right, up + table.insert(b, {x= Loader.length/2, y= -w/2-Loader.width}) -- left, up + table.insert(b, {x=-Loader.length/2, y= -w/2-Loader.width}) -- left, down + table.insert(b, {x=-Loader.length/2, y= -w/2}) -- right, down + elseif Loader.type:lower()=="right" then + table.insert(b, {x= Loader.length/2, y= w/2}) -- right, up + table.insert(b, {x= Loader.length/2, y= w/2+Loader.width}) -- left, up + table.insert(b, {x=-Loader.length/2, y= w/2+Loader.width}) -- left, down + table.insert(b, {x=-Loader.length/2, y= w/2}) -- right, down + else + -- All aspect. Rectangle around the unit but need to cut out the area of the unit itself. + b[1]={x= l/2, y=-w/2} --DCS#Vec2 + b[2]={x= l/2, y= w/2} --DCS#Vec2 + b[3]={x=-l/2, y= w/2} --DCS#Vec2 + b[4]={x=-l/2, y=-w/2} --DCS#Vec2 + table.insert(b, {x=b[1].x+Loader.length, y=b[1].y-Loader.width}) + table.insert(b, {x=b[2].x+Loader.length, y=b[2].y+Loader.width}) + table.insert(b, {x=b[3].x-Loader.length, y=b[3].y+Loader.width}) + table.insert(b, {x=b[4].x-Loader.length, y=b[4].y-Loader.width}) + end + + -- Rotate edges to match the current heading of the unit. + for i,p in pairs(b) do + b[i]=UTILS.Vec2Rotate2D(p, heading) + end + + -- Translate box to the current position of the unit. + local vec2=self:GetVec2(Element.name) + local d=UTILS.Vec2Norm(vec2) + local h=UTILS.Vec2Hdg(vec2) + + for i,p in pairs(b) do + b[i]=UTILS.Vec2Translate(p, d, h) + end + + -- Update existing zone. + Zone:UpdateFromVec2(b) + + return Zone + 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.Shells=0 + Ammo.Guns=0 + Ammo.Cannons=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 or {}) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit and unit:IsExist() then + + -- Get ammo of the unit. + local ammo=self:GetAmmoUnit(unit) + + -- Add up total. + Ammo.Total=Ammo.Total+ammo.Total + Ammo.Shells=Ammo.Shells+ammo.Shells + Ammo.Guns=Ammo.Guns+ammo.Guns + Ammo.Cannons=Ammo.Cannons+ammo.Cannons + 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 nguns=0 + local ncannons=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 + + unit=unit or self.group:GetUnit(1) + + if unit and unit:IsExist() then + + -- 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 + + --self:I(ammotable) + --UTILS.PrintTableToLog(ammotable) + + -- Loop over all weapons. + for w=1,weapons do + + -- Number of current weapon. + local Nammo=ammotable[w]["count"] + + -- Range in meters. Seems only to exist for missiles (not shells). + local rmin=ammotable[w]["desc"]["rangeMin"] or 0 + local rmax=ammotable[w]["desc"]["rangeMaxAltMin"] or 0 + + -- 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 + + -- Add small and large caliber shells for guns and cannons + if ammotable[w]["desc"]["warhead"] and ammotable[w]["desc"]["warhead"]["caliber"] then + local caliber=ammotable[w]["desc"]["warhead"]["caliber"] + if caliber<25 then + nguns=nguns+Nammo + else + ncannons=ncannons+Nammo + end + end + + -- Debug info. + text=text..string.format("- %d shells of type %s, range=%d - %d meters\n", Nammo, _weaponName, rmin, rmax) + + 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, rmin, rmax) + + 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 + nmissilesBM=nmissilesBM+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, range=%d - %d meters\n", Nammo, self:_MissileCategoryName(MissileCategory), _weaponName, rmin, rmax) + + 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 + + end + + -- Total amount of ammunition. + nammo=nshells+nrockets+nmissiles+nbombs+ntorps + + local ammo={} --#OPSGROUP.Ammo + ammo.Total=nammo + ammo.Shells=nshells + ammo.Guns=nguns + ammo.Cannons=ncannons + 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 + +--- Set passed final waypoint value. +-- @param #OPSGROUP self +-- @param #boolean final If `true`, final waypoint was passed. +-- @param #string comment Some comment as to why the final waypoint was passed. +function OPSGROUP:_PassedFinalWaypoint(final, comment) + + -- Debug info. + self:T(self.lid..string.format("Passed final waypoint=%s [from %s]: comment \"%s\"", tostring(final), tostring(self.passedfinalwp), tostring(comment))) + + if final==true and not self.passedfinalwp then + self:PassedFinalWaypoint() + end + + -- Set value. + self.passedfinalwp=final +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 then + 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") + local coord=Object:GetCoordinate() + return coord + else + self:T(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!") + end + end + else + self:T(self.lid.."ERROR: Object passed is nil!") + end + + return nil +end + +--- Check if a unit is an element of the flightgroup. +-- @param #OPSGROUP self +-- @param #string unitname Name of unit. +-- @return #boolean If true, unit is element of the flight group or false if otherwise. +function OPSGROUP:_IsElement(unitname) + + for _,_element in pairs(self.elements) do + local element=_element --Ops.OpsGroup#OPSGROUP.Element + + if element.name==unitname then + return true + end + + end + + return false +end + +--- Count elements of the group. +-- @param #OPSGROUP self +-- @param #table States (Optional) Only count elements in specific states. Can also be a single state passed as #string. +-- @return #number Number of elements. +function OPSGROUP:CountElements(States) + + if States then + if type(States)=="string" then + States={States} + end + else + States=OPSGROUP.ElementStatus + end + + local IncludeDeads=true + + local N=0 + for _,_element in pairs(self.elements) do + local element=_element --#OPSGROUP.Element + if element and (IncludeDeads or element.status~=OPSGROUP.ElementStatus.DEAD) then + for _,state in pairs(States) do + if element.status==state then + N=N+1 + break + end + end + end + end + + return N +end + +--- Add a unit/element to the OPS group. +-- @param #OPSGROUP self +-- @param #string unitname Name of unit. +-- @return #OPSGROUP.Element The element or nil. +function OPSGROUP:_AddElementByName(unitname) + + local unit=UNIT:FindByName(unitname) + + if unit then + + -- Element table. + local element=self:GetElementByName(unitname) + + -- Add element to table. + if element then + -- We already know this element. + else + -- Add a new element. + element={} + element.status=OPSGROUP.ElementStatus.INUTERO + table.insert(self.elements, element) + end + + -- Name and status. + element.name=unitname + + -- Unit and group. + element.unit=unit + element.DCSunit=Unit.getByName(unitname) + element.gid=element.DCSunit:getNumber() + element.uid=element.DCSunit:getID() + --element.group=unit:GetGroup() + element.controller=element.DCSunit:getController() + element.Nhit=0 + element.opsgroup=self + + -- Get unit template. + local unittemplate=unit:GetTemplate() + + if unittemplate==nil then + if element.DCSunit:getPlayerName() then + element.skill="Client" + end + else + element.skill=unittemplate~=nil and unittemplate.skill or "Unknown" + end + + -- Skill etc. + if element.skill=="Client" or element.skill=="Player" then + element.ai=false + element.client=CLIENT:FindByName(unitname) + element.playerName=element.DCSunit:getPlayerName() + else + element.ai=true + end + + -- Descriptors and type/category. + element.descriptors=unit:GetDesc() + element.category=unit:GetUnitCategory() + element.categoryname=unit:GetCategoryName() + element.typename=unit:GetTypeName() + + -- Describtors. + --self:I({desc=element.descriptors}) + + -- Ammo. + element.ammo0=self:GetAmmoUnit(unit, false) + + -- Life points. + element.life=unit:GetLife() + element.life0=math.max(unit:GetLife0(), element.life) -- Some units report a life0 that is smaller than its initial life points. + + -- Size and dimensions. + element.size, element.length, element.height, element.width=unit:GetObjectSize() + + -- Weight and cargo. + element.weightEmpty=element.descriptors.massEmpty or 666 + + if self.isArmygroup then + + element.weightMaxTotal=element.weightEmpty+10*95 --If max mass is not given, we assume 10 soldiers. + + elseif self.isNavygroup then + + element.weightMaxTotal=element.weightEmpty+10*1000 + + else + + -- Looks like only aircraft have a massMax value in the descriptors. + element.weightMaxTotal=element.descriptors.massMax or element.weightEmpty+8*95 --If max mass is not given, we assume 8 soldiers. + + end + + -- Max cargo weight: + unit:SetCargoBayWeightLimit() + element.weightMaxCargo=unit.__.CargoBayWeightLimit + + -- Cargo bay (empty). + if element.cargoBay then + -- After a respawn, the cargo bay might not be empty! + element.weightCargo=self:GetWeightCargo(element.name, false) + else + element.cargoBay={} + element.weightCargo=0 + end + element.weight=element.weightEmpty+element.weightCargo + + -- FLIGHTGROUP specific. + element.callsign=element.unit:GetCallsign() + element.fuelmass=element.fuelmass0 or 99999 + element.fuelrel=element.unit:GetFuel() or 1 + + if self.isFlightgroup and unittemplate then + element.modex=unittemplate.onboard_num + element.payload=unittemplate.payload + element.pylons=unittemplate.payload and unittemplate.payload.pylons or nil + element.fuelmass0=unittemplate.payload and unittemplate.payload.fuel or 0 + else + element.callsign="Peter-1-1" + element.modex="000" + element.payload={} + element.pylons={} + end + + -- Debug text. + local text=string.format("Adding element %s: status=%s, skill=%s, life=%.1f/%.1f category=%s (%d), type=%s, size=%.1f (L=%.1f H=%.1f W=%.1f), weight=%.1f/%.1f (cargo=%.1f/%.1f)", + element.name, element.status, element.skill, element.life, element.life0, element.categoryname, element.category, element.typename, + element.size, element.length, element.height, element.width, element.weight, element.weightMaxTotal, element.weightCargo, element.weightMaxCargo) + self:T(self.lid..text) + + -- Trigger spawned event if alive. + if unit:IsAlive() and element.status~=OPSGROUP.ElementStatus.SPAWNED then + -- This needs to be slightly delayed (or moved elsewhere) or the first element will always trigger the group spawned event as it is not known that more elements are in the group. + self:__ElementSpawned(0.05, element) + end + + return element + end + + return nil +end + +--- Set the template of the group. +-- @param #OPSGROUP self +-- @param #table Template Template to set. Default is from the GROUP. +-- @return #OPSGROUP self +function OPSGROUP:_SetTemplate(Template) + + -- Set the template. + self.template=Template or UTILS.DeepCopy(_DATABASE:GetGroupTemplate(self.groupname)) --self.group:GetTemplate() + + -- Debug info. + self:T3(self.lid.."Setting group template") + + return self +end + +--- Get the template of the group. +-- @param #OPSGROUP self +-- @param #boolean Copy Get a deep copy of the template. +-- @return #table Template table. +function OPSGROUP:_GetTemplate(Copy) + + if self.template then + + if Copy then + local template=UTILS.DeepCopy(self.template) + return template + else + return self.template + end + + else + self:T(self.lid..string.format("ERROR: No template was set yet!")) + end + + return nil +end + +--- Clear waypoints. +-- @param #OPSGROUP self +-- @param #number IndexMin Clear waypoints up to this min WP index. Default 1. +-- @param #number IndexMax Clear waypoints up to this max WP index. Default `#self.waypoints`. +function OPSGROUP:ClearWaypoints(IndexMin, IndexMax) + + IndexMin=IndexMin or 1 + IndexMax=IndexMax or #self.waypoints + + -- Clear all waypoints. + for i=IndexMax,IndexMin,-1 do + table.remove(self.waypoints, i) + end + --self.waypoints={} +end + +--- Get target group. +-- @param #OPSGROUP self +-- @return Wrapper.Group#GROUP Detected target group. +-- @return #number Distance to target. +function OPSGROUP:_GetDetectedTarget() + + -- Target. + local targetgroup=nil --Wrapper.Group#GROUP + local targetdist=math.huge + + -- Loop over detected groups. + for _,_group in pairs(self.detectedgroups:GetSet()) do + local group=_group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + -- Get 3D vector of target. + local targetVec3=group:GetVec3() + + -- Distance to target. + local distance=UTILS.VecDist3D(self.position, targetVec3) + + if distance<=self.engagedetectedRmax and distance SCHEDULED --> EXECUTING --> DELIVERED + self:AddTransition("*", "Planned", OPSTRANSPORT.Status.PLANNED) -- Cargo transport was planned. + self:AddTransition(OPSTRANSPORT.Status.PLANNED, "Queued", OPSTRANSPORT.Status.QUEUED) -- Cargo is queued at at least one carrier. + self:AddTransition(OPSTRANSPORT.Status.QUEUED, "Requested", OPSTRANSPORT.Status.REQUESTED) -- Transport assets have been requested from a warehouse. + self:AddTransition(OPSTRANSPORT.Status.REQUESTED, "Scheduled", OPSTRANSPORT.Status.SCHEDULED) -- Cargo is queued at at least one carrier. + self:AddTransition(OPSTRANSPORT.Status.PLANNED, "Scheduled", OPSTRANSPORT.Status.SCHEDULED) -- Cargo is queued at at least one carrier. + self:AddTransition(OPSTRANSPORT.Status.SCHEDULED, "Executing", OPSTRANSPORT.Status.EXECUTING) -- Cargo is being transported. + self:AddTransition("*", "Delivered", OPSTRANSPORT.Status.DELIVERED) -- Cargo was delivered. + + self:AddTransition("*", "StatusUpdate", "*") + self:AddTransition("*", "Stop", "*") + + self:AddTransition("*", "Cancel", OPSTRANSPORT.Status.CANCELLED) -- Command to cancel the transport. + + self:AddTransition("*", "Loaded", "*") + self:AddTransition("*", "Unloaded", "*") + + self:AddTransition("*", "DeadCarrierUnit", "*") + self:AddTransition("*", "DeadCarrierGroup", "*") + self:AddTransition("*", "DeadCarrierAll", "*") + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "StatusUpdate". + -- @function [parent=#OPSTRANSPORT] StatusUpdate + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#OPSTRANSPORT] __StatusUpdate + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Planned". + -- @function [parent=#OPSTRANSPORT] Planned + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Planned" after a delay. + -- @function [parent=#OPSTRANSPORT] __Planned + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Planned" event. + -- @function [parent=#OPSTRANSPORT] OnAfterPlanned + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Queued". + -- @function [parent=#OPSTRANSPORT] Queued + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Queued" after a delay. + -- @function [parent=#OPSTRANSPORT] __Queued + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Queued" event. + -- @function [parent=#OPSTRANSPORT] OnAfterQueued + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Requested". + -- @function [parent=#OPSTRANSPORT] Requested + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Requested" after a delay. + -- @function [parent=#OPSTRANSPORT] __Requested + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Requested" event. + -- @function [parent=#OPSTRANSPORT] OnAfterRequested + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Scheduled". + -- @function [parent=#OPSTRANSPORT] Scheduled + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Scheduled" after a delay. + -- @function [parent=#OPSTRANSPORT] __Scheduled + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Scheduled" event. + -- @function [parent=#OPSTRANSPORT] OnAfterScheduled + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Executing". + -- @function [parent=#OPSTRANSPORT] Executing + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Executing" after a delay. + -- @function [parent=#OPSTRANSPORT] __Executing + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Executing" event. + -- @function [parent=#OPSTRANSPORT] OnAfterExecuting + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Delivered". + -- @function [parent=#OPSTRANSPORT] Delivered + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Delivered" after a delay. + -- @function [parent=#OPSTRANSPORT] __Delivered + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Delivered" event. + -- @function [parent=#OPSTRANSPORT] OnAfterDelivered + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Cancel". + -- @function [parent=#OPSTRANSPORT] Cancel + -- @param #OPSTRANSPORT self + + --- Triggers the FSM event "Cancel" after a delay. + -- @function [parent=#OPSTRANSPORT] __Cancel + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + + --- On after "Cancel" event. + -- @function [parent=#OPSTRANSPORT] OnAfterCancel + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Loaded". + -- @function [parent=#OPSTRANSPORT] Loaded + -- @param #OPSTRANSPORT self + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element. + + --- Triggers the FSM event "Loaded" after a delay. + -- @function [parent=#OPSTRANSPORT] __Loaded + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element. + + --- On after "Loaded" event. + -- @function [parent=#OPSTRANSPORT] OnAfterLoaded + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier. + -- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element. + + + --- Triggers the FSM event "Unloaded". + -- @function [parent=#OPSTRANSPORT] Unloaded + -- @param #OPSTRANSPORT self + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo. + + --- Triggers the FSM event "Unloaded" after a delay. + -- @function [parent=#OPSTRANSPORT] __Unloaded + -- @param #OPSTRANSPORT self + -- @param #number delay Delay in seconds. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo. + + --- On after "Unloaded" event. + -- @function [parent=#OPSTRANSPORT] OnAfterUnloaded + -- @param #OPSTRANSPORT self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. + -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo. + + + --TODO: Psydofunctions + + -- Call status update. + self:__StatusUpdate(-1) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add pickup and deploy zone combination. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP CargoGroups Groups to be transported as cargo. Can also be a single @{Wrapper.Group#GROUP} or @{Ops.OpsGroup#OPSGROUP} object. +-- @param Core.Zone#ZONE PickupZone Zone where the troops are picked up. +-- @param Core.Zone#ZONE DeployZone Zone where the troops are picked up. +-- @return #OPSTRANSPORT.TransportZoneCombo Transport zone table. +function OPSTRANSPORT:AddTransportZoneCombo(CargoGroups, PickupZone, DeployZone) + + -- Increase counter. + self.tzcCounter=self.tzcCounter+1 + + local tzcombo={} --#OPSTRANSPORT.TransportZoneCombo + + -- Init. + tzcombo.uid=self.tzcCounter + tzcombo.Ncarriers=0 + tzcombo.Ncargo=0 + tzcombo.Cargos={} + tzcombo.RequiredCargos={} + tzcombo.DisembarkCarriers={} + tzcombo.PickupPaths={} + tzcombo.TransportPaths={} + + -- Set zones. + self:SetPickupZone(PickupZone, tzcombo) + self:SetDeployZone(DeployZone, tzcombo) + self:SetEmbarkZone(nil, tzcombo) + + -- Add cargo groups (could also be added later). + if CargoGroups then + self:AddCargoGroups(CargoGroups, tzcombo) + end + + -- Add to table. + table.insert(self.tzCombos, tzcombo) + + return tzcombo +end + +--- Add cargo groups to be transported. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP GroupSet Set of groups to be transported. Can also be passed as a single GROUP or OPSGROUP object. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @param #boolean DisembarkActivation If `true`, cargo group is activated when disembarked. If `false`, cargo groups are late activated when disembarked. Default `nil` (usually activated). +-- @param Core.Zone#ZONE DisembarkZone Zone where the groups disembark to. +-- @param Core.Set#SET_OPSGROUP DisembarkCarriers Carrier groups where the cargo directly disembarks to. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddCargoGroups(GroupSet, TransportZoneCombo, DisembarkActivation, DisembarkZone, DisembarkCarriers) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + -- Check type of GroupSet provided. + if GroupSet:IsInstanceOf("GROUP") or GroupSet:IsInstanceOf("OPSGROUP") then + + -- We got a single GROUP or OPSGROUP object. + local cargo=self:_CreateCargoGroupData(GroupSet, TransportZoneCombo, DisembarkActivation, DisembarkZone, DisembarkCarriers) + + if cargo then + + -- Add to main table. + --table.insert(self.cargos, cargo) + self.Ncargo=self.Ncargo+1 + + -- Add to TZC table. + table.insert(TransportZoneCombo.Cargos, cargo) + TransportZoneCombo.Ncargo=TransportZoneCombo.Ncargo+1 + + cargo.opsgroup:_AddMyLift(self) + + end + + else + + -- We got a SET_GROUP object. + + for _,group in pairs(GroupSet.Set) do + + -- Call iteravely for each group. + self:AddCargoGroups(group, TransportZoneCombo, DisembarkActivation) + + end + + -- Use FSM function to keep the SET up-to-date. Note that it overwrites the user FMS function, which cannot be used any more now. + local groupset=GroupSet --Core.Set#SET_OPSGROUP + function groupset.OnAfterAdded(groupset, From, Event, To, ObjectName, Object) + + self:T(self.lid..string.format("Adding Cargo Group %s", tostring(ObjectName))) + self:AddCargoGroups(Object, TransportZoneCombo, DisembarkActivation, DisembarkZone, DisembarkCarriers) + + end + + end + + -- Debug info. + if self.verbose>=1 then + local text=string.format("Added cargo groups:") + local Weight=0 + for _,_cargo in pairs(self:GetCargos()) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + local weight=cargo.opsgroup:GetWeightTotal() + Weight=Weight+weight + text=text..string.format("\n- %s [%s] weight=%.1f kg", cargo.opsgroup:GetName(), cargo.opsgroup:GetState(), weight) + end + text=text..string.format("\nTOTAL: Ncargo=%d, Weight=%.1f kg", self.Ncargo, Weight) + self:I(self.lid..text) + end + + + return self +end + +--- Add cargo warehouse storage to be transported. This adds items such as fuel, weapons and other equipment, which is to be transported +-- from one DCS warehouse to another. +-- For weapons and equipment, the weight per item has to be specified explicitly as these cannot be retrieved by the DCS API. For liquids the +-- default value of 1 kg per item should be used as the amount of liquid is already given in kg. +-- @param #OPSTRANSPORT self +-- @param Wrapper.Storage#STORAGE StorageFrom Storage warehouse from which the cargo is taken. +-- @param Wrapper.Storage#STORAGE StorageTo Storage warehouse to which the cargo is delivered. +-- @param #string CargoType Type of cargo, *e.g.* `"weapons.bombs.Mk_84"` or liquid type as #number. +-- @param #number CargoAmount Amount of cargo. Liquids in kg. +-- @param #number CargoWeight Weight of a single cargo item in kg. Default 1 kg. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo if other than default. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddCargoStorage(StorageFrom, StorageTo, CargoType, CargoAmount, CargoWeight, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + -- Cargo data. + local cargo=self:_CreateCargoStorage(StorageFrom,StorageTo, CargoType, CargoAmount, CargoWeight, TransportZoneCombo) + + if cargo then + + -- Add total amount of ever assigned cargos. + self.Ncargo=self.Ncargo+1 + + -- Add to TZC table. + table.insert(TransportZoneCombo.Cargos, cargo) + + end + +end + + +--- Set pickup zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE PickupZone Zone where the troops are picked up. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetPickupZone(PickupZone, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + TransportZoneCombo.PickupZone=PickupZone + + if PickupZone and PickupZone:IsInstanceOf("ZONE_AIRBASE") then + TransportZoneCombo.PickupAirbase=PickupZone._.ZoneAirbase + end + + return self +end + +--- Get pickup zone. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return Core.Zone#ZONE Zone where the troops are picked up. +function OPSTRANSPORT:GetPickupZone(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.PickupZone +end + +--- Set deploy zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE DeployZone Zone where the troops are deployed. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDeployZone(DeployZone, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + -- Set deploy zone. + TransportZoneCombo.DeployZone=DeployZone + + -- Check if this is an airbase. + if DeployZone and DeployZone:IsInstanceOf("ZONE_AIRBASE") then + TransportZoneCombo.DeployAirbase=DeployZone._.ZoneAirbase + end + + return self +end + +--- Get deploy zone. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return Core.Zone#ZONE Zone where the troops are deployed. +function OPSTRANSPORT:GetDeployZone(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.DeployZone +end + +--- Set embark zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE EmbarkZone Zone where the troops are embarked. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetEmbarkZone(EmbarkZone, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + TransportZoneCombo.EmbarkZone=EmbarkZone or TransportZoneCombo.PickupZone + + return self +end + +--- Get embark zone. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return Core.Zone#ZONE Zone where the troops are embarked from. +function OPSTRANSPORT:GetEmbarkZone(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.EmbarkZone +end + +--- Set disembark zone. +-- @param #OPSTRANSPORT self +-- @param Core.Zone#ZONE DisembarkZone Zone where the troops are disembarked. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkZone(DisembarkZone, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + TransportZoneCombo.DisembarkZone=DisembarkZone + + return self +end + +--- Get disembark zone. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return Core.Zone#ZONE Zone where the troops are disembarked to. +function OPSTRANSPORT:GetDisembarkZone(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.DisembarkZone +end + +--- Set activation status of group when disembarked from transport carrier. +-- @param #OPSTRANSPORT self +-- @param #boolean Active If `true` or `nil`, group is activated when disembarked. If `false`, group is late activated and needs to be activated manually. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkActivation(Active, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + if Active==true or Active==nil then + TransportZoneCombo.disembarkActivation=true + else + TransportZoneCombo.disembarkActivation=false + end + + return self +end + +--- Get disembark activation. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #boolean If `true`, groups are spawned in late activated state. +function OPSTRANSPORT:GetDisembarkActivation(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.disembarkActivation +end + +--- Set/add transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP Carriers Carrier set. Can also be passed as a #GROUP, #OPSGROUP or #SET_OPSGROUP object. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkCarriers(Carriers, TransportZoneCombo) + + -- Debug info. + self:T(self.lid.."Setting transfer carriers!") + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + -- Set that we want to disembark to carriers. + TransportZoneCombo.disembarkToCarriers=true + + self:_AddDisembarkCarriers(Carriers, TransportZoneCombo.DisembarkCarriers) + + return self +end + +--- Set/add transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP Carriers Carrier set. Can also be passed as a #GROUP, #OPSGROUP or #SET_OPSGROUP object. +-- @param #table Table the table to add. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:_AddDisembarkCarriers(Carriers, Table) + + if Carriers:IsInstanceOf("GROUP") or Carriers:IsInstanceOf("OPSGROUP") then + + local carrier=self:_GetOpsGroupFromObject(Carriers) + if carrier then + table.insert(Table, carrier) + end + + elseif Carriers:IsInstanceOf("SET_GROUP") or Carriers:IsInstanceOf("SET_OPSGROUP") then + + for _,object in pairs(Carriers:GetSet()) do + local carrier=self:_GetOpsGroupFromObject(object) + if carrier then + table.insert(Table, carrier) + end + end + + else + self:E(self.lid.."ERROR: Carriers must be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object!") + end + + +end + +--- Get transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #table Table of carrier OPS groups. +function OPSTRANSPORT:GetDisembarkCarriers(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.DisembarkCarriers +end + + +--- Set if group remains *in utero* after disembarkment from carrier. Can be used to directly load the group into another carrier. Similar to disembark in late activated state. +-- @param #OPSTRANSPORT self +-- @param #boolean InUtero If `true` or `nil`, group remains *in utero* after disembarkment. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetDisembarkInUtero(InUtero, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + if InUtero==true or InUtero==nil then + TransportZoneCombo.disembarkInUtero=true + else + TransportZoneCombo.disembarkInUtero=false + end + + return self +end + +--- Get disembark in utero. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #boolean If `true`, groups stay in utero after disembarkment. +function OPSTRANSPORT:GetDisembarkInUtero(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.disembarkInUtero +end + +--- Set pickup formation. +-- @param #OPSTRANSPORT self +-- @param #number Formation Pickup formation. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetFormationPickup(Formation, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + TransportZoneCombo.PickupFormation=Formation + + return self +end + +--- Get pickup formation. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP OpsGroup +-- @return #string Formation. +function OPSTRANSPORT:_GetFormationDefault(OpsGroup) + + if OpsGroup.isArmygroup then + + return self.formationArmy + + elseif OpsGroup.isFlightgroup then + + if OpsGroup.isHelo then + return self.formationHelo + else + return self.formationPlane + end + + else + return ENUMS.Formation.Vehicle.OffRoad + end + + return nil +end + +--- Get pickup formation. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup +-- @return #number Formation. +function OPSTRANSPORT:_GetFormationPickup(TransportZoneCombo, OpsGroup) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + local formation=TransportZoneCombo.PickupFormation or self:_GetFormationDefault(OpsGroup) + + return formation +end + +--- Set transport formation. +-- @param #OPSTRANSPORT self +-- @param #number Formation Pickup formation. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetFormationTransport(Formation, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + TransportZoneCombo.TransportFormation=Formation + + return self +end + +--- Get transport formation. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup +-- @return #number Formation. +function OPSTRANSPORT:_GetFormationTransport(TransportZoneCombo, OpsGroup) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + local formation=TransportZoneCombo.TransportFormation or self:_GetFormationDefault(OpsGroup) + + return formation +end + + + +--- Set required cargo. This is a list of cargo groups that need to be loaded before the **first** transport will start. +-- @param #OPSTRANSPORT self +-- @param Core.Set#SET_GROUP Cargos Required cargo set. Can also be passed as a #GROUP, #OPSGROUP or #SET_OPSGROUP object. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetRequiredCargos(Cargos, TransportZoneCombo) + + -- Debug info. + self:T(self.lid.."Setting required cargos!") + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + -- Create table. + TransportZoneCombo.RequiredCargos=TransportZoneCombo.RequiredCargos or {} + + if Cargos:IsInstanceOf("GROUP") or Cargos:IsInstanceOf("OPSGROUP") then + + local cargo=self:_GetOpsGroupFromObject(Cargos) + if cargo then + table.insert(TransportZoneCombo.RequiredCargos, cargo) + end + + elseif Cargos:IsInstanceOf("SET_GROUP") or Cargos:IsInstanceOf("SET_OPSGROUP") then + + for _,object in pairs(Cargos:GetSet()) do + local cargo=self:_GetOpsGroupFromObject(object) + if cargo then + table.insert(TransportZoneCombo.RequiredCargos, cargo) + end + end + + else + self:E(self.lid.."ERROR: Required Cargos must be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object!") + end + + return self +end + +--- Get required cargos. This is a list of cargo groups that need to be loaded before the **first** transport will start. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #table Table of required cargo ops groups. +function OPSTRANSPORT:GetRequiredCargos(TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + return TransportZoneCombo.RequiredCargos +end + +--- Set number of required carrier groups for an OPSTRANSPORT assignment. Only used if transport is assigned at **LEGION** or higher level. +-- @param #OPSTRANSPORT self +-- @param #number NcarriersMin Number of carriers *at least* required. Default 1. +-- @param #number NcarriersMax Number of carriers *at most* used for transportation. Default is same as `NcarriersMin`. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetRequiredCarriers(NcarriersMin, NcarriersMax) + + self.nCarriersMin=NcarriersMin or 1 + + self.nCarriersMax=NcarriersMax or self.nCarriersMin + + -- Ensure that max is at least equal to min. + if self.nCarriersMax0 then + self:ScheduleOnce(Delay, OPSTRANSPORT._DelCarrier, self, CarrierGroup) + else + if self:IsCarrier(CarrierGroup) then + + for i=#self.carriers,1,-1 do + local carrier=self.carriers[i] --Ops.OpsGroup#OPSGROUP + if carrier.groupname==CarrierGroup.groupname then + self:T(self.lid..string.format("Removing carrier %s", CarrierGroup.groupname)) + table.remove(self.carriers, i) + end + end + + end + end + + return self +end + +--- Get a list of alive carriers. +-- @param #OPSTRANSPORT self +-- @return #table Names of all carriers +function OPSTRANSPORT:_GetCarrierNames() + + local names={} + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if carrier:IsAlive()~=nil then + table.insert(names, carrier.groupname) + end + end + + return names +end + +--- Get (all) cargo @{Ops.OpsGroup#OPSGROUP}s. Optionally, only delivered or undelivered groups can be returned. +-- @param #OPSTRANSPORT self +-- @param #boolean Delivered If `true`, only delivered groups are returned. If `false` only undelivered groups are returned. If `nil`, all groups are returned. +-- @param Ops.OpsGroup#OPSGROUP Carrier (Optional) Only count cargo groups that fit into the given carrier group. Current cargo is not a factor. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #table Cargo Ops groups. Can be and empty table `{}`. +function OPSTRANSPORT:GetCargoOpsGroups(Delivered, Carrier, TransportZoneCombo) + + local cargos=self:GetCargos(TransportZoneCombo, Carrier, Delivered) + + local opsgroups={} + for _,_cargo in pairs(cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + if cargo.type=="OPSGROUP" then + if cargo.opsgroup and not (cargo.opsgroup:IsDead() or cargo.opsgroup:IsStopped()) then + table.insert(opsgroups, cargo.opsgroup) + end + end + end + + return opsgroups +end + +--- Get (all) cargo @{Ops.OpsGroup#OPSGROUP}s. Optionally, only delivered or undelivered groups can be returned. +-- @param #OPSTRANSPORT self +-- @param #boolean Delivered If `true`, only delivered groups are returned. If `false` only undelivered groups are returned. If `nil`, all groups are returned. +-- @param Ops.OpsGroup#OPSGROUP Carrier (Optional) Only count cargo groups that fit into the given carrier group. Current cargo is not a factor. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #table Cargo Ops groups. Can be and empty table `{}`. +function OPSTRANSPORT:GetCargoStorages(Delivered, Carrier, TransportZoneCombo) + + local cargos=self:GetCargos(TransportZoneCombo, Carrier, Delivered) + + local opsgroups={} + for _,_cargo in pairs(cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + if cargo.type=="STORAGE" then + table.insert(opsgroups, cargo.storage) + end + end + + return opsgroups +end + +--- Get carriers. +-- @param #OPSTRANSPORT self +-- @return #table Carrier Ops groups. +function OPSTRANSPORT:GetCarriers() + return self.carriers +end + +--- Get cargos. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @param Ops.OpsGroup#OPSGROUP Carrier Specific carrier. +-- @param #boolean Delivered Delivered status. +-- @return #table Cargos. +function OPSTRANSPORT:GetCargos(TransportZoneCombo, Carrier, Delivered) + + local tczs=self.tzCombos + if TransportZoneCombo then + tczs={TransportZoneCombo} + end + + local cargos={} + for _,_tcz in pairs(tczs) do + local tcz=_tcz --#OPSTRANSPORT.TransportZoneCombo + for _,_cargo in pairs(tcz.Cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + if Delivered==nil or cargo.delivered==Delivered then + if Carrier==nil or Carrier:CanCargo(cargo) then + table.insert(cargos, cargo) + end + end + end + end + + return cargos +end + +--- Get total weight. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP.CargoGroup Cargo Cargo data. +-- @param #boolean IncludeReserved Include reserved cargo. +-- @return #number Weight in kg. +function OPSTRANSPORT:GetCargoTotalWeight(Cargo, IncludeReserved) + + local weight=0 + + if Cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then + weight=Cargo.opsgroup:GetWeightTotal(nil, IncludeReserved) + else + if type(Cargo.storage.cargoType)=="number" then + if IncludeReserved then + return Cargo.storage.cargoAmount+Cargo.storage.cargoReserved + else + return Cargo.storage.cargoAmount + end + else + if IncludeReserved then + return Cargo.storage.cargoAmount*100 -- Assume 100 kg per item + else + return (Cargo.storage.cargoAmount+Cargo.storage.cargoReserved)*100 -- Assume 100 kg per item + end + end + end + + return weight +end + +--- Set transport start and stop time. +-- @param #OPSTRANSPORT self +-- @param #string ClockStart Time the transport 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 transport 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 #OPSTRANSPORT self +function OPSTRANSPORT: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 #OPSTRANSPORT self +-- @param #number Prio Priority 1=high, 100=low. Default 50. +-- @param #number Importance Number 1-10. If missions with lower value are in the queue, these have to be finished first. Default is `nil`. +-- @param #boolean Urgent If *true*, another running mission might be cancelled if it has a lower priority. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetPriority(Prio, Importance, Urgent) + self.prio=Prio or 50 + self.urgent=Urgent + self.importance=Importance + return self +end + +--- Set verbosity. +-- @param #OPSTRANSPORT self +-- @param #number Verbosity Be more verbose. Default 0 +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetVerbosity(Verbosity) + self.verbose=Verbosity or 0 + return self +end + +--- Add start condition. +-- @param #OPSTRANSPORT self +-- @param #function ConditionFunction Function that needs to be true before the transport can be started. Must return a #boolean. +-- @param ... Condition function arguments if any. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddConditionStart(ConditionFunction, ...) + + if ConditionFunction then + + local condition={} --#OPSTRANSPORT.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionStart, condition) + + end + + return self +end + +--- Add path used for transportation from the pickup to the deploy zone. +-- If multiple paths are defined, a random one is chosen. The path is retrieved from the waypoints of a given group. +-- **NOTE** that the category group defines for which carriers this path is valid. +-- For example, if you specify a GROUND group to provide the waypoints, only assigned GROUND carriers will use the +-- path. +-- @param #OPSTRANSPORT self +-- @param Wrapper.Group#GROUP PathGroup A (late activated) GROUP defining a transport path by their waypoints. +-- @param #number Radius Randomization radius in meters. Default 0 m. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport Zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddPathTransport(PathGroup, Reversed, Radius, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + if type(PathGroup)=="string" then + PathGroup=GROUP:FindByName(PathGroup) + end + + local path={} --#OPSTRANSPORT.Path + path.category=PathGroup:GetCategory() + path.radius=Radius or 0 + path.waypoints=PathGroup:GetTaskRoute() + + -- TODO: Check that only flyover waypoints are given for aircraft. + + -- Add path. + table.insert(TransportZoneCombo.TransportPaths, path) + + return self +end + +--- Get a path for transportation. +-- @param #OPSTRANSPORT self +-- @param #number Category Group category. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport Zone combo. +-- @return #OPSTRANSPORT.Path The path object. +function OPSTRANSPORT:_GetPathTransport(Category, TransportZoneCombo) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + local pathsTransport=TransportZoneCombo.TransportPaths + + if pathsTransport and #pathsTransport>0 then + + local paths={} + + for _,_path in pairs(pathsTransport) do + local path=_path --#OPSTRANSPORT.Path + if path.category==Category then + table.insert(paths, path) + end + end + + if #paths>0 then + + local path=paths[math.random(#paths)] --#OPSTRANSPORT.Path + + return path + end + end + + return nil +end + +--- Add a carrier assigned for this transport. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Carrier OPSGROUP. +-- @param #string Status Carrier Status. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetCarrierTransportStatus(CarrierGroup, Status) + + -- Old status + local oldstatus=self:GetCarrierTransportStatus(CarrierGroup) + + -- Debug info. + self:T(self.lid..string.format("New carrier transport status for %s: %s --> %s", CarrierGroup:GetName(), oldstatus, Status)) + + -- Set new status. + self.carrierTransportStatus[CarrierGroup.groupname]=Status + + return self +end + +--- Get carrier transport status. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Carrier OPSGROUP. +-- @return #string Carrier status. +function OPSTRANSPORT:GetCarrierTransportStatus(CarrierGroup) + local status=self.carrierTransportStatus[CarrierGroup.groupname] or "unknown" + return status +end + +--- Get unique ID of the transport assignment. +-- @param #OPSTRANSPORT self +-- @return #number UID. +function OPSTRANSPORT:GetUID() + return self.uid +end + +--- Get number of delivered cargo groups. +-- @param #OPSTRANSPORT self +-- @return #number Total number of delivered cargo groups. +function OPSTRANSPORT:GetNcargoDelivered() + return self.Ndelivered +end + +--- Get number of cargo groups. +-- @param #OPSTRANSPORT self +-- @return #number Total number of cargo groups. +function OPSTRANSPORT:GetNcargoTotal() + return self.Ncargo +end + +--- Get number of carrier groups assigned for this transport. +-- @param #OPSTRANSPORT self +-- @return #number Total number of carrier groups. +function OPSTRANSPORT:GetNcarrier() + return self.Ncarrier +end + +--- Add carrier asset to transport. +-- @param #OPSTRANSPORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be added. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddAsset(Asset, TransportZoneCombo) + + -- Debug info + self:T(self.lid..string.format("Adding asset carrier \"%s\" to transport", tostring(Asset.spawngroupname))) + + -- Add asset to table. + self.assets=self.assets or {} + + table.insert(self.assets, Asset) + + return self +end + +--- Delete carrier asset from transport. +-- @param #OPSTRANSPORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be removed. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:DelAsset(Asset) + + for i,_asset in pairs(self.assets or {}) do + local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem + + if asset.uid==Asset.uid then + self:T(self.lid..string.format("Removing asset \"%s\" from transport", tostring(Asset.spawngroupname))) + table.remove(self.assets, i) + return self + end + + end + + return self +end + +--- Add cargo asset. +-- @param #OPSTRANSPORT self +-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be added. +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddAssetCargo(Asset, TransportZoneCombo) + + -- Debug info + self:T(self.lid..string.format("Adding asset cargo \"%s\" to transport and TZC=%s", tostring(Asset.spawngroupname), TransportZoneCombo and TransportZoneCombo.uid or "N/A")) + + -- Add asset to table. + self.assetsCargo=self.assetsCargo or {} + + table.insert(self.assetsCargo, Asset) + + TransportZoneCombo.assetsCargo=TransportZoneCombo.assetsCargo or {} + + TransportZoneCombo.assetsCargo[Asset.spawngroupname]=Asset + + return self +end + +--- Get transport zone combo of cargo group. +-- @param #OPSTRANSPORT self +-- @param #string GroupName Group name of cargo. +-- @return #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +function OPSTRANSPORT:GetTZCofCargo(GroupName) + + for _,_tzc in pairs(self.tzCombos) do + local tzc=_tzc --#OPSTRANSPORT.TransportZoneCombo + for _,_cargo in pairs(tzc.Cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + if cargo.opsgroup:GetName()==GroupName then + return tzc + end + end + end + + return nil +end + +--- Add LEGION to the transport. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:AddLegion(Legion) + + -- Debug info. + self:T(self.lid..string.format("Adding legion %s", Legion.alias)) + + -- Add legion to table. + table.insert(self.legions, Legion) + + return self +end + +--- Remove LEGION from transport. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:RemoveLegion(Legion) + + -- Loop over legions + for i=#self.legions,1,-1 do + local legion=self.legions[i] --Ops.Legion#LEGION + if legion.alias==Legion.alias then + -- Debug info. + self:T(self.lid..string.format("Removing legion %s", Legion.alias)) + table.remove(self.legions, i) + return self + end + end + + self:E(self.lid..string.format("ERROR: Legion %s not found and could not be removed!", Legion.alias)) + return self +end + +--- Check if an OPS group is assigned as carrier for this transport. +-- @param #OPSTRANSPORT self +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Potential carrier OPSGROUP. +-- @return #boolean If true, group is an assigned carrier. +function OPSTRANSPORT:IsCarrier(CarrierGroup) + + if CarrierGroup then + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if carrier.groupname==CarrierGroup.groupname then + return true + end + end + end + + return false +end + +--- Check if transport is ready to be started. +-- * Start time passed. +-- * Stop time did not pass already. +-- * All start conditions are true. +-- @param #OPSTRANSPORT self +-- @return #boolean If true, mission can be started. +function OPSTRANSPORT:IsReadyToGo() + + -- Debug text. + local text=self.lid.."Is ReadyToGo? " + + -- Current abs time. + local Tnow=timer.getAbsTime() + + -- Pickup AND deploy zones must be set. + local gotzones=false + for _,_tz in pairs(self.tzCombos) do + local tz=_tz --#OPSTRANSPORT.TransportZoneCombo + if tz.PickupZone and tz.DeployZone then + gotzones=true + break + end + end + if not gotzones then + text=text.."No, pickup/deploy zone combo not yet defined!" + return false + end + + -- Start time did not pass yet. + if self.Tstart and Tnowself.Tstop then + text=text.."Nope, stop time already passed!" + self:T(text) + return false + end + + -- All start conditions true? + local startme=self:EvalConditionsAll(self.conditionStart) + + -- Nope, not yet. + if not startme then + text=text..("No way, at least one start condition is not true!") + self:T(text) + return false + end + + -- We're good to go! + text=text.."Yes!" + self:T(text) + return true +end + +--- Set LEGION transport status. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion The legion. +-- @param #string Status New status. +-- @return #OPSTRANSPORT self +function OPSTRANSPORT:SetLegionStatus(Legion, Status) + + -- Old status + local status=self:GetLegionStatus(Legion) + + -- Debug info. + self:T(self.lid..string.format("Setting LEGION %s to status %s-->%s", Legion.alias, tostring(status), tostring(Status))) + + -- New status. + self.statusLegion[Legion.alias]=Status + + return self +end + +--- Get LEGION transport status. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion The legion. +-- @return #string status Current status. +function OPSTRANSPORT:GetLegionStatus(Legion) + + -- Current status. + local status=self.statusLegion[Legion.alias] or "unknown" + + return status +end + +--- Check if state is PLANNED. +-- @param #OPSTRANSPORT self +-- @return #boolean If true, status is PLANNED. +function OPSTRANSPORT:IsPlanned() + local is=self:is(OPSTRANSPORT.Status.PLANNED) + return is +end + +--- Check if state is QUEUED. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion (Optional) Check if transport is queued at this legion. +-- @return #boolean If true, status is QUEUED. +function OPSTRANSPORT:IsQueued(Legion) + local is=self:is(OPSTRANSPORT.Status.QUEUED) + if Legion then + is=self:GetLegionStatus(Legion)==OPSTRANSPORT.Status.QUEUED + end + return is +end + +--- Check if state is REQUESTED. +-- @param #OPSTRANSPORT self +-- @param Ops.Legion#LEGION Legion (Optional) Check if transport is queued at this legion. +-- @return #boolean If true, status is REQUESTED. +function OPSTRANSPORT:IsRequested(Legion) + local is=self:is(OPSTRANSPORT.Status.REQUESTED) + if Legion then + is=self:GetLegionStatus(Legion)==OPSTRANSPORT.Status.REQUESTED + end + return is +end + +--- Check if state is SCHEDULED. +-- @param #OPSTRANSPORT self +-- @return #boolean If true, status is SCHEDULED. +function OPSTRANSPORT:IsScheduled() + local is=self:is(OPSTRANSPORT.Status.SCHEDULED) + return is +end + +--- Check if state is EXECUTING. +-- @param #OPSTRANSPORT self +-- @return #boolean If true, status is EXECUTING. +function OPSTRANSPORT:IsExecuting() + local is=self:is(OPSTRANSPORT.Status.EXECUTING) + return is +end + +--- Check if all cargo was delivered (or is dead). +-- @param #OPSTRANSPORT self +-- @param #number Nmin Number of groups that must be actually delivered (and are not dead). Default 0. +-- @return #boolean If true, all possible cargo was delivered. +function OPSTRANSPORT:IsDelivered(Nmin) + local is=self:is(OPSTRANSPORT.Status.DELIVERED) + +-- Nmin=Nmin or 0 +-- if Nmin>self.Ncargo then +-- Nmin=self.Ncargo +-- end +-- +-- if self.Ndelivered=math.min(self.Ncargo, Nmin) then + is=true + end + + return is +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Update +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "StatusUpdate" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterStatusUpdate(From, Event, To) + + -- Current FSM state. + local fsmstate=self:GetState() + + if self.verbose>=1 then + + -- Info text. + local text=string.format("%s: Ncargo=%d/%d, Ncarrier=%d/%d, Nlegions=%d", fsmstate:upper(), self.Ncargo, self.Ndelivered, #self.carriers, self.Ncarrier, #self.legions) + + -- Info about cargo and carrier. + if self.verbose>=2 then + + for i,_tz in pairs(self.tzCombos) do + local tz=_tz --#OPSTRANSPORT.TransportZoneCombo + local pickupzone=tz.PickupZone and tz.PickupZone:GetName() or "Unknown" + local deployzone=tz.DeployZone and tz.DeployZone:GetName() or "Unknown" + text=text..string.format("\n[%d] %s --> %s: Ncarriers=%d, Ncargo=%d (%d)", i, pickupzone, deployzone, tz.Ncarriers, #tz.Cargos, tz.Ncargo) + end + + end + + -- Info about cargo and carrier. + if self.verbose>=3 then + + text=text..string.format("\nCargos:") + for _,_cargo in pairs(self:GetCargos()) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then + local carrier=cargo.opsgroup:_GetMyCarrierElement() + local name=carrier and carrier.name or "none" + local cstate=carrier and carrier.status or "N/A" + text=text..string.format("\n- %s: %s [%s], weight=%d kg, carrier=%s [%s], delivered=%s [UID=%s]", + cargo.opsgroup:GetName(), cargo.opsgroup.cargoStatus:upper(), cargo.opsgroup:GetState(), cargo.opsgroup:GetWeightTotal(), name, cstate, tostring(cargo.delivered), tostring(cargo.opsgroup.cargoTransportUID)) + else + --TODO: Storage + local storage=cargo.storage + text=text..string.format("\n- storage type=%s: amount: total=%d loaded=%d, lost=%d, delivered=%d, delivered=%s [UID=%s]", + storage.cargoType, storage.cargoAmount, storage.cargoLoaded, storage.cargoLost, storage.cargoDelivered, tostring(cargo.delivered), tostring(cargo.uid)) + end + end + + text=text..string.format("\nCarriers:") + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + text=text..string.format("\n- %s: %s [%s], Cargo Bay [current/reserved/total]=%d/%d/%d kg [free %d/%d/%d kg]", + carrier:GetName(), carrier.carrierStatus:upper(), carrier:GetState(), + carrier:GetWeightCargo(nil, false), carrier:GetWeightCargo(), carrier:GetWeightCargoMax(), + carrier:GetFreeCargobay(nil, false), carrier:GetFreeCargobay(), carrier:GetFreeCargobayMax()) + end + end + + self:I(self.lid..text) + end + + -- Check if all cargo was delivered (or is dead). + self:_CheckDelivered() + + -- Update status again. + if not self:IsDelivered() then + self:__StatusUpdate(-30) + end +end + +--- Check if a cargo group was delivered. +-- @param #OPSTRANSPORT self +-- @param #string GroupName Name of the group. +-- @return #boolean If `true`, cargo was delivered. +function OPSTRANSPORT:IsCargoDelivered(GroupName) + + for _,_cargo in pairs(self:GetCargos()) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + if cargo.opsgroup:GetName()==GroupName then + return cargo.delivered + end + + end + + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "Planned" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterPlanned(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) +end + +--- On after "Scheduled" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterScheduled(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) +end + +--- On after "Executing" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterExecuting(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) +end + +--- On before "Delivered" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onbeforeDelivered(From, Event, To) + + -- Check that we do not call delivered again. + if From==OPSTRANSPORT.Status.DELIVERED then + return false + end + + return true +end + +--- On after "Delivered" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterDelivered(From, Event, To) + self:T(self.lid..string.format("New status: %s-->%s", From, To)) + + -- Inform all assigned carriers that cargo was delivered. They can have this in the queue or are currently processing this transport. + for i=#self.carriers, 1, -1 do + local carrier=self.carriers[i] --Ops.OpsGroup#OPSGROUP + if self:GetCarrierTransportStatus(carrier)~=OPSTRANSPORT.Status.DELIVERED then + carrier:Delivered(self) + end + end + +end + +--- On after "Loaded" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier. +-- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element. +function OPSTRANSPORT:onafterLoaded(From, Event, To, OpsGroupCargo, OpsGroupCarrier, CarrierElement) + self:I(self.lid..string.format("Loaded OPSGROUP %s into carrier %s", OpsGroupCargo:GetName(), tostring(CarrierElement.name))) +end + +--- On after "Unloaded" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier. +-- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo. +function OPSTRANSPORT:onafterUnloaded(From, Event, To, OpsGroupCargo, OpsGroupCarrier) + self:I(self.lid..string.format("Unloaded OPSGROUP %s", OpsGroupCargo:GetName())) +end + +--- On after "DeadCarrierGroup" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Ops.OpsGroup#OPSGROUP OpsGroup Carrier OPSGROUP that is dead. +function OPSTRANSPORT:onafterDeadCarrierGroup(From, Event, To, OpsGroup) + self:I(self.lid..string.format("Carrier OPSGROUP %s dead!", OpsGroup:GetName())) + + -- Increase dead counter. + self.NcarrierDead=self.NcarrierDead+1 + + -- Remove group from carrier list/table. + self:_DelCarrier(OpsGroup) + + if #self.carriers==0 then + self:DeadCarrierAll() + end +end + +--- On after "DeadCarrierAll" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterDeadCarrierAll(From, Event, To) + self:I(self.lid..string.format("ALL Carrier OPSGROUPs are dead!")) + + if self.opszone then + + self:I(self.lid..string.format("Cancelling transport on CHIEF level")) + self.chief:TransportCancel(self) + + --for _,_legion in pairs(self.legions) do + -- local legion=_legion --Ops.Legion#LEGION + -- legion:TransportCancel(self) + --end + + else + + -- Check if cargo was delivered. + self:_CheckDelivered() + + -- Set state back to PLANNED if not delivered. + if not self:IsDelivered() then + self:Planned() + end + + end + +end + +--- On after "Cancel" event. +-- @param #OPSTRANSPORT self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSTRANSPORT:onafterCancel(From, Event, To) + + -- Number of OPSGROUPS assigned and alive. + local Ngroups = #self.carriers + + -- Debug info. + self:I(self.lid..string.format("CANCELLING transport in status %s. Will wait for %d carrier groups to report DONE before evaluation", self:GetState(), Ngroups)) + + -- Time stamp. + self.Tover=timer.getAbsTime() + + + if self.chief then + + -- Debug info. + self:T(self.lid..string.format("CHIEF will cancel the transport. Will wait for mission DONE before evaluation!")) + + -- CHIEF will cancel the transport. + self.chief:TransportCancel(self) + + elseif self.commander then + + -- Debug info. + self:T(self.lid..string.format("COMMANDER will cancel the transport. Will wait for transport DELIVERED before evaluation!")) + + -- COMMANDER will cancel the transport. + self.commander:TransportCancel(self) + + elseif self.legions and #self.legions>0 then + + -- Loop over all LEGIONs. + for _,_legion in pairs(self.legions or {}) do + local legion=_legion --Ops.Legion#LEGION + + -- Debug info. + self:T(self.lid..string.format("LEGION %s will cancel the transport. Will wait for transport DELIVERED before evaluation!", legion.alias)) + + -- Legion will cancel all flight missions and remove queued request from warehouse queue. + legion:TransportCancel(self) + + end + + else + + -- Debug info. + self:T(self.lid..string.format("No legion, commander or chief. Attached OPS groups will cancel the transport on their own. Will wait for transport DELIVERED before evaluation!")) + + -- Loop over all carrier groups. + for _,_carrier in pairs(self:GetCarriers()) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + carrier:TransportCancel(self) + end + + -- Delete awaited transport. + local cargos=self:GetCargoOpsGroups(false) + for _,_cargo in pairs(cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP + cargo:_DelMyLift(self) + end + + + end + + -- Special mission states. + if self:IsPlanned() or self:IsQueued() or self:IsRequested() or Ngroups==0 then + self:T(self.lid..string.format("Cancelled transport was in %s stage with %d carrier groups assigned and alive. Call it DELIVERED!", self:GetState(), Ngroups)) + self:Delivered() + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if all cargo of this transport assignment was delivered. +-- @param #OPSTRANSPORT self +function OPSTRANSPORT:_CheckDelivered() + + -- First check that at least one cargo was added (as we allow to do that later). + if self.Ncargo>0 then + + local done=true + local dead=true + for _,_cargo in pairs(self:GetCargos()) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + + if cargo.delivered then + -- This one is delivered. + dead=false + elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup==nil then + -- This one is nil?! + dead=false + elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup:IsDestroyed() then + -- This one was destroyed. + elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup:IsDead() then + -- This one is dead. + elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup:IsStopped() then + -- This one is stopped. + dead=false + else + done=false --Someone is not done! + dead=false + end + + end + + if dead then + self:I(self.lid.."All cargo DEAD ==> Delivered!") + self:Delivered() + elseif done then + self:I(self.lid.."All cargo DONE ==> Delivered!") + self:Delivered() + end + + end + +end + +--- Check if all required cargos are loaded. +-- @param #OPSTRANSPORT self +-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo. +-- @param Ops.OpsGroup#OPSGROUP CarrierGroup The carrier group asking. +-- @return #boolean If true, all required cargos are loaded or there is no required cargo or asking carrier is full. +function OPSTRANSPORT:_CheckRequiredCargos(TransportZoneCombo, CarrierGroup) + + -- Use default TZC if no transport zone combo is provided. + TransportZoneCombo=TransportZoneCombo or self.tzcDefault + + -- Use input or take all cargos. + local requiredCargos=TransportZoneCombo.Cargos + + -- Check if required cargos was set by user. + if TransportZoneCombo.RequiredCargos and #TransportZoneCombo.RequiredCargos>0 then + requiredCargos=TransportZoneCombo.RequiredCargos + else + requiredCargos={} + for _,_cargo in pairs(TransportZoneCombo.Cargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup + table.insert(requiredCargos, cargo.opsgroup) + end + end + + if requiredCargos==nil or #requiredCargos==0 then + return true + end + + -- All carrier names. + local carrierNames=self:_GetCarrierNames() + + -- Cargo groups not loaded yet. + local weightmin=nil + + for _,_cargo in pairs(requiredCargos) do + local cargo=_cargo --Ops.OpsGroup#OPSGROUP + + -- Is this cargo loaded into any carrier? + local isLoaded=cargo:IsLoaded(carrierNames) + + if not isLoaded then + local weight=cargo:GetWeightTotal() + + if weightmin==nil or weight=1 then + + -- Distance to the carrier in meters. + local dist=tz.PickupZone:Get2DDistance(vec2) + + local ncarriers=0 + for _,_carrier in pairs(self.carriers) do + local carrier=_carrier --Ops.OpsGroup#OPSGROUP + if carrier and carrier:IsAlive() and carrier.cargoTZC and carrier.cargoTZC.uid==tz.uid then + ncarriers=ncarriers+1 + end + end + + -- New candidate. + local candidate={tzc=tz, distance=dist/1000, ncargo=ncargo, ncarriers=ncarriers} + + -- Calculdate penalty of candidate. + candidate.penalty=penalty(candidate) + + -- Add candidate. + table.insert(candidates, candidate) + + end + end + end + + if #candidates>0 then + + -- Minimize penalty. + local function optTZC(candA, candB) + return candA.penalty=3 then + local text="TZC optimized" + for i,candidate in pairs(candidates) do + text=text..string.format("\n[%d] TPZ=%d, Ncarriers=%d, Ncargo=%d, Distance=%.1f km, PENALTY=%d", i, candidate.tzc.uid, candidate.ncarriers, candidate.ncargo, candidate.distance, candidate.penalty) + end + self:I(self.lid..text) + end + + -- Return best candidate. + return candidates[1].tzc + else + -- No candidates. + self:T(self.lid..string.format("Could NOT find a pickup zone (with cargo) for carrier group %s", Carrier:GetName())) + end + + return nil +end + +--- Get an OPSGROUP from a given OPSGROUP or GROUP object. If the object is a GROUP, an OPSGROUP is created automatically. +-- @param #OPSTRANSPORT self +-- @param Core.Base#BASE Object The object, which can be a GROUP or OPSGROUP. +-- @return Ops.OpsGroup#OPSGROUP Ops Group. +function OPSTRANSPORT:_GetOpsGroupFromObject(Object) + + local opsgroup=nil + + if Object:IsInstanceOf("OPSGROUP") then + -- We already have an OPSGROUP + opsgroup=Object + elseif Object:IsInstanceOf("GROUP") then + + -- Look into DB and try to find an existing OPSGROUP. + opsgroup=_DATABASE:GetOpsGroup(Object) + + if not opsgroup then + if Object:IsAir() then + opsgroup=FLIGHTGROUP:New(Object) + elseif Object:IsShip() then + opsgroup=NAVYGROUP:New(Object) + else + opsgroup=ARMYGROUP:New(Object) + end + end + + else + self:E(self.lid.."ERROR: Object must be a GROUP or OPSGROUP object!") + return nil + end + + return opsgroup +end diff --git a/Moose Development/Moose/Ops/OpsZone.lua b/Moose Development/Moose/Ops/OpsZone.lua new file mode 100644 index 000000000..9a47b16ae --- /dev/null +++ b/Moose Development/Moose/Ops/OpsZone.lua @@ -0,0 +1,1668 @@ +--- **Ops** - Strategic Zone. +-- +-- **Main Features:** +-- +-- * Monitor if a zone is captured +-- * Monitor if an airbase is captured +-- * Define conditions under which zones are captured/held +-- * Supports circular and polygon zone shapes +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- @module Ops.OpsZone +-- @image OPS_OpsZone.png + + +--- OPSZONE class. +-- @type OPSZONE +-- @field #string ClassName Name of the class. +-- @field #string lid DCS log ID string. +-- @field #number verbose Verbosity of output. +-- @field Core.Zone#ZONE zone The zone. +-- @field Core.Zone#ZONE_RADIUS zoneCircular The circular zone. +-- @field Wrapper.Airbase#AIRBASE airbase The airbase that is monitored. +-- @field #string airbaseName Name of the airbase that is monitored. +-- @field #string zoneName Name of the zone. +-- @field #number zoneRadius Radius of the zone in meters. +-- @field #number ownerCurrent Coalition of the current owner of the zone. +-- @field #number ownerPrevious Coalition of the previous owner of the zone. +-- @field Core.Timer#TIMER timerStatus Timer for calling the status update. +-- @field #number Nred Number of red units in the zone. +-- @field #number Nblu Number of blue units in the zone. +-- @field #number Nnut Number of neutral units in the zone. +-- @field #table Ncoal Number of units in zone for each coalition. +-- @field #number Tred Threat level of red units in the zone. +-- @field #number Tblu Threat level of blue units in the zone. +-- @field #number Tnut Threat level of neutral units in the zone. +-- @field #number TminCaptured Time interval in seconds how long an attacker must have troops inside the zone to capture. +-- @field #number Tcaptured Time stamp (abs.) when the attacker destroyed all owning troops. +-- @field #table ObjectCategories Object categories for the scan. +-- @field #table UnitCategories Unit categories for the scan. +-- @field #number Tattacked Abs. mission time stamp when an attack was started. +-- @field #number dTCapture Time interval in seconds until a zone is captured. +-- @field #boolean neutralCanCapture Neutral units can capture. Default `false`. +-- @field #boolean drawZone If `true`, draw the zone on the F10 map. +-- @field #boolean markZone If `true`, mark the zone on the F10 map. +-- @field Wrapper.Marker#MARKER marker Marker on the F10 map. +-- @field #string markerText Text shown in the maker. +-- @field #table chiefs Chiefs that monitor this zone. +-- @field #table Missions Missions that are attached to this OpsZone. +-- @field #number nunitsCapture Number of units necessary to capture a zone. +-- @field #number threatlevelCapture Threat level necessary to capture a zone. +-- @field Core.Set#SET_UNIT ScanUnitSet Set of scanned units. +-- @field Core.Set#SET_GROUP ScanGroupSet Set of scanned groups. +-- @field #number UpdateSeconds Run status every this many seconds. +-- @extends Core.Fsm#FSM + +--- *Gentlemen, when the enemy is committed to a mistake we must not interrupt him too soon.* --- Horation Nelson +-- +-- === +-- +-- # The OPSZONE Concept +-- +-- An OPSZONE is a strategically important area. +-- +-- +-- @field #OPSZONE +OPSZONE = { + ClassName = "OPSZONE", + verbose = 0, + Nred = 0, + Nblu = 0, + Nnut = 0, + Ncoal = {}, + Tred = 0, + Tblu = 0, + Tnut = 0, + chiefs = {}, + Missions = {}, + UpdateSeconds = 120, +} + +--- OPSZONE.MISSION +-- @type OPSZONE.MISSION +-- @field #number Coalition Coalition +-- @field #string Type Type of mission +-- @field Ops.Auftrag#AUFTRAG Mission The actual attached mission + + +--- Type of zone we are dealing with. +-- @type OPSZONE.ZoneType +-- @field #string Circular Zone is circular. +-- @field #string Polygon Zone is a polygon. +OPSZONE.ZoneType={ + Circular="Circular", + Polygon="Polygon", +} + +--- OPSZONE class version. +-- @field #string version +OPSZONE.version="0.6.2" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Pause/unpause evaluations. +-- TODO: Differentiate between ground attack and boming by air or arty. +-- DONE: Polygon zones. +-- DONE: Capture time, i.e. time how long a single coalition has to be inside the zone to capture it. +-- DONE: Capturing based on (total) threat level threshold. Unarmed units do not pose a threat and should not be able to hold a zone. +-- DONE: Can neutrals capture? No, since they are _neutral_! +-- DONE: Capture airbases. +-- DONE: Can statics capture or hold a zone? No, unless explicitly requested by mission designer. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new OPSZONE class object. +-- @param #OPSZONE self +-- @param Core.Zone#ZONE Zone The zone. Can be passed as ZONE\_RADIUS, ZONE_POLYGON, ZONE\_AIRBASE or simply as the name of the airbase. +-- @param #number CoalitionOwner Initial owner of the coaliton. Default `coalition.side.NEUTRAL`. +-- @return #OPSZONE self +-- @usage +-- myopszone = OPSZONE:New(ZONE:FindByName("OpsZoneOne"), coalition.side.RED) -- base zone from the mission editor +-- myopszone = OPSZONE:New(ZONE_RADIUS:New("OpsZoneTwo", mycoordinate:GetVec2(),5000),coalition.side.BLUE) -- radius zone of 5km at a coordinate +-- myopszone = OPSZONE:New(ZONE_RADIUS:New("Batumi")) -- airbase zone from Batumi Airbase, ca 2500m radius +-- myopszone = OPSZONE:New(ZONE_AIRBASE:New("Batumi",6000),coalition.side.BLUE) -- airbase zone from Batumi Airbase, but with a specific radius of 6km +-- +function OPSZONE:New(Zone, CoalitionOwner) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #OPSZONE + + -- Check if zone name instead of ZONE object was passed. + if Zone then + if type(Zone)=="string" then + -- Convert string into a ZONE or ZONE_AIRBASE + local Name=Zone + Zone=ZONE:FindByName(Name) + if not Zone then + local airbase=AIRBASE:FindByName(Name) + if airbase then + Zone=ZONE_AIRBASE:New(Name, 2000) + end + end + if not Zone then + self:E(string.format("ERROR: No ZONE or ZONE_AIRBASE found for name: %s", Name)) + return nil + end + end + else + self:E("ERROR: First parameter Zone is nil in OPSZONE:New(Zone) call!") + return nil + end + + -- Basic checks. + if Zone:IsInstanceOf("ZONE_AIRBASE") then + self.airbase=Zone._.ZoneAirbase + self.airbaseName=self.airbase:GetName() + self.zoneType=OPSZONE.ZoneType.Circular + self.zoneCircular=Zone + elseif Zone:IsInstanceOf("ZONE_RADIUS") then + -- Nothing to do. + self.zoneType=OPSZONE.ZoneType.Circular + self.zoneCircular=Zone + elseif Zone:IsInstanceOf("ZONE_POLYGON_BASE") then + -- Nothing to do. + self.zoneType=OPSZONE.ZoneType.Polygon + local zone=Zone --Core.Zone#ZONE_POLYGON + self.zoneCircular=zone:GetZoneRadius(nil, true) + else + self:E("ERROR: OPSZONE must be a SPHERICAL zone due to DCS restrictions!") + return nil + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("OPSZONE %s | ", Zone:GetName()) + + -- Set some values. + self.zone=Zone + self.zoneName=Zone:GetName() + self.zoneRadius=self.zoneCircular:GetRadius() + self.Missions = {} + self.ScanUnitSet=SET_UNIT:New():FilterZones({Zone}) + self.ScanGroupSet=SET_GROUP:New():FilterZones({Zone}) + + -- Add to database. + _DATABASE:AddOpsZone(self) + + -- Current and previous owners. + self.ownerCurrent=CoalitionOwner or coalition.side.NEUTRAL + self.ownerPrevious=CoalitionOwner or coalition.side.NEUTRAL + + -- Contested. + self.isContested=false + + self.Ncoal[coalition.side.BLUE]=0 + self.Ncoal[coalition.side.RED]=0 + self.Ncoal[coalition.side.NEUTRAL]=0 + + -- We take the airbase coalition. + if self.airbase then + self.ownerCurrent=self.airbase:GetCoalition() + self.ownerPrevious=self.airbase:GetCoalition() + end + + -- Set object categories. + self:SetObjectCategories() + self:SetUnitCategories() + + -- Draw zone. Default is on. + self:SetDrawZone() + self:SetMarkZone(true) + + -- Default capture parameters. + self:SetCaptureTime() + self:SetCaptureNunits() + self:SetCaptureThreatlevel() + + -- Status timer. + self.timerStatus=TIMER:New(OPSZONE.Status, self) + + -- FMS start state is STOPPED. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Empty") -- Start FSM. + self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. + + self:AddTransition("*", "Evaluated", "*") -- Evaluation done. + + self:AddTransition("*", "Captured", "Guarded") -- Zone was captured. + + self:AddTransition("Empty", "Guarded", "Guarded") -- Owning coalition left the zone and returned. + + self:AddTransition("*", "Empty", "Empty") -- No red or blue units inside the zone. + + self:AddTransition("*", "Attacked", "Attacked") -- A guarded zone is under attack. + self:AddTransition("*", "Defeated", "Guarded") -- The owning coalition defeated an attack. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". + -- @function [parent=#OPSZONE] Start + -- @param #OPSZONE self + + --- Triggers the FSM event "Start" after a delay. + -- @function [parent=#OPSZONE] __Start + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". + -- @function [parent=#OPSZONE] Stop + -- @param #OPSZONE self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#OPSZONE] __Stop + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Evaluated". + -- @function [parent=#OPSZONE] Evaluated + -- @param #OPSZONE self + + --- Triggers the FSM event "Evaluated" after a delay. + -- @function [parent=#OPSZONE] __Evaluated + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + --- On after "Evaluated" event. + -- @function [parent=#OPSZONE] OnAfterEvaluated + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Captured". + -- @function [parent=#OPSZONE] Captured + -- @param #OPSZONE self + -- @param #number Coalition Coalition side that captured the zone. + + --- Triggers the FSM event "Captured" after a delay. + -- @function [parent=#OPSZONE] __Captured + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + -- @param #number Coalition Coalition side that captured the zone. + + --- On after "Captured" event. + -- @function [parent=#OPSZONE] OnAfterCaptured + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Coalition Coalition side that captured the zone. + + + --- Triggers the FSM event "Guarded". + -- @function [parent=#OPSZONE] Guarded + -- @param #OPSZONE self + + --- Triggers the FSM event "Guarded" after a delay. + -- @function [parent=#OPSZONE] __Guarded + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + --- On after "Guarded" event. + -- @function [parent=#OPSZONE] OnAfterGuarded + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Empty". + -- @function [parent=#OPSZONE] Empty + -- @param #OPSZONE self + + --- Triggers the FSM event "Empty" after a delay. + -- @function [parent=#OPSZONE] __Empty + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + + --- On after "Empty" event. + -- @function [parent=#OPSZONE] OnAfterEmpty + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Attacked". + -- @function [parent=#OPSZONE] Attacked + -- @param #OPSZONE self + -- @param #number AttackerCoalition Coalition side that is attacking the zone. + + --- Triggers the FSM event "Attacked" after a delay. + -- @function [parent=#OPSZONE] __Attacked + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + -- @param #number AttackerCoalition Coalition side that is attacking the zone. + + --- On after "Attacked" event. + -- @function [parent=#OPSZONE] OnAfterAttacked + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number AttackerCoalition Coalition side that is attacking the zone. + + + --- Triggers the FSM event "Defeated". + -- @function [parent=#OPSZONE] Defeated + -- @param #OPSZONE self + -- @param #number DefeatedCoalition Coalition side that was defeated. + + --- Triggers the FSM event "Defeated" after a delay. + -- @function [parent=#OPSZONE] __Defeated + -- @param #OPSZONE self + -- @param #number delay Delay in seconds. + -- @param #number DefeatedCoalition Coalition side that was defeated. + + --- On after "Defeated" event. + -- @function [parent=#OPSZONE] OnAfterDefeated + -- @param #OPSZONE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number DefeatedCoalition Coalition side that was defeated. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set verbosity level. +-- @param #OPSZONE self +-- @param #number VerbosityLevel Level of output (higher=more). Default 0. +-- @return #OPSZONE self +function OPSZONE:SetVerbosity(VerbosityLevel) + self.verbose=VerbosityLevel or 0 + return self +end + +--- Set categories of objects that can capture or hold the zone. +-- +-- * Default is {Object.Category.UNIT, Object.Category.STATIC} so units and statics can capture and hold zones. +-- * Set to `{Object.Category.UNIT}` if only units should be able to capture and hold zones +-- +-- Which units can capture zones can be further refined by `:SetUnitCategories()`. +-- +-- @param #OPSZONE self +-- @param #table Categories Object categories. Default is `{Object.Category.UNIT, Object.Category.STATIC}`. +-- @return #OPSZONE self +function OPSZONE:SetObjectCategories(Categories) + + -- Ensure table if something was passed. + if Categories and type(Categories)~="table" then + Categories={Categories} + end + + -- Set categories. + self.ObjectCategories=Categories or {Object.Category.UNIT, Object.Category.STATIC} + + return self +end + +--- Set categories of units that can capture or hold the zone. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). +-- @param #OPSZONE self +-- @param #table Categories Table of unit categories. Default `{Unit.Category.GROUND_UNIT}`. +-- @return #OPSZONE self +function OPSZONE:SetUnitCategories(Categories) + + -- Ensure table. + if Categories and type(Categories)~="table" then + Categories={Categories} + end + + -- Set categories. + self.UnitCategories=Categories or {Unit.Category.GROUND_UNIT} + + return self +end + +--- Set threat level threshold that the offending units must have to capture a zone. +-- The reason why you might want to set this is that unarmed units (*e.g.* fuel trucks) should not be able to capture a zone as they do not pose a threat. +-- @param #OPSZONE self +-- @param #number Threatlevel Threat level threshold. Default 0. +-- @return #OPSZONE self +function OPSZONE:SetCaptureThreatlevel(Threatlevel) + + self.threatlevelCapture=Threatlevel or 0 + + return self +end + +--- Set how many units must be present in a zone to capture it. By default, one unit is enough. +-- @param #OPSZONE self +-- @param #number Nunits Number of units. Default 1. +-- @return #OPSZONE self +function OPSZONE:SetCaptureNunits(Nunits) + + Nunits=Nunits or 1 + + self.nunitsCapture=Nunits + + return self +end + + +--- Set time how long an attacking coalition must have troops inside a zone before it captures the zone. +-- @param #OPSZONE self +-- @param #number Tcapture Time in seconds. Default 0. +-- @return #OPSZONE self +function OPSZONE:SetCaptureTime(Tcapture) + + self.TminCaptured=Tcapture or 0 + + return self +end + + +--- Set whether *neutral* units can capture the zone. +-- @param #OPSZONE self +-- @param #boolean CanCapture If `true`, neutral units can. +-- @return #OPSZONE self +function OPSZONE:SetNeutralCanCapture(CanCapture) + self.neutralCanCapture=CanCapture + return self +end + +--- Set if zone is drawn on the F10 map. Color will change depending on current owning coalition. +-- @param #OPSZONE self +-- @param #boolean Switch If `true` or `nil`, draw zone. If `false`, zone is not drawn. +-- @return #OPSZONE self +function OPSZONE:SetDrawZone(Switch) + if Switch==false then + self.drawZone=false + else + self.drawZone=true + end + return self +end + +--- Set if zone is drawn on the F10 map for the owner coalition only. +-- @param #OPSZONE self +-- @param #boolean Switch If `false` or `nil`, draw zone for all coalitions. If `true`, zone is drawn for the owning coalition only if drawZone is true. +-- @return #OPSZONE self +function OPSZONE:SetDrawZoneForCoalition(Switch) + if Switch==true then + self.drawZoneForCoalition=true + else + self.drawZoneForCoalition=false + end + return self +end + +--- Set if a marker on the F10 map shows the current zone status. +-- @param #OPSZONE self +-- @param #boolean Switch If `true`, zone is marked. If `false` or `nil`, zone is not marked. +-- @param #boolean ReadOnly If `true` or `nil` then mark is read only. +-- @return #OPSZONE self +function OPSZONE:SetMarkZone(Switch, ReadOnly) + if Switch then + self.markZone=true + local Coordinate=self:GetCoordinate() + self.markerText=self:_GetMarkerText() + self.marker=self.marker or MARKER:New(Coordinate, self.markerText) + if ReadOnly==false then + self.marker.readonly=false + else + self.marker.readonly=true + end + self.marker:ToAll() + else + if self.marker then + self.marker:Remove() + end + self.marker=nil + self.markZone=false + end + return self +end + + +--- Get current owner of the zone. +-- @param #OPSZONE self +-- @return #number Owner coalition. +function OPSZONE:GetOwner() + return self.ownerCurrent +end + +--- Get coalition name of current owner of the zone. +-- @param #OPSZONE self +-- @return #string Owner coalition. +function OPSZONE:GetOwnerName() + return UTILS.GetCoalitionName(self.ownerCurrent) +end + +--- Get coordinate of zone. +-- @param #OPSZONE self +-- @return Core.Point#COORDINATE Coordinate of the zone. +function OPSZONE:GetCoordinate() + local coordinate=self.zone:GetCoordinate() + return coordinate +end + +--- Get scanned units inside the zone. +-- @param #OPSZONE self +-- @return Core.Set#SET_UNIT Set of units inside the zone. +function OPSZONE:GetScannedUnitSet() + return self.ScanUnitSet +end + + +--- Get scanned groups inside the zone. +-- @param #OPSZONE self +-- @return Core.Set#SET_GROUP Set of groups inside the zone. +function OPSZONE:GetScannedGroupSet() + return self.ScanGroupSet +end + +--- Returns a random coordinate in the zone. +-- @param #OPSZONE self +-- @param #number inner (Optional) Minimal distance from the center of the zone in meters. Default is 0 m. +-- @param #number outer (Optional) Maximal distance from the outer edge of the zone in meters. Default is the radius of the zone. +-- @param #table surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 1000 times to find the right type! +-- @return Core.Point#COORDINATE The random coordinate. +function OPSZONE:GetRandomCoordinate(inner, outer, surfacetypes) + + local zone=self:GetZone() + + local coord=zone:GetRandomCoordinate(inner, outer, surfacetypes) + + return coord +end + +--- Get zone name. +-- @param #OPSZONE self +-- @return #string Name of the zone. +function OPSZONE:GetName() + return self.zoneName +end + +--- Get the zone object. +-- @param #OPSZONE self +-- @return Core.Zone#ZONE The zone. +function OPSZONE:GetZone() + return self.zone +end + +--- Get previous owner of the zone. +-- @param #OPSZONE self +-- @return #number Previous owner coalition. +function OPSZONE:GetPreviousOwner() + return self.ownerPrevious +end + +--- Get duration of the current attack. +-- @param #OPSZONE self +-- @return #number Duration in seconds since when the last attack began. Is `nil` if the zone is not under attack currently. +function OPSZONE:GetAttackDuration() + if self:IsAttacked() and self.Tattacked then + + local dT=timer.getAbsTime()-self.Tattacked + return dT + end + + return nil +end + +--- Find an OPSZONE using the Zone Name. +-- @param #OPSZONE self +-- @param #string ZoneName The zone name. +-- @return #OPSZONE The OPSZONE or nil if not found. +function OPSZONE:FindByName( ZoneName ) + local Found = _DATABASE:FindOpsZone( ZoneName ) + return Found +end + +--- Check if the red coalition is currently owning the zone. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is red. +function OPSZONE:IsRed() + local is=self.ownerCurrent==coalition.side.RED + return is +end + +--- Check if the blue coalition is currently owning the zone. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is blue. +function OPSZONE:IsBlue() + local is=self.ownerCurrent==coalition.side.BLUE + return is +end + +--- Check if the neutral coalition is currently owning the zone. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is neutral. +function OPSZONE:IsNeutral() + local is=self.ownerCurrent==coalition.side.NEUTRAL + return is +end + +--- Check if a certain coalition is currently owning the zone. +-- @param #OPSZONE self +-- @param #number Coalition The Coalition that is supposed to own the zone. +-- @return #boolean If `true`, zone is owned by the given coalition. +function OPSZONE:IsCoalition(Coalition) + local is=self.ownerCurrent==Coalition + return is +end + +--- Check if zone is started (not stopped). +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is started. +function OPSZONE:IsStarted() + local is=not self:IsStopped() + return is +end + +--- Check if zone is stopped. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is stopped. +function OPSZONE:IsStopped() + local is=self:is("Stopped") + return is +end + +--- Check if zone is guarded. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is guarded. +function OPSZONE:IsGuarded() + local is=self:is("Guarded") + return is +end + +--- Check if zone is empty. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is empty. +function OPSZONE:IsEmpty() + local is=self:is("Empty") + return is +end + +--- Check if zone is being attacked by the opposite coalition. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is being attacked. +function OPSZONE:IsAttacked() + local is=self:is("Attacked") + return is +end + +--- Check if zone is contested. Contested here means red *and* blue units are present in the zone. +-- @param #OPSZONE self +-- @return #boolean If `true`, zone is contested. +function OPSZONE:IsContested() + return self.isContested +end + +--- Check if FMS is stopped. +-- @param #OPSZONE self +-- @return #boolean If `true`, FSM is stopped +function OPSZONE:IsStopped() + local is=self:is("Stopped") + return is +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start/Stop Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start OPSZONE FSM. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #OPSZONE self +function OPSZONE:onafterStart(From, Event, To) + + -- Info. + self:I(self.lid..string.format("Starting OPSZONE v%s", OPSZONE.version)) + + -- Reinit the timer. + self.timerStatus=self.timerStatus or TIMER:New(OPSZONE.Status, self) + + -- Status update. + local EveryUpdateIn = self.UpdateSeconds or 120 + self.timerStatus:Start(1, EveryUpdateIn) + + -- Handle base captured event. + if self.airbase then + self:HandleEvent(EVENTS.BaseCaptured) + end + + return self +end + +--- Stop OPSZONE FSM. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onafterStop(From, Event, To) + + -- Info. + self:I(self.lid..string.format("Stopping OPSZONE")) + + -- Reinit the timer. + self.timerStatus:Stop() + + -- Undraw zone. + self.zone:UndrawZone() + + -- Remove marker. + if self.markZone then + self.marker:Remove() + end + + -- Unhandle events. + self:UnHandleEvent(EVENTS.BaseCaptured) + + -- Stop FSM scheduler. + self.CallScheduler:Clear() + if self.Scheduler then + self.Scheduler:Clear() + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Update status. +-- @param #OPSZONE self +function OPSZONE:Status() + + -- Current FSM state. + local fsmstate=self:GetState() + + -- Get contested. + local contested=tostring(self:IsContested()) + + -- Info message. + if self.verbose>=1 then + local text=string.format("State %s: Owner %d (previous %d), contested=%s, Nunits: red=%d, blue=%d, neutral=%d", fsmstate, self.ownerCurrent, self.ownerPrevious, contested, self.Nred, self.Nblu, self.Nnut) + self:I(self.lid..text) + end + + -- Scanning zone. + self:Scan() + + -- Evaluate the scan result. + self:EvaluateZone() + + -- Update F10 marker (only if enabled). + self:_UpdateMarker() + + -- Undraw zone. + if self.zone.DrawID and not self.drawZone then + self.zone:UndrawZone() + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On before "Captured" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number NewOwnerCoalition Coalition of the new owner. +function OPSZONE:onbeforeCaptured(From, Event, To, NewOwnerCoalition) + + -- Check if owner changed. + + if self.ownerCurrent==NewOwnerCoalition then + self:T(self.lid.."") + end + + return true +end + +--- On after "Captured" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number NewOwnerCoalition Coalition of the new owner. +function OPSZONE:onafterCaptured(From, Event, To, NewOwnerCoalition) + + -- Debug info. + self:T(self.lid..string.format("Zone captured by coalition=%d", NewOwnerCoalition)) + + -- Set owners. + self.ownerPrevious=self.ownerCurrent + self.ownerCurrent=NewOwnerCoalition + + if self.drawZone then + + self.zone:UndrawZone() + + local color=self:_GetZoneColor() + + local coalition = nil + if self.drawZoneForCoalition then + coalition = self.ownerCurrent + end + self.zone:DrawZone(coalition, color, 1.0, color, 0.5) + end + + for _,_chief in pairs(self.chiefs) do + local chief=_chief --Ops.Chief#CHIEF + if chief.coalition==self.ownerCurrent then + chief:ZoneCaptured(self) + else + chief:ZoneLost(self) + end + end + +end + +--- On after "Empty" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onafterEmpty(From, Event, To) + + -- Debug info. + self:T(self.lid..string.format("Zone is empty EVENT")) + +end + +--- On after "Attacked" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number AttackerCoalition Coalition of the attacking ground troops. +function OPSZONE:onafterAttacked(From, Event, To, AttackerCoalition) + + -- Debug info. + self:T(self.lid..string.format("Zone is being attacked by coalition=%s!", tostring(AttackerCoalition))) + +end + +--- On after "Defeated" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number DefeatedCoalition Coalition side that was defeated. +function OPSZONE:onafterDefeated(From, Event, To, DefeatedCoalition) + + -- Debug info. + self:T(self.lid..string.format("Defeated attack on zone by coalition=%d", DefeatedCoalition)) + + -- Not attacked any more. + self.Tattacked=nil + +end + +--- On enter "Guarded" state. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onenterGuarded(From, Event, To) + + if From~=To then + + -- Debug info. + self:T(self.lid..string.format("Zone is guarded")) + + -- Not attacked any more. + self.Tattacked=nil + + if self.drawZone then + + self.zone:UndrawZone() + + local color=self:_GetZoneColor() + + local coalition = nil + if self.drawZoneForCoalition then + coalition = self.ownerCurrent + end + self.zone:DrawZone(coalition, color, 1.0, color, 0.5) + end + + end + +end + +--- On enter "Attacked" state. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number AttackerCoalition Coalition of the attacking ground troops. +function OPSZONE:onenterAttacked(From, Event, To, AttackerCoalition) + + -- Time stamp when the attack started. + if From~="Attacked" then + + -- Debug info. + self:T(self.lid..string.format("Zone is Attacked")) + + -- Set time stamp. + self.Tattacked=timer.getAbsTime() + + -- Inform chief. + if AttackerCoalition then + for _,_chief in pairs(self.chiefs) do + local chief=_chief --Ops.Chief#CHIEF + if chief.coalition~=AttackerCoalition then + chief:ZoneAttacked(self) + end + end + end + + -- Draw zone? + if self.drawZone then + self.zone:UndrawZone() + + -- Color. + local color={1, 204/255, 204/255} + + local coalition = nil + if self.drawZoneForCoalition then + coalition = self.ownerCurrent + end + -- Draw zone. + self.zone:DrawZone(coalition, color, 1.0, color, 0.5) + end + + self:_CleanMissionTable() + + end + +end + +--- On enter "Empty" event. +-- @param #OPSZONE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function OPSZONE:onenterEmpty(From, Event, To) + + if From~=To then + + -- Debug info. + self:T(self.lid..string.format("Zone is empty now")) + + -- Inform chief. + for _,_chief in pairs(self.chiefs) do + local chief=_chief --Ops.Chief#CHIEF + chief:ZoneEmpty(self) + end + + if self.drawZone then + self.zone:UndrawZone() + + local color=self:_GetZoneColor() + + local coalition = nil + if self.drawZoneForCoalition then + coalition = self.ownerCurrent + end + self.zone:DrawZone(coalition, color, 1.0, color, 0.2) + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Scan Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Scan zone. +-- @param #OPSZONE self +-- @return #OPSZONE self +function OPSZONE:Scan() + + -- Debug info. + if self.verbose>=3 then + local text=string.format("Scanning zone %s R=%.1f m", self.zoneName, self.zoneRadius) + self:I(self.lid..text) + end + + -- Search. + local SphereSearch={id=world.VolumeType.SPHERE, params={point=self.zone:GetVec3(), radius=self.zoneRadius}} + + -- Init number of red, blue and neutral units. + local Nred=0 + local Nblu=0 + local Nnut=0 + + local Tred=0 + local Tblu=0 + local Tnut=0 + + self.ScanGroupSet:Clear(false) + self.ScanUnitSet:Clear(false) + + --- Function to evaluate the world search + local function EvaluateZone(_ZoneObject) + + local ZoneObject=_ZoneObject --DCS#Object + + if ZoneObject then + + -- Object category. + local ObjectCategory=Object.getCategory(ZoneObject) + + if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() then + + --- + -- UNIT + --- + + -- This is a DCS unit object. + local DCSUnit=ZoneObject --DCS#Unit + + --- Function to check if unit category is included. + local function Included() + + if not self.UnitCategories then + -- Any unit is included. + return true + else + -- Check if found object is in specified categories. + local CategoryDCSUnit = ZoneObject:getDesc().category + + for _,UnitCategory in pairs(self.UnitCategories) do + if UnitCategory==CategoryDCSUnit then + return true + end + end + + end + + return false + end + + + if Included() then + + -- Get Coalition. + local Coalition=DCSUnit:getCoalition() + + local tl=0 + local unit=UNIT:Find(DCSUnit) + if unit then + + -- Inside zone. + local inzone=true + if self.zoneType==OPSZONE.ZoneType.Polygon then + + -- Check if unit is really inside the zone. + inzone=unit:IsInZone(self.zone) + + -- Debug marker. + -- Debug: Had cases where a (red) unit was clearly not inside the zone but the scan did find it! + --unit:GetCoordinate():MarkToAll(string.format("Unit %s inzone=%s", unit:GetName(), tostring(inzone))) + end + + if inzone then + + -- Threat level of unit. + tl=unit:GetThreatLevel() + + -- Add unit to set. + self.ScanUnitSet:AddUnit(unit) + + -- Get group of unit. + local group=unit:GetGroup() + + -- Add group to scanned set. + if group then + self.ScanGroupSet:AddGroup(group, true) + end + + -- Increase counter. + if Coalition==coalition.side.RED then + Nred=Nred+1 + Tred=Tred+tl + elseif Coalition==coalition.side.BLUE then + Nblu=Nblu+1 + Tblu=Tblu+tl + elseif Coalition==coalition.side.NEUTRAL then + Nnut=Nnut+1 + Tnut=Tnut+tl + end + + -- Debug info. + if self.verbose>=4 then + self:I(self.lid..string.format("Found unit %s (coalition=%d)", DCSUnit:getName(), Coalition)) + end + + end + end + + end + + elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then + + --- + -- STATIC + --- + + -- This is a DCS static object. + local DCSStatic=ZoneObject --DCS#StaticObject + + -- Get coalition. + local Coalition=DCSStatic:getCoalition() + + -- CAREFUL! Downed pilots break routine here without any error thrown. + --local unit=STATIC:Find(DCSStatic) + + -- Inside zone. + local inzone=true + if self.zoneType==OPSZONE.ZoneType.Polygon then + + local Vec3=DCSStatic:getPoint() + + inzone=self.zone:IsVec3InZone(Vec3) + + end + + if inzone then + + -- Increase counter. + if Coalition==coalition.side.RED then + Nred=Nred+1 + elseif Coalition==coalition.side.BLUE then + Nblu=Nblu+1 + elseif Coalition==coalition.side.NEUTRAL then + Nnut=Nnut+1 + end + + -- Debug info + if self.verbose>=4 then + self:I(self.lid..string.format("Found static %s (coalition=%d)", DCSStatic:getName(), Coalition)) + end + + end + + elseif ObjectCategory==Object.Category.SCENERY then + + --- + -- SCENERY + --- + + local SceneryType = ZoneObject:getTypeName() + local SceneryName = ZoneObject:getName() + + -- Debug info. + self:T2(self.lid..string.format("Found scenery type=%s, name=%s", SceneryType, SceneryName)) + end + + end + + return true + end + + -- Search objects. + world.searchObjects(self.ObjectCategories, SphereSearch, EvaluateZone) + + -- Debug info. + if self.verbose>=3 then + local text=string.format("Scan result Nred=%d, Nblue=%d, Nneutral=%d", Nred, Nblu, Nnut) + if self.verbose>=4 then + for _,_unit in pairs(self.ScanUnitSet:GetSet()) do + local unit=_unit --Wrapper.Unit#UNIT + text=text..string.format("\nUnit %s coalition=%s", unit:GetName(), unit:GetCoalitionName()) + end + + for _,_group in pairs(self.ScanGroupSet:GetSet()) do + local group=_group --Wrapper.Group#GROUP + text=text..string.format("\nGroup %s coalition=%s", group:GetName(), group:GetCoalitionName()) + end + end + self:I(self.lid..text) + end + + -- Set values. + self.Nred=Nred + self.Nblu=Nblu + self.Nnut=Nnut + + + self.Ncoal[coalition.side.BLUE]=Nblu + self.Ncoal[coalition.side.RED]=Nred + self.Ncoal[coalition.side.NEUTRAL]=Nnut + + self.Tblu=Tblu + self.Tred=Tred + self.Tnut=Tnut + + return self +end + +--- Evaluate zone. +-- @param #OPSZONE self +-- @return #OPSZONE self +function OPSZONE:EvaluateZone() + + -- Set values. + local Nred=self.Nred + local Nblu=self.Nblu + local Nnut=self.Nnut + + local Tnow=timer.getAbsTime() + + --- Capture + -- @param #number coal Coaltion capturing. + local function captured(coal) + + -- Blue captured red zone. + if not self.airbase then + + -- Set time stamp if it does not exist. + if not self.Tcaptured then + self.Tcaptured=Tnow + end + + -- Check if enough time elapsed. + if Tnow-self.Tcaptured>=self.TminCaptured then + self:Captured(coal) + self.Tcaptured=nil + end + end + + end + + + if self:IsRed() then + + --- + -- RED zone + --- + + if Nred==0 then + + -- No red units in red zone any more. + + if Nblu>=self.nunitsCapture and self.Tblu>=self.threatlevelCapture then + + -- Blue captued red zone. + captured(coalition.side.BLUE) + + elseif Nnut>=self.nunitsCapture and self.Tnut>=self.threatlevelCapture and self.neutralCanCapture then + + -- Neutral captured red zone. + captured(coalition.side.NEUTRAL) + + end + + else + + -- Red units in red zone. + + if Nblu>0 then + + if not self:IsAttacked() and self.Tblu>=self.threatlevelCapture then + self:Attacked(coalition.side.BLUE) + end + + elseif Nblu==0 then + + if self:IsAttacked() and self:IsContested() then + self:Defeated(coalition.side.BLUE) + elseif self:IsEmpty() then + -- Red units left zone and returned (or from initial Empty state). + self:Guarded() + end + + end + + end + + -- Contested by blue? + if Nblu==0 then + self.isContested=false + else + self.isContested=true + end + + elseif self:IsBlue() then + + --- + -- BLUE zone + --- + + if Nblu==0 then + + -- No blue units in blue zone any more. + + if Nred>=self.nunitsCapture and self.Tred>=self.threatlevelCapture then + + -- Red captured blue zone. + captured(coalition.side.RED) + + elseif Nnut>=self.nunitsCapture and self.Tnut>=self.threatlevelCapture and self.neutralCanCapture then + + -- Neutral captured blue zone. + captured(coalition.side.NEUTRAL) + + end + + else + + -- Still blue units in blue zone. + + if Nred>0 then + + if not self:IsAttacked() and self.Tred>=self.threatlevelCapture then + -- Red is attacking blue zone. + self:Attacked(coalition.side.RED) + end + + elseif Nred==0 then + + if self:IsAttacked() and self:IsContested() then + -- Blue defeated read attack. + self:Defeated(coalition.side.RED) + elseif self:IsEmpty() then + -- Blue units left zone and returned (or from initial Empty state). + self:Guarded() + end + + end + + end + + -- Contested by red? + if Nred==0 then + self.isContested=false + else + self.isContested=true + end + + elseif self:IsNeutral() then + + --- + -- NEUTRAL zone + --- + + -- Not checked as neutrals cant capture (for now). + --if Nnut==0 then + + -- No neutral units in neutral zone any more. + + if Nred>0 and Nblu>0 then + self:T(self.lid.."FF neutrals left neutral zone and red and blue are present! What to do?") + if not self:IsAttacked() then + self:Attacked() + end + self.isContested=true + elseif Nred>=self.nunitsCapture and self.Tred>=self.threatlevelCapture then + -- Red captured neutral zone. + captured(coalition.side.RED) + elseif Nblu>=self.nunitsCapture and self.Tblu>=self.threatlevelCapture then + -- Blue captured neutral zone. + captured(coalition.side.BLUE) + end + + --end + + else + self:E(self.lid.."ERROR: Unknown coaliton!") + end + + + -- No units of any coalition in zone any more ==> Empty! + if Nblu==0 and Nred==0 and Nnut==0 and (not self:IsEmpty()) then + self:Empty() + end + + -- Finally, check airbase coalition + if self.airbase then + + -- Current coalition. + local airbasecoalition=self.airbase:GetCoalition() + + if airbasecoalition~=self.ownerCurrent then + self:T(self.lid..string.format("Captured airbase %s: Coaltion %d-->%d", self.airbaseName, self.ownerCurrent, airbasecoalition)) + self:Captured(airbasecoalition) + end + + end + + -- Trigger event. + self:Evaluated() + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Monitor hit events. +-- @param #OPSZONE self +-- @param Core.Event#EVENTDATA EventData The event data. +function OPSZONE:OnEventHit(EventData) + + if self.HitsOn then + + local UnitHit = EventData.TgtUnit + + -- 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.ownerCurrent then + + -- Update last hit time. + self.HitTimeLast=timer.getTime() + + -- Only trigger attacked event if not already in state "Attacked". + if not self:IsAttacked() then + self:T3(self.lid.."Hit ==> Attack") + self:Attacked() + end + + end + + end + +end + +--- Monitor base captured events. +-- @param #OPSZONE self +-- @param Core.Event#EVENTDATA EventData The event data. +function OPSZONE:OnEventBaseCaptured(EventData) + + if EventData and EventData.Place and self.airbase and self.airbaseName 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 the airbase + local CoalitionNew=airbase:GetCoalition() + + -- Debug info. + self:I(self.lid..string.format("EVENT BASE CAPTURED: New coalition of airbase %s: %d [previous=%d]", self.airbaseName, CoalitionNew, self.ownerCurrent)) + + -- Check that coalition actually changed. + if CoalitionNew~=self.ownerCurrent then + self:Captured(CoalitionNew) + end + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get RGB color of zone depending on current owner. +-- @param #OPSZONE self +-- @return #table RGB color. +function OPSZONE:_GetZoneColor() + + local color={0,0,0} + + if self.ownerCurrent==coalition.side.NEUTRAL then + color=self.ZoneOwnerNeutral or {1, 1, 1} + elseif self.ownerCurrent==coalition.side.BLUE then + color=self.ZoneOwnerBlue or {0, 0, 1} + elseif self.ownerCurrent==coalition.side.RED then + color=self.ZoneOwnerRed or {1, 0, 0} + else + + end + + return color +end + +--- Set custom RGB color of zone depending on current owner. +-- @param #OPSZONE self +-- @param #table Neutral Color is a table of RGB values 0..1 for Red, Green, and Blue respectively, e.g. {1,0,0} for red. +-- @param #table Blue Color is a table of RGB values 0..1 for Red, Green, and Blue respectively, e.g. {0,1,0} for green. +-- @param #table Red Color is a table of RGB values 0..1 for Red, Green, and Blue respectively, e.g. {0,0,1} for blue. +-- @return #OPSZONE self +function OPSZONE:SetZoneColor(Neutral, Blue, Red) + + self.ZoneOwnerNeutral = Neutral or {1, 1, 1} + self.ZoneOwnerBlue = Blue or {0, 0, 1} + self.ZoneOwnerRed = Red or {1, 0, 0} + + return self +end + +--- Update marker on the F10 map. +-- @param #OPSZONE self +function OPSZONE:_UpdateMarker() + + if self.markZone then + + -- Get marker text. + local text=self:_GetMarkerText() + + -- Chck if marker text changed and if so, update the marker. + if text~=self.markerText then + self.markerText=text + self.marker:UpdateText(self.markerText) + end + + --TODO: Update position if changed. + + end + +end + +--- Get marker text. +-- @param #OPSZONE self +-- @return #string Marker text. +function OPSZONE:_GetMarkerText() + + local owner=UTILS.GetCoalitionName(self.ownerCurrent) + local prevowner=UTILS.GetCoalitionName(self.ownerPrevious) + + -- Get marker text. + local text=string.format("%s [N=%d, TL=%d T=%d]:\nOwner=%s [%s]\nState=%s [Contested=%s]\nBlue=%d [TL=%d]\nRed=%d [TL=%d]\nNeutral=%d [TL=%d]", + self.zoneName, self.nunitsCapture or 0, self.threatlevelCapture or 0, self.TminCaptured or 0, + owner, prevowner, self:GetState(), tostring(self:IsContested()), + self.Nblu, self.Tblu, self.Nred, self.Tred, self.Nnut, self.Tnut) + + return text +end + +--- Add a chief that monitors this zone. Chief will be informed about capturing etc. +-- @param #OPSZONE self +-- @param Ops.Chief#CHIEF Chief The chief. +-- @return #table RGB color. +function OPSZONE:_AddChief(Chief) + + -- Add chief. + table.insert(self.chiefs, Chief) + +end + +--- Add an entry to the OpsZone mission table +-- @param #OPSZONE self +-- @param #number Coalition Coalition of type e.g. coalition.side.NEUTRAL +-- @param #string Type Type of mission, e.g. AUFTRAG.Type.CAS +-- @param Ops.Auftrag#AUFTRAG Auftrag The Auftrag itself +-- @return #OPSZONE self +function OPSZONE:_AddMission(Coalition,Type,Auftrag) + + -- Add a mission + local entry = {} -- #OPSZONE.MISSION + entry.Coalition = Coalition or coalition.side.NEUTRAL + entry.Type = Type or "" + entry.Mission = Auftrag or nil + + table.insert(self.Missions,entry) + + return self +end + +--- Get the OpsZone mission table. #table of #OPSZONE.MISSION entries +-- @param #OPSZONE self +-- @return #table Missions +function OPSZONE:_GetMissions() + return self.Missions +end + +--- Add an entry to the OpsZone mission table. +-- @param #OPSZONE self +-- @param #number Coalition Coalition of type e.g. `coalition.side.NEUTRAL`. +-- @param #string Type Type of mission, e.g. `AUFTRAG.Type.CAS`. +-- @return #boolean found True if we have that kind of mission, else false. +-- @return #table Missions Table of `Ops.Auftrag#AUFTRAG` entries. +function OPSZONE:_FindMissions(Coalition, Type) + -- search the table + local foundmissions = {} + local found = false + for _,_entry in pairs(self.Missions) do + local entry = _entry -- #OPSZONE.MISSION + if entry.Coalition == Coalition and entry.Type == Type and entry.Mission and entry.Mission:IsNotOver() then + table.insert(foundmissions,entry.Mission) + found = true + end + end + return found, foundmissions +end + +--- Clean mission table from missions that are over. +-- @param #OPSZONE self +-- @return #OPSZONE self +function OPSZONE:_CleanMissionTable() + local missions = {} + for _,_entry in pairs(self.Missions) do + local entry = _entry -- #OPSZONE.MISSION + if entry.Mission and entry.Mission:IsNotOver() then + table.insert(missions,entry) + end + end + self.Missions = missions + return self +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Platoon.lua b/Moose Development/Moose/Ops/Platoon.lua new file mode 100644 index 000000000..8b380a134 --- /dev/null +++ b/Moose Development/Moose/Ops/Platoon.lua @@ -0,0 +1,175 @@ +--- **Ops** - Brigade Platoon. +-- +-- **Main Features:** +-- +-- * Set parameters like livery, skill valid for all platoon members. +-- * Define modex and callsigns. +-- * Define mission types, this platoon can perform (see Ops.Auftrag#AUFTRAG). +-- * Pause/unpause platoon operations. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Platoon +-- @image OPS_Platoon.png + + +--- PLATOON class. +-- @type PLATOON +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field Ops.OpsGroup#OPSGROUP.WeaponData weaponData Weapon data table with key=BitType. +-- @extends Ops.Cohort#COHORT + +--- *Some cool cohort quote* -- Known Author +-- +-- === +-- +-- # The PLATOON Concept +-- +-- A PLATOON is essential part of an BRIGADE. +-- +-- +-- +-- @field #PLATOON +PLATOON = { + ClassName = "PLATOON", + verbose = 0, + weaponData = {}, +} + +--- PLATOON class version. +-- @field #string version +PLATOON.version="0.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: A lot. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new PLATOON object and start the FSM. +-- @param #PLATOON self +-- @param #string TemplateGroupName Name of the template group. +-- @param #number Ngroups Number of asset groups of this platoon. Default 3. +-- @param #string PlatoonName Name of the platoon. Must be **unique**! +-- @return #PLATOON self +function PLATOON:New(TemplateGroupName, Ngroups, PlatoonName) + + -- Inherit everything from COHORT class. + local self=BASE:Inherit(self, COHORT:New(TemplateGroupName, Ngroups, PlatoonName)) -- #PLATOON + + -- All platoons get mission type Nothing. + self:AddMissionCapability(AUFTRAG.Type.NOTHING, 50) + + -- Is ground. + self.isGround=true + + -- Get ammo. + self.ammo=self:_CheckAmmo() + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + -- TODO: Platoon specific user functions. + +--- Set brigade of this platoon. +-- @param #PLATOON self +-- @param Ops.Brigade#BRIGADE Brigade The brigade. +-- @return #PLATOON self +function PLATOON:SetBrigade(Brigade) + self.legion=Brigade + return self +end + +--- Get brigade of this platoon. +-- @param #PLATOON self +-- @return Ops.Brigade#BRIGADE The brigade. +function PLATOON:GetBrigade() + return self.legion +end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--[[ +--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers. +-- @param #PLATOON self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function PLATOON:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting %s v%s %s", self.ClassName, self.version, self.name) + self:I(self.lid..text) + + -- Start the status monitoring. + self:__Status(-1) +end +]] + +--- On after "Status" event. +-- @param #PLATOON self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function PLATOON: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 skill=self.skill and tostring(self.skill) or "N/A" + + local NassetsTot=#self.assets + local NassetsInS=self:CountAssets(true) + local NassetsQP=0 ; local NassetsP=0 ; local NassetsQ=0 + if self.legion then + NassetsQP, NassetsP, NassetsQ=self.legion:CountAssetsOnMission(nil, self) + end + + -- Short info. + local text=string.format("%s [Type=%s, Call=%s, Skill=%s]: Assets Total=%d, Stock=%d, Mission=%d [Active=%d, Queue=%d]", + fsmstate, self.aircrafttype, callsign, skill, NassetsTot, NassetsInS, NassetsQP, NassetsP, NassetsQ) + self:T(self.lid..text) + + -- Weapon data info. + if self.verbose>=3 and self.weaponData then + local text="Weapon Data:" + for bit,_weapondata in pairs(self.weaponData) do + local weapondata=_weapondata --Ops.OpsGroup#OPSGROUP.WeaponData + text=text..string.format("\n- Bit=%s: Rmin=%.1f km, Rmax=%.1f km", bit, weapondata.RangeMin/1000, weapondata.RangeMax/1000) + end + self:I(self.lid..text) + end + + -- Check if group has detected any units. + self:_CheckAssetStatus() + + end + + if not self:IsStopped() then + self:__Status(-60) + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Misc functions. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/PlayerRecce.lua b/Moose Development/Moose/Ops/PlayerRecce.lua new file mode 100644 index 000000000..544c25034 --- /dev/null +++ b/Moose Development/Moose/Ops/PlayerRecce.lua @@ -0,0 +1,2314 @@ +--- **Ops** - Allow a player in a helo like the Gazelle, KA-50 to recon and lase ground targets. +-- +-- ## Features: +-- +-- * Allow a player in a helicopter to detect, smoke, flare, lase and report ground units to others. +-- * Implements visual detection from the helo +-- * Implements optical detection via the Gazelle Vivianne system and lasing +-- * KA-50 BlackShark basic support +-- * Everyone else gets visual detection only +-- * Upload target info to a PLAYERTASKCONTROLLER Instance +-- +-- === +-- +-- # Demo Missions +-- +-- ### Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/). +-- +-- === +-- +-- +-- ### Authors: +-- +-- * Applevangelist (Design & Programming) +-- +-- === +-- +-- @module Ops.PlayerRecce +-- @image Ops_PlayerRecce.png + +------------------------------------------------------------------------------------------------------------------- +-- PLAYERRECCE +-- TODO: PLAYERRECCE +-- DONE: No messages when no targets to flare or smoke +-- DONE: Smoke not all targets +-- DONE: Messages to Attack Group, use client settings +-- DONE: Lasing dist 8km +-- DONE: Reference Point RP +-- DONE: Sort for multiple targets in one direction +-- DONE: Targets with forget timeout, also report +------------------------------------------------------------------------------------------------------------------- + +--- PLAYERRECCE class. +-- @type PLAYERRECCE +-- @field #string ClassName Name of the class. +-- @field #boolean verbose Switch verbosity. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string version +-- @field #table ViewZone +-- @field #table ViewZoneVisual +-- @field #table ViewZoneLaser +-- @field #table LaserFOV +-- @field #table LaserTarget +-- @field Core.Set#SET_CLIENT PlayerSet +-- @field #string Name +-- @field #number Coalition +-- @field #string CoalitionName +-- @field #boolean debug +-- @field #table LaserSpots +-- @field #table UnitLaserCodes +-- @field #table LaserCodes +-- @field #table ClientMenus +-- @field #table OnStation +-- @field #number minthreatlevel +-- @field #number lasingtime +-- @field #table AutoLase +-- @field Core.Set#SET_CLIENT AttackSet +-- @field #boolean TransmitOnlyWithPlayers +-- @field Sound.SRS#MSRS SRS +-- @field Sound.SRS#MSRSQUEUE SRSQueue +-- @field #boolean UseController +-- @field Ops.PlayerTask#PLAYERTASKCONTROLLER Controller +-- @field #boolean ShortCallsign +-- @field #boolean Keepnumber +-- @field #table CallsignTranslations +-- @field Core.Point#COORDINATE ReferencePoint +-- @field #string RPName +-- @field Wrapper.Marker#MARKER RPMarker +-- @field #number TForget +-- @field Utilities.FiFo#FIFO TargetCache +-- @field #boolean smokeownposition +-- @field #table SmokeOwn +-- @field #boolean smokeaveragetargetpos +-- @field #boolean reporttostringbullsonly +-- @extends Core.Fsm#FSM + +--- +-- +-- *It is our attitude at the beginning of a difficult task which, more than anything else, which will affect its successful outcome.* (William James) +-- +-- === +-- +-- # PLAYERRECCE +-- +-- * Allow a player in a helicopter to detect, smoke, flare, lase and report ground units to others. +-- * Implements visual detection from the helo +-- * Implements optical detection via the Gazelle Vivianne system and lasing +-- * KA-50 BlackShark basic support +-- * Everyone else gets visual detection only +-- * Upload target info to a PLAYERTASKCONTROLLER Instance +-- +-- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) channel. +-- +-- +-- @field #PLAYERRECCE +PLAYERRECCE = { + ClassName = "PLAYERRECCE", + verbose = true, + lid = nil, + version = "0.1.26", + ViewZone = {}, + ViewZoneVisual = {}, + ViewZoneLaser = {}, + LaserFOV = {}, + LaserTarget = {}, + PlayerSet = nil, + debug = false, + LaserSpots = {}, + UnitLaserCodes = {}, + LaserCodes = {}, + ClientMenus = {}, + OnStation = {}, + minthreatlevel = 0, + lasingtime = 60, + AutoLase = {}, + AttackSet = nil, + TransmitOnlyWithPlayers = true, + UseController = false, + Controller = nil, + ShortCallsign = true, + Keepnumber = true, + CallsignTranslations = nil, + ReferencePoint = nil, + TForget = 600, + TargetCache = nil, + smokeownposition = false, + SmokeOwn = {}, + smokeaveragetargetpos = true, + reporttostringbullsonly = true, +} + +--- +-- @type PlayerRecceDetected +-- @field #boolean detected +-- @field Wrapper.Client#CLIENT recce +-- @field #string playername +-- @field #number timestamp + +--- +-- @type LaserRelativePos +-- @field #string typename Unit type name +PLAYERRECCE.LaserRelativePos = { + ["SA342M"] = { x = 1.7, y = 1.2, z = 0 }, + ["SA342Mistral"] = { x = 1.7, y = 1.2, z = 0 }, + ["SA342Minigun"] = { x = 1.7, y = 1.2, z = 0 }, + ["SA342L"] = { x = 1.7, y = 1.2, z = 0 }, + ["Ka-50"] = { x = 6.1, y = -0.85 , z = 0 }, + ["Ka-50_3"] = { x = 6.1, y = -0.85 , z = 0 }, + ["OH58D"] = {x = 0, y = 2.8, z = 0}, +} + +--- +-- @type MaxViewDistance +-- @field #string typename Unit type name +PLAYERRECCE.MaxViewDistance = { + ["SA342M"] = 8000, + ["SA342Mistral"] = 8000, + ["SA342Minigun"] = 8000, + ["SA342L"] = 8000, + ["Ka-50"] = 8000, + ["Ka-50_3"] = 8000, + ["OH58D"] = 8000, +} + +--- +-- @type Cameraheight +-- @field #string typename Unit type name +PLAYERRECCE.Cameraheight = { + ["SA342M"] = 2.85, + ["SA342Mistral"] = 2.85, + ["SA342Minigun"] = 2.85, + ["SA342L"] = 2.85, + ["Ka-50"] = 0.5, + ["Ka-50_3"] = 0.5, + ["OH58D"] = 4.25, +} + +--- +-- @type CanLase +-- @field #string typename Unit type name +PLAYERRECCE.CanLase = { + ["SA342M"] = true, + ["SA342Mistral"] = true, + ["SA342Minigun"] = false, -- no optics + ["SA342L"] = true, + ["Ka-50"] = true, + ["Ka-50_3"] = true, + ["OH58D"] = false, -- has onboard and useable laser +} + +--- +-- @type SmokeColor +-- @field #string color +PLAYERRECCE.SmokeColor = { + ["highsmoke"] = SMOKECOLOR.Orange, + ["medsmoke"] = SMOKECOLOR.White, + ["lowsmoke"] = SMOKECOLOR.Green, + ["lasersmoke"] = SMOKECOLOR.Red, + ["ownsmoke"] = SMOKECOLOR.Blue, +} + +--- +-- @type FlareColor +-- @field #string color +PLAYERRECCE.FlareColor = { + ["highflare"] =FLARECOLOR.Yellow, + ["medflare"] = FLARECOLOR.White, + ["lowflare"] = FLARECOLOR.Green, + ["laserflare"] = FLARECOLOR.Red, + ["ownflare"] = FLARECOLOR.Green, +} + +--- Create and run a new PlayerRecce instance. +-- @param #PLAYERRECCE self +-- @param #string Name The name of this instance +-- @param #number Coalition, e.g. coalition.side.BLUE +-- @param Core.Set#SET_CLIENT PlayerSet The set of pilots working as recce +-- @return #PLAYERRECCE self +function PLAYERRECCE:New(Name, Coalition, PlayerSet) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #PLAYERRECCE + + self.Name = Name or "Blue FACA" + self.Coalition = Coalition or coalition.side.BLUE + self.CoalitionName = UTILS.GetCoalitionName(Coalition) + self.PlayerSet = PlayerSet + + self.lid=string.format("PlayerForwardController %s %s | ", self.Name, self.version) + + self:SetLaserCodes( { 1688, 1130, 4785, 6547, 1465, 4578 } ) -- set self.LaserCodes + self.lasingtime = 60 + + self.minthreatlevel = 0 + + self.reporttostringbullsonly = true + + self.TForget = 600 + self.TargetCache = FIFO:New() + + -- FSM start state is STOPPED. + self:SetStartState("Stopped") + + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "RecceOnStation", "*") + self:AddTransition("*", "RecceOffStation", "*") + self:AddTransition("*", "TargetDetected", "*") + self:AddTransition("*", "TargetsSmoked", "*") + self:AddTransition("*", "TargetsFlared", "*") + self:AddTransition("*", "Illumination", "*") + self:AddTransition("*", "TargetLasing", "*") + self:AddTransition("*", "TargetLOSLost", "*") + self:AddTransition("*", "TargetReport", "*") + self:AddTransition("*", "TargetReportSent", "*") + self:AddTransition("*", "Shack", "*") + self:AddTransition("Running", "Stop", "Stopped") + + -- Player Events + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler) + self:HandleEvent(EVENTS.Ejection, self._EventHandler) + self:HandleEvent(EVENTS.Crash, self._EventHandler) + self:HandleEvent(EVENTS.PilotDead, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) + + self:__Start(-1) + local starttime = math.random(5,10) + self:__Status(-starttime) + + self:I(self.lid.." Started.") + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the PLAYERRECCE. Note: Start() is called automatically after New(). + -- @function [parent=#PLAYERRECCE] Start + -- @param #PLAYERRECCE self + + --- Triggers the FSM event "Start" after a delay. Starts the PLAYERRECCE. Note: Start() is called automatically after New(). + -- @function [parent=#PLAYERRECCE] __Start + -- @param #PLAYERRECCE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the PLAYERRECCE and all its event handlers. + -- @param #PLAYERRECCE self + + --- Triggers the FSM event "Stop" after a delay. Stops the PLAYERRECCE and all its event handlers. + -- @function [parent=#PLAYERRECCE] __Stop + -- @param #PLAYERRECCE self + -- @param #number delay Delay in seconds. + + --- FSM Function OnAfterRecceOnStation. Recce came on station. + -- @function [parent=#PLAYERRECCE] OnAfterRecceOnStation + -- @param #PLAYERRECCE self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Client#CLIENT Client + -- @param #string Playername + -- @return #PLAYERRECCE self + + --- FSM Function OnAfterRecceOffStation. Recce went off duty. + -- @function [parent=#PLAYERRECCE] OnAfterRecceOffStation + -- @param #PLAYERRECCE self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Client#CLIENT Client + -- @param #string Playername + -- @return #PLAYERRECCE self + + --- FSM Function OnAfterTargetDetected. Targets detected. + -- @function [parent=#PLAYERRECCE] OnAfterTargetDetected + -- @param #PLAYERRECCE self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param #table Targetsbyclock #table with index 1..12 containing a #table of Wrapper.Unit#UNIT objects each. + -- @param Wrapper.Client#CLIENT Client + -- @param #string Playername + -- @return #PLAYERRECCE self + + --- FSM Function OnAfterTargetsSmoked. Smoke grenade shot. + -- @function [parent=#PLAYERRECCE] OnAfterTargetsSmoked + -- @param #PLAYERRECCE self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Client#CLIENT Client + -- @param #string Playername + -- @param Core.Set#SET_UNIT TargetSet + -- @return #PLAYERRECCE self + + --- FSM Function OnAfterTargetsFlared. Flares shot. + -- @function [parent=#PLAYERRECCE] OnAfterTargetsFlared + -- @param #PLAYERRECCE self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Client#CLIENT Client + -- @param #string Playername + -- @param Core.Set#SET_UNIT TargetSet + -- @return #PLAYERRECCE self + + --- FSM Function OnAfterIllumination. Illumination rocket shot. + -- @function [parent=#PLAYERRECCE] OnAfterIllumination + -- @param #PLAYERRECCE self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Client#CLIENT Client + -- @param #string Playername + -- @param Core.Set#SET_UNIT TargetSet + -- @return #PLAYERRECCE self + + --- FSM Function OnAfterTargetLasing. Lasing a new target. + -- @function [parent=#PLAYERRECCE] OnAfterTargetLasing + -- @param #PLAYERRECCE self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Client#CLIENT Client + -- @param Wrapper.Unit#UNIT Target + -- @param #number Lasercode + -- @param #number Lasingtime + -- @return #PLAYERRECCE self + + --- FSM Function OnAfterTargetLOSLost. Lost LOS on lased target. + -- @function [parent=#PLAYERRECCE] OnAfterTargetLOSLost + -- @param #PLAYERRECCE self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Client#CLIENT Client + -- @param Wrapper.Unit#UNIT Target + -- @return #PLAYERRECCE self + + --- FSM Function OnAfterTargetReport. Laser target report sent. + -- @function [parent=#PLAYERRECCE] OnAfterTargetReport + -- @param #PLAYERRECCE self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Client#CLIENT Client + -- @param Core.Set#SET_UNIT TargetSet + -- @param Wrapper.Unit#UNIT Target Target currently lased + -- @param #string Text + -- @return #PLAYERRECCE self + + --- FSM Function OnAfterTargetReportSent. All targets report sent. + -- @function [parent=#PLAYERRECCE] OnAfterTargetReportSent + -- @param #PLAYERRECCE self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Client#CLIENT Client Client sending the report + -- @param #string Playername Player name + -- @param Core.Set#SET_UNIT TargetSet Set of targets + -- @return #PLAYERRECCE self + + --- FSM Function OnAfterShack. Lased target has been destroyed. + -- @function [parent=#PLAYERRECCE] OnAfterShack + -- @param #PLAYERRECCE self + -- @param #string From State. + -- @param #string Event Trigger. + -- @param #string To State. + -- @param Wrapper.Client#CLIENT Client + -- @param Wrapper.Unit#UNIT Target The destroyed target (if obtainable) + -- @return #PLAYERRECCE self + + return self +end + +------------------------------------------------------------------------------------------ +-- TODO: Functions +------------------------------------------------------------------------------------------ + +--- [Internal] Event handling +-- @param #PLAYERRECCE self +-- @param Core.Event#EVENTDATA EventData +-- @return #PLAYERRECCE self +function PLAYERRECCE:_EventHandler(EventData) + self:T(self.lid.."_EventHandler: "..EventData.id) + if EventData.id == EVENTS.PlayerLeaveUnit or EventData.id == EVENTS.Ejection or EventData.id == EVENTS.Crash or EventData.id == EVENTS.PilotDead then + if EventData.IniPlayerName then + self:T(self.lid.."Event for player: "..EventData.IniPlayerName) + if self.ClientMenus[EventData.IniPlayerName] then + self.ClientMenus[EventData.IniPlayerName]:Remove() + end + self.ClientMenus[EventData.IniPlayerName] = nil + self.LaserSpots[EventData.IniPlayerName] = nil + self.OnStation[EventData.IniPlayerName] = false + self.LaserFOV[EventData.IniPlayerName] = nil + self.UnitLaserCodes[EventData.IniPlayerName] = nil + self.LaserTarget[EventData.IniPlayerName] = nil + self.AutoLase[EventData.IniPlayerName] = false + if self.ViewZone[EventData.IniPlayerName] then self.ViewZone[EventData.IniPlayerName]:UndrawZone() end + if self.ViewZoneLaser[EventData.IniPlayerName] then self.ViewZoneLaser[EventData.IniPlayerName]:UndrawZone() end + if self.ViewZoneVisual[EventData.IniPlayerName] then self.ViewZoneVisual[EventData.IniPlayerName]:UndrawZone() end + end + elseif EventData.id == EVENTS.PlayerEnterAircraft and EventData.IniCoalition == self.Coalition then + if EventData.IniPlayerName then + self:T(self.lid.."Event for player: "..EventData.IniPlayerName) + self.UnitLaserCodes[EventData.IniPlayerName] = 1688 + self.ClientMenus[EventData.IniPlayerName] = nil + self.LaserSpots[EventData.IniPlayerName] = nil + self.OnStation[EventData.IniPlayerName] = false + self.LaserFOV[EventData.IniPlayerName] = nil + self.UnitLaserCodes[EventData.IniPlayerName] = nil + self.LaserTarget[EventData.IniPlayerName] = nil + self.AutoLase[EventData.IniPlayerName] = false + if self.ViewZone[EventData.IniPlayerName] then self.ViewZone[EventData.IniPlayerName]:UndrawZone() end + if self.ViewZoneLaser[EventData.IniPlayerName] then self.ViewZoneLaser[EventData.IniPlayerName]:UndrawZone() end + if self.ViewZoneVisual[EventData.IniPlayerName] then self.ViewZoneVisual[EventData.IniPlayerName]:UndrawZone() end + self:_BuildMenus() + end + end + return self +end + +--- (Internal) Function to determine clockwise direction to target. +-- @param #PLAYERRECCE self +-- @param Wrapper.Unit#UNIT unit The Helicopter +-- @param Wrapper.Unit#UNIT target The downed Group +-- @return #number direction +function PLAYERRECCE:_GetClockDirection(unit, target) + self:T(self.lid .. " _GetClockDirection") + + local _playerPosition = unit:GetCoordinate() -- get position of helicopter + local _targetpostions = target:GetCoordinate() -- get position of downed pilot + local _heading = unit:GetHeading() -- heading + --self:I("Heading = ".._heading) + local DirectionVec3 = _playerPosition:GetDirectionVec3( _targetpostions ) + local Angle = _playerPosition:GetAngleDegrees( DirectionVec3 ) + --self:I("Angle = "..Angle) + local clock = 12 + local hours = 0 + if _heading and Angle then + clock = 12 + --if angle == 0 then angle = 360 end + clock = _heading-Angle + hours = (clock/30)*-1 + --self:I("hours = "..hours) + clock = 12+hours + clock = UTILS.Round(clock,0) + if clock > 12 then clock = clock-12 end + if clock == 0 then clock = 12 end + end + --self:I("Clock ="..clock) + return clock +end + +--- [User] Set a table of possible laser codes. +-- Each new RECCE can select a code from this table, default is { 1688, 1130, 4785, 6547, 1465, 4578 }. +-- @param #PLAYERRECCE self +-- @param #list<#number> LaserCodes +-- @return #PLAYERRECCE +function PLAYERRECCE:SetLaserCodes( LaserCodes ) + self.LaserCodes = ( type( LaserCodes ) == "table" ) and LaserCodes or { LaserCodes } + return self +end + +--- [User] Set a reference point coordinate for A2G Operations. Will be used in coordinate references. +-- @param #PLAYERRECCE self +-- @param Core.Point#COORDINATE Coordinate Coordinate of the RP +-- @param #string Name Name of the RP +-- @return #PLAYERRECCE +function PLAYERRECCE:SetReferencePoint(Coordinate,Name) + self.ReferencePoint = Coordinate + self.RPName = Name + if self.RPMarker then + self.RPMarker:Remove() + end + local llddm = Coordinate:ToStringLLDDM() + local lldms = Coordinate:ToStringLLDMS() + local mgrs = Coordinate:ToStringMGRS() + local text = string.format("%s RP %s\n%s\n%s\n%s",self.Name,Name,llddm,lldms,mgrs) + self.RPMarker = MARKER:New(Coordinate,text) + self.RPMarker:ReadOnly() + self.RPMarker:ToCoalition(self.Coalition) + return self +end + +--- [User] Set PlayerTaskController. Allows to upload target reports to the controller, in turn creating tasks for other players. +-- @param #PLAYERRECCE self +-- @param Ops.PlayerTask#PLAYERTASKCONTROLLER Controller +-- @return #PLAYERRECCE +function PLAYERRECCE:SetPlayerTaskController(Controller) + self.UseController = true + self.Controller = Controller + return self +end + +--- [User] Set a set of clients which will receive target reports +-- @param #PLAYERRECCE self +-- @param Core.Set#SET_CLIENT AttackSet +-- @return #PLAYERRECCE +function PLAYERRECCE:SetAttackSet(AttackSet) + self.AttackSet = AttackSet + return self +end + +---[Internal] Check Helicopter camera in on +-- @param #PLAYERRECCE self +-- @param Wrapper.Client#CLIENT client +-- @param #string playername +-- @return #boolean OnOff +function PLAYERRECCE:_CameraOn(client,playername) + local camera = true + local unit = client -- Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + local typename = unit:GetTypeName() + if string.find(typename,"SA342") then + local dcsunit = Unit.getByName(client:GetName()) + local vivihorizontal = dcsunit:getDrawArgumentValue(215) or 0 -- (not in MiniGun) 1 to -1 -- zero is straight ahead, 1/-1 = 180 deg + if vivihorizontal < -0.7 or vivihorizontal > 0.7 then + camera = false + end + elseif string.find(typename,"OH58") then + local dcsunit = Unit.getByName(client:GetName()) + local vivihorizontal = dcsunit:getDrawArgumentValue(528) or 0 -- Kiow + if vivihorizontal < -0.527 or vivihorizontal > 0.527 then + camera = false + end + elseif string.find(typename,"Ka-50") then + camera = true + end + end + return camera +end + +--- [Internal] Get the view parameters from a Kiowa MMS camera +-- @param #PLAYERRECCE self +-- @param Wrapper.Unit#UNIT Kiowa +-- @return #number cameraheading in degrees. +-- @return #number cameranodding in degrees. +-- @return #number maxview in meters. +-- @return #boolean cameraison If true, camera is on, else off. +function PLAYERRECCE:_GetKiowaMMSSight(Kiowa) + self:T(self.lid.."_GetKiowaMMSSight") + local unit = Kiowa -- Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + local dcsunit = Unit.getByName(Kiowa:GetName()) + --[[ + shagrat — 01/01/2025 23:13 + Found the necessary ARGS for the Kiowa MMS angle and rotation: + Arg 527 vertical movement + 0 = neutral + -1.0 = max depression (30° max depression angle) + +1.0 = max elevation angle (30° max elevation angle) + + Arg 528 horizontal movement + 0 = forward (0 degr) + -0.25 = 90° left + -0.5 = rear (180°) left (max 190° = -0.527 + +0.25 = 90° right + +0.5 = 180° right (max 190° = 0.527) + --]] + local mmshorizontal = dcsunit:getDrawArgumentValue(528) or 0 + local mmsvertical = dcsunit:getDrawArgumentValue(527) or 0 + self:T(string.format("Kiowa MMS Arguments Read: H %.3f V %.3f",mmshorizontal,mmsvertical)) + local mmson = true + if mmshorizontal < -0.527 or mmshorizontal > 0.527 then mmson = false end + local horizontalview = mmshorizontal / 0.527 * 190 + local heading = unit:GetHeading() + local mmsheading = (heading+horizontalview)%360 + --local mmsyaw = mmsvertical * 30 + local mmsyaw = math.atan(mmsvertical)*40 + local maxview = self:_GetActualMaxLOSight(unit,mmsheading, mmsyaw,not mmson) + if maxview > 8000 then maxview = 8000 end + self:T(string.format("Kiowa MMS Heading %d, Yaw %d, MaxView %dm MMS On %s",mmsheading,mmsyaw,maxview,tostring(mmson))) + return mmsheading,mmsyaw,maxview,mmson + end + return 0,0,0,false +end + + +--- [Internal] Get the view parameters from a Gazelle camera +-- @param #PLAYERRECCE self +-- @param Wrapper.Unit#UNIT Gazelle +-- @return #number cameraheading in degrees. +-- @return #number cameranodding in degrees. +-- @return #number maxview in meters. +-- @return #boolean cameraison If true, camera is on, else off. +function PLAYERRECCE:_GetGazelleVivianneSight(Gazelle) + self:T(self.lid.."GetGazelleVivianneSight") + local unit = Gazelle -- Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + local dcsunit = Unit.getByName(Gazelle:GetName()) + local vivihorizontal = dcsunit:getDrawArgumentValue(215) or 0 -- (not in MiniGun) 1 to -1 -- zero is straight ahead, 1/-1 = 180 deg, + local vivivertical = dcsunit:getDrawArgumentValue(216) or 0 -- L/Mistral/Minigun model has no 216, ca 10deg up (=1) and down (=-1) + -- vertical model limits 1.53846, -1.10731 + local vivioff = false + -- -1 = -180, 1 = 180 + -- Actual model view -0,66 to 0,66 + -- Nick view 1.53846, -1.10731 for - 30° to +45° + if vivihorizontal < -0.67 then -- model end + vivihorizontal = -0.67 + vivioff = false + --return 0,0,0,false + elseif vivihorizontal > 0.67 then -- vivi off + vivihorizontal = 0.67 + vivioff = true + return 0,0,0,false + end + + local horizontalview = vivihorizontal * -180 + --local verticalview = vivivertical * 30 -- ca +/- 30° + local verticalview = math.atan(vivivertical) + + local heading = unit:GetHeading() + local viviheading = (heading+horizontalview)%360 + local maxview = self:_GetActualMaxLOSight(unit,viviheading, verticalview,vivioff) + if maxview > 8000 then maxview = 8000 end + return viviheading, verticalview,maxview, not vivioff + end + return 0,0,0,false +end + +--- [Internal] Get the max line of sight based on unit head and camera nod via trigonometry. Returns 0 if camera is off. +-- @param #PLAYERRECCE self +-- @param Wrapper.Unit#UNIT unit The unit which LOS we want +-- @param #number vheading Heading where the unit or camera is looking +-- @param #number vnod Nod down in degrees +-- @param #boolean vivoff Camera on or off +-- @return #number maxview Max view distance in meters +function PLAYERRECCE:_GetActualMaxLOSight(unit,vheading, vnod, vivoff) + self:T(self.lid.."_GetActualMaxLOSight") + if vivoff then return 0 end + --if vnod < -0.03 then vnod = -0.03 end + local maxview = 0 + if unit and unit:IsAlive() then + local typename = unit:GetTypeName() + maxview = self.MaxViewDistance[typename] or 8000 + local CamHeight = self.Cameraheight[typename] or 1 + if vnod < -2 then + -- Looking down + -- determine max distance we're looking at + local beta = 90 + local gamma = 90-math.abs(vnod) + local alpha = 90-gamma + local a = unit:GetHeight()-unit:GetCoordinate():GetLandHeight()+CamHeight + local b = a / math.sin(math.rad(alpha)) + local c = b * math.sin(math.rad(gamma)) + maxview = c*1.2 -- +20% + end + end + return math.ceil(math.abs(maxview)) +end + +--- [User] Set callsign options for TTS output. See @{Wrapper.Group#GROUP.GetCustomCallSign}() on how to set customized callsigns. +-- @param #PLAYERRECCE self +-- @param #boolean ShortCallsign If true, only call out the major flight number +-- @param #boolean Keepnumber If true, keep the **customized callsign** in the #GROUP name for players as-is, no amendments or numbers. +-- @param #table CallsignTranslations (optional) Table to translate between DCS standard callsigns and bespoke ones. Does not apply if using customized +-- callsigns from playername or group name. +-- @param #func CallsignCustomFunc (Optional) For player names only(!). If given, this function will return the callsign. Needs to take the groupname and the playername as first two arguments. +-- @param #arg ... (Optional) Comma separated arguments to add to the custom function call after groupname and playername. +-- @return #PLAYERRECCE self +function PLAYERRECCE:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations,CallsignCustomFunc,...) + if not ShortCallsign or ShortCallsign == false then + self.ShortCallsign = false + else + self.ShortCallsign = true + end + self.Keepnumber = Keepnumber or false + self.CallsignTranslations = CallsignTranslations + self.CallsignCustomFunc = CallsignCustomFunc + self.CallsignCustomArgs = arg or {} + return self +end + +--- [Internal] Build a ZONE_POLYGON from a given viewport of a unit +-- @param #PLAYERRECCE self +-- @param Wrapper.Unit#UNIT unit The unit which is looking +-- @param #number vheading Heading where the unit or camera is looking +-- @param #number minview Min line of sight - for lasing +-- @param #number maxview Max line of sight +-- @param #number angle Angle left/right to be added to heading to form a triangle +-- @param #boolean camon Camera is switched on +-- @param #boolean laser Zone is for lasing +-- @return Core.Zone#ZONE_POLYGON ViewZone or nil if camera is off +function PLAYERRECCE:_GetViewZone(unit, vheading, minview, maxview, angle, camon, laser) + self:T(self.lid.."_GetViewZone") + local viewzone = nil + if not camon then return nil end + if unit and unit:IsAlive() then + local unitname = unit:GetName() + if not laser then + -- Triangle + local startpos = unit:GetCoordinate() + local heading1 = (vheading+angle)%360 + local heading2 = (vheading-angle)%360 + local pos1 = startpos:Translate(maxview,heading1) + local pos2 = startpos:Translate(maxview,heading2) + local array = {} + table.insert(array,startpos:GetVec2()) + table.insert(array,pos1:GetVec2()) + table.insert(array,pos2:GetVec2()) + viewzone = ZONE_POLYGON:NewFromPointsArray(unitname,array) + else + -- Square + local startp = unit:GetCoordinate() + local heading1 = (vheading+90)%360 + local heading2 = (vheading-90)%360 + self:T({heading1,heading2}) + local startpos = startp:Translate(minview,vheading) + local pos1 = startpos:Translate(12.5,heading1) + local pos2 = startpos:Translate(12.5,heading2) + local pos3 = pos1:Translate(maxview,vheading) + local pos4 = pos2:Translate(maxview,vheading) + local array = {} + table.insert(array,pos1:GetVec2()) + table.insert(array,pos2:GetVec2()) + table.insert(array,pos4:GetVec2()) + table.insert(array,pos3:GetVec2()) + viewzone = ZONE_POLYGON:NewFromPointsArray(unitname,array) + end + end + return viewzone +end + +--- [Internal] +--@param #PLAYERRECCE self +--@param Wrapper.Client#CLIENT client +--@return Core.Set#SET_UNIT Set of targets, can be empty! +--@return #number count Count of targets +function PLAYERRECCE:_GetKnownTargets(client) + self:T(self.lid.."_GetKnownTargets") + local finaltargets = SET_UNIT:New() + local targets = self.TargetCache:GetDataTable() + local playername = client:GetPlayerName() + for _,_target in pairs(targets) do + local targetdata = _target.PlayerRecceDetected -- Ops.PlayerRecce#PLAYERRECCE.PlayerRecceDetected + if targetdata.playername == playername then + finaltargets:Add(_target:GetName(),_target) + end + end + return finaltargets,finaltargets:CountAlive() +end + +--- [Internal] +--@param #PLAYERRECCE self +--@return #PLAYERRECCE self +function PLAYERRECCE:_CleanupTargetCache() + self:T(self.lid.."_CleanupTargetCache") + local cleancache = FIFO:New() + self.TargetCache:ForEach( + function(unit) + local pull = false + if unit and unit:IsAlive() and unit:GetLife() > 1 then + if unit.PlayerRecceDetected and unit.PlayerRecceDetected.timestamp then + local TNow = timer.getTime() + if TNow-unit.PlayerRecceDetected.timestamp > self.TForget then + -- Forget this unit + pull = true + unit.PlayerRecceDetected=nil + end + else + -- no timestamp + pull = true + end + else + -- dead + pull = true + end + if not pull then + cleancache:Push(unit,unit:GetName()) + end + end + ) + self.TargetCache = nil + self.TargetCache = cleancache + return self +end + +--- [Internal] +--@param #PLAYERRECCE self +--@param Wrapper.Unit#UNIT unit The FACA unit +--@param #boolean camera If true, use the unit's camera for targets in sight +--@param #laser laser Use laser zone +--@return Core.Set#SET_UNIT Set of targets, can be empty! +--@return #number count Count of targets +function PLAYERRECCE:_GetTargetSet(unit,camera,laser) + self:T(self.lid.."_GetTargetSet") + local finaltargets = SET_UNIT:New() + local finalcount = 0 + local minview = 0 + local typename = unit:GetTypeName() + local playername = unit:GetPlayerName() + local maxview = self.MaxViewDistance[typename] or 8000 + local heading,nod,maxview,angle = 0,30,8000,10 + local camon = false + local name = unit:GetName() + if string.find(typename,"SA342") and camera then + heading,nod,maxview,camon = self:_GetGazelleVivianneSight(unit) + angle=10 + -- Model nod and actual TV view don't compute + maxview = self.MaxViewDistance[typename] or 8000 + elseif string.find(typename,"Ka-50") and camera then + heading = unit:GetHeading() + nod,maxview,camon = 10,1000,true + angle = 10 + maxview = self.MaxViewDistance[typename] or 8000 + elseif string.find(typename,"OH58") and camera then + --heading = unit:GetHeading() + nod,maxview,camon = 0,8000,true + heading,nod,maxview,camon = self:_GetKiowaMMSSight(unit) + angle = 8 + if maxview == 0 then + maxview = self.MaxViewDistance[typename] or 8000 + end + else + -- visual + heading = unit:GetHeading() + nod,maxview,camon = 10,3000,true + maxview = self.MaxViewDistance[typename] or 3000 + angle = 45 + end + if laser then + -- get min/max values + if not self.LaserFOV[playername] then + minview = 100 + maxview = 2000 + self.LaserFOV[playername] = { + min=100, + max=2000, + } + else + minview = self.LaserFOV[playername].min + maxview = self.LaserFOV[playername].max + end + end + local zone = self:_GetViewZone(unit,heading,minview,maxview,angle,camon,laser) + if zone then + local redcoalition = "red" + if self.Coalition == coalition.side.RED then + redcoalition = "blue" + end + -- determine what we can see + local startpos = unit:GetCoordinate() + local targetset = SET_UNIT:New():FilterCategories("ground"):FilterActive(true):FilterZones({zone}):FilterCoalitions(redcoalition):FilterOnce() + self:T("Prefilter Target Count = "..targetset:CountAlive()) + -- TODO - Threat level filter? + -- TODO - Min distance from unit? + targetset:ForEach( + function(_unit) + local _unit = _unit -- Wrapper.Unit#UNIT + local _unitpos = _unit:GetCoordinate() + if startpos:IsLOS(_unitpos) and _unit:IsAlive() and _unit:GetLife()>1 then + self:T("Adding to final targets: ".._unit:GetName()) + finaltargets:Add(_unit:GetName(),_unit) + end + end + ) + finalcount = finaltargets:CountAlive() + self:T(string.format("%s Unit: %s | Targets in view %s",self.lid,name,finalcount)) + end + return finaltargets, finalcount, zone +end + +---[Internal] +--@param #PLAYERRECCE self +--@param Core.Set#SET_UNIT targetset Set of targets, can be empty! +--@return Wrapper.Unit#UNIT Target or nil +function PLAYERRECCE:_GetHVTTarget(targetset) + self:T(self.lid.."_GetHVTTarget") + -- sort units + local unitsbythreat = {} + local minthreat = self.minthreatlevel or 0 + for _,_unit in pairs(targetset.Set) do + local unit = _unit -- Wrapper.Unit#UNIT + if unit and unit:IsAlive() and unit:GetLife() >1 then + local threat = unit:GetThreatLevel() + if threat >= minthreat then + -- prefer radar units + if unit:HasAttribute("RADAR_BAND1_FOR_ARM") or unit:HasAttribute("RADAR_BAND2_FOR_ARM") or unit:HasAttribute("Optical Tracker") then + threat = 11 + end + table.insert(unitsbythreat,{unit,threat}) + end + end + end + + table.sort(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) + + if unitsbythreat[1] and unitsbythreat[1][1] then + return unitsbythreat[1][1] + else + return nil + end +end + +--- [Internal] +--@param #PLAYERRECCE self +--@param Wrapper.Client#CLIENT client The FACA unit +--@param Core.Set#SET_UNIT targetset Set of targets, can be empty! +--@return #PLAYERRECCE self +function PLAYERRECCE:_LaseTarget(client,targetset) + self:T(self.lid.."_LaseTarget") + -- get one target + local target = self:_GetHVTTarget(targetset) -- Wrapper.Unit#UNIT + local playername = client:GetPlayerName() + local laser = nil -- Core.Spot#SPOT + -- set laser + if not self.LaserSpots[playername] then + laser = SPOT:New(client) + if not self.UnitLaserCodes[playername] then + self.UnitLaserCodes[playername] = 1688 + end + laser.LaserCode = self.UnitLaserCodes[playername] or 1688 + self.LaserSpots[playername] = laser + else + laser = self.LaserSpots[playername] + end + -- old target + if self.LaserTarget[playername] then + -- still looking at target? + local target=self.LaserTarget[playername] -- Ops.Target#TARGET + local oldtarget = target:GetObject() --or laser.Target + self:T("Targetstate: "..target:GetState()) + self:T("Laser State: "..tostring(laser:IsLasing())) + if (not oldtarget) or targetset:IsNotInSet(oldtarget) or target:IsDead() or target:IsDestroyed() then + -- lost LOS or dead + laser:LaseOff() + self:T(self.lid.."Target Life Points: "..target:GetLife() or "none") + if target:IsDead() or target:IsDestroyed() or target:GetDamage() > 79 or target:GetLife() <= 1 then + self:__Shack(-1,client,oldtarget) + --self.LaserTarget[playername] = nil + else + self:__TargetLOSLost(-1,client,oldtarget) + --self.LaserTarget[playername] = nil + end + self.LaserTarget[playername] = nil + oldtarget = nil + self.LaserSpots[playername] = nil + elseif oldtarget and laser and (not laser:IsLasing()) then + --laser:LaseOff() + self:T("Switching laser back on ..") + local lasercode = self.UnitLaserCodes[playername] or laser.LaserCode or 1688 + local lasingtime = self.lasingtime or 60 + --local targettype = target:GetTypeName() + laser:LaseOn(oldtarget,lasercode,lasingtime) + --self:__TargetLasing(-1,client,oldtarget,lasercode,lasingtime) + else + -- we should not be here... + self:T("Target alive and laser is on!") + --self.LaserSpots[playername] = nil + end + -- new target + elseif (not laser:IsLasing()) and target then + local relativecam = self.LaserRelativePos[client:GetTypeName()] + laser:SetRelativeStartPosition(relativecam) + local lasercode = self.UnitLaserCodes[playername] or laser.LaserCode or 1688 + local lasingtime = self.lasingtime or 60 + --local targettype = target:GetTypeName() + laser:LaseOn(target,lasercode,lasingtime) + self.LaserTarget[playername] = TARGET:New(target) + --self.LaserTarget[playername].TStatus = 9 + self:__TargetLasing(-1,client,target,lasercode,lasingtime) + end + return self +end + +--- [Internal] +-- @param #PLAYERRECCE self +-- @param Wrapper.Client#CLIENT client +-- @param Wrapper.Group#GROUP group +-- @param #string playername +-- @return #PLAYERRECCE self +function PLAYERRECCE:_SetClientLaserCode(client,group,playername,code) + self:T(self.lid.."_SetClientLaserCode") + self.UnitLaserCodes[playername] = code or 1688 + if self.ClientMenus[playername] then + self.ClientMenus[playername]:Remove() + self.ClientMenus[playername]=nil + end + self:_BuildMenus() + return self +end + +--- [Internal] +-- @param #PLAYERRECCE self +-- @param Wrapper.Client#CLIENT client +-- @param Wrapper.Group#GROUP group +-- @param #string playername +-- @return #PLAYERRECCE self +function PLAYERRECCE:_SwitchOnStation(client,group,playername) + self:T(self.lid.."_SwitchOnStation") + if not self.OnStation[playername] then + self.OnStation[playername] = true + self:__RecceOnStation(-1,client,playername) + else + self.OnStation[playername] = false + self:__RecceOffStation(-1,client,playername) + end + if self.ClientMenus[playername] then + self.ClientMenus[playername]:Remove() + self.ClientMenus[playername]=nil + end + self:_BuildMenus(client) + return self +end + +--- [Internal] +-- @param #PLAYERRECCE self +-- @param Wrapper.Client#CLIENT client +-- @param Wrapper.Group#GROUP group +-- @param #string playername +-- @return #PLAYERRECCE self +function PLAYERRECCE:_SwitchSmoke(client,group,playername) + self:T(self.lid.."_SwitchLasing") + if not self.SmokeOwn[playername] then + self.SmokeOwn[playername] = true + MESSAGE:New("Smoke self is now ON",10,self.Name or "FACA"):ToClient(client) + else + self.SmokeOwn[playername] = false + MESSAGE:New("Smoke self is now OFF",10,self.Name or "FACA"):ToClient(client) + end + if self.ClientMenus[playername] then + self.ClientMenus[playername]:Remove() + self.ClientMenus[playername]=nil + end + self:_BuildMenus(client) + return self +end + +--- [Internal] +-- @param #PLAYERRECCE self +-- @param Wrapper.Client#CLIENT client +-- @param Wrapper.Group#GROUP group +-- @param #string playername +-- @return #PLAYERRECCE self +function PLAYERRECCE:_SwitchLasing(client,group,playername) + self:T(self.lid.."_SwitchLasing") + if not self.AutoLase[playername] then + self.AutoLase[playername] = true + MESSAGE:New("Lasing is now ON",10,self.Name or "FACA"):ToClient(client) + else + self.AutoLase[playername] = false + if self.LaserSpots[playername] then + local laser = self.LaserSpots[playername] -- Core.Spot#SPOT + if laser:IsLasing() then + laser:LaseOff() + end + self.LaserSpots[playername] = nil + end + MESSAGE:New("Lasing is now OFF",10,self.Name or "FACA"):ToClient(client) + end + if self.ClientMenus[playername] then + self.ClientMenus[playername]:Remove() + self.ClientMenus[playername]=nil + end + self:_BuildMenus(client) + return self +end + +--- [Internal] +-- @param #PLAYERRECCE self +-- @param Wrapper.Client#CLIENT client +-- @param Wrapper.Group#GROUP group +-- @param #string playername +-- @param #number mindist +-- @param #number maxdist +-- @return #PLAYERRECCE self +function PLAYERRECCE:_SwitchLasingDist(client,group,playername,mindist,maxdist) + self:T(self.lid.."_SwitchLasingDist") + local mind = mindist or 100 + local maxd = maxdist or 2000 + if not self.LaserFOV[playername] then + self.LaserFOV[playername] = { + min=mind, + max=maxd, + } + else + self.LaserFOV[playername].min=mind + self.LaserFOV[playername].max=maxd + end + MESSAGE:New(string.format("Laser distance set to %d-%dm!",mindist,maxdist),10,"FACA"):ToClient(client) + if self.ClientMenus[playername] then + self.ClientMenus[playername]:Remove() + self.ClientMenus[playername]=nil + end + self:_BuildMenus(client) + return self +end + +--- [Internal] +-- @param #PLAYERRECCE self +-- @param Wrapper.Client#CLIENT client +-- @param Wrapper.Group#GROUP group +-- @param #string playername +-- @return #PLAYERRECCE self +function PLAYERRECCE:_WIP(client,group,playername) + self:T(self.lid.."_WIP") + return self +end + +--- [Internal] +-- @param #PLAYERRECCE self +-- @param Wrapper.Client#CLIENT client +-- @param Wrapper.Group#GROUP group +-- @param #string playername +-- @return #PLAYERRECCE self +function PLAYERRECCE:_SmokeTargets(client,group,playername) + self:T(self.lid.."_SmokeTargets") + local cameraset = self:_GetTargetSet(client,true) -- Core.Set#SET_UNIT + local visualset = self:_GetTargetSet(client,false) -- Core.Set#SET_UNIT + + if cameraset:CountAlive() > 0 or visualset:CountAlive() > 0 then + self:__TargetsSmoked(-1,client,playername,cameraset) + else + return self + end + + local highsmoke = self.SmokeColor.highsmoke + local medsmoke = self.SmokeColor.medsmoke + local lowsmoke = self.SmokeColor.lowsmoke + local lasersmoke = self.SmokeColor.lasersmoke + local laser = self.LaserSpots[playername] -- Core.Spot#SPOT + + -- laser targer gets extra smoke + if laser and laser.Target and laser.Target:IsAlive() then + laser.Target:GetCoordinate():Smoke(lasersmoke) + end + + local coord = visualset:GetCoordinate() + if coord and self.smokeaveragetargetpos then + coord:SetAtLandheight() + coord:Smoke(medsmoke) + else + -- smoke everything + for _,_unit in pairs(visualset.Set) do + local unit = _unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + local coord = unit:GetCoordinate() + local threat = unit:GetThreatLevel() + if coord then + local color = lowsmoke + if threat > 7 then + color = highsmoke + elseif threat > 2 then + color = medsmoke + end + coord:Smoke(color) + end + end + end + end + if self.SmokeOwn[playername] then + local cc = client:GetVec2() + -- don't smoke mid-air + local lc = COORDINATE:NewFromVec2(cc,1) + local color = self.SmokeColor.ownsmoke + lc:Smoke(color) + end + + return self +end + +--- [Internal] +-- @param #PLAYERRECCE self +-- @param Wrapper.Client#CLIENT client +-- @param Wrapper.Group#GROUP group +-- @param #string playername +-- @return #PLAYERRECCE self +function PLAYERRECCE:_FlareTargets(client,group,playername) + self:T(self.lid.."_FlareTargets") + local cameraset = self:_GetTargetSet(client,true) -- Core.Set#SET_UNIT + local visualset = self:_GetTargetSet(client,false) -- Core.Set#SET_UNIT + cameraset:AddSet(visualset) + if cameraset:CountAlive() > 0 then + self:__TargetsFlared(-1,client,playername,cameraset) + end + local highsmoke = self.FlareColor.highflare + local medsmoke = self.FlareColor.medflare + local lowsmoke = self.FlareColor.lowflare + local lasersmoke = self.FlareColor.laserflare + local laser = self.LaserSpots[playername] -- Core.Spot#SPOT + -- laser targer gets extra smoke + if laser and laser.Target and laser.Target:IsAlive() then + laser.Target:GetCoordinate():Flare(lasersmoke) + if cameraset:IsInSet(laser.Target) then + cameraset:Remove(laser.Target:GetName(),true) + end + end + -- smoke everything else + for _,_unit in pairs(cameraset.Set) do + local unit = _unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + local coord = unit:GetCoordinate() + local threat = unit:GetThreatLevel() + if coord then + local color = lowsmoke + if threat > 7 then + color = highsmoke + elseif threat > 2 then + color = medsmoke + end + coord:Flare(color) + end + end + end + return self +end + +--- [Internal] +-- @param #PLAYERRECCE self +-- @param Wrapper.Client#CLIENT client +-- @param Wrapper.Group#GROUP group +-- @param #string playername +-- @return #PLAYERRECCE self +function PLAYERRECCE:_IlluTargets(client,group,playername) + self:T(self.lid.."_IlluTargets") + local totalset, count = self:_GetKnownTargets(client) -- Core.Set#SET_UNIT + if count > 0 then + local coord = totalset:GetCoordinate() -- Core.Point#COORDINATE + coord.y = coord.y + 200 + coord:IlluminationBomb(nil,1) + self:__Illumination(1,client,playername,totalset) + end + return self +end + +--- [Internal] +-- @param #PLAYERRECCE self +-- @param Wrapper.Client#CLIENT client +-- @param Wrapper.Group#GROUP group +-- @param #string playername +-- @return #PLAYERRECCE self +function PLAYERRECCE:_UploadTargets(client,group,playername) + self:T(self.lid.."_UploadTargets") + --local targetset, number = self:_GetTargetSet(client,true) + --local vtargetset, vnumber = self:_GetTargetSet(client,false) + local totalset, count = self:_GetKnownTargets(client) + --local totalset = SET_UNIT:New() + -- totalset:AddSet(targetset) + --totalset:AddSet(vtargetset) + if count > 0 then + self.Controller:AddTarget(totalset) + self:__TargetReportSent(1,client,playername,totalset) + end + return self +end + +--- [Internal] +-- @param #PLAYERRECCE self +-- @param Wrapper.Client#CLIENT client +-- @param Wrapper.Group#GROUP group +-- @param #string playername +-- @return #PLAYERRECCE self +function PLAYERRECCE:_ReportLaserTargets(client,group,playername) +self:T(self.lid.."_ReportLaserTargets") + local targetset, number = self:_GetTargetSet(client,true,true) + if number > 0 and self.AutoLase[playername] then + local Settings = ( client and _DATABASE:GetPlayerSettings( playername ) ) or _SETTINGS + local target = self:_GetHVTTarget(targetset) -- the one we're lasing + local ThreatLevel = target:GetThreatLevel() or 1 + local ThreatLevelText = "high" + if ThreatLevel > 3 and ThreatLevel < 8 then + ThreatLevelText = "medium" + elseif ThreatLevel <= 3 then + ThreatLevelText = "low" + end + local ThreatGraph = "[" .. string.rep( "■", ThreatLevel ) .. string.rep( "□", 10 - ThreatLevel ) .. "]: "..ThreatLevel + local report = REPORT:New("Lasing Report") + report:Add(string.rep("-",15)) + report:Add("Target type: "..target:GetTypeName() or "unknown") + report:Add("Threat Level: "..ThreatGraph.." ("..ThreatLevelText..")") + if not self.ReferencePoint then + report:Add("Location: "..client:GetCoordinate():ToStringBULLS(self.Coalition,Settings)) + if self.reporttostringbullsonly ~= true then + report:Add("Location: "..client:GetCoordinate():ToStringA2G(nil,Settings)) + end + else + report:Add("Location: "..client:GetCoordinate():ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings)) + end + report:Add("Laser Code: "..self.UnitLaserCodes[playername] or 1688) + report:Add(string.rep("-",15)) + local text = report:Text() + self:__TargetReport(1,client,targetset,target,text) + else + local report = REPORT:New("Lasing Report") + report:Add(string.rep("-",15)) + report:Add("N O T A R G E T S") + report:Add(string.rep("-",15)) + local text = report:Text() + self:__TargetReport(1,client,nil,nil,text) + end + return self +end + +--- [Internal] +-- @param #PLAYERRECCE self +-- @param Wrapper.Client#CLIENT client +-- @param Wrapper.Group#GROUP group +-- @param #string playername +-- @return #PLAYERRECCE self +function PLAYERRECCE:_ReportVisualTargets(client,group,playername) + self:T(self.lid.."_ReportVisualTargets") + local targetset, number = self:_GetKnownTargets(client) + if number > 0 then + local Settings = ( client and _DATABASE:GetPlayerSettings( playername ) ) or _SETTINGS + local ThreatLevel = targetset:CalculateThreatLevelA2G() + local ThreatLevelText = "high" + if ThreatLevel > 3 and ThreatLevel < 8 then + ThreatLevelText = "medium" + elseif ThreatLevel <= 3 then + ThreatLevelText = "low" + end + local ThreatGraph = "[" .. string.rep( "■", ThreatLevel ) .. string.rep( "□", 10 - ThreatLevel ) .. "]: "..ThreatLevel + local report = REPORT:New("Target Report") + report:Add(string.rep("-",15)) + report:Add("Target count: "..number) + report:Add("Threat Level: "..ThreatGraph.." ("..ThreatLevelText..")") + if not self.ReferencePoint then + report:Add("Location: "..client:GetCoordinate():ToStringBULLS(self.Coalition,Settings)) + if self.reporttostringbullsonly ~= true then + report:Add("Location: "..client:GetCoordinate():ToStringA2G(nil,Settings)) + end + else + report:Add("Location: "..client:GetCoordinate():ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings)) + if self.reporttostringbullsonly ~= true then + report:Add("Location: "..client:GetCoordinate():ToStringA2G(nil,Settings)) + end + end + report:Add(string.rep("-",15)) + local text = report:Text() + self:__TargetReport(1,client,targetset,nil,text) + else + local report = REPORT:New("Target Report") + report:Add(string.rep("-",15)) + report:Add("N O T A R G E T S") + report:Add(string.rep("-",15)) + local text = report:Text() + self:__TargetReport(1,client,nil,nil,text) + end + return self +end + +--- [Internal] Build Menus +-- @param #PLAYERRECCE self +-- @param Wrapper.Client#CLIENT Client (optional) Client object +-- @return #PLAYERRECCE self +function PLAYERRECCE:_BuildMenus(Client) + self:T(self.lid.."_BuildMenus") + local clients = self.PlayerSet -- Core.Set#SET_CLIENT + local clientset = clients:GetSetObjects() + if Client then clientset = {Client} end + for _,_client in pairs(clientset) do + local client = _client -- Wrapper.Client#CLIENT + if client and client:IsAlive() then + local playername = client:GetPlayerName() + self:T("Menu for "..playername) + if not self.UnitLaserCodes[playername] then + self:_SetClientLaserCode(nil,nil,playername,1688) + end + if self.SmokeOwn[playername] == nil then + self.SmokeOwn[playername] = self.smokeownposition + end + local group = client:GetGroup() + if not self.ClientMenus[playername] then + self:T("Start Menubuild for "..playername) + local canlase = self.CanLase[client:GetTypeName()] + self.ClientMenus[playername] = MENU_GROUP:New(group,self.MenuName or self.Name or "RECCE") + local txtonstation = self.OnStation[playername] and "ON" or "OFF" + local text = string.format("Switch On-Station (%s)",txtonstation) + local onstationmenu = MENU_GROUP_COMMAND:New(group,text,self.ClientMenus[playername],self._SwitchOnStation,self,client,group,playername) + if self.OnStation[playername] then + local smoketopmenu = MENU_GROUP:New(group,"Visual Markers",self.ClientMenus[playername]) + local smokemenu = MENU_GROUP_COMMAND:New(group,"Smoke Targets",smoketopmenu,self._SmokeTargets,self,client,group,playername) + local flaremenu = MENU_GROUP_COMMAND:New(group,"Flare Targets",smoketopmenu,self._FlareTargets,self,client,group,playername) + local illumenu = MENU_GROUP_COMMAND:New(group,"Illuminate Area",smoketopmenu,self._IlluTargets,self,client,group,playername) + local ownsm = self.SmokeOwn[playername] and "ON" or "OFF" + local owntxt = string.format("Switch smoke self (%s)",ownsm) + local ownsmoke = MENU_GROUP_COMMAND:New(group,owntxt,smoketopmenu,self._SwitchSmoke,self,client,group,playername) + if canlase then + local txtonstation = self.AutoLase[playername] and "ON" or "OFF" + local text = string.format("Switch Lasing (%s)",txtonstation) + local lasemenu = MENU_GROUP_COMMAND:New(group,text,self.ClientMenus[playername],self._SwitchLasing,self,client,group,playername) + local lasedist = MENU_GROUP:New(group,"Set Laser Distance",self.ClientMenus[playername]) + local mindist = 100 + local maxdist = 2000 + if self.LaserFOV[playername] and self.LaserFOV[playername].max then + maxdist = self.LaserFOV[playername].max + end + local laselist={} + for i=2,8 do + local dist1 = (i*1000)-1000 + local dist2 = i*1000 + dist1 = dist1 == 1000 and 100 or dist1 + local text = string.format("%d-%dm",dist1,dist2) + if dist2 == maxdist then + text = text .. " (*)" + end + laselist[i] = MENU_GROUP_COMMAND:New(group,text,lasedist,self._SwitchLasingDist,self,client,group,playername,dist1,dist2) + end + end + local targetmenu = MENU_GROUP:New(group,"Target Report",self.ClientMenus[playername]) + if canlase then + local reportL = MENU_GROUP_COMMAND:New(group,"Laser Target",targetmenu,self._ReportLaserTargets,self,client,group,playername) + end + local reportV = MENU_GROUP_COMMAND:New(group,"Visual Targets",targetmenu,self._ReportVisualTargets,self,client,group,playername) + if self.UseController then + local text = string.format("Target Upload to %s",self.Controller.MenuName or self.Controller.Name) + local upload = MENU_GROUP_COMMAND:New(group,text,targetmenu,self._UploadTargets,self,client,group,playername) + end + if canlase then + local lasecodemenu = MENU_GROUP:New(group,"Set Laser Code",self.ClientMenus[playername]) + local codemenu = {} + for _,_code in pairs(self.LaserCodes) do + --self._SetClientLaserCode,self,client,group,playername) + if _code == self.UnitLaserCodes[playername] then + _code = tostring(_code).."(*)" + end + codemenu[playername.._code] = MENU_GROUP_COMMAND:New(group,tostring(_code),lasecodemenu,self._SetClientLaserCode,self,client,group,playername,_code) + end + end + end + end + end + end + return self +end + +--- [Internal] +-- @param #PLAYERRECCE self +-- @param Core.Set#SET_UNIT targetset +-- @param Wrapper.Client#CLIENT client +-- @param #string playername +-- @return #PLAYERRECCE self +function PLAYERRECCE:_CheckNewTargets(targetset,client,playername) + self:T(self.lid.."_CheckNewTargets") + local tempset = SET_UNIT:New() + targetset:ForEach( + function(unit) + if unit and unit:IsAlive() then + self:T("Report unit: "..unit:GetName()) + if not unit.PlayerRecceDetected then + self:T("New unit: "..unit:GetName()) + unit.PlayerRecceDetected = { + detected = true, + recce = client, + playername = playername, + timestamp = timer.getTime() + } + --self:TargetDetected(unit,client,playername) + tempset:Add(unit:GetName(),unit) + if not self.TargetCache:HasUniqueID(unit:GetName()) then + self.TargetCache:Push(unit,unit:GetName()) + end + end + if unit.PlayerRecceDetected and unit.PlayerRecceDetected.timestamp then + local TNow = timer.getTime() + if TNow-unit.PlayerRecceDetected.timestamp > self.TForget then + unit.PlayerRecceDetected = { + detected = true, + recce = client, + playername = playername, + timestamp = timer.getTime() + } + if not self.TargetCache:HasUniqueID(unit:GetName()) then + self.TargetCache:Push(unit,unit:GetName()) + end + tempset:Add(unit:GetName(),unit) + end + end + end + end + ) + local targetsbyclock = {} + for i=1,12 do + targetsbyclock[i] = {} + end + tempset:ForEach( + function (object) + local obj=object -- Wrapper.Unit#UNIT + local clock = self:_GetClockDirection(client,obj) + table.insert(targetsbyclock[clock],obj) + end + ) + self:T("Known target Count: "..self.TargetCache:Count()) + if tempset:CountAlive() > 0 then + self:TargetDetected(targetsbyclock,client,playername) + end + return self +end + +--- [User] Set SRS TTS details - see @{Sound.SRS} for details +-- @param #PLAYERRECCE self +-- @param #number Frequency Frequency to be used. Can also be given as a table of multiple frequencies, e.g. 271 or {127,251}. There needs to be exactly the same number of modulations! +-- @param #number Modulation Modulation to be used. Can also be given as a table of multiple modulations, e.g. radio.modulation.AM or {radio.modulation.FM,radio.modulation.AM}. There needs to be exactly the same number of frequencies! +-- @param #string PathToSRS Defaults to "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" +-- @param #string Gender (Optional) Defaults to "male" +-- @param #string Culture (Optional) Defaults to "en-US" +-- @param #number Port (Optional) Defaults to 5002 +-- @param #string Voice (Optional) Use a specifc voice with the @{Sound.SRS#SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. +-- Note that this must be installed on your windows system. Can also be Google voice types, if you are using Google TTS. +-- @param #number Volume (Optional) Volume - between 0.0 (silent) and 1.0 (loudest) +-- @param #string PathToGoogleKey (Optional) Path to your google key if you want to use google TTS +-- @param #string Backend (optional) Backend to be used, can be MSRS.Backend.SRSEXE or MSRS.Backend.GRPC +-- @return #PLAYERRECCE self +function PLAYERRECCE:SetSRS(Frequency,Modulation,PathToSRS,Gender,Culture,Port,Voice,Volume,PathToGoogleKey,Backend) + self:T(self.lid.."SetSRS") + self.PathToSRS = PathToSRS or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" -- + self.Gender = Gender or MSRS.gender or "male" -- + self.Culture = Culture or MSRS.culture or "en-US" -- + self.Port = Port or MSRS.port or 5002 -- + self.Voice = Voice or MSRS.voice -- + self.PathToGoogleKey = PathToGoogleKey -- + self.Volume = Volume or 1.0 -- + self.UseSRS = true + self.Frequency = Frequency or {127,251} -- + self.BCFrequency = self.Frequency + self.Modulation = Modulation or {radio.modulation.FM,radio.modulation.AM} -- + self.BCModulation = self.Modulation + -- set up SRS + self.SRS=MSRS:New(self.PathToSRS,self.Frequency,self.Modulation) + self.SRS:SetCoalition(self.Coalition) + self.SRS:SetLabel(self.MenuName or self.Name) + self.SRS:SetGender(self.Gender) + self.SRS:SetCulture(self.Culture) + self.SRS:SetPort(self.Port) + self.SRS:SetVolume(self.Volume) + if Backend then + self.SRS:SetBackend(Backend) + end + if self.PathToGoogleKey then + self.SRS:SetProviderOptionsGoogle(self.PathToGoogleKey,self.PathToGoogleKey) + self.SRS:SetProvider(MSRS.Provider.GOOGLE) + end + -- Pre-configured Google? + if (not PathToGoogleKey) and self.SRS:GetProvider() == MSRS.Provider.GOOGLE then + self.PathToGoogleKey = MSRS.poptions.gcloud.credentials + self.Voice = Voice or MSRS.poptions.gcloud.voice + end + self.SRS:SetVoice(self.Voice) + self.SRSQueue = MSRSQUEUE:New(self.MenuName or self.Name) + self.SRSQueue:SetTransmitOnlyWithPlayers(self.TransmitOnlyWithPlayers) + return self +end + +--- [User] For SRS - Switch to only transmit if there are players on the server. +-- @param #PLAYERRECCE self +-- @param #boolean Switch If true, only send SRS if there are alive Players. +-- @return #PLAYERRECCE self +function PLAYERRECCE:SetTransmitOnlyWithPlayers(Switch) + self.TransmitOnlyWithPlayers = Switch + if self.SRSQueue then + self.SRSQueue:SetTransmitOnlyWithPlayers(Switch) + end + return self +end + +--- [User] Set the top menu name to a custom string. +-- @param #PLAYERRECCE self +-- @param #string Name The name to use as the top menu designation. +-- @return #PLAYERRECCE self +function PLAYERRECCE:SetMenuName(Name) + self:T(self.lid.."SetMenuName: "..Name) + self.MenuName = Name + return self +end + +--- [User] Set reporting to be BULLS only or BULLS plus playersettings based coordinate. +-- @param #PLAYERRECCE self +-- @param #boolean OnOff +-- @return #PLAYERRECCE self +function PLAYERRECCE:SetReportBullsOnly(OnOff) + self:T(self.lid.."SetReportBullsOnly: "..tostring(OnOff)) + self.reporttostringbullsonly = OnOff + return self +end + +--- [User] Enable smoking of own position +-- @param #PLAYERRECCE self +-- @return #PLAYERRECCE self +function PLAYERRECCE:EnableSmokeOwnPosition() + self:T(self.lid.."EnableSmokeOwnPosition") + self.smokeownposition = true + return self +end + +--- [User] Enable auto lasing for the Kiowa OH-58D. +-- @param #PLAYERRECCE self +-- @return #PLAYERRECCE self +function PLAYERRECCE:EnableKiowaAutolase() + self:T(self.lid.."EnableKiowaAutolase") + self.CanLase.OH58D = true + return self +end + +--- [User] Disable smoking of own position +-- @param #PLAYERRECCE self +-- @return #PLAYERRECCE +function PLAYERRECCE:DisableSmokeOwnPosition() + self:T(self.lid.."DisableSmokeOwnPosition") + self.smokeownposition = false + return self +end + +--- [User] Enable smoking of average target positions, instead of all targets visible. Loses smoke per threatlevel -- each is med threat. Default is - smoke all positions. +-- @param #PLAYERRECCE self +-- @return #PLAYERRECCE self +function PLAYERRECCE:EnableSmokeAverageTargetPosition() + self:T(self.lid.."ENableSmokeOwnPosition") + self.smokeaveragetargetpos = true + return self +end + +--- [User] Disable smoking of average target positions, instead of all targets visible. Default is - smoke all positions. +-- @param #PLAYERRECCE self +-- @return #PLAYERRECCE +function PLAYERRECCE:DisableSmokeAverageTargetPosition() + self:T(self.lid.."DisableSmokeAverageTargetPosition") + self.smokeaveragetargetpos = false + return self +end + +--- [Internal] Get text for text-to-speech. +-- Numbers are spaced out, e.g. "Heading 180" becomes "Heading 1 8 0 ". +-- @param #PLAYERRECCE self +-- @param #string text Original text. +-- @return #string Spoken text. +function PLAYERRECCE:_GetTextForSpeech(text) + + -- Space out numbers. + text=string.gsub(text,"%d","%1 ") + -- get rid of leading or trailing spaces + text=string.gsub(text,"^%s*","") + text=string.gsub(text,"%s*$","") + text=string.gsub(text,"0","zero") + text=string.gsub(text,"9","niner") + text=string.gsub(text," "," ") + + return text +end + +------------------------------------------------------------------------------------------ +-- TODO: FSM Functions +------------------------------------------------------------------------------------------ + +--- [Internal] Status Loop +-- @param #PLAYERRECCE self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERRECCE self +function PLAYERRECCE:onafterStatus(From, Event, To) + self:T({From, Event, To}) + + if not self.timestamp then + self.timestamp = timer.getTime() + else + local tNow = timer.getTime() + if tNow - self.timestamp >= 60 then + self:_CleanupTargetCache() + self.timestamp = timer.getTime() + end + end + + self:_BuildMenus() + + self.PlayerSet:ForEachClient( + function(Client) + local client = Client -- Wrapper.Client#CLIENT + local playername = client:GetPlayerName() + local cameraison = self:_CameraOn(client,playername) + if client and client:IsAlive() and self.OnStation[playername] then + --- + local targetset, targetcount, tzone = nil,0,nil + local laserset, lzone = nil,nil + local vistargetset, vistargetcount, viszone = nil,0,nil + -- targets on camera + if cameraison then + targetset, targetcount, tzone = self:_GetTargetSet(client,true) + if targetset then + if self.ViewZone[playername] then + self.ViewZone[playername]:UndrawZone() + end + if self.debug and tzone then + self.ViewZone[playername]=tzone:DrawZone(self.Coalition,{0,0,1},nil,nil,nil,1) + end + end + self:T({targetcount=targetcount}) + end + -- lase targets on camera + if self.AutoLase[playername] and cameraison then + laserset, targetcount, lzone = self:_GetTargetSet(client,true,true) + if targetcount > 0 or self.LaserTarget[playername] then + if self.CanLase[client:GetTypeName()] then + -- DONE move to lase at will + self:_LaseTarget(client,laserset) + end + end + if lzone then + if self.ViewZoneLaser[playername] then + self.ViewZoneLaser[playername]:UndrawZone() + end + if self.debug and tzone then + self.ViewZoneLaser[playername]=lzone:DrawZone(self.Coalition,{0,1,0},nil,nil,nil,1) + end + end + self:T({lasercount=targetcount}) + end + -- visual targets + vistargetset, vistargetcount, viszone = self:_GetTargetSet(client,false) + if vistargetset then + if self.ViewZoneVisual[playername] then + self.ViewZoneVisual[playername]:UndrawZone() + end + if self.debug and viszone then + self.ViewZoneVisual[playername]=viszone:DrawZone(self.Coalition,{1,0,0},nil,nil,nil,3) + end + end + self:T({visualtargetcount=vistargetcount}) + if targetset then + vistargetset:AddSet(targetset) + end + if laserset then + vistargetset:AddSet(laserset) + end + if not cameraison and self.debug then + if self.ViewZoneLaser[playername] then + self.ViewZoneLaser[playername]:UndrawZone() + end + if self.ViewZone[playername] then + self.ViewZone[playername]:UndrawZone() + end + end + self:_CheckNewTargets(vistargetset,client,playername) + end + end + ) + + self:__Status(-10) + return self +end + +--- [Internal] Recce on station +-- @param #PLAYERRECCE self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Wrapper.Client#CLIENT Client +-- @param #string Playername +-- @return #PLAYERRECCE self +function PLAYERRECCE:onafterRecceOnStation(From, Event, To, Client, Playername) + self:T({From, Event, To}) + local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) + local coord = Client:GetCoordinate() + local coordtext = coord:ToStringBULLS(self.Coalition) + if self.ReferencePoint then + local Settings = Client and _DATABASE:GetPlayerSettings(Playername) or _SETTINGS -- Core.Settings#SETTINGS + coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings) + end + local text1 = "Party time!" + local text2 = string.format("All stations, FACA %s on station\nat %s!",callsign, coordtext) + local text2tts = string.format(" All stations, FACA %s on station at %s!",callsign, coordtext) + text2tts = self:_GetTextForSpeech(text2tts) + if self.debug then + self:T(text2.."\n"..text2tts) + end + if self.UseSRS then + local grp = Client:GetGroup() + local coord = grp:GetCoordinate() + if coord then + self.SRS:SetCoordinate(coord) + end + self.SRSQueue:NewTransmission(text1,nil,self.SRS,nil,2) + self.SRSQueue:NewTransmission(text2tts,nil,self.SRS,nil,3) + MESSAGE:New(text2,10,self.Name or "FACA"):ToCoalition(self.Coalition) + else + MESSAGE:New(text1,10,self.Name or "FACA"):ToClient(Client) + MESSAGE:New(text2,10,self.Name or "FACA"):ToCoalition(self.Coalition) + end + return self +end + +--- [Internal] Recce off station +-- @param #PLAYERRECCE self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Wrapper.Client#CLIENT Client +-- @param #string Playername +-- @return #PLAYERRECCE self +function PLAYERRECCE:onafterRecceOffStation(From, Event, To, Client, Playername) + self:T({From, Event, To}) + local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) + local coord = Client:GetCoordinate() + local coordtext = coord:ToStringBULLS(self.Coalition) + if self.ReferencePoint then + local Settings = Client and _DATABASE:GetPlayerSettings(Playername) or _SETTINGS -- Core.Settings#SETTINGS + coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings) + end + local text = string.format("All stations, FACA %s leaving station\nat %s, good bye!",callsign, coordtext) + local texttts = string.format("All stations, FACA %s leaving station at %s, good bye!",callsign, coordtext) + texttts = self:_GetTextForSpeech(texttts) + if self.debug then + self:T(text.."\n"..texttts) + end + local text1 = "Going home!" + if self.UseSRS then + local grp = Client:GetGroup() + local coord = grp:GetCoordinate() + if coord then + self.SRS:SetCoordinate(coord) + end + self.SRSQueue:NewTransmission(text1,nil,self.SRS,nil,2) + self.SRSQueue:NewTransmission(texttts,nil,self.SRS,nil,3) + MESSAGE:New(text,10,self.Name or "FACA"):ToCoalition(self.Coalition) + else + MESSAGE:New(text,10,self.Name or "FACA"):ToCoalition(self.Coalition) + end + return self +end + +--- [Internal] Target Detected +-- @param #PLAYERRECCE self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #table Targetsbyclock. #table with index 1..12 containing a #table of Wrapper.Unit#UNIT objects each. +-- @param Wrapper.Client#CLIENT Client +-- @param #string Playername +-- @return #PLAYERRECCE self +function PLAYERRECCE:onafterTargetDetected(From, Event, To, Targetsbyclock, Client, Playername) + self:T({From, Event, To}) + + local dunits = "meters" + local Settings = Client and _DATABASE:GetPlayerSettings(Playername) or _SETTINGS -- Core.Settings#SETTINGS + local clientcoord = Client:GetCoordinate() + + for i=1,12 do + local targets = Targetsbyclock[i] --#table + local targetno = #targets + if targetno == 1 then + -- only one + local targetdistance = clientcoord:Get2DDistance(targets[1]:GetCoordinate()) or 100 + local Threatlvl = targets[1]:GetThreatLevel() + local ThreatTxt = "Low" + if Threatlvl >=7 then + ThreatTxt = "Medium" + elseif Threatlvl >=3 then + ThreatTxt = "High" + end + if Settings:IsMetric() then + targetdistance = UTILS.Round(targetdistance,-2) + if targetdistance >= 1000 then + targetdistance = UTILS.Round(targetdistance/1000,0) + dunits = "kilometer" + end + else + if UTILS.MetersToNM(targetdistance) >=1 then + targetdistance = UTILS.Round(UTILS.MetersToNM(targetdistance),0) + dunits = "miles" + else + targetdistance = UTILS.Round(UTILS.MetersToFeet(targetdistance),-2) + dunits = "feet" + end + end + local text = string.format("Target! %s! %s o\'clock, %d %s!", ThreatTxt,i, targetdistance, dunits) + local ttstext = string.format("Target! %s! %s oh clock, %d %s!", ThreatTxt, i, targetdistance, dunits) + if self.UseSRS then + local grp = Client:GetGroup() + if clientcoord then + self.SRS:SetCoordinate(clientcoord) + end + self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) + else + MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) + end + elseif targetno > 1 then + -- multiple + local function GetNearest(TTable) + local distance = 10000000 + for _,_unit in pairs(TTable) do + local dist = clientcoord:Get2DDistance(_unit:GetCoordinate()) or 100 + if dist < distance then + distance = dist + end + end + return distance + end + local targetdistance = GetNearest(targets) + if Settings:IsMetric() then + targetdistance = UTILS.Round(targetdistance,-2) + if targetdistance >= 1000 then + targetdistance = UTILS.Round(targetdistance/1000,0) + dunits = "kilometer" + end + else + if UTILS.MetersToNM(targetdistance) >=1 then + targetdistance = UTILS.Round(UTILS.MetersToNM(targetdistance),0) + dunits = "miles" + else + targetdistance = UTILS.Round(UTILS.MetersToFeet(targetdistance),-2) + dunits = "feet" + end + end + local text = string.format(" %d targets! %s o\'clock, %d %s!", targetno, i, targetdistance, dunits) + local ttstext = string.format("%d targets! %s oh clock, %d %s!", targetno, i, targetdistance, dunits) + if self.UseSRS then + local grp = Client:GetGroup() + local coord = grp:GetCoordinate() + if coord then + self.SRS:SetCoordinate(coord) + end + self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) + else + MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) + end + end + end + return self +end + +--- [Internal] Targets Illuminated +-- @param #PLAYERRECCE self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Wrapper.Client#CLIENT Client +-- @param #string Playername +-- @param Core.Set#SET_UNIT TargetSet +-- @return #PLAYERRECCE self +function PLAYERRECCE:onafterIllumination(From, Event, To, Client, Playername, TargetSet) + self:T({From, Event, To}) + local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) + local coord = Client:GetCoordinate() + local coordtext = coord:ToStringBULLS(self.Coalition) + if self.AttackSet then + for _,_client in pairs(self.AttackSet.Set) do + local client = _client --Wrapper.Client#CLIENT + if client and client:IsAlive() then + local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName()) or _SETTINGS + local coordtext = coord:ToStringA2G(client,Settings) + if self.ReferencePoint then + coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) + end + local text = string.format("All stations, %s fired illumination\nat %s!",callsign, coordtext) + MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client) + end + end + end + local text = "Sunshine!" + local ttstext = "Sunshine!" + if self.UseSRS then + local grp = Client:GetGroup() + local coord = grp:GetCoordinate() + if coord then + self.SRS:SetCoordinate(coord) + end + self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) + else + MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) + end + return self +end + +--- [Internal] Targets Smoked +-- @param #PLAYERRECCE self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Wrapper.Client#CLIENT Client +-- @param #string Playername +-- @param Core.Set#SET_UNIT TargetSet +-- @return #PLAYERRECCE self +function PLAYERRECCE:onafterTargetsSmoked(From, Event, To, Client, Playername, TargetSet) + self:T({From, Event, To}) + local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) + local coord = Client:GetCoordinate() + local coordtext = coord:ToStringBULLS(self.Coalition) + if self.AttackSet then + for _,_client in pairs(self.AttackSet.Set) do + local client = _client --Wrapper.Client#CLIENT + if client and client:IsAlive() then + local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName()) or _SETTINGS + local coordtext = coord:ToStringA2G(client,Settings) + if self.ReferencePoint then + coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) + end + local text = string.format("All stations, %s smoked targets\nat %s!",callsign, coordtext) + MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client) + end + end + end + local text = "Smoke on!" + local ttstext = "Smoke and Mirrors!" + if self.UseSRS then + local grp = Client:GetGroup() + local coord = grp:GetCoordinate() + if coord then + self.SRS:SetCoordinate(coord) + end + self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) + else + MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) + end + return self +end + +--- [Internal] Targets Flared +-- @param #PLAYERRECCE self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Wrapper.Client#CLIENT Client +-- @param #string Playername +-- @param Core.Set#SET_UNIT TargetSet +-- @return #PLAYERRECCE self +function PLAYERRECCE:onafterTargetsFlared(From, Event, To, Client, Playername, TargetSet) + self:T({From, Event, To}) + local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) + local coord = Client:GetCoordinate() + local coordtext = coord:ToStringBULLS(self.Coalition) + if self.AttackSet then + for _,_client in pairs(self.AttackSet.Set) do + local client = _client --Wrapper.Client#CLIENT + if client and client:IsAlive() then + local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName()) or _SETTINGS + if self.ReferencePoint then + coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) + end + local coordtext = coord:ToStringA2G(client,Settings) + local text = string.format("All stations, %s flared targets\nat %s!",callsign, coordtext) + MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client) + end + end + end + local text = "Fireworks!" + local ttstext = "Fire works!" + if self.UseSRS then + local grp = Client:GetGroup() + local coord = grp:GetCoordinate() + if coord then + self.SRS:SetCoordinate(coord) + end + self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) + else + MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) + end + return self +end + +--- [Internal] Target lasing +-- @param #PLAYERRECCE self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Wrapper.Client#CLIENT Client +-- @param Wrapper.Unit#UNIT Target +-- @param #number Lasercode +-- @param #number Lasingtime +-- @return #PLAYERRECCE self +function PLAYERRECCE:onafterTargetLasing(From, Event, To, Client, Target, Lasercode, Lasingtime) + self:T({From, Event, To}) + local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) + local Settings = ( Client and _DATABASE:GetPlayerSettings( Client:GetPlayerName() ) ) or _SETTINGS + local coord = Client:GetCoordinate() + local coordtext = coord:ToStringBULLS(self.Coalition,Settings) + if self.ReferencePoint then + coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings) + end + local targettype = Target:GetTypeName() + if self.AttackSet then + for _,_client in pairs(self.AttackSet.Set) do + local client = _client --Wrapper.Client#CLIENT + if client and client:IsAlive() then + local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName()) or _SETTINGS + if self.ReferencePoint then + coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) + end + local coordtext = coord:ToStringA2G(client,Settings) + local text = string.format("All stations, %s lasing %s\nat %s!\nCode %d, Duration %d plus seconds!",callsign, targettype, coordtext, Lasercode, Lasingtime) + MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client) + end + end + end + local text = "Lasing!" + local ttstext = "Laser on!" + if self.UseSRS then + local grp = Client:GetGroup() + local coord = grp:GetCoordinate() + if coord then + self.SRS:SetCoordinate(coord) + end + self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) + else + MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) + end + return self +end + +--- [Internal] Lased target destroyed +-- @param #PLAYERRECCE self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Wrapper.Client#CLIENT Client +-- @param Wrapper.Unit#UNIT Target +-- @return #PLAYERRECCE self +function PLAYERRECCE:onafterShack(From, Event, To, Client, Target) + self:T({From, Event, To}) + local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) + local Settings = ( Client and _DATABASE:GetPlayerSettings( Client:GetPlayerName() ) ) or _SETTINGS + local coord = Client:GetCoordinate() + local coordtext = coord:ToStringBULLS(self.Coalition,Settings) + if self.ReferencePoint then + coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings) + end + local targettype = "target" + if self.AttackSet then + for _,_client in pairs(self.AttackSet.Set) do + local client = _client --Wrapper.Client#CLIENT + if client and client:IsAlive() then + local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName()) or _SETTINGS + if self.ReferencePoint then + coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) + end + local coordtext = coord:ToStringA2G(client,Settings) + local text = string.format("All stations, %s good hit on %s\nat %s!",callsign, targettype, coordtext) + MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client) + end + end + end + local text = "Shack!" + local ttstext = "Shack!" + if self.UseSRS then + local grp = Client:GetGroup() + local coord = grp:GetCoordinate() + if coord then + self.SRS:SetCoordinate(coord) + end + self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1) + else + MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) + end + return self +end + +--- [Internal] Laser lost LOS +-- @param #PLAYERRECCE self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Wrapper.Client#CLIENT Client +-- @param Wrapper.Unit#UNIT Target +-- @return #PLAYERRECCE self +function PLAYERRECCE:onafterTargetLOSLost(From, Event, To, Client, Target) + self:T({From, Event, To}) + local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) + local Settings = ( Client and _DATABASE:GetPlayerSettings( Client:GetPlayerName() ) ) or _SETTINGS + local coord = Client:GetCoordinate() + local coordtext = coord:ToStringBULLS(self.Coalition,Settings) + if self.ReferencePoint then + coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings) + end + local targettype = "target" --Target:GetTypeName() + if self.AttackSet then + for _,_client in pairs(self.AttackSet.Set) do + local client = _client --Wrapper.Client#CLIENT + if client and client:IsAlive() then + local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName()) or _SETTINGS + if self.ReferencePoint then + coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings) + end + local coordtext = coord:ToStringA2G(client,Settings) + local text = string.format("All stations, %s lost sight of %s\nat %s!",callsign, targettype, coordtext) + MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client) + end + end + end + local text = "Lost LOS!" + local ttstext = "Lost L O S!" + if self.UseSRS then + local grp = Client:GetGroup() + local coord = grp:GetCoordinate() + if coord then + self.SRS:SetCoordinate(coord) + end + self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10) + else + MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) + end + return self +end + +--- [Internal] Target report +-- @param #PLAYERRECCE self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Wrapper.Client#CLIENT Client +-- @param Core.Set#SET_UNIT TargetSet +-- @param Wrapper.Unit#UNIT Target +-- @param #string Text +-- @return #PLAYERRECCE self +function PLAYERRECCE:onafterTargetReport(From, Event, To, Client, TargetSet, Target, Text) + self:T({From, Event, To}) + MESSAGE:New(Text,45,self.Name or "FACA"):ToClient(Client) + if self.AttackSet then + -- send message to AttackSet + for _,_client in pairs(self.AttackSet.Set) do + local client = _client -- Wrapper.Client#CLIENT + if client and client:IsAlive() and client ~= Client then + MESSAGE:New(Text,45,self.Name or "FACA"):ToClient(client) + end + end + end + return self +end + +--- [Internal] Target data upload +-- @param #PLAYERRECCE self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Wrapper.Client#CLIENT Client Client sending the report +-- @param #string Playername Player name +-- @param Core.Set#SET_UNIT TargetSet Set of targets +-- @return #PLAYERRECCE self +function PLAYERRECCE:onafterTargetReportSent(From, Event, To, Client, Playername, TargetSet) + self:T({From, Event, To}) + local text = "Upload completed!" + if self.UseSRS then + local grp = Client:GetGroup() + local coord = grp:GetCoordinate() + if coord then + self.SRS:SetCoordinate(coord) + end + self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,1,{grp},text,10) + else + MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client) + end + return self +end + +--- [Internal] Stop +-- @param #PLAYERRECCE self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERRECCE self +function PLAYERRECCE:onafterStop(From, Event, To) + self:I({From, Event, To}) + -- Player Events + self:UnHandleEvent(EVENTS.PlayerLeaveUnit) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.PilotDead) + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + return self +end + +------------------------------------------------------------------------------------------ +-- TODO: END PLAYERRECCE +------------------------------------------------------------------------------------------ diff --git a/Moose Development/Moose/Ops/PlayerTask.lua b/Moose Development/Moose/Ops/PlayerTask.lua new file mode 100644 index 000000000..1f5269ce9 --- /dev/null +++ b/Moose Development/Moose/Ops/PlayerTask.lua @@ -0,0 +1,5023 @@ +--- **Ops** - PlayerTask (mission) for Players. +-- +-- ## Main Features: +-- +-- * Simplifies defining and executing Player tasks +-- * FSM events when a mission is added, done, successful or failed, replanned +-- * Ready to use SRS and localization +-- * Mission locations can be smoked, flared, illuminated and marked on the map +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/PlayerTask). +-- +-- === +-- +-- ### Author: **Applevangelist** +-- ### Special thanks to: Streakeagle +-- +-- === +-- @module Ops.PlayerTask +-- @image OPS_PlayerTask.jpg +-- @date Last Update Oct 2025 + + +do +------------------------------------------------------------------------------------------------------------------- +-- PLAYERTASK +-- TODO: PLAYERTASK +------------------------------------------------------------------------------------------------------------------- + +--- PLAYERTASK class. +-- @type PLAYERTASK +-- @field #string ClassName Name of the class. +-- @field #boolean verbose Switch verbosity. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number PlayerTaskNr (Globally unique) Number of the task. +-- @field Ops.Auftrag#AUFTRAG.Type Type The type of the task +-- @field Ops.Target#TARGET Target The target for this Task +-- @field Utilities.FiFo#FIFO Clients FiFo of Wrapper.Client#CLIENT planes executing this task +-- @field #boolean Repeat +-- @field #number repeats +-- @field #number RepeatNo +-- @field Wrapper.Marker#MARKER TargetMarker +-- @field #number SmokeColor +-- @field #number FlareColor +-- @field #table conditionSuccess +-- @field #table conditionFailure +-- @field Ops.PlayerTask#PLAYERTASKCONTROLLER TaskController +-- @field #number timestamp +-- @field #number lastsmoketime +-- @field #number coalition +-- @field #string Freetext +-- @field #string FreetextTTS +-- @field #string TaskSubType +-- @field #table NextTaskSuccess +-- @field #table NextTaskFailure +-- @field #string FinalState +-- @field #string TypeName +-- @field #number PreviousCount +-- @field #boolean CanSmoke +-- @field #boolean ShowThreatDetails +-- @extends Core.Fsm#FSM + + +--- Global PlayerTaskNr counter +_PlayerTaskNr = 0 + +--- +-- @field #PLAYERTASK +PLAYERTASK = { + ClassName = "PLAYERTASK", + verbose = false, + lid = nil, + PlayerTaskNr = nil, + Type = nil, + TTSType = nil, + Target = nil, + Clients = nil, + Repeat = false, + repeats = 0, + RepeatNo = 1, + TargetMarker = nil, + SmokeColor = nil, + FlareColor = nil, + conditionSuccess = {}, + conditionFailure = {}, + TaskController = nil, + timestamp = 0, + lastsmoketime = 0, + Freetext = nil, + FreetextTTS = nil, + TaskSubType = nil, + NextTaskSuccess = {}, + NextTaskFailure = {}, + FinalState = "none", + PreviousCount = 0, + CanSmoke = true, + ShowThreatDetails = true, + } + +--- PLAYERTASK class version. +-- @field #string version +PLAYERTASK.version="0.1.29" + +--- Generic task condition. +-- @type PLAYERTASK.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. + +--- Constructor +-- @param #PLAYERTASK self +-- @param Ops.Auftrag#AUFTRAG.Type Type Type of this task +-- @param Ops.Target#TARGET Target Target for this task +-- @param #boolean Repeat Repeat this task if true (default = false) +-- @param #number Times Repeat on failure this many times if Repeat is true (default = 1) +-- @param #string TTSType TTS friendly task type name +-- @return #PLAYERTASK self +function PLAYERTASK:New(Type, Target, Repeat, Times, TTSType) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #PLAYERTASK + + self.Type = Type + + self.Repeat = false + self.repeats = 0 + self.RepeatNo = 1 + self.Clients = FIFO:New() -- Utilities.FiFo#FIFO + self.TargetMarker = nil -- Wrapper.Marker#MARKER + self.SmokeColor = SMOKECOLOR.Red + self.conditionSuccess = {} + self.conditionFailure = {} + self.TaskController = nil -- Ops.PlayerTask#PLAYERTASKCONTROLLER + self.timestamp = timer.getAbsTime() + self.TTSType = TTSType or "close air support" + self.lastsmoketime = 0 + + if type(Repeat) == "boolean" and Repeat == true and type(Times) == "number" and Times > 1 then + self.Repeat = true + self.RepeatNo = Times or 1 + end + + _PlayerTaskNr = _PlayerTaskNr + 1 + + self.PlayerTaskNr = _PlayerTaskNr + + self.lid=string.format("PlayerTask #%d %s | ", self.PlayerTaskNr, tostring(self.Type)) + + if Target and Target.ClassName and Target.ClassName == "TARGET" then + self.Target = Target + elseif Target and Target.ClassName then + self.Target = TARGET:New(Target) + else + self:E(self.lid.."*** NO VALID TARGET!") + return self + end + + self.PreviousCount = self.Target:CountTargets() + + self:T(self.lid.."Created.") + + -- FMS start state is PLANNED. + self:SetStartState("Planned") + + -- PLANNED --> REQUESTED --> EXECUTING --> DONE + self:AddTransition("*", "Planned", "Planned") -- Task is in planning stage. + self:AddTransition("*", "Requested", "Requested") -- Task clients have been requested to join. + self:AddTransition("*", "ClientAdded", "*") -- Client has been added to the task + self:AddTransition("*", "ClientRemoved", "*") -- Client has been removed from the task + self:AddTransition("*", "Executing", "Executing") -- First client is executing the Task. + self:AddTransition("*", "Progress", "*") -- Task target count reduced - progress + self:AddTransition("*", "Done", "Done") -- All clients have reported that Task is done. + self:AddTransition("*", "Cancel", "Done") -- Command to cancel the Task. + self:AddTransition("*", "Success", "Done") + self:AddTransition("*", "ClientAborted", "*") + self:AddTransition("*", "Failed", "Failed") -- Done or repeat --> PLANNED + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Stop", "Stopped") + + self:__Status(-5) + return self + + --- + -- Pseudo Functions + --- + + --- On After "Planned" event. Task has been planned. + -- @function [parent=#PLAYERTASK] OnAfterPlanned + -- @param #PLAYERTASK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "Requested" event. Task has been Requested. + -- @function [parent=#PLAYERTASK] OnAfterRequested + -- @param #PLAYERTASK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "ClientAdded" event. Client has been added to the task. + -- @function [parent=#PLAYERTASK] OnAfterClientAdded + -- @param #PLAYERTASK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Client#CLIENT Client + + --- On After "ClientRemoved" event. Client has been removed from the task. + -- @function [parent=#PLAYERTASK] OnAfterClientRemoved + -- @param #PLAYERTASK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "Executing" event. Task is executed by the 1st client. + -- @function [parent=#PLAYERTASK] OnAfterExecuting + -- @param #PLAYERTASK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "Done" event. Task is done. + -- @function [parent=#PLAYERTASK] OnAfterDone + -- @param #PLAYERTASK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "Cancel" event. Task has been cancelled. + -- @function [parent=#PLAYERTASK] OnAfterCancel + -- @param #PLAYERTASK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #boolen Silent If true, suppress message output on cancel. + + --- On After "Planned" event. Task has been planned. + -- @function [parent=#PLAYERTASK] OnAfterPilotPlanned + -- @param #PLAYERTASK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "Success" event. Task has been a success. + -- @function [parent=#PLAYERTASK] OnAfterSuccess + -- @param #PLAYERTASK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "ClientAborted" event. A client has aborted the task. + -- @function [parent=#PLAYERTASK] OnAfterClientAborted + -- @param #PLAYERTASK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "Failed" event. Task has been a failure. + -- @function [parent=#PLAYERTASK] OnAfterFailed + -- @param #PLAYERTASK self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + +end + +--- Constructor that automatically determines the task type based on the target. +-- @param #PLAYERTASK self +-- @param Ops.Target#TARGET Target Target for this task +-- @param #boolean Repeat Repeat this task if true (default = false) +-- @param #number Times Repeat on failure this many times if Repeat is true (default = 1) +-- @param #string TTSType TTS friendly task type name +-- @return #PLAYERTASK self +function PLAYERTASK:NewFromTarget(Target, Repeat, Times, TTSType) + return PLAYERTASK:New(self:_GetTaskTypeForTarget(Target), Target, Repeat, Times, TTSType) +end + +--- [Internal] Determines AUFTRAG type based on the target characteristics. +-- @param #PLAYERTASK self +-- @param Ops.Target#TARGET Target Target for this task +-- @return #string AUFTRAG.Type +function PLAYERTASK:_GetTaskTypeForTarget(Target) + + local group = nil --Wrapper.Group#GROUP + local auftrag = nil + + if Target:IsInstanceOf("GROUP") then + group = Target --Target is already a group. + elseif Target:IsInstanceOf("SET_GROUP") then + group = Target:GetFirst() + elseif Target:IsInstanceOf("UNIT") then + group = Target:GetGroup() + elseif Target:IsInstanceOf("SET_UNIT") then + group = Target:GetFirst():GetGroup() + elseif Target:IsInstanceOf("AIRBASE") then + + auftrag = AUFTRAG.Type.BOMBRUNWAY + + elseif Target:IsInstanceOf("STATIC") + or Target:IsInstanceOf("SET_STATIC") + or Target:IsInstanceOf("SCENERY") + or Target:IsInstanceOf("SET_SCENERY") then + + auftrag = AUFTRAG.Type.BOMBING + + elseif Target:IsInstanceOf("OPSZONE") + or Target:IsInstanceOf("SET_OPSZONE") then + auftrag = AUFTRAG.Type.CAPTUREZONE + end + + if group then + + local category = group:GetCategory() + local attribute = group:GetAttribute() + + if (category == Group.Category.AIRPLANE or category == Group.Category.HELICOPTER) + and group:InAir() then + + auftrag = AUFTRAG.Type.INTERCEPT + + elseif category == Group.Category.GROUND or category == Group.Category.TRAIN then + + if attribute == GROUP.Attribute.GROUND_SAM + or attribute == GROUP.Attribute.GROUND_EWR then + + auftrag = AUFTRAG.Type.SEAD + + elseif attribute == GROUP.Attribute.GROUND_AAA + or attribute == GROUP.Attribute.GROUND_APC + or attribute == GROUP.Attribute.GROUND_IFV + or attribute == GROUP.Attribute.GROUND_TRUCK + or attribute == GROUP.Attribute.GROUND_TRAIN then + + auftrag = AUFTRAG.Type.BAI + + elseif attribute == GROUP.Attribute.GROUND_INFANTRY + or attribute == GROUP.Attribute.GROUND_ARTILLERY + or attribute == GROUP.Attribute.GROUND_TANK then + + auftrag = AUFTRAG.Type.CAS + + else + + auftrag = AUFTRAG.Type.BAI + + end + + elseif category == Group.Category.SHIP then + + auftrag = AUFTRAG.Type.ANTISHIP + + else + self:T(self.lid .. "ERROR: Unknown Group category!") + end + end + + return auftrag + +end + + +--- [Internal] Check OpsZone capture success condition. +-- @param #PLAYERTASK self +-- @param Ops.OpsZone#OPSZONE OpsZone The OpsZone target object. +-- @param #string CaptureSquadGroupNamePrefix The prefix of the group name that needs to capture the zone. +-- @param #number Coalition The coalition that needs to capture the zone. +-- @param #boolean CheckClientInZone Check if any of the clients are in zone. +-- @return #PLAYERTASK self +function PLAYERTASK:_CheckCaptureOpsZoneSuccess(OpsZone, CaptureSquadGroupNamePrefix, Coalition, CheckClientInZone) + local isClientInZone = true + if CheckClientInZone then + isClientInZone = false + for _, client in ipairs(self:GetClientObjects()) do + local clientCoord = client:GetCoordinate() + if OpsZone.zone:IsCoordinateInZone(clientCoord) then + isClientInZone = true + break + end + end + end + + local isCaptureGroupInZone = false + OpsZone:GetScannedGroupSet():ForEachGroup(function(group) + if string.find(group:GetName(), CaptureSquadGroupNamePrefix) then + isCaptureGroupInZone = true + end + end) + + return OpsZone:GetOwner() == Coalition and isClientInZone and isCaptureGroupInZone +end + +--- [User] Override this function in order to implement custom logic if a player can join a task or not. +-- @param #PLAYERTASK self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #boolean Outcome True if player can join the task, false if not +function PLAYERTASK:CanJoinTask(Group, Client) + return true +end + +--- [Internal] Add a PLAYERTASKCONTROLLER for this task +-- @param #PLAYERTASK self +-- @param Ops.PlayerTask#PLAYERTASKCONTROLLER Controller +-- @return #PLAYERTASK self +function PLAYERTASK:_SetController(Controller) + self:T(self.lid.."_SetController") + self.TaskController = Controller + return self +end + +--- [User] Set a coalition side for this task +-- @param #PLAYERTASK self +-- @param #number Coalition Coaltion side to add, e.g. coalition.side.BLUE +-- @return #PLAYERTASK self +function PLAYERTASK:SetCoalition(Coalition) + self:T(self.lid.."SetCoalition") + self.coalition = Coalition or coalition.side.BLUE + return self +end + +--- [User] Get the coalition side for this task +-- @param #PLAYERTASK self +-- @return #number Coalition Coaltion side, e.g. coalition.side.BLUE, or nil if not set +function PLAYERTASK:GetCoalition() + self:T(self.lid.."GetCoalition") + return self.coalition +end + +--- [User] Get the Ops.Target#TARGET object for this task +-- @param #PLAYERTASK self +-- @return Ops.Target#TARGET Target +function PLAYERTASK:GetTarget() + self:T(self.lid.."GetTarget") + return self.Target +end + +--- [USER] Add a free text description to this task. +-- @param #PLAYERTASK self +-- @param #string Text +-- @return #PLAYERTASK self +function PLAYERTASK:AddFreetext(Text) + self:T(self.lid.."AddFreetext") + self.Freetext = Text + return self +end + +--- [USER] Query if a task has free text description. +-- @param #PLAYERTASK self +-- @return #PLAYERTASK self +function PLAYERTASK:HasFreetext() + self:T(self.lid.."HasFreetext") + return self.Freetext ~= nil and true or false +end + +--- [USER] Query if a task has free text TTS description. +-- @param #PLAYERTASK self +-- @return #PLAYERTASK self +function PLAYERTASK:HasFreetextTTS() + self:T(self.lid.."HasFreetextTTS") + return self.FreetextTTS ~= nil and true or false +end + +--- [USER] Set a task sub type description to this task. +-- @param #PLAYERTASK self +-- @param #string Type +-- @return #PLAYERTASK self +function PLAYERTASK:SetSubType(Type) + self:T(self.lid.."AddSubType") + self.TaskSubType = Type + return self +end + +--- [USER] Set if a task can have a smoke marker. +-- @param #PLAYERTASK self +-- @param #boolean OnOff If true (default) it can be smoke, false if not. +-- @return #PLAYERTASK self +function PLAYERTASK:SetCanSmoke(OnOff) + self:T(self.lid.."AddSSetCanSmokeubType") + self.CanSmoke = OnOff + return self +end + +--- [USER] Set if a task can show threat details. +-- @param #PLAYERTASK self +-- @param #boolean OnOff If true (default) it can be shown, false if not. +-- @return #PLAYERTASK self +function PLAYERTASK:SetShowThreatDetails(OnOff) + self:T(self.lid.."SetShowThreatDetails") + self.ShowThreatDetails = OnOff + return self +end + +--- [USER] Get task sub type description from this task. +-- @param #PLAYERTASK self +-- @return #string Type or nil +function PLAYERTASK:GetSubType() + self:T(self.lid.."GetSubType") + return self.TaskSubType +end + +--- [USER] Get the free text description from this task. +-- @param #PLAYERTASK self +-- @return #string Text +function PLAYERTASK:GetFreetext() + self:T(self.lid.."GetFreetext") + return self.Freetext or self.FreetextTTS or "No Details" +end + +--- [USER] Add a free text description for TTS to this task. +-- @param #PLAYERTASK self +-- @param #string TextTTS +-- @return #PLAYERTASK self +function PLAYERTASK:AddFreetextTTS(TextTTS) + self:T(self.lid.."AddFreetextTTS") + self.FreetextTTS = TextTTS + return self +end + +--- [USER] Get the free text TTS description from this task. +-- @param #PLAYERTASK self +-- @return #string Text +function PLAYERTASK:GetFreetextTTS() + self:T(self.lid.."GetFreetextTTS") + return self.FreetextTTS or self.Freetext or "No Details" +end + +--- [USER] Add a short free text description for the menu entry of this task. +-- @param #PLAYERTASK self +-- @param #string Text +-- @return #PLAYERTASK self +function PLAYERTASK:SetMenuName(Text) + self:T(self.lid.."SetMenuName") + self.Target.name = Text + return self +end + +--- [USER] Adds task success condition for dead STATIC, SET_STATIC, SCENERY or SET_SCENERY target object. +-- @return #PLAYERTASK self +-- @usage +-- -- We can use either STATIC, SET_STATIC, SCENERY or SET_SCENERY as target objects. +-- local mytask = PLAYERTASK:NewFromTarget(static, true, 50, "Destroy the target") +-- mytask:SetMenuName("Destroy Power Plant") +-- mytask:AddFreetext("Locate and destroy the power plant near Olenya.") +-- mytask:AddStaticObjectSuccessCondition() +-- +-- playerTaskManager:AddPlayerTaskToQueue(mytask) +function PLAYERTASK:AddStaticObjectSuccessCondition() + local task = self + -- TODO Check if the killer is one of the task clients + task:AddConditionSuccess( + function(target) + if target == nil then return false end + + local isDead = false + if target:IsInstanceOf("STATIC") + or target:IsInstanceOf("SCENERY") + or target:IsInstanceOf("SET_SCENERY") then + isDead = (not target) or target:GetLife() < 1 or target:GetLife() < 0.2* target:GetLife0() + elseif target:IsInstanceOf("SET_STATIC") then + local deadCount = 0 + target:ForEachStatic(function(static) + if static:GetLife() < 1 or static:GetLife() < 0.2* static:GetLife0() then + deadCount = deadCount + 1 + end + end) + + if deadCount == target:Count() then + isDead = true + end + end + + return isDead + end, task:GetTarget() + ) + + -- TODO Check if the killer is one of the task clients + --task:AddConditionFailure( + -- function() + -- + -- end) + return self +end + +--- [USER] Adds task success condition for AUFTRAG.Type.CAPTUREZONE for OpsZone or OpsZone set target object. +--- At least one of the task clients and one capture group need to be inside the zone in order for the capture to be successful. +-- @param #PLAYERTASK self +-- @param #SET_BASE CaptureSquadGroupNamePrefix The prefix of the group name that needs to capture the zone. +-- @param #number Coalition The coalition that needs to capture the zone. +-- @param #boolean CheckClientInZone If true, a CLIENT assigned to this task also needs to be in the zone for the task to be successful. +-- @return #PLAYERTASK self +-- @usage +-- -- We can use either STATIC, SET_STATIC, SCENERY or SET_SCENERY as target objects. +-- local opsZone = OPSZONE:New(zone, coalition.side.RED) +-- +-- ... +-- +-- -- We can use either OPSZONE or SET_OPSZONE. +-- local mytask = PLAYERTASK:NewFromTarget(opsZone, true, 50, "Capture the zone") +-- mytask:SetMenuName("Capture the ops zone") +-- mytask:AddFreetext("Transport capture squad to the ops zone.") +-- +-- -- We set CaptureSquadGroupNamePrefix the group name prefix as set in the ME or the spawn of the group that need to be present at the OpsZone like a capture squad, +-- -- and set the capturing Coalition in order to trigger a successful task. +-- mytask:AddOpsZoneCaptureSuccessCondition("capture-squad", coalition.side.BLUE, false) +-- +-- playerTaskManager:AddPlayerTaskToQueue(mytask) +function PLAYERTASK:AddOpsZoneCaptureSuccessCondition(CaptureSquadGroupNamePrefix, Coalition, CheckClientInZone) + local task = self + task:AddConditionSuccess( + function(target) + if target:IsInstanceOf("OPSZONE") then + return task:_CheckCaptureOpsZoneSuccess(target, CaptureSquadGroupNamePrefix, Coalition, CheckClientInZone or true) + elseif target:IsInstanceOf("SET_OPSZONE") then + local successes = 0 + local isClientInZone = false + target:ForEachZone(function(opszone) + if task:_CheckCaptureOpsZoneSuccess(opszone, CaptureSquadGroupNamePrefix, Coalition, CheckClientInZone or true) then + successes = successes + 1 + end + + for _, client in ipairs(task:GetClientObjects()) do + local clientCoord = client:GetCoordinate() + if opszone.zone:IsCoordinateInZone(clientCoord) then + isClientInZone = true + break + end + end + end) + return successes == target:Count() and isClientInZone + end + + return false + end, task:GetTarget() + ) + return self +end + +--- [USER] Adds task success condition for AUFTRAG.Type.RECON when a client is at a certain LOS distance from the target. +-- @param #PLAYERTASK self +-- @param #number MinDistance (Optional) Minimum distance in meters from client to target in LOS for success condition. (Default 5 NM) +-- @return #PLAYERTASK self +-- @usage +-- -- target can be any object that has a `GetCoordinate()` function like STATIC, GROUP, ZONE... +-- local mytask = PLAYERTASK:New(AUFTRAG.Type.RECON, ZONE:New("WF Zone"), true, 50, "Deep Earth") +-- mytask:SetMenuName("Recon weapon factory") +-- mytask:AddFreetext("Locate and investigate underground weapons factory near Kovdor.") +-- +-- -- We set the MinDistance (optional) in meters for the client to be in LOS from the target in order to trigger a successful task. +-- mytask:AddReconSuccessCondition(10000) -- 10 km (default is 5 NM if not set) +-- +-- playerTaskManager:AddPlayerTaskToQueue(mytask) +function PLAYERTASK:AddReconSuccessCondition(MinDistance) + local task = self + task:AddConditionSuccess( + function(target) + local targetLocation = target:GetCoordinate() + local minD = MinDistance or UTILS.NMToMeters(5) + for _, client in ipairs(task:GetClientObjects()) do + local clientCoord = client:GetCoordinate() + local distance = clientCoord:Get2DDistance(targetLocation) + local isLos = land.isVisible(clientCoord:GetVec3(), targetLocation:GetVec3()) + + if distance < minD and isLos then + return true + end + end + return false + end, task:GetTarget()) + + return self +end + +--- [USER] Adds a time limit for the task to be completed. +-- @param #PLAYERTASK self +-- @param #number TimeLimit Time limit in seconds for the task to be completed. (Default 0 = no time limit) +-- @return #PLAYERTASK self +-- @usage +-- local mytask = PLAYERTASK:New(AUFTRAG.Type.RECON, ZONE:New("WF Zone"), true, 50, "Deep Earth") +-- mytask:SetMenuName("Recon weapon factory") +-- mytask:AddFreetext("Locate and investigate underground weapons factory near Kovdor.") +-- mytask:AddReconSuccessCondition(10000) -- 10 km +-- +-- -- We set the TimeLimit to 10 minutes (600 seconds) from the moment the task is started, once the time has passed and the task is not yet successful it will trigger a failure. +-- mytask:AddTimeLimitFailureCondition(600) +-- +-- playerTaskManager:AddPlayerTaskToQueue(mytask) +function PLAYERTASK:AddTimeLimitFailureCondition(TimeLimit) + local task = self + TimeLimit = TimeLimit or 0 + task.StartTime = -1 + task:AddConditionFailure( + function() + if task.StartTime == -1 then + task.StartTime = timer.getTime() + end + return TimeLimit > 0 and timer.getTime() - task.StartTime > TimeLimit + end) + return self +end + +--- [USER] Add a task to be assigned to same clients when task was a success. +-- @param #PLAYERTASK self +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASK self +function PLAYERTASK:AddNextTaskAfterSuccess(Task) + self:T(self.lid.."AddNextTaskAfterSuccess") + table.insert(self.NextTaskSuccess,Task) + return self +end + +--- [USER] Add a task to be assigned to same clients when task was a failure. +-- @param #PLAYERTASK self +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASK self +function PLAYERTASK:AddNextTaskAfterFailure(Task) + self:T(self.lid.."AddNextTaskAfterFailure") + table.insert(self.NextTaskFailure,Task) + return self +end + +--- [User] Check if task is done +-- @param #PLAYERTASK self +-- @return #boolean done +function PLAYERTASK:IsDone() + self:T(self.lid.."IsDone?") + local IsDone = false + local state = self:GetState() + if state == "Done" or state == "Stopped" then + IsDone = true + end + return IsDone +end + +--- [User] Check if task is NOT done +-- @param #PLAYERTASK self +-- @return #boolean done +function PLAYERTASK:IsNotDone() + self:T(self.lid.."IsNotDone?") + local IsNotDone = not self:IsDone() + return IsNotDone +end + +--- [User] Check if PLAYERTASK has clients assigned to it. +-- @param #PLAYERTASK self +-- @return #boolean hasclients +function PLAYERTASK:HasClients() + self:T(self.lid.."HasClients?") + local hasclients = self:CountClients() > 0 and true or false + return hasclients +end + +--- [User] Get client names assigned as table of #strings +-- @param #PLAYERTASK self +-- @return #table clients +-- @return #number clientcount +function PLAYERTASK:GetClients() + self:T(self.lid.."GetClients") + local clientlist = self.Clients:GetIDStackSorted() or {} + local count = self.Clients:Count() + return clientlist, count +end + +--- [User] Get #CLIENT objects assigned as table +-- @param #PLAYERTASK self +-- @return #table clients +-- @return #number clientcount +function PLAYERTASK:GetClientObjects() + self:T(self.lid.."GetClientObjects") + local clientlist = self.Clients:GetDataTable() or {} + local count = self.Clients:Count() + return clientlist, count +end + +--- [User] Count clients +-- @param #PLAYERTASK self +-- @return #number clientcount +function PLAYERTASK:CountClients() + self:T(self.lid.."CountClients") + return self.Clients:Count() +end + +--- [User] Check if a player name is assigned to this task +-- @param #PLAYERTASK self +-- @param #string Name +-- @return #boolean HasName +function PLAYERTASK:HasPlayerName(Name) + self:T(self.lid.."HasPlayerName?") + return self.Clients:HasUniqueID(Name) +end + +--- [User] Add a client to this task +-- @param #PLAYERTASK self +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASK self +function PLAYERTASK:AddClient(Client) + self:T(self.lid.."AddClient") + local name = Client:GetPlayerName() + if not self.Clients:HasUniqueID(name) then + self.Clients:Push(Client,name) + self:__ClientAdded(-2,Client) + end + if self.TaskController and self.TaskController.Scoring then + self.TaskController.Scoring:_AddPlayerFromUnit(Client) + end + return self +end + +--- [User] Remove a client from this task +-- @param #PLAYERTASK self +-- @param Wrapper.Client#CLIENT Client +-- @param #string Name Name of the client +-- @return #PLAYERTASK self +function PLAYERTASK:RemoveClient(Client,Name) + self:T(self.lid.."RemoveClient") + local name = Name or Client:GetPlayerName() + if self.Clients:HasUniqueID(name) then + self.Clients:PullByID(name) + if self.verbose then + self.Clients:Flush() + end + self:__ClientRemoved(-2,Client) + if self.Clients:Count() == 0 then + self:__Failed(-1) + end + end + return self +end + +--- [User] Client has aborted task this task +-- @param #PLAYERTASK self +-- @param Wrapper.Client#CLIENT Client (optional) +-- @return #PLAYERTASK self +function PLAYERTASK:ClientAbort(Client) + self:T(self.lid.."ClientAbort") + if Client and Client:IsAlive() then + self:RemoveClient(Client) + self:__ClientAborted(-1,Client) + return self + else + -- no client given, abort whole task if no one else is assigned + if self.Clients:Count() == 0 then + -- return to planned state if repeat + self:__Failed(-1) + end + end + return self +end + +--- [User] Create target mark on F10 map +-- @param #PLAYERTASK self +-- @param #string Text (optional) Text to show on the marker +-- @param #number Coalition (optional) Coalition this marker is for. Default = All. +-- @param #boolean ReadOnly (optional) Make target marker read-only. Default = false. +-- @return #PLAYERTASK self +function PLAYERTASK:MarkTargetOnF10Map(Text,Coalition,ReadOnly) + self:T(self.lid.."MarkTargetOnF10Map") + if self.Target then + local coordinate = self.Target:GetCoordinate() + if coordinate then + if self.TargetMarker then + -- Marker exists, delete one first + self.TargetMarker:Remove() + end + local text = Text or ("Target of "..self.lid) + self.TargetMarker = MARKER:New(coordinate,text) + if ReadOnly then + self.TargetMarker:ReadOnly() + end + if Coalition then + self.TargetMarker:ToCoalition(Coalition) + else + self.TargetMarker:ToAll() + end + end + end + return self +end + +--- [User] Smoke Target +-- @param #PLAYERTASK self +-- @param #number Color, defaults to SMOKECOLOR.Red +-- @return #PLAYERTASK self +function PLAYERTASK:SmokeTarget(Color) + self:T(self.lid.."SmokeTarget") + local color = Color or SMOKECOLOR.Red + if not self.lastsmoketime then self.lastsmoketime = 0 end + local TDiff = timer.getAbsTime() - self.lastsmoketime + if self.Target and TDiff > 299 then + local coordinate = self.Target:GetAverageCoordinate() + if coordinate then + coordinate:Smoke(color) + self.lastsmoketime = timer.getAbsTime() + end + end + return self +end + +--- [User] Flare Target +-- @param #PLAYERTASK self +-- @param #number Color, defaults to FLARECOLOR.Red +-- @return #PLAYERTASK self +function PLAYERTASK:FlareTarget(Color) + self:T(self.lid.."SmokeTarget") + local color = Color or FLARECOLOR.Red + if self.Target then + local coordinate = self.Target:GetAverageCoordinate() + if coordinate then + coordinate:Flare(color,0) + end + end + return self +end + +--- [User] Illuminate Target Area +-- @param #PLAYERTASK self +-- @param #number Power Power of illumination bomb in Candela. Default 1000 cd. +-- @param #number Height Height above target used to release the bomb, default 150m. +-- @return #PLAYERTASK self +function PLAYERTASK:IlluminateTarget(Power,Height) + self:T(self.lid.."IlluminateTarget") + local Power = Power or 1000 + local Height = Height or 150 + if self.Target then + local coordinate = self.Target:GetAverageCoordinate() + if coordinate then + local bcoord = COORDINATE:NewFromVec2( coordinate:GetVec2(), Height ) + bcoord:IlluminationBomb(Power) + end + end + return self +end + +-- success / failure function addion courtesy @FunkyFranky. + +--- [User] Add success condition. +-- @param #PLAYERTASK self +-- @param #function ConditionFunction If this function returns `true`, the mission is cancelled. +-- @param ... Condition function arguments if any. +-- @return #PLAYERTASK self +function PLAYERTASK:AddConditionSuccess(ConditionFunction, ...) + + local condition={} --#PLAYERTASK.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionSuccess, condition) + + return self +end + +--- [User] Add failure condition. +-- @param #PLAYERTASK self +-- @param #function ConditionFunction If this function returns `true`, the task is cancelled. +-- @param ... Condition function arguments if any. +-- @return #PLAYERTASK self +function PLAYERTASK:AddConditionFailure(ConditionFunction, ...) + + local condition={} --#PLAYERTASK.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionFailure, condition) + + return self +end + +--- [Internal] Check if any of the given conditions is true. +-- @param #PLAYERTASK self +-- @param #table Conditions Table of conditions. +-- @return #boolean If true, at least one condition is true. +function PLAYERTASK:_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 + +--- [Internal] On after status call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterStatus(From, Event, To) + self:T({From, Event, To}) + self:T(self.lid.."onafterStatus") + + local status = self:GetState() + + if status == "Stopped" then return self end + + -- update marker in case target is moving + if self.TargetMarker then + local coordinate = self.Target:GetCoordinate() + self.TargetMarker:UpdateCoordinate(coordinate,0.5) + end + + -- Check Target status + local targetdead = false + + if self.Type ~= AUFTRAG.Type.CTLD and self.Type ~= AUFTRAG.Type.CSAR then + if self.Target:IsDead() or self.Target:IsDestroyed() or self.Target:CountTargets() == 0 then + targetdead = true + self:__Success(-2) + status = "Success" + return self + end + end + + local clientsalive = false + + if status == "Executing" then + -- Check Clients alive + local ClientTable = self.Clients:GetDataTable() + for _,_client in pairs(ClientTable) do + local client = _client -- Wrapper.Client#CLIENT + if client:IsAlive() then + clientsalive=true -- one or more clients alive + end + end + + -- Failed? + if status == "Executing" and (not clientsalive) and (not targetdead) then + self:__Failed(-2) + status = "Failed" + end + end + + -- Continue if we are not done + if status ~= "Done" and status ~= "Stopped" then + -- Any success condition true? + local successCondition=self:_EvalConditionsAny(self.conditionSuccess) + + -- Any failure condition true? + local failureCondition=self:_EvalConditionsAny(self.conditionFailure) + + if failureCondition and status ~= "Failed" then + self:__Failed(-2) + status = "Failed" + elseif successCondition then + self:__Success(-2) + status = "Success" + end + + if status ~= "Failed" and status ~= "Success" then + -- Partial Success? + local targetcount = self.Target:CountTargets() + if targetcount < self.PreviousCount then + -- Progress + self:__Progress(-2,targetcount) + self.PreviousCount = targetcount + end + end + + if self.verbose then + self:I(self.lid.."Target dead: "..tostring(targetdead).." | Clients alive: " .. tostring(clientsalive)) + end + + self:__Status(-20) + elseif status ~= "Stopped" then + self:__Stop(-1) + end + + return self +end + + +--- [Internal] On after progress call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #number TargetCount +-- @return #PLAYERTASK self +function PLAYERTASK:onafterProgress(From, Event, To, TargetCount) + self:T({From, Event, To}) + if self.TaskController then + if self.TaskController.Scoring then + local clients,count = self:GetClientObjects() + if count > 0 then + for _,_client in pairs(clients) do + self.TaskController.Scoring:AddGoalScore(_client,self.Type,nil,10) + end + end + end + self.TaskController:__TaskProgress(-1,self,TargetCount) + end + return self +end + +--- [Internal] On after planned call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterPlanned(From, Event, To) + self:T({From, Event, To}) + self.timestamp = timer.getAbsTime() + return self +end + +--- [Internal] On after requested call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterRequested(From, Event, To) + self:T({From, Event, To}) + self.timestamp = timer.getAbsTime() + return self +end + +--- [Internal] On after executing call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterExecuting(From, Event, To) + self:T({From, Event, To}) + self.timestamp = timer.getAbsTime() + return self +end + +--- [Internal] On after status call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterStop(From, Event, To) + self:T({From, Event, To}) + self.timestamp = timer.getAbsTime() + return self +end + +--- [Internal] On after client added call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASK self +function PLAYERTASK:onafterClientAdded(From, Event, To, Client) + self:T({From, Event, To}) + if Client and self.verbose then + local text = string.format("Player %s joined task %03d!",Client:GetPlayerName() or "Generic",self.PlayerTaskNr) + self:T(self.lid..text) + end + self.timestamp = timer.getAbsTime() + return self +end + +--- [Internal] On after done call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterDone(From, Event, To) + self:T({From, Event, To}) + if self.TaskController then + self.TaskController:__TaskDone(-1,self) + end + self.timestamp = timer.getAbsTime() + self:__Stop(-1) + return self +end + +--- [Internal] On after cancel call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param #boolean Silent +-- @return #PLAYERTASK self +function PLAYERTASK:onafterCancel(From, Event, To, Silent) + self:T({From, Event, To}) + if self.TaskController then + self.TaskController:__TaskCancelled(-1,self, Silent) + end + self.timestamp = timer.getAbsTime() + self.FinalState = "Cancelled" + self:__Done(-1) + return self +end + +--- [Internal] On after success call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterSuccess(From, Event, To) + self:T({From, Event, To}) + if self.TaskController then + self.TaskController:__TaskSuccess(-1,self) + end + if self.TargetMarker then + self.TargetMarker:Remove() + end + if self.TaskController and self.TaskController.Scoring then + local clients,count = self:GetClientObjects() + if count > 0 then + for _,_client in pairs(clients) do + local auftrag = self:GetSubType() + self.TaskController.Scoring:AddGoalScore(_client,self.Type,nil,self.TaskController.Scores[self.Type]) + end + end + end + self.timestamp = timer.getAbsTime() + self.FinalState = "Success" + self:__Done(-1) + return self +end + +--- [Internal] On after failed call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterFailed(From, Event, To) + self:T({From, Event, To}) + self.repeats = self.repeats + 1 + -- repeat on failed? + if self.Repeat and (self.repeats <= self.RepeatNo) then + if self.TaskController then + self.TaskController:__TaskRepeatOnFailed(-1,self) + end + self:__Planned(-1) + return self + else + if self.TargetMarker then + self.TargetMarker:Remove() + end + self.FinalState = "Failed" + if self.TaskController then + self.TaskController:__TaskFailed(-1,self) + end + self:__Done(-1.5) + end + if self.TaskController.Scoring then + local clients,count = self:GetClientObjects() + if count > 0 then + for _,_client in pairs(clients) do + local auftrag = self:GetSubType() + self.TaskController.Scoring:AddGoalScore(_client,self.Type,nil,-self.TaskController.Scores[self.Type]) + end + end + end + self.timestamp = timer.getAbsTime() + return self +end +------------------------------------------------------------------------------------------------------------------- +-- END PLAYERTASK +------------------------------------------------------------------------------------------------------------------- +end + +do +------------------------------------------------------------------------------------------------------------------- +-- PLAYERTASKCONTROLLER +-- TODO: PLAYERTASKCONTROLLER +-- DONE Playername customized +-- DONE Coalition-level screen info to SET based +-- DONE Flash directions +-- DONE less rebuilds menu, Task info menu available after join +-- DONE Limit menu entries +-- DONE Integrated basic CTLD tasks +-- DONE Integrate basic CSAR tasks +------------------------------------------------------------------------------------------------------------------- + +--- PLAYERTASKCONTROLLER class. +-- @type PLAYERTASKCONTROLLER +-- @field #string ClassName Name of the class. +-- @field #boolean verbose Switch verbosity. +-- @field #string lid Class id string for output to DCS log file. +-- @field Utilities.FiFo#FIFO TargetQueue +-- @field Utilities.FiFo#FIFO TaskQueue +-- @field Utilities.FiFo#FIFO TasksPerPlayer +-- @field Utilities.FiFo#FIFO PrecisionTasks +-- @field Core.Set#SET_CLIENT ClientSet +-- @field Core.Set#SET_CLIENT ActiveClientSet +-- @field #string ClientFilter +-- @field #string Name +-- @field #string Type +-- @field #boolean UseGroupNames +-- @field #table PlayerMenu +-- @field #boolean usecluster +-- @field #number ClusterRadius +-- @field #string MenuName +-- @field #boolean NoScreenOutput +-- @field #number TargetRadius +-- @field #boolean UseWhiteList +-- @field #table WhiteList +-- @field #boolean UseBlackList +-- @field #table BlackList +-- @field Core.TextAndSound#TEXTANDSOUND gettext +-- @field #string locale +-- @field #boolean precisionbombing +-- @field Ops.FlightGroup#FLIGHTGROUP LasingDrone +-- @field Core.MarkerOps_Base#MARKEROPS_BASE MarkerOps +-- @field #boolean taskinfomenu +-- @field #boolean MarkerReadOnly +-- @field #table FlashPlayer List of player who switched Flashing Direction Info on +-- @field #boolean AllowFlash Flashing directions for players allowed +-- @field #number menuitemlimit +-- @field #boolean activehasinfomenu +-- @field #number holdmenutime +-- @field #table customcallsigns +-- @field #boolean ShortCallsign +-- @field #boolean Keepnumber +-- @field #table CallsignTranslations +-- @field #table PlayerFlashMenu +-- @field #table PlayerJoinMenu +-- @field #table PlayerInfoMenu +-- @field #boolean noflaresmokemenu +-- @field #boolean illumenu +-- @field #boolean TransmitOnlyWithPlayers +-- @field #boolean buddylasing +-- @field Ops.PlayerRecce#PLAYERRECCE PlayerRecce +-- @field #number Coalition +-- @field Core.Menu#MENU_MISSION MenuParent +-- @field #boolean ShowMagnetic Also show magnetic angles +-- @field #boolean InfoHasCoordinate +-- @field #boolean InfoHasLLDDM +-- @field #table PlayerMenuTag +-- @field #boolean UseTypeNames +-- @field Functional.Scoring#SCORING Scoring +-- @field Core.ClientMenu#CLIENTMENUMANAGER JoinTaskMenuTemplate +-- @field Core.ClientMenu#CLIENTMENU JoinMenu +-- @field Core.ClientMenu#CLIENTMENU JoinTopMenu +-- @field Core.ClientMenu#CLIENTMENU JoinInfoMenu +-- @field Core.ClientMenu#CLIENTMENUMANAGER ActiveTaskMenuTemplate +-- @field Core.ClientMenu#CLIENTMENU ActiveTopMenu +-- @field Core.ClientMenu#CLIENTMENU ActiveInfoMenu +-- @field Core.ClientMenu#CLIENTMENU MenuNoTask +-- @field #boolean InformationMenu Show Radio Info Menu +-- @field #number TaskInfoDuration How long to show the briefing info on the screen +-- @extends Core.Fsm#FSM + +--- +-- +-- *It is our attitude at the beginning of a difficult task which, more than anything else, which will affect its successful outcome.* (William James) +-- +-- === +-- +-- # PLAYERTASKCONTROLLER +-- +-- * Simplifies defining, executing and controlling of Player tasks +-- * FSM events when a mission is added, done, successful or failed, replanned +-- * Ready to use SRS and localization +-- * Mission locations can be smoked, flared and marked on the map +-- +-- ## 1 Overview +-- +-- PLAYERTASKCONTROLLER is used to auto-create (optional) and control tasks for players. It can be set up as Air-to-Ground (A2G, main focus), Air-to-Ship (A2S) or Air-to-Air (A2A) controller. +-- For the latter task type, also have a look at the @{Ops.AWACS#AWACS} class which allows for more complex scenarios. +-- One task at a time can be joined by the player from the F10 menu. A task can be joined by multiple players. Once joined, task information is available via the F10 menu, the task location +-- can be marked on the map and for A2G/S targets, the target can be marked with smoke and flares. +-- +-- For the mission designer, tasks can be auto-created by means of detection with the integrated @{Ops.Intel#INTEL} class setup, or be manually added to the task queue. +-- +-- ## 2 Task Types +-- +-- Targets can be of types GROUP, SET\_GROUP, UNIT, SET\_UNIT, STATIC, SET\_STATIC, SET\_SCENERY, AIRBASE, ZONE or COORDINATE. The system will auto-create tasks for players from these targets. +-- Tasks are created as @{Ops.PlayerTask#PLAYERTASK} objects, which leverage @{Ops.Target#TARGET} for the management of the actual target. The system creates these task types +-- from the target objects: +-- +-- * A2A - AUFTRAG.Type.INTERCEPT +-- * A2S - AUFTRAG.Type.ANTISHIP +-- * A2G - AUFTRAG.Type.CAS, AUFTRAG.Type.BAI, AUFTRAG.Type.SEAD, AUFTRAG.Type.BOMBING, AUFTRAG.Type.PRECISIONBOMBING, AUFTRAG.Type.BOMBRUNWAY +-- * A2GS - A2S and A2G combined +-- +-- Task types are derived from @{Ops.Auftrag#AUFTRAG}: +-- +-- * CAS - Close air support, created to attack ground units, where friendly ground units are around the location in a bespoke radius (default: 500m/1km diameter) +-- * BAI - Battlefield air interdiction, same as above, but no friendlies around +-- * SEAD - Same as CAS, but the enemy ground units field AAA, SAM or EWR units +-- * Bombing - Against static targets +-- * Precision Bombing - (if enabled) Laser-guided bombing, against **static targets** and **high-value (non-SAM) ground targets (MBTs etc)** +-- * Bomb Runway - Against Airbase runways (in effect, drop bombs over the runway) +-- * ZONE and COORDINATE - Targets will be scanned for GROUND or STATIC enemy units and tasks created from these +-- * Intercept - Any airborne targets, if the controller is of type "A2A" +-- * Anti-Ship - Any ship targets, if the controller is of type "A2S" +-- * CTLD - Combat transport and logistics deployment +-- * CSAR - Combat search and rescue +-- * RECON - Identify targets +-- * CAPTUREZONE - Capture an Ops.OpsZone#OPSZONE +-- * Any #string name can be passed as Auftrag type, but then you need to make sure to define a success condition, and possibly also add the task type to the standard scoring list: `PLAYERTASKCONTROLLER.Scores["yournamehere"]=100` +-- +-- ## 3 Task repetition +-- +-- On failure, tasks will be replanned by default for a maximum of 5 times. +-- +-- ## 3.1 Pre-configured success conditions +-- +-- Pre-configured success conditions for #PLAYERTASK tasks are available as follows: +-- +-- `mytask:AddStaticObjectSuccessCondition()` -- success if static object is at least 80% dead +-- +-- `mytask:AddOpsZoneCaptureSuccessCondition(CaptureSquadGroupNamePrefix,Coalition)` -- success if a squad of the given (partial) name and coalition captures the OpsZone +-- +-- `mytask:AddReconSuccessCondition(MinDistance)` -- success if object is in line-of-sight with the given min distance in NM +-- +-- `mytask:AddTimeLimitSuccessCondition(TimeLimit)` -- failure if the task is not completed within the time limit in seconds given +-- +-- ## 3.2 Task chaining +-- +-- You can create chains of tasks, which will depend on success or failure of the previous task with the following commands: +-- +-- `mytask:AddNextTaskAfterSuccess(FollowUpTask)` and +-- +-- `mytask:AddNextTaskAfterFailure(FollowUpTask)` +-- +-- ## 4 SETTINGS, SRS and language options (localization) +-- +-- The system can optionally communicate to players via SRS. Also localization is available, both "en" and "de" has been build in already. +-- Player and global @{Core.Settings#SETTINGS} for coordinates will be observed. +-- +-- ## 5 Setup +-- +-- A basic setup is very simple: +-- +-- -- Settings - we want players to have a settings menu, be on imperial measures, and get directions as BR +-- _SETTINGS:SetPlayerMenuOn() +-- _SETTINGS:SetImperial() +-- _SETTINGS:SetA2G_BR() +-- +-- -- Set up the A2G task controller for the blue side named "82nd Airborne" +-- local taskmanager = PLAYERTASKCONTROLLER:New("82nd Airborne",coalition.side.BLUE,PLAYERTASKCONTROLLER.Type.A2G) +-- +-- -- set locale to English +-- taskmanager:SetLocale("en") +-- +-- -- Set up detection with grup names *containing* "Blue Recce", these will add targets to our controller via detection. Can be e.g. a drone. +-- taskmanager:SetupIntel("Blue Recce") +-- +-- -- Add a single Recce group name "Blue Humvee" +-- taskmanager:AddAgent(GROUP:FindByName("Blue Humvee")) +-- +-- -- Set the callsign for SRS and Menu name to be "Groundhog" +-- taskmanager:SetMenuName("Groundhog") +-- +-- -- Add accept- and reject-zones for detection +-- -- Accept zones are handy to limit e.g. the engagement to a certain zone. The example is a round, mission editor created zone named "AcceptZone" +-- taskmanager:AddAcceptZone(ZONE:New("AcceptZone")) +-- +-- -- Reject zones are handy to create borders. The example is a ZONE_POLYGON, created in the mission editor, late activated with waypoints, +-- -- named "AcceptZone#ZONE_POLYGON" +-- taskmanager:AddRejectZone(ZONE:FindByName("RejectZone")) +-- +-- -- Set up using SRS for messaging +-- local hereSRSPath = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" +-- local hereSRSPort = 5002 +-- -- local hereSRSGoogle = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio\\yourkey.json" +-- taskmanager:SetSRS({130,255},{radio.modulation.AM,radio.modulation.AM},hereSRSPath,"female","en-GB",hereSRSPort,"Microsoft Hazel Desktop",0.7,hereSRSGoogle) +-- +-- -- Controller will announce itself under these broadcast frequencies, handy to use cold-start frequencies here of your aircraft +-- taskmanager:SetSRSBroadcast({127.5,305},{radio.modulation.AM,radio.modulation.AM}) +-- +-- -- Example: Manually add an AIRBASE as a target +-- taskmanager:AddTarget(AIRBASE:FindByName(AIRBASE.Caucasus.Senaki_Kolkhi)) +-- +-- -- Example: Manually add a COORDINATE as a target +-- taskmanager:AddTarget(GROUP:FindByName("Scout Coordinate"):GetCoordinate()) +-- +-- -- Set a whitelist for tasks, e.g. skip SEAD tasks +-- taskmanager:SetTaskWhiteList({AUFTRAG.Type.CAS, AUFTRAG.Type.BAI, AUFTRAG.Type.BOMBING, AUFTRAG.Type.BOMBRUNWAY}) +-- +-- -- Set target radius +-- taskmanager:SetTargetRadius(1000) +-- +-- ## 6 Localization +-- +-- Localization for English and German texts are build-in. Default setting is English. Change with @{#PLAYERTASKCONTROLLER.SetLocale}() +-- +-- ### 6.1 Adding Localization +-- +-- A list of fields to be defined follows below. **Note** that in some cases `string.format()` is used to format texts for screen and SRS. +-- Hence, the `%d`, `%s` and `%f` special characters need to appear in the exact same amount and order of appearance in the localized text or it will create errors. +-- To add a localization, the following texts need to be translated and set in your mission script **before** @{#PLAYERTASKCONTROLLER.New}(): +-- +-- PLAYERTASKCONTROLLER.Messages = { +-- EN = { +-- TASKABORT = "Task aborted!", +-- NOACTIVETASK = "No active task!", +-- FREQUENCIES = "frequencies ", +-- FREQUENCY = "frequency %.3f", +-- BROADCAST = "%s, %s, switch to %s for task assignment!", +-- CASTTS = "close air support", +-- SEADTTS = "suppress air defense", +-- BOMBTTS = "bombing", +-- PRECBOMBTTS = "precision bombing", +-- BAITTS = "battle field air interdiction", +-- ANTISHIPTTS = "anti-ship", +-- INTERCEPTTS = "intercept", +-- BOMBRUNWAYTTS = "bomb runway", +-- HAVEACTIVETASK = "You already have one active task! Complete it first!", +-- PILOTJOINEDTASK = "%s, %s. You have been assigned %s task %03d", +-- TASKNAME = "%s Task ID %03d", +-- TASKNAMETTS = "%s Task ID %03d", +-- THREATHIGH = "high", +-- THREATMEDIUM = "medium", +-- THREATLOW = "low", +-- THREATTEXT = "%s\nThreat: %s\nTargets left: %d\nCoord: %s", +-- THREATTEXTTTS = "%s, %s. Target information for %s. Threat level %s. Targets left %d. Target location %s.", +-- MARKTASK = "%s, %s, copy, task %03d location marked on map!", +-- SMOKETASK = "%s, %s, copy, task %03d location smoked!", +-- FLARETASK = "%s, %s, copy, task %03d location illuminated!", +-- ABORTTASK = "All stations, %s, %s has aborted %s task %03d!", +-- UNKNOWN = "Unknown", +-- MENUTASKING = " Tasking ", +-- MENUACTIVE = "Active Task", +-- MENUINFO = "Info", +-- MENUMARK = "Mark on map", +-- MENUSMOKE = "Smoke", +-- MENUFLARE = "Flare", +-- MENUILLU = "Illuminate", +-- MENUABORT = "Abort", +-- MENUJOIN = "Join Task", +-- MENUTASKINFO = Task Info", +-- MENUTASKNO = "TaskNo", +-- MENUNOTASKS = "Currently no tasks available.", +-- TASKCANCELLED = "Task #%03d %s is cancelled!", +-- TASKCANCELLEDTTS = "%s, task %03d %s is cancelled!", +-- TASKSUCCESS = "Task #%03d %s completed successfully!", +-- TASKSUCCESSTTS = "%s, task %03d %s completed successfully!", +-- TASKFAILED = "Task #%03d %s was a failure!", +-- TASKFAILEDTTS = "%s, task %03d %s was a failure!", +-- TASKFAILEDREPLAN = "Task #%03d %s was a failure! Replanning!", +-- TASKFAILEDREPLANTTS = "%s, task %03d %s was a failure! Replanning!", +-- TASKADDED = "%s has a new task %s available!", +-- PILOTS = "\nPilot(s): ", +-- PILOTSTTS = ". Pilot(s): ", +-- YES = "Yes", +-- NO = "No", +-- NONE = "None", +-- POINTEROVERTARGET = "%s, %s, pointer in reach for task %03d, lasing!", +-- POINTERTARGETREPORT = "\nPointer in reach: %s\nLasing: %s", +-- RECCETARGETREPORT = "\nRecce %s in reach: %s\nLasing: %s", +-- POINTERTARGETLASINGTTS = ". Pointer in reach and lasing.", +-- TARGET = "Target", +-- FLASHON = "%s - Flashing directions is now ON!", +-- FLASHOFF = "%s - Flashing directions is now OFF!", +-- FLASHMENU = "Flash Directions Switch", +-- BRIEFING = "Briefing", +-- TARGETLOCATION ="Target location", +-- COORDINATE = "Coordinate", +-- INFANTRY = "Infantry", +-- TECHNICAL = "Technical", +-- ARTILLERY = "Artillery", +-- TANKS = "Tanks", +-- AIRDEFENSE = "Airdefense", +-- SAM = "SAM", +-- GROUP = "Group", +-- ELEVATION = "\nTarget Elevation: %s %s", +-- METER = "meter", +-- FEET = "feet", +-- }, +-- +-- e.g. +-- +-- taskmanager.Messages = { +-- FR = { +-- TASKABORT = "Tâche abandonnée!", +-- NOACTIVETASK = "Aucune tâche active!", +-- FREQUENCIES = "fréquences ", +-- FREQUENCY = "fréquence %.3f", +-- BROADCAST = "%s, %s, passer au %s pour l'attribution des tâches!", +-- ... +-- TASKADDED = "%s a créé une nouvelle tâche %s", +-- PILOTS = "\nPilote(s): ", +-- PILOTSTTS = ". Pilote(s): ", +-- }, +-- +-- and then `taskmanager:SetLocale("fr")` **after** @{#PLAYERTASKCONTROLLER.New}() in your script. +-- +-- If you just want to replace a **single text block** in the table, you can do this like so: +-- +-- mycontroller.Messages.EN.NOACTIVETASK = "Choose a task first!" +-- mycontroller.Messages.FR.YES = "Oui" +-- +-- ## 7 Events +-- +-- The class comes with a number of FSM-based events that missions designers can use to shape their mission. +-- These are: +-- +-- ### 7.1 TaskAdded. +-- +-- The event is triggered when a new task is added to the controller. Use @{#PLAYERTASKCONTROLLER.OnAfterTaskAdded}() to link into this event: +-- +-- function taskmanager:OnAfterTaskAdded(From, Event, To, Task) +-- ... your code here ... +-- end +-- +-- ### 7.2 TaskDone. +-- +-- The event is triggered when a task has ended. Use @{#PLAYERTASKCONTROLLER.OnAfterTaskDone}() to link into this event: +-- +-- function taskmanager:OnAfterTaskDone(From, Event, To, Task) +-- ... your code here ... +-- end +-- +-- ### 7.3 TaskCancelled. +-- +-- The event is triggered when a task was cancelled manually. Use @{#PLAYERTASKCONTROLLER.OnAfterTaskCancelled}()` to link into this event: +-- +-- function taskmanager:OnAfterTaskCancelled(From, Event, To, Task, Silent) +-- ... your code here ... +-- end +-- +-- ### 7.4 TaskSuccess. +-- +-- The event is triggered when a task completed successfully. Use @{#PLAYERTASKCONTROLLER.OnAfterTaskSuccess}() to link into this event: +-- +-- function taskmanager:OnAfterTaskSuccess(From, Event, To, Task) +-- ... your code here ... +-- end +-- +-- ### 7.5 TaskFailed. +-- +-- The event is triggered when a task failed, no repeats. Use @{#PLAYERTASKCONTROLLER.OnAfterTaskFailed}() to link into this event: +-- +-- function taskmanager:OnAfterTaskFailed(From, Event, To, Task) +-- ... your code here ... +-- end +-- +-- ### 7.6 TaskRepeatOnFailed. +-- +-- The event is triggered when a task failed and is re-planned for execution. Use @{#PLAYERTASKCONTROLLER.OnAfterRepeatOnFailed}() to link into this event: +-- +-- function taskmanager:OnAfterRepeatOnFailed(From, Event, To, Task) +-- ... your code here ... +-- end +-- +-- ## 8 Using F10 map markers to create new targets +-- +-- You can use F10 map markers to create new target points for player tasks. +-- Enable this option with e.g., setting the tag to be used to "TARGET": +-- +-- taskmanager:EnableMarkerOps("TARGET") +-- +-- Set a marker on the map and add the following text to create targets from it: "TARGET". This is effectively the same as adding a COORDINATE object as target. +-- The marker can be deleted any time. +-- +-- ## 9 Discussion +-- +-- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-playertask channel. +-- +-- +-- @field #PLAYERTASKCONTROLLER +PLAYERTASKCONTROLLER = { + ClassName = "PLAYERTASKCONTROLLER", + verbose = false, + lid = nil, + TargetQueue = nil, + ClientSet = nil, + UseGroupNames = true, + PlayerMenu = {}, + usecluster = false, + MenuName = nil, + ClusterRadius = 0.5, + NoScreenOutput = false, + TargetRadius = 500, + UseWhiteList = false, + WhiteList = {}, + gettext = nil, + locale = "en", + precisionbombing = false, + taskinfomenu = false, + activehasinfomenu = false, + MarkerReadOnly = false, + customcallsigns = {}, + ShortCallsign = true, + Keepnumber = false, + CallsignTranslations = nil, + PlayerFlashMenu = {}, + PlayerJoinMenu = {}, + PlayerInfoMenu = {}, + PlayerMenuTag = {}, + noflaresmokemenu = false, + illumenu = false, + TransmitOnlyWithPlayers = true, + buddylasing = false, + PlayerRecce = nil, + Coalition = nil, + MenuParent = nil, + ShowMagnetic = true, + InfoHasLLDDM = false, + InfoHasCoordinate = false, + UseTypeNames = false, + Scoring = nil, + MenuNoTask = nil, + InformationMenu = false, + TaskInfoDuration = 30, + } + +--- +-- @type Type +-- @field #string A2A Air-to-Air Controller +-- @field #string A2G Air-to-Ground Controller +-- @field #string A2S Air-to-Ship Controller +-- @field #string A2GS Air-to-Ground-and-Ship Controller +PLAYERTASKCONTROLLER.Type = { + A2A = "Air-To-Air", + A2G = "Air-To-Ground", + A2S = "Air-To-Sea", + A2GS = "Air-To-Ground-Sea", +} + +--- Define new AUFTRAG Types +AUFTRAG.Type.PRECISIONBOMBING = "Precision Bombing" +AUFTRAG.Type.CTLD = "Combat Transport" +AUFTRAG.Type.CSAR = "Combat Rescue" +AUFTRAG.Type.CONQUER = "Conquer" +--- +-- @type Scores +PLAYERTASKCONTROLLER.Scores = { + [AUFTRAG.Type.PRECISIONBOMBING] = 100, + [AUFTRAG.Type.CTLD] = 100, + [AUFTRAG.Type.CSAR] = 100, + [AUFTRAG.Type.INTERCEPT] = 100, + [AUFTRAG.Type.ANTISHIP] = 100, + [AUFTRAG.Type.CAS] = 100, + [AUFTRAG.Type.BAI] = 100, + [AUFTRAG.Type.SEAD] = 100, + [AUFTRAG.Type.BOMBING] = 100, + [AUFTRAG.Type.BOMBRUNWAY] = 100, + [AUFTRAG.Type.CONQUER] = 100, + [AUFTRAG.Type.RECON] = 100, + [AUFTRAG.Type.ESCORT] = 100, + [AUFTRAG.Type.CAP] = 100, + [AUFTRAG.Type.CAPTUREZONE] = 100, +} + +--- +-- @type SeadAttributes +-- @field #number SAM GROUP.Attribute.GROUND_SAM +-- @field #number AAA GROUP.Attribute.GROUND_AAA +-- @field #number EWR GROUP.Attribute.GROUND_EWR +PLAYERTASKCONTROLLER.SeadAttributes = { + SAM = GROUP.Attribute.GROUND_SAM, + AAA = GROUP.Attribute.GROUND_AAA, + EWR = GROUP.Attribute.GROUND_EWR, +} + +--- +-- @field Messages +PLAYERTASKCONTROLLER.Messages = { + EN = { + TASKABORT = "Task aborted!", + NOACTIVETASK = "No active task!", + FREQUENCIES = "frequencies ", + FREQUENCY = "frequency %.3f", + BROADCAST = "%s, %s, switch to %s for task assignment!", + CASTTS = "close air support", + SEADTTS = "suppress air defense", + BOMBTTS = "bombing", + PRECBOMBTTS = "precision bombing", + BAITTS = "battle field air interdiction", + ANTISHIPTTS = "anti-ship", + INTERCEPTTS = "intercept", + BOMBRUNWAYTTS = "bomb runway", + HAVEACTIVETASK = "You already have one active task! Complete it first!", + PILOTJOINEDTASK = "%s, %s. You have been assigned %s task %03d", + TASKNAME = "%s Task ID %03d", + TASKNAMETTS = "%s Task ID %03d", + THREATHIGH = "high", + THREATMEDIUM = "medium", + THREATLOW = "low", + THREATTEXT = "%s\nThreat: %s\nTargets left: %d\nCoord: %s", + NOTHREATTEXT = "%s\nNo target information available.", + ELEVATION = "\nTarget Elevation: %s %s", + METER = "meter", + FEET = "feet", + THREATTEXTTTS = "%s, %s. Target information for %s. Threat level %s. Targets left %d. Target location %s.", + NOTHREATTEXTTTS = "%s, %s. No target information available.", + MARKTASK = "%s, %s, copy, task %03d location marked on map!", + SMOKETASK = "%s, %s, copy, task %03d location smoked!", + NOSMOKETASK = "%s, %s, negative, task %03d location cannot be smoked!", + FLARETASK = "%s, %s, copy, task %03d location illuminated!", + ABORTTASK = "All stations, %s, %s has aborted %s task %03d!", + UNKNOWN = "Unknown", + MENUTASKING = " Tasking ", + MENUACTIVE = "Active Task", + MENUINFO = "Info", + MENUMARK = "Mark on map", + MENUSMOKE = "Smoke", + MENUFLARE = "Flare", + MENUILLU = "Illuminate", + MENUABORT = "Abort", + MENUJOIN = "Join Task", + MENUTASKINFO = "Task Info", + MENUTASKNO = "TaskNo", + MENUNOTASKS = "Currently no tasks available.", + TASKCANCELLED = "Task #%03d %s is cancelled!", + TASKCANCELLEDTTS = "%s, task %03d %s is cancelled!", + TASKSUCCESS = "Task #%03d %s completed successfully!", + TASKSUCCESSTTS = "%s, task %03d %s completed successfully!", + TASKFAILED = "Task #%03d %s was a failure!", + TASKFAILEDTTS = "%s, task %03d %s was a failure!", + TASKFAILEDREPLAN = "Task #%03d %s available for reassignment!", + TASKFAILEDREPLANTTS = "%s, task %03d %s vailable for reassignment!", + TASKADDED = "%s has a new %s task available!", + PILOTS = "\nPilot(s): ", + PILOTSTTS = ". Pilot(s): ", + YES = "Yes", + NO = "No", + NONE = "None", + POINTEROVERTARGET = "%s, %s, pointer in reach for task %03d, lasing!", + POINTERTARGETREPORT = "\nPointer in reach: %s\nLasing: %s", + RECCETARGETREPORT = "\nRecce %s in reach: %s\nLasing: %s", + POINTERTARGETLASINGTTS = ". Pointer in reach and lasing.", + TARGET = "Target", + FLASHON = "%s - Flashing directions is now ON!", + FLASHOFF = "%s - Flashing directions is now OFF!", + FLASHMENU = "Flash Directions Switch", + BRIEFING = "Briefing", + TARGETLOCATION ="Target location", + COORDINATE = "Coordinate", + INFANTRY = "Infantry", + TECHNICAL = "Technical", + ARTILLERY = "Artillery", + TANKS = "Tanks", + AIRDEFENSE = "Airdefense", + SAM = "SAM", + GROUP = "Group", + UNARMEDSHIP = "Merchant", + LIGHTARMEDSHIP = "Light Boat", + CORVETTE = "Corvette", + FRIGATE = "Frigate", + CRUISER = "Cruiser", + DESTROYER = "Destroyer", + CARRIER = "Aircraft Carrier", + RADIOS = "Radios", + }, + DE = { + TASKABORT = "Auftrag abgebrochen!", + NOACTIVETASK = "Kein aktiver Auftrag!", + FREQUENCIES = "Frequenzen ", + FREQUENCY = "Frequenz %.3f", + BROADCAST = "%s, %s, Radio %s für Aufgabenzuteilung!", + CASTTS = "Nahbereichsunterstützung", + SEADTTS = "Luftabwehr ausschalten", + BOMBTTS = "Bombardieren", + PRECBOMBTTS = "Präzisionsbombardieren", + BAITTS = "Luftunterstützung", + ANTISHIPTTS = "Anti-Schiff", + INTERCEPTTS = "Abfangen", + BOMBRUNWAYTTS = "Startbahn Bombardieren", + HAVEACTIVETASK = "Du hast einen aktiven Auftrag! Beende ihn zuerst!", + PILOTJOINEDTASK = "%s, %s hat Auftrag %s %03d angenommen", + TASKNAME = "%s Auftrag ID %03d", + TASKNAMETTS = "%s Auftrag ID %03d", + THREATHIGH = "hoch", + THREATMEDIUM = "mittel", + THREATLOW = "niedrig", + THREATTEXT = "%s\nGefahrstufe: %s\nZiele: %d\nKoord: %s", + NOTHREATTEXT = "%s\nKeine Zielinformation verfügbar.", + ELEVATION = "\nZiel Höhe: %s %s", + METER = "Meter", + FEET = "Fuss", + THREATTEXTTTS = "%s, %s. Zielinformation zu %s. Gefahrstufe %s. Ziele %d. Zielposition %s.", + NOTHREATTEXTTTS = "%s, %s. Keine Zielinformation verfügbar.", + MARKTASK = "%s, %s, verstanden, Zielposition %03d auf der Karte markiert!", + SMOKETASK = "%s, %s, verstanden, Zielposition %03d mit Rauch markiert!", + NOSMOKETASK = "%s, %s, negativ, Zielposition %03d kann nicht markiert werden!", + FLARETASK = "%s, %s, verstanden, Zielposition %03d beleuchtet!", + ABORTTASK = "%s, an alle, %s hat Auftrag %s %03d abgebrochen!", + UNKNOWN = "Unbekannt", + MENUTASKING = " Aufträge ", + MENUACTIVE = "Aktiver Auftrag", + MENUINFO = "Information", + MENUMARK = "Kartenmarkierung", + MENUSMOKE = "Rauchgranate", + MENUFLARE = "Leuchtgranate", + MENUILLU = "Feldbeleuchtung", + MENUABORT = "Abbrechen", + MENUJOIN = "Auftrag annehmen", + MENUTASKINFO = "Auftrag Briefing", + MENUTASKNO = "AuftragsNr", + MENUNOTASKS = "Momentan keine Aufträge verfügbar.", + TASKCANCELLED = "Auftrag #%03d %s wurde beendet!", + TASKCANCELLEDTTS = "%s, Auftrag %03d %s wurde beendet!", + TASKSUCCESS = "Auftrag #%03d %s erfolgreich!", + TASKSUCCESSTTS = "%s, Auftrag %03d %s erfolgreich!", + TASKFAILED = "Auftrag #%03d %s gescheitert!", + TASKFAILEDTTS = "%s, Auftrag %03d %s gescheitert!", + TASKFAILEDREPLAN = "Auftrag #%03d %s gescheitert! Neuplanung!", + TASKFAILEDREPLANTTS = "%s, Auftrag %03d %s gescheitert! Neuplanung!", + TASKADDED = "%s hat einen neuen Auftrag %s erstellt!", + PILOTS = "\nPilot(en): ", + PILOTSTTS = ". Pilot(en): ", + YES = "Ja", + NO = "Nein", + NONE = "Keine", + POINTEROVERTARGET = "%s, %s, Marker im Zielbereich für %03d, Laser an!", + POINTERTARGETREPORT = "\nMarker im Zielbereich: %s\nLaser an: %s", + RECCETARGETREPORT = "\nSpäher % im Zielbereich: %s\nLasing: %s", + POINTERTARGETLASINGTTS = ". Marker im Zielbereich, Laser is an.", + TARGET = "Ziel", + FLASHON = "%s - Richtungsangaben einblenden ist EIN!", + FLASHOFF = "%s - Richtungsangaben einblenden ist AUS!", + FLASHMENU = "Richtungsangaben Schalter", + BRIEFING = "Briefing", + TARGETLOCATION ="Zielposition", + COORDINATE = "Koordinate", + INFANTRY = "Infantrie", + TECHNICAL = "Technische", + ARTILLERY = "Artillerie", + TANKS = "Panzer", + AIRDEFENSE = "Flak", + SAM = "Luftabwehr", + GROUP = "Einheit", + UNARMEDSHIP = "Handelsschiff", + LIGHTARMEDSHIP = "Tender", + CORVETTE = "Korvette", + FRIGATE = "Fregatte", + CRUISER = "Kreuzer", + DESTROYER = "Zerstörer", + CARRIER = "Flugzeugträger", + RADIOS = "Frequenzen", + }, +} + +--- PLAYERTASK class version. +-- @field #string version +PLAYERTASKCONTROLLER.version="0.1.71" + +--- Create and run a new TASKCONTROLLER instance. +-- @param #PLAYERTASKCONTROLLER self +-- @param #string Name Name of this controller +-- @param #number Coalition of this controller, e.g. coalition.side.BLUE +-- @param #string Type Type of the tasks controlled, defaults to PLAYERTASKCONTROLLER.Type.A2G +-- @param #string ClientFilter (optional) Additional prefix filter for the SET_CLIENT. Can be handed as @{Core.Set#SET_CLIENT} also. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #PLAYERTASKCONTROLLER + + self.Name = Name or "CentCom" + self.Coalition = Coalition or coalition.side.BLUE + self.CoalitionName = UTILS.GetCoalitionName(Coalition) + self.Type = Type or PLAYERTASKCONTROLLER.Type.A2G + + self.usecluster = false + if self.Type == PLAYERTASKCONTROLLER.Type.A2A then + self.usecluster = true + end + + self.ClusterRadius = 0.5 + self.TargetRadius = 500 + + self.ClientFilter = ClientFilter --or "" + + self.TargetQueue = FIFO:New() -- Utilities.FiFo#FIFO + self.TaskQueue = FIFO:New() -- Utilities.FiFo#FIFO + self.TasksPerPlayer = FIFO:New() -- Utilities.FiFo#FIFO + self.PrecisionTasks = FIFO:New() -- Utilities.FiFo#FIFO + self.LasingDroneSet = SET_OPSGROUP:New() -- Core.Set#SET_OPSGROUP + --self.PlayerMenu = {} -- #table + self.FlashPlayer = {} -- #table + self.AllowFlash = false + self.lasttaskcount = 0 + + self.taskinfomenu = false + self.activehasinfomenu = false + self.MenuName = nil + self.menuitemlimit = 6 + self.holdmenutime = 30 + + self.MarkerReadOnly = false + + self.repeatonfailed = true + self.repeattimes = 5 + self.UseGroupNames = true + + self.customcallsigns = {} + self.ShortCallsign = true + self.Keepnumber = false + self.CallsignTranslations = nil + + self.noflaresmokemenu = false + self.illumenu = false + + self.ShowMagnetic = true + + self.UseTypeNames = false + + self.InformationMenu = false + + self.TaskInfoDuration = 30 + + self.IsClientSet = false + + if ClientFilter and type(ClientFilter) == "table" and ClientFilter.ClassName and ClientFilter.ClassName == "SET_CLIENT" then + -- we have a predefined SET_CLIENT + self.ClientSet = ClientFilter + self.IsClientSet = true + end + + if ClientFilter and not self.IsClientSet then + self.ClientSet = SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterPrefixes(ClientFilter):FilterStart() + elseif not self.IsClientSet then + self.ClientSet = SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterStart() + end + + self.ActiveClientSet = SET_CLIENT:New() + + self.lid=string.format("PlayerTaskController %s %s | ", self.Name, tostring(self.Type)) + + self:_InitLocalization() + + -- FSM start state is STOPPED. + self:SetStartState("Stopped") + + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "TaskAdded", "*") + self:AddTransition("*", "TaskDone", "*") + self:AddTransition("*", "TaskCancelled", "*") + self:AddTransition("*", "TaskSuccess", "*") + self:AddTransition("*", "TaskFailed", "*") + self:AddTransition("*", "TaskProgress", "*") + self:AddTransition("*", "TaskTargetSmoked", "*") + self:AddTransition("*", "TaskTargetFlared", "*") + self:AddTransition("*", "TaskTargetIlluminated", "*") + self:AddTransition("*", "TaskRepeatOnFailed", "*") + self:AddTransition("*", "PlayerJoinedTask", "*") + self:AddTransition("*", "PlayerAbortedTask", "*") + self:AddTransition("*", "Stop", "Stopped") + + self:__Start(2) + local starttime = math.random(5,10) + self:__Status(starttime) + + self:I(self.lid..self.version.." Started.") + + return self + + --- + -- Pseudo Functions + --- + + --- On After "TaskAdded" event. Task has been added. + -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskAdded + -- @param #PLAYERTASKCONTROLLER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.PlayerTask#PLAYERTASK Task + + --- On After "TaskDone" event. Task is done. + -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskDone + -- @param #PLAYERTASKCONTROLLER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.PlayerTask#PLAYERTASK Task + + --- On After "TaskCancelled" event. Task has been cancelled. + -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskCancelled + -- @param #PLAYERTASKCONTROLLER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.PlayerTask#PLAYERTASK Task + -- @param #boolean Silent If true suppress message output. + + --- On After "TaskFailed" event. Task has failed. + -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskFailed + -- @param #PLAYERTASKCONTROLLER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.PlayerTask#PLAYERTASK Task + + --- On After "TaskSuccess" event. Task has been a success. + -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskSuccess + -- @param #PLAYERTASKCONTROLLER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.PlayerTask#PLAYERTASK Task + + --- On After "TaskProgress" event. Task target count has been reduced. + -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskProgress + -- @param #PLAYERTASKCONTROLLER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.PlayerTask#PLAYERTASK Task The current Task. + -- @param #number TargetCount Targets left over + + --- On After "TaskRepeatOnFailed" event. Task has failed and will be repeated. + -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskRepeatOnFailed + -- @param #PLAYERTASKCONTROLLER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.PlayerTask#PLAYERTASK Task + + --- On After "TaskTargetSmoked" event. Task smoked. + -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskTargetSmoked + -- @param #PLAYERTASKCONTROLLER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.PlayerTask#PLAYERTASK Task + + --- On After "TaskTargetFlared" event. Task flared. + -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskTargetFlared + -- @param #PLAYERTASKCONTROLLER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.PlayerTask#PLAYERTASK Task + + --- On After "TaskTargetIlluminated" event. Task illuminated. + -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskTargetIlluminated + -- @param #PLAYERTASKCONTROLLER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Ops.PlayerTask#PLAYERTASK Task + + --- On After "PlayerJoinedTask" event. Player joined a task. + -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterPlayerJoinedTask + -- @param #PLAYERTASKCONTROLLER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#GROUP Group The player group object + -- @param Wrapper.Client#CLIENT Client The player client object + -- @param Ops.PlayerTask#PLAYERTASK Task + + --- On After "PlayerAbortedTask" event. Player aborted a task. + -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterPlayerAbortedTask + -- @param #PLAYERTASKCONTROLLER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#GROUP Group The player group object + -- @param Wrapper.Client#CLIENT Client The player client object + -- @param Ops.PlayerTask#PLAYERTASK Task + +end + +--- [User] Set or create a SCORING object for this taskcontroller +-- @param #PLAYERTASKCONTROLLER self +-- @param Functional.Scoring#SCORING Scoring (optional) the Scoring object +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:EnableScoring(Scoring) + self.Scoring = Scoring or SCORING:New(self.Name) + return self +end + +--- [User] Remove the SCORING object from this taskcontroller +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:DisableScoring() + self.Scoring = nil + return self +end + +--- [Internal] Init localization +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_InitLocalization() + self:T(self.lid.."_InitLocalization") + self.gettext = TEXTANDSOUND:New("PLAYERTASKCONTROLLER","en") -- Core.TextAndSound#TEXTANDSOUND + self.locale = "en" + for locale,table in pairs(self.Messages) do + local Locale = string.lower(tostring(locale)) + self:T("**** Adding locale: "..Locale) + for ID,Text in pairs(table) do + self:T(string.format('Adding ID %s',tostring(ID))) + self.gettext:AddEntry(Locale,tostring(ID),Text) + end + end + return self +end + +--- [User] Show target menu entries of type names for GROUND targets (off by default!), e.g. "Tank Group..." +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetEnableUseTypeNames() + self:T(self.lid.."SetEnableUseTypeNames") + self.UseTypeNames = true + return self +end + +--- [User] Do not show target menu entries of type names for GROUND targets +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetDisableUseTypeNames() + self:T(self.lid.."SetDisableUseTypeNames") + self.UseTypeNames = false + return self +end + +--- [User] Set flash directions option for player (player based info) +-- @param #PLAYERTASKCONTROLLER self +-- @param #boolean OnOff Set to `true` to switch on and `false` to switch off. Default is OFF. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetAllowFlashDirection(OnOff) + self:T(self.lid.."SetAllowFlashDirection") + self.AllowFlash = OnOff + return self +end + +--- [User] Set to show a menu entry to retrieve the radio frequencies used. +-- @param #PLAYERTASKCONTROLLER self +-- @param #boolean OnOff Set to `true` to switch on and `false` to switch off. Default is OFF. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetShowRadioInfoMenu(OnOff) + self:T(self.lid.."SetAllowRadioInfoMenu") + self.InformationMenu = OnOff + return self +end + +--- [User] Do not show menu entries to smoke or flare targets +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetDisableSmokeFlareTask() + self:T(self.lid.."SetDisableSmokeFlareTask") + self.noflaresmokemenu = true + return self +end + +--- [User] For SRS - Switch to only transmit if there are players on the server. +-- @param #PLAYERTASKCONTROLLER self +-- @param #boolean Switch If true, only send SRS if there are alive Players. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetTransmitOnlyWithPlayers(Switch) + self.TransmitOnlyWithPlayers = Switch + if self.SRSQueue then + self.SRSQueue:SetTransmitOnlyWithPlayers(Switch) + end + return self +end + +--- [User] Show menu entries to smoke or flare targets (on by default!) +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetEnableSmokeFlareTask() + self:T(self.lid.."SetEnableSmokeFlareTask") + self.noflaresmokemenu = false + return self +end + +--- [User] Show menu entries to illuminate targets. Needs smoke/flare enabled. +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetEnableIlluminateTask() + self:T(self.lid.."SetEnableSmokeFlareTask") + self.illumenu = true + return self +end + +--- [User] Do not show menu entries to illuminate targets. +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetDisableIlluminateTask() + self:T(self.lid.."SetDisableIlluminateTask") + self.illumenu = false + return self +end + +--- [User] Show info text on screen with a coordinate info in any case (OFF by default) +-- @param #PLAYERTASKCONTROLLER self +-- @param #boolean OnOff Switch on = true or off = false +-- @param #boolean LLDDM Show LLDDM = true or LLDMS = false +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetInfoShowsCoordinate(OnOff,LLDDM) + self:T(self.lid.."SetInfoShowsCoordinate") + self.InfoHasCoordinate = OnOff + self.InfoHasLLDDM = LLDDM + return self +end + +--- [User] Set callsign options for TTS output. See @{Wrapper.Group#GROUP.GetCustomCallSign}() on how to set customized callsigns. +-- @param #PLAYERTASKCONTROLLER self +-- @param #boolean ShortCallsign If true, only call out the major flight number +-- @param #boolean Keepnumber If true, keep the **customized callsign** in the #GROUP name for players as-is, no amendments or numbers. +-- @param #table CallsignTranslations (optional) Table to translate between DCS standard callsigns and bespoke ones. Does not apply if using customized +-- callsigns from playername or group name. +-- @param #func CallsignCustomFunc (Optional) For player names only(!). If given, this function will return the callsign. Needs to take the groupname and the playername as first two arguments. +-- @param #arg ... (Optional) Comma separated arguments to add to the custom function call after groupname and playername. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations,CallsignCustomFunc,...) + if not ShortCallsign or ShortCallsign == false then + self.ShortCallsign = false + else + self.ShortCallsign = true + end + self.Keepnumber = Keepnumber or false + self.CallsignTranslations = CallsignTranslations + self.CallsignCustomFunc = CallsignCustomFunc + self.CallsignCustomArgs = arg or {} + return self +end + +--- [Internal] Get text for text-to-speech. +-- Numbers are spaced out, e.g. "Heading 180" becomes "Heading 1 8 0 ". +-- @param #PLAYERTASKCONTROLLER self +-- @param #string text Original text. +-- @return #string Spoken text. +function PLAYERTASKCONTROLLER:_GetTextForSpeech(text) + self:T(self.lid.."_GetTextForSpeech") + -- Space out numbers. + text=string.gsub(text,"%d","%1 ") + -- get rid of leading or trailing spaces + text=string.gsub(text,"^%s*","") + text=string.gsub(text,"%s*$","") + text=string.gsub(text," "," ") + + return text +end + +--- [User] Set repetition options for tasks. +-- @param #PLAYERTASKCONTROLLER self +-- @param #boolean OnOff Set to `true` to switch on and `false` to switch off (defaults to true) +-- @param #number Repeats Number of repeats (defaults to 5) +-- @return #PLAYERTASKCONTROLLER self +-- @usage `taskmanager:SetTaskRepetition(true, 5)` +function PLAYERTASKCONTROLLER:SetTaskRepetition(OnOff, Repeats) + self:T(self.lid.."SetTaskRepetition") + if OnOff then + self.repeatonfailed = true + self.repeattimes = Repeats or 5 + else + self.repeatonfailed = false + self.repeattimes = Repeats or 5 + end + return self +end + +--- [User] Set how long the briefing is shown on screen. +-- @param #PLAYERTASKCONTROLLER self +-- @param #number Seconds Duration in seconds. Defaults to 30 seconds. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetBriefingDuration(Seconds) + self:T(self.lid.."SetBriefingDuration") + self.TaskInfoDuration = Seconds or 30 + return self +end + +--- [Internal] Send message to SET_CLIENT of players +-- @param #PLAYERTASKCONTROLLER self +-- @param #string Text the text to be send +-- @param #number Seconds (optional) Seconds to show, default 10 +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_SendMessageToClients(Text,Seconds) + self:T(self.lid.."_SendMessageToClients") + local seconds = Seconds or 10 + self.ClientSet:ForEachClient( + function (Client) + if Client ~= nil and Client:IsActive() then + local m = MESSAGE:New(Text,seconds,"Tasking"):ToClient(Client) + end + end + ) + return self +end + +--- [User] Allow precision laser-guided bombing on statics and "high-value" ground units (MBT etc) +-- @param #PLAYERTASKCONTROLLER self +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FlightGroup (e.g. drone) to be used for lasing (one unit in one group only). +-- Can optionally be handed as Ops.ArmyGroup#ARMYGROUP - **Note** might not find an LOS spot or get lost on the way. Cannot island-hop. +-- @param #number LaserCode The lasercode to be used. Defaults to 1688. +-- @param Core.Point#COORDINATE HoldingPoint (Optional) Point where the drone should initially circle. If not set, defaults to BullsEye of the coalition. +-- @param #number Alt (Optional) Altitude in feet. Only applies if using a FLIGHTGROUP object! Defaults to 10000. +-- @param #number Speed (Optional) Speed in knots. Only applies if using a FLIGHTGROUP object! Defaults to 120. +-- @param #number MaxTravelDist (Optional) Max distance to travel to traget. Only applies if using a FLIGHTGROUP object! Defaults to 100 NM. +-- @return #PLAYERTASKCONTROLLER self +-- @usage +-- -- Set up precision bombing, FlightGroup as lasing unit +-- local FlightGroup = FLIGHTGROUP:New("LasingUnit") +-- FlightGroup:Activate() +-- taskmanager:EnablePrecisionBombing(FlightGroup,1688) +-- +-- -- Alternatively, set up precision bombing, ArmyGroup as lasing unit +-- local ArmyGroup = ARMYGROUP:New("LasingUnit") +-- ArmyGroup:SetDefaultROE(ENUMS.ROE.WeaponHold) +-- ArmyGroup:SetDefaultInvisible(true) +-- ArmyGroup:Activate() +-- taskmanager:EnablePrecisionBombing(ArmyGroup,1688) +-- +function PLAYERTASKCONTROLLER:EnablePrecisionBombing(FlightGroup,LaserCode,HoldingPoint,Alt,Speed,MaxTravelDist) + self:T(self.lid.."EnablePrecisionBombing") + + if not self.LasingDroneSet then + self.LasingDroneSet = SET_OPSGROUP:New() + end + + local LasingDrone -- Ops.FlightGroup#FLIGHTGROUP FlightGroup + + if FlightGroup then + if FlightGroup.ClassName and (FlightGroup.ClassName == "FLIGHTGROUP" or FlightGroup.ClassName == "ARMYGROUP")then + -- ok we have a FG + LasingDrone = FlightGroup -- Ops.FlightGroup#FLIGHTGROUP FlightGroup + + self.precisionbombing = true + + LasingDrone.playertask = {} + LasingDrone.playertask.id = 0 + LasingDrone.playertask.busy = false + LasingDrone.playertask.lasercode = LaserCode or 1688 + LasingDrone:SetLaser(LasingDrone.playertask.lasercode) + LasingDrone.playertask.template = LasingDrone:_GetTemplate(true) + LasingDrone.playertask.alt = Alt or 10000 + LasingDrone.playertask.speed = Speed or 120 + LasingDrone.playertask.maxtravel = UTILS.NMToMeters(MaxTravelDist or 50) + + -- let it orbit the BullsEye if FG + if LasingDrone:IsFlightgroup() then + --settings.IsFlightgroup = true + local BullsCoordinate = COORDINATE:NewFromVec3( coalition.getMainRefPoint( self.Coalition )) + if HoldingPoint then BullsCoordinate = HoldingPoint end + local Orbit = AUFTRAG:NewORBIT_CIRCLE(BullsCoordinate,Alt,Speed) + Orbit:SetMissionAltitude(Alt) + LasingDrone:AddMission(Orbit) + elseif LasingDrone:IsArmygroup() then + --settings.IsArmygroup = true + local BullsCoordinate = COORDINATE:NewFromVec3( coalition.getMainRefPoint( self.Coalition )) + if HoldingPoint then BullsCoordinate = HoldingPoint end + local Orbit = AUFTRAG:NewONGUARD(BullsCoordinate) + LasingDrone:AddMission(Orbit) + end + + self.LasingDroneSet:AddObject(FlightGroup) + + elseif FlightGroup.ClassName and (FlightGroup.ClassName == "SET_OPSGROUP") then --SET_OPSGROUP + FlightGroup:ForEachGroup( + function(group) + self:EnablePrecisionBombing(group,LaserCode,HoldingPoint,Alt,Speed,MaxTravelDist) + end + ) + else + self:E(self.lid.."No OPSGROUP/SET_OPSGROUP object passed or object is not alive!") + end + else + self.autolase = nil + self.precisionbombing = false + end + return self +end + +--- [User] Convenience function - add done or ground allowing precision laser-guided bombing on statics and "high-value" ground units (MBT etc) +-- @param #PLAYERTASKCONTROLLER self +-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FlightGroup (e.g. drone) to be used for lasing (one unit in one group only). +-- Can optionally be handed as Ops.ArmyGroup#ARMYGROUP - **Note** might not find an LOS spot or get lost on the way. Cannot island-hop. +-- @param #number LaserCode The lasercode to be used. Defaults to 1688. +-- @param Core.Point#COORDINATE HoldingPoint (Optional) Point where the drone should initially circle. If not set, defaults to BullsEye of the coalition. +-- @param #number Alt (Optional) Altitude in feet. Only applies if using a FLIGHTGROUP object! Defaults to 10000. +-- @param #number Speed (Optional) Speed in knots. Only applies if using a FLIGHTGROUP object! Defaults to 120. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:AddPrecisionBombingOpsGroup(FlightGroup,LaserCode,HoldingPoint, Alt, Speed) + self:EnablePrecisionBombing(FlightGroup,LaserCode,HoldingPoint,Alt,Speed) + return self +end + + +--- [User] Allow precision laser-guided bombing on statics and "high-value" ground units (MBT etc) with player units lasing. +-- @param #PLAYERTASKCONTROLLER self +-- @param Ops.PlayerRecce#PLAYERRECCE Recce (Optional) The PLAYERRECCE object governing the lasing players. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:EnableBuddyLasing(Recce) + self:T(self.lid.."EnableBuddyLasing") + self.buddylasing = true + self.PlayerRecce = Recce + return self +end + +--- [User] Allow precision laser-guided bombing on statics and "high-value" ground units (MBT etc) with player units lasing. +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:DisableBuddyLasing() + self:T(self.lid.."DisableBuddyLasing") + self.buddylasing = false + return self +end + +--- [User] Allow addition of targets with user F10 map markers. +-- @param #PLAYERTASKCONTROLLER self +-- @param #string Tag (Optional) The tagname to use to identify commands, defaults to "TASK" +-- @return #PLAYERTASKCONTROLLER self +-- @usage +-- Enable the function like so: +-- mycontroller:EnableMarkerOps("TASK") +-- Then as a player in a client slot, you can add a map marker on the F10 map. Next edit the text +-- in the marker to make it identifiable, e.g +-- +-- TASK Name=Tanks Sochi, Text=Destroy tank group located near Sochi! +-- +-- Where **TASK** is the tag that tells the controller this mark is a target location (must). +-- **Name=** ended by a comma **,** tells the controller the supposed menu entry name (optional). No extra spaces! End with a comma! +-- **Text=** tells the controller the supposed free text task description (optional, only taken if **Name=** is present first). No extra spaces! +function PLAYERTASKCONTROLLER:EnableMarkerOps(Tag) + self:T(self.lid.."EnableMarkerOps") + + local tag = Tag or "TASK" + local MarkerOps = MARKEROPS_BASE:New(tag,{"Name","Text"},true) + + local function Handler(Keywords,Coord,Text) + if self.verbose then + local m = MESSAGE:New(string.format("Target added from marker at: %s", Coord:ToStringA2G(nil, nil, self.ShowMagnetic)),15,"INFO"):ToAll() + local m = MESSAGE:New(string.format("Text: %s", Text),15,"INFO"):ToAll() + end + local menuname = string.match(Text,"Name=(.+),") + local freetext = string.match(Text,"Text=(.+)") + if menuname then + Coord.menuname = menuname + if freetext then + Coord.freetext = freetext + end + end + self:AddTarget(Coord) + end + + -- Event functions + function MarkerOps:OnAfterMarkAdded(From,Event,To,Text,Keywords,Coord) + Handler(Keywords,Coord,Text) + end + + function MarkerOps:OnAfterMarkChanged(From,Event,To,Text,Keywords,Coord) + Handler(Keywords,Coord,Text) + end + + self.MarkerOps = MarkerOps + + return self +end + +--- [Internal] Get player name +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Client#CLIENT Client +-- @return #string playername +-- @return #string ttsplayername +function PLAYERTASKCONTROLLER:_GetPlayerName(Client) + self:T(self.lid.."_GetPlayerName") + local playername = Client:GetPlayerName() + local ttsplayername = nil + if not self.customcallsigns[playername] then + local playergroup = Client:GetGroup() + if playergroup ~= nil then + ttsplayername = playergroup:GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) + local newplayername = self:_GetTextForSpeech(ttsplayername) + self.customcallsigns[playername] = newplayername + ttsplayername = newplayername + end + else + ttsplayername = self.customcallsigns[playername] + end + return playername, ttsplayername +end + +--- [User] Disable precision laser-guided bombing on statics and "high-value" ground units (MBT etc) +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:DisablePrecisionBombing(FlightGroup,LaserCode) + self:T(self.lid.."DisablePrecisionBombing") + self.autolase = nil + self.precisionbombing = false + return self +end + +--- [User] Enable extra menu to show task detail information before joining +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:EnableTaskInfoMenu() + self:T(self.lid.."EnableTaskInfoMenu") + self.taskinfomenu = true + return self +end + +--- [User] Disable extra menu to show task detail information before joining +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:DisableTaskInfoMenu() + self:T(self.lid.."DisableTaskInfoMenu") + self.taskinfomenu = false + return self +end + +--- [User] Set menu build fine-tuning options +-- @param #PLAYERTASKCONTROLLER self +-- @param #boolean InfoMenu If `true` this option will allow to show the Task Info-Menu also when a player has an active task. +-- Since the menu isn't refreshed if a player holds an active task, the info in there might be stale. +-- @param #number ItemLimit Number of items per task type to show, default 5. +-- @param #number HoldTime Minimum number of seconds between menu refreshes (called every 30 secs) if a player has **no active task**. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetMenuOptions(InfoMenu,ItemLimit,HoldTime) + self:T(self.lid.."SetMenuOptions") + self.activehasinfomenu = InfoMenu or false + if self.activehasinfomenu then + self:EnableTaskInfoMenu() + end + self.menuitemlimit = ItemLimit+1 or 6 + self.holdmenutime = HoldTime or 30 + return self +end + +--- [User] Forbid F10 markers to be deleted by pilots. Note: Marker will auto-delete when the undelying task is done. +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetMarkerReadOnly() + self:T(self.lid.."SetMarkerReadOnly") + self.MarkerReadOnly = true + return self +end + +--- [User] Allow F10 markers to be deleted by pilots. Note: Marker will auto-delete when the undelying task is done. +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetMarkerDeleteable() + self:T(self.lid.."SetMarkerDeleteable") + self.MarkerReadOnly = false + return self +end + +--- [Internal] Event handling +-- @param #PLAYERTASKCONTROLLER self +-- @param Core.Event#EVENTDATA EventData +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_EventHandler(EventData) + self:T(self.lid.."_EventHandler: "..EventData.id) + --self:T(self.lid.."_EventHandler: "..EventData.IniPlayerName) + if EventData.id == EVENTS.UnitLost or EventData.id == EVENTS.PlayerLeaveUnit or EventData.id == EVENTS.Ejection or EventData.id == EVENTS.Crash or EventData.id == EVENTS.PilotDead then + if EventData.IniPlayerName then + self:T(self.lid.."Event for player: "..EventData.IniPlayerName) + --if self.PlayerMenu[EventData.IniPlayerName] then + --self.PlayerMenu[EventData.IniPlayerName]:Remove() + --self.PlayerMenu[EventData.IniPlayerName] = nil + --end + local text = "" + if self.TasksPerPlayer:HasUniqueID(EventData.IniPlayerName) then + local task = self.TasksPerPlayer:PullByID(EventData.IniPlayerName) -- Ops.PlayerTask#PLAYERTASK + local Client = _DATABASE:FindClient( EventData.IniPlayerName ) + if Client then + task:RemoveClient(Client) + --text = "Task aborted!" + text = self.gettext:GetEntry("TASKABORT",self.locale) + self.ActiveTaskMenuTemplate:ResetMenu(Client) + self.JoinTaskMenuTemplate:ResetMenu(Client) + else + task:RemoveClient(nil,EventData.IniPlayerName) + --text = "Task aborted!" + text = self.gettext:GetEntry("TASKABORT",self.locale) + end + else + --text = "No active task!" + text = self.gettext:GetEntry("NOACTIVETASK",self.locale) + end + self:T(self.lid..text) + end + elseif EventData.id == EVENTS.PlayerEnterAircraft and EventData.IniCoalition == self.Coalition then + if EventData.IniPlayerName and EventData.IniGroup then + --if self.IsClientSet and self.ClientSet:IsNotInSet(CLIENT:FindByName(EventData.IniUnitName)) then + if self.IsClientSet and (not self.ClientSet:IsIncludeObject(CLIENT:FindByName(EventData.IniUnitName))) then + self:T(self.lid.."Client not in SET: "..EventData.IniPlayerName) + return self + end + self:T(self.lid.."Event for player: "..EventData.IniPlayerName) + + if self.UseSRS then + local frequency = self.Frequency + local freqtext = "" + if type(frequency) == "table" then + freqtext = self.gettext:GetEntry("FREQUENCIES",self.locale) + freqtext = freqtext..table.concat(frequency,", ") + else + local freqt = self.gettext:GetEntry("FREQUENCY",self.locale) + freqtext = string.format(freqt,frequency) + end + local modulation = self.Modulation + if type(modulation) == "table" then modulation = modulation[1] end + modulation = UTILS.GetModulationName(modulation) + local switchtext = self.gettext:GetEntry("BROADCAST",self.locale) + + local playername = EventData.IniPlayerName + if EventData.IniGroup then + -- personalized flight name in player naming + if self.customcallsigns[playername] then + self.customcallsigns[playername] = nil + end + playername = EventData.IniGroup:GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) + end + playername = self:_GetTextForSpeech(playername) + --local text = string.format("%s, %s, switch to %s for task assignment!",EventData.IniPlayerName,self.MenuName or self.Name,freqtext) + local text = string.format(switchtext,playername,self.MenuName or self.Name,freqtext) + self.SRSQueue:NewTransmission(text,nil,self.SRS,timer.getAbsTime()+60,2,{EventData.IniGroup},text,30,self.BCFrequency,self.BCModulation) + end + if EventData.IniPlayerName then + --self.PlayerMenu[EventData.IniPlayerName] = nil + local player = _DATABASE:FindClient( EventData.IniUnitName ) + self:_SwitchMenuForClient(player,"Info") + end + end + end + return self +end + +--- [User] Set locale for localization. Defaults to "en" +-- @param #PLAYERTASKCONTROLLER self +-- @param #string Locale The locale to use +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetLocale(Locale) + self:T(self.lid.."SetLocale") + self.locale = Locale or "en" + return self +end + +--- [User] Switch screen output. +-- @param #PLAYERTASKCONTROLLER self +-- @param #boolean OnOff. Switch screen output off (true) or on (false) +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SuppressScreenOutput(OnOff) + self:T(self.lid.."SuppressScreenOutput") + self.NoScreenOutput = OnOff or false + return self +end + +--- [User] Set target radius. Determines the zone radius to distinguish CAS from BAI tasks and to find enemies if the TARGET object is a COORDINATE. +-- @param #PLAYERTASKCONTROLLER self +-- @param #number Radius Radius to use in meters. Defaults to 500 meters. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetTargetRadius(Radius) + self:T(self.lid.."SetTargetRadius") + self.TargetRadius = Radius or 500 + return self +end + +--- [User] Set the cluster radius if you want to use target clusters rather than single group detection. +-- Note that for a controller type A2A target clustering is on by default. Also remember that the diameter of the resulting zone is double the radius. +-- @param #PLAYERTASKCONTROLLER self +-- @param #number Radius Target cluster radius in kilometers. Default is 0.5km. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetClusterRadius(Radius) + self:T(self.lid.."SetClusterRadius") + self.ClusterRadius = Radius or 0.5 + self.usecluster = true + return self +end + +--- [User] Manually cancel a specific task +-- @param #PLAYERTASKCONTROLLER self +-- @param Ops.PlayerTask#PLAYERTASK Task The task to be cancelled. +-- @param #boolean Silent If true suppress message output. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:CancelTask(Task,Silent) + self:T(self.lid.."CancelTask") + Task:__Cancel(-1,Silent) + return self +end + +--- [User] Switch usage of target names for menu entries on or off +-- @param #PLAYERTASKCONTROLLER self +-- @param #boolean OnOff If true, set to on (default), if nil or false, set to off +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SwitchUseGroupNames(OnOff) + self:T(self.lid.."SwitchUseGroupNames") + if OnOff then + self.UseGroupNames = true + else + self.UseGroupNames = false + end + return self +end + +--- [User] Switch showing additional magnetic angles +-- @param #PLAYERTASKCONTROLLER self +-- @param #boolean OnOff If true, set to on (default), if nil or false, set to off +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SwitchMagenticAngles(OnOff) + self:T(self.lid.."SwitchMagenticAngles") + if OnOff then + self.ShowMagnetic = true + else + self.ShowMagnetic = false + end + return self +end + +--- [Internal] Get task types for the menu +-- @param #PLAYERTASKCONTROLLER self +-- @return #table TaskTypes +function PLAYERTASKCONTROLLER:_GetAvailableTaskTypes() + self:T(self.lid.."_GetAvailableTaskTypes") + local tasktypes = {} + self.TaskQueue:ForEach( + function (Task) + local task = Task -- Ops.PlayerTask#PLAYERTASK + local type = Task.Type + tasktypes[type] = {} + end + ) + return tasktypes +end + +--- [Internal] Get task per type for the menu +-- @param #PLAYERTASKCONTROLLER self +-- @return #table TasksPerTypes +function PLAYERTASKCONTROLLER:_GetTasksPerType() + self:T(self.lid.."_GetTasksPerType") + local tasktypes = self:_GetAvailableTaskTypes() + + --self:I({tasktypes}) + + -- Sort tasks per threat level first + local datatable = self.TaskQueue:GetDataTable() + local threattable = {} + for _,_task in pairs(datatable) do + local task = _task -- Ops.PlayerTask#PLAYERTASK + local threat = task.Target:GetThreatLevelMax() + if not task:IsDone() then + threattable[#threattable+1]={task=task,threat=threat} + end + end + + table.sort(threattable, function (k1, k2) return k1.threat > k2.threat end ) + + for _id,_data in pairs(threattable) do + local threat=_data.threat + local task = _data.task -- Ops.PlayerTask#PLAYERTASK + local type = task.Type + local name = task.Target:GetName() + --self:I(name) + if not task:IsDone() then + --self:I(name) + table.insert(tasktypes[type],task) + end + end + + --[[ + for _type,_data in pairs(tasktypes) do + self:I("Task Type: ".._type) + for _id,_task in pairs(_data) do + self:I("Task Name: ".._task.Target:GetName()) + end + end + --]] + + return tasktypes +end + +--- [Internal] Check target queue +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_CheckTargetQueue() + self:T(self.lid.."_CheckTargetQueue") + if self.TargetQueue:Count() > 0 then + local object = self.TargetQueue:Pull() -- Wrapper.Positionable#POSITIONABLE + local target = TARGET:New(object) + if object.menuname then + target.menuname = object.menuname + if object.freetext then + target.freetext = object.freetext + end + end + + if object:IsInstanceOf("UNIT") or object:IsInstanceOf("GROUP") then + + if self.UseTypeNames and object:IsGround() then + -- * 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. + local threat = object:GetThreatLevel() + local typekey = "INFANTRY" + if threat == 0 or threat == 2 then + typekey = "TECHNICAL" + elseif threat == 3 then + typekey = "ARTILLERY" + elseif threat == 4 or threat == 5 then + typekey = "TANKS" + elseif threat == 6 or threat == 7 then + typekey = "AIRDEFENSE" + elseif threat >= 8 then + typekey = "SAM" + end + local typename = self.gettext:GetEntry(typekey,self.locale) + local gname = self.gettext:GetEntry("GROUP",self.locale) + target.TypeName = string.format("%s %s",typename,gname) + --self:T(self.lid.."Target TypeName = "..target.TypeName) + end + + if self.UseTypeNames and object:IsShip() then + local threat = object:GetThreatLevel() + local typekey = "UNARMEDSHIP" + if threat == 1 then + typekey = "LIGHTARMEDSHIP" + elseif threat == 2 then + typekey = "CORVETTE" + elseif threat == 3 or threat == 4 then + typekey = "FRIGATE" + elseif threat == 5 or threat == 6 then + typekey = "CRUISER" + elseif threat == 7 or threat == 8 then + typekey = "DESTROYER" + elseif threat >= 9 then + typekey = "CARRIER" + end + local typename = self.gettext:GetEntry(typekey,self.locale) + target.TypeName = typename + --self:T(self.lid.."Target TypeName = "..target.TypeName) + end + + end + + self:_AddTask(target) + end + return self +end + +--- [Internal] Check task queue +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_CheckTaskQueue() + self:T(self.lid.."_CheckTaskQueue") + if self.TaskQueue:Count() > 0 then + -- remove done tasks + local tasks = self.TaskQueue:GetIDStack() + for _id,_entry in pairs(tasks) do + local data = _entry.data -- Ops.PlayerTask#PLAYERTASK + self:T("Looking at Task: "..data.PlayerTaskNr.." Type: "..data.Type.." State: "..data:GetState()) + if data:GetState() == "Done" or data:GetState() == "Stopped" then + local task = self.TaskQueue:ReadByID(_id) -- Ops.PlayerTask#PLAYERTASK + -- DONE: Remove clients from the task + local clientsattask = task.Clients:GetIDStackSorted() + for _,_id in pairs(clientsattask) do + self:T("*****Removing player " .. _id) + self.TasksPerPlayer:PullByID(_id) + end + local clients=task:GetClientObjects() + for _,client in pairs(clients) do + self:_RemoveMenuEntriesForTask(task,client) + --self:_SwitchMenuForClient(client,"Info") + end + for _,client in pairs(clients) do + -- self:_RemoveMenuEntriesForTask(Task,client) + self:_SwitchMenuForClient(client,"Info",5) + end + -- Follow-up tasks? + local nexttasks = {} + if task.FinalState == "Success" then + nexttasks = task.NextTaskSuccess + elseif task.FinalState == "Failed" then + nexttasks = task.NextTaskFailure + end + local clientlist, count = task:GetClientObjects() + if count > 0 then + for _,_client in pairs(clientlist) do + local client = _client --Wrapper.Client#CLIENT + local group = client:GetGroup() + for _,task in pairs(nexttasks) do + self:_JoinTask(task,true,group,client) + end + end + end + local TNow = timer.getAbsTime() + if TNow - task.timestamp > 5 then + self:_RemoveMenuEntriesForTask(task) + local task = self.TaskQueue:PullByID(_id) -- Ops.PlayerTask#PLAYERTASK + task = nil + end + end + end + end + return self +end + +--- [Internal] Check precision task queue +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_CheckPrecisionTasks() + self:T(self.lid.."_CheckPrecisionTasks") + self:T({count=self.PrecisionTasks:Count(),enabled=self.precisionbombing}) + if self.PrecisionTasks:Count() > 0 and self.precisionbombing then + + -- alive checks + self.LasingDroneSet:ForEachGroup( + function(LasingDrone) + if not LasingDrone or LasingDrone:IsDead() then + -- we need a new drone + self:E(self.lid.."Lasing drone is dead ... creating a new one!") + if LasingDrone then + LasingDrone:_Respawn(1,nil,true) + else + --[[ + -- DONE: Handle ArmyGroup + if LasingDrone:IsFlightgroup() then + local FG = FLIGHTGROUP:New(LasingDroneTemplate) + FG:Activate() + self:EnablePrecisionBombing(FG,self.LaserCode or 1688) + else + local FG = ARMYGROUP:New(LasingDroneTemplate) + FG:Activate() + self:EnablePrecisionBombing(FG,self.LaserCode or 1688) + end -- if LasingDroneIsFlightgroup + --]] + end -- if LasingDrone + end -- if not LasingDrone + end -- function + ) + + local function SelectDrone(coord) + local selected = nil + local mindist = math.huge + local dist = math.huge + self.LasingDroneSet:ForEachGroup( + function(grp) + if grp.playertask and (not grp.playertask.busy) then + local gc = grp:GetCoordinate() + if coord and gc then + dist = coord:Get2DDistance(gc) + end + if dist < mindist then + selected = grp + mindist = dist + end + end + end + ) + return selected + end + + local task = self.PrecisionTasks:Pull() -- Ops.PlayerTask#PLAYERTASK + local taskpt = task.Target:GetCoordinate() + + local SelectedDrone = SelectDrone(taskpt) -- Ops.OpsGroup#OPSGROUP + + -- do we have a lasing unit assignable? + if SelectedDrone and SelectedDrone:IsAlive() then + if SelectedDrone.playertask and (not SelectedDrone.playertask.busy) then + -- not busy, get a task + self:T(self.lid.."Sending lasing unit to target") + local isassigned = self:_FindLasingDroneForTaskID(task.PlayerTaskNr) + -- distance check + local startpoint = SelectedDrone:GetCoordinate() + local endpoint = task.Target:GetCoordinate() + local dist = math.huge + if startpoint and endpoint then + dist = startpoint:Get2DDistance(endpoint) + end + if dist <= SelectedDrone.playertask.maxtravel and (not isassigned) then + SelectedDrone.playertask.id = task.PlayerTaskNr + SelectedDrone.playertask.busy = true + SelectedDrone.playertask.inreach = false + SelectedDrone.playertask.reachmessage = false + -- move the drone to target + if SelectedDrone:IsFlightgroup() then + SelectedDrone:CancelAllMissions() + local auftrag = AUFTRAG:NewORBIT_CIRCLE(task.Target:GetCoordinate(),SelectedDrone.playertask.alt,SelectedDrone.playertask.speed) + SelectedDrone:AddMission(auftrag) + elseif SelectedDrone:IsArmygroup() then + local tgtcoord = task.Target:GetCoordinate() + local tgtzone = ZONE_RADIUS:New("ArmyGroup-"..math.random(1,10000),tgtcoord:GetVec2(),3000) + local finalpos=nil -- Core.Point#COORDINATE + for i=1,50 do + finalpos = tgtzone:GetRandomCoordinate(2500,0,{land.SurfaceType.LAND,land.SurfaceType.ROAD,land.SurfaceType.SHALLOW_WATER}) + if finalpos then + if finalpos:IsLOS(tgtcoord,0) then + break + end + end + end + if finalpos then + SelectedDrone:CancelAllMissions() + -- yeah we got one + local auftrag = AUFTRAG:NewARMOREDGUARD(finalpos,"Off road") + SelectedDrone:AddMission(auftrag) + else + -- could not find LOS position! + self:E("***Could not find LOS position to post ArmyGroup for lasing!") + SelectedDrone.playertask.id = 0 + SelectedDrone.playertask.busy = false + SelectedDrone.playertask.inreach = false + SelectedDrone.playertask.reachmessage = false + end + end + else + self:T(self.lid.."Lasing unit too far from target") + end + + end + end + + self.PrecisionTasks:Push(task,task.PlayerTaskNr) + + + local function DronesWithTask(SelectedDrone) + -- handle drones with a task + if SelectedDrone.playertask and SelectedDrone.playertask.busy then + -- drone is busy, set up laser when over target + local task = self.PrecisionTasks:ReadByID(SelectedDrone.playertask.id) -- Ops.PlayerTask#PLAYERTASK + self:T("Looking at Task: "..task.PlayerTaskNr.." Type: "..task.Type.." State: "..task:GetState()) + if (not task) or task:GetState() == "Done" or task:GetState() == "Stopped" then + -- we're done here + local task = self.PrecisionTasks:PullByID(SelectedDrone.playertask.id) -- Ops.PlayerTask#PLAYERTASK + self:_CheckTaskQueue() + task = nil + if SelectedDrone:IsLasing() then + SelectedDrone:__LaserOff(-1) + end + SelectedDrone.playertask.busy = false + SelectedDrone.playertask.inreach = false + SelectedDrone.playertask.id = 0 + SelectedDrone.playertask.reachmessage = false + self:T(self.lid.."Laser Off") + else + -- not done yet + self:T(self.lid.."Not done yet") + local dcoord = SelectedDrone:GetCoordinate() + local tcoord = task.Target:GetCoordinate() + tcoord.y = tcoord.y + 2 + local dist = dcoord:Get2DDistance(tcoord) + self:T(self.lid.."Dist "..dist) + -- close enough? + if dist < 3000 and not SelectedDrone:IsLasing() then + self:T(self.lid.."Laser On") + SelectedDrone:__LaserOn(-1,tcoord) + SelectedDrone.playertask.inreach = true + if not SelectedDrone.playertask.reachmessage then + --local textmark = self.gettext:GetEntry("FLARETASK",self.locale) + SelectedDrone.playertask.reachmessage = true + local clients = task:GetClients() + local text = "" + for _,playername in pairs(clients) do + local pointertext = self.gettext:GetEntry("POINTEROVERTARGET",self.locale) + local ttsplayername = playername + if self.customcallsigns[playername] then + ttsplayername = self.customcallsigns[playername] + end -- + --text = string.format("%s, %s, pointer over target for task %03d, lasing!", playername, self.MenuName or self.Name, task.PlayerTaskNr) + text = string.format(pointertext, ttsplayername, self.MenuName or self.Name, task.PlayerTaskNr) + if not self.NoScreenOutput then + local client = nil + self.ClientSet:ForEachClient( + function(Client) + if Client:GetPlayerName() == playername then client = Client end + end + ) + if client then + local m = MESSAGE:New(text,15,"Tasking"):ToClient(client) + end -- + end -- + end -- + if self.UseSRS then + self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2) + end -- + end -- + end -- + end -- end else + end -- end handle drones with a task + end -- end function + + self.LasingDroneSet:ForEachGroup(DronesWithTask) + + end -- + return self +end + +--- [Internal] Check task queue for a specific player name +-- @param #PLAYERTASKCONTROLLER self +-- @return #boolean outcome +function PLAYERTASKCONTROLLER:_CheckPlayerHasTask(PlayerName) + self:T(self.lid.."_CheckPlayerHasTask") + return self.TasksPerPlayer:HasUniqueID(PlayerName) +end + +--- [User] Add a target object to the target queue +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Positionable#POSITIONABLE Target The target GROUP, SET\_GROUP, UNIT, SET\_UNIT, STATIC, SET\_STATIC, AIRBASE, ZONE or COORDINATE. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:AddTarget(Target) + self:T(self.lid.."AddTarget") + self.TargetQueue:Push(Target) + return self +end + +--- [Internal] Check for allowed task type, if there is a (positive) whitelist +-- @param #PLAYERTASKCONTROLLER self +-- @param #string Type +-- @return #boolean Outcome +function PLAYERTASKCONTROLLER:_CheckTaskTypeAllowed(Type) + self:T(self.lid.."_CheckTaskTypeAllowed") + local Outcome = false + if self.UseWhiteList then + for _,_type in pairs(self.WhiteList) do + if Type == _type then + Outcome = true + break + end + end + else + return true + end + return Outcome +end + +--- [Internal] Check for allowed task type, if there is a (negative) blacklist +-- @param #PLAYERTASKCONTROLLER self +-- @param #string Type +-- @return #boolean Outcome +function PLAYERTASKCONTROLLER:_CheckTaskTypeDisallowed(Type) + self:T(self.lid.."_CheckTaskTypeDisallowed") + local Outcome = false + if self.UseBlackList then + for _,_type in pairs(self.BlackList) do + if Type == _type then + Outcome = true + break + end + end + else + return true + end + return Outcome +end + +--- [User] Set up a (positive) whitelist of allowed task types. Only these types will be generated. +-- @param #PLAYERTASKCONTROLLER self +-- @param #table WhiteList Table of task types that can be generated. Use to restrict available types. +-- @return #PLAYERTASKCONTROLLER self +-- @usage Currently, the following task types will be generated, if detection has been set up: +-- A2A - AUFTRAG.Type.INTERCEPT +-- A2S - AUFTRAG.Type.ANTISHIP +-- A2G - AUFTRAG.Type.CAS, AUFTRAG.Type.BAI, AUFTRAG.Type.SEAD, AUFTRAG.Type.BOMBING, AUFTRAG.Type.PRECISIONBOMBING, AUFTRAG.Type.BOMBRUNWAY +-- A2GS - A2G + A2S +-- If you don't want SEAD tasks generated, use as follows where "mycontroller" is your PLAYERTASKCONTROLLER object: +-- +-- `mycontroller:SetTaskWhiteList({AUFTRAG.Type.CAS, AUFTRAG.Type.BAI, AUFTRAG.Type.BOMBING, AUFTRAG.Type.BOMBRUNWAY})` +-- +function PLAYERTASKCONTROLLER:SetTaskWhiteList(WhiteList) + self:T(self.lid.."SetTaskWhiteList") + self.WhiteList = WhiteList + self.UseWhiteList = true + return self +end + +--- [User] Set up a (negative) blacklist of forbidden task types. These types will **not** be generated. +-- @param #PLAYERTASKCONTROLLER self +-- @param #table BlackList Table of task types that cannot be generated. Use to restrict available types. +-- @return #PLAYERTASKCONTROLLER self +-- @usage Currently, the following task types will be generated, if detection has been set up: +-- A2A - AUFTRAG.Type.INTERCEPT +-- A2S - AUFTRAG.Type.ANTISHIP +-- A2G - AUFTRAG.Type.CAS, AUFTRAG.Type.BAI, AUFTRAG.Type.SEAD, AUFTRAG.Type.BOMBING, AUFTRAG.Type.PRECISIONBOMBING, AUFTRAG.Type.BOMBRUNWAY +-- A2GS - A2G + A2S +-- If you don't want SEAD tasks generated, use as follows where "mycontroller" is your PLAYERTASKCONTROLLER object: +-- +-- `mycontroller:SetTaskBlackList({AUFTRAG.Type.SEAD})` +-- +function PLAYERTASKCONTROLLER:SetTaskBlackList(BlackList) + self:T(self.lid.."SetTaskBlackList") + self.BlackList = BlackList + self.UseBlackList = true + return self +end + +--- [User] Change the list of attributes, which are considered on GROUP or SET\_GROUP level of a target to create SEAD player tasks. +-- @param #PLAYERTASKCONTROLLER self +-- @param #table Attributes Table of attribute types considered to lead to a SEAD type player task. +-- @return #PLAYERTASKCONTROLLER self +-- @usage +-- Default attribute types are: GROUP.Attribute.GROUND_SAM, GROUP.Attribute.GROUND_AAA, and GROUP.Attribute.GROUND_EWR. +-- If you want to e.g. exclude AAA, so target groups with this attribute are assigned CAS or BAI tasks, and not SEAD, use this function as follows: +-- +-- `mycontroller:SetSEADAttributes({GROUP.Attribute.GROUND_SAM, GROUP.Attribute.GROUND_EWR})` +-- +function PLAYERTASKCONTROLLER:SetSEADAttributes(Attributes) + self:T(self.lid.."SetSEADAttributes") + if type(Attributes) ~= "table" then + Attributes = {Attributes} + end + self.SeadAttributes = Attributes + return self +end + +--- [Internal] Function the check against SeadAttributes +-- @param #PLAYERTASKCONTROLLER self +-- @param #string Attribute +-- @return #boolean IsSead +function PLAYERTASKCONTROLLER:_IsAttributeSead(Attribute) + self:T(self.lid.."_IsAttributeSead?") + local IsSead = false + for _,_attribute in pairs(self.SeadAttributes) do + if Attribute == _attribute then + IsSead = true + break + end + end + return IsSead +end + +--- [Internal] Add a task to the task queue +-- @param #PLAYERTASKCONTROLLER self +-- @param Ops.Target#TARGET Target +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_AddTask(Target) + self:T(self.lid.."_AddTask") + local cat = Target:GetCategory() + local threat = Target:GetThreatLevelMax() + local type = AUFTRAG.Type.CAS + + --local ttstype = "close air support" + local ttstype = self.gettext:GetEntry("CASTTS",self.locale) + + if cat == TARGET.Category.GROUND then + type = AUFTRAG.Type.CAS + -- TODO: debug BAI, CAS, SEAD + local targetobject = Target:GetObject() -- Wrapper.Positionable#POSITIONABLE + if targetobject:IsInstanceOf("UNIT") then + self:T("SEAD Check UNIT") + if targetobject:HasSEAD() then + type = AUFTRAG.Type.SEAD + --ttstype = "suppress air defense" + ttstype = self.gettext:GetEntry("SEADTTS",self.locale) + end + elseif targetobject:IsInstanceOf("GROUP") then + self:T("SEAD Check GROUP") + local attribute = targetobject:GetAttribute() + if self:_IsAttributeSead(attribute) then + type = AUFTRAG.Type.SEAD + --ttstype = "suppress air defense" + ttstype = self.gettext:GetEntry("SEADTTS",self.locale) + end + elseif targetobject:IsInstanceOf("SET_GROUP") then + self:T("SEAD Check SET_GROUP") + targetobject:ForEachGroup( + function (group) + local attribute = group:GetAttribute() + if self:_IsAttributeSead(attribute) then + type = AUFTRAG.Type.SEAD + --ttstype = "suppress air defense" + ttstype = self.gettext:GetEntry("SEADTTS",self.locale) + end + end + ) + elseif targetobject:IsInstanceOf("SET_UNIT") then + self:T("SEAD Check SET_UNIT") + targetobject:ForEachUnit( + function (unit) + if unit:HasSEAD() then + type = AUFTRAG.Type.SEAD + --ttstype = "suppress air defenses" + ttstype = self.gettext:GetEntry("SEADTTS",self.locale) + end + end + ) + elseif targetobject:IsInstanceOf("SET_STATIC") or targetobject:IsInstanceOf("STATIC") then + self:T("(PRECISION-)BOMBING SET_STATIC or STATIC") + if self.precisionbombing then + type = AUFTRAG.Type.PRECISIONBOMBING + ttstype = self.gettext:GetEntry("PRECBOMBTTS",self.locale) + else + type = AUFTRAG.Type.BOMBING + ttstype = self.gettext:GetEntry("BOMBTTS",self.locale) + end + + end + -- if there are no friendlies nearby ~0.5km and task isn't SEAD, then it's BAI + local targetcoord = Target:GetCoordinate() + local targetvec2 = targetcoord:GetVec2() + local targetzone = ZONE_RADIUS:New(self.Name,targetvec2,self.TargetRadius) + local coalition = targetobject:GetCoalitionName() or "Blue" + coalition = string.lower(coalition) + self:T("Target coalition is "..tostring(coalition)) + local filtercoalition = "blue" + if coalition == "blue" then filtercoalition = "red" end + local friendlyset = SET_GROUP:New():FilterCategoryGround():FilterCoalitions(filtercoalition):FilterZones({targetzone}):FilterOnce() + if friendlyset:Count() == 0 and type == AUFTRAG.Type.CAS then + type = AUFTRAG.Type.BAI + --ttstype = "battle field air interdiction" + ttstype = self.gettext:GetEntry("BAITTS",self.locale) + end + -- see if we can do precision bombing + if (type == AUFTRAG.Type.BAI or type == AUFTRAG.Type.CAS) and (self.precisionbombing or self.buddylasing) then + -- threatlevel between 3 and 6 means, it's artillery, tank, modern tank or AAA + if threat > 2 and threat < 7 then + type = AUFTRAG.Type.PRECISIONBOMBING + ttstype = self.gettext:GetEntry("PRECBOMBTTS",self.locale) + end + end + elseif cat == TARGET.Category.NAVAL then + type = AUFTRAG.Type.ANTISHIP + --ttstype = "anti-ship" + ttstype = self.gettext:GetEntry("ANTISHIPTTS",self.locale) + elseif cat == TARGET.Category.AIRCRAFT then + type = AUFTRAG.Type.INTERCEPT + --ttstype = "intercept" + ttstype = self.gettext:GetEntry("INTERCEPTTS",self.locale) + elseif cat == TARGET.Category.AIRBASE then + --TODO: Define Success Criteria, AB hit? Runway blocked, how to determine? change of coalition? Void of enemies? + -- Current implementation - bombing in AFB zone (EVENTS.Shot) + type = AUFTRAG.Type.BOMBRUNWAY + -- ttstype = "bomb runway" + ttstype = self.gettext:GetEntry("BOMBRUNWAYTTS",self.locale) + elseif cat == TARGET.Category.COORDINATE or cat == TARGET.Category.ZONE then + --TODO: Define Success Criteria, void of enemies? + -- Current implementation - find SET of enemies in ZONE or 500m radius around coordinate, and assign as targets + local zone = Target:GetObject() + if cat == TARGET.Category.COORDINATE then + zone = ZONE_RADIUS:New("TargetZone-"..math.random(1,10000),Target:GetVec2(),self.TargetRadius) + end + -- find some enemies around there... + local enemies = self.CoalitionName == "Blue" and "red" or "blue" + local enemysetg = SET_GROUP:New():FilterCoalitions(enemies):FilterCategoryGround():FilterActive(true):FilterZones({zone}):FilterOnce() + local enemysets = SET_STATIC:New():FilterCoalitions(enemies):FilterZones({zone}):FilterOnce() + local countg = enemysetg:Count() + local counts = enemysets:Count() + if countg > 0 then + -- observe Tags coming from MarkerOps + if Target.menuname then + enemysetg.menuname = Target.menuname + if Target.freetext then + enemysetg.freetext = Target.freetext + end + end + self:AddTarget(enemysetg) + end + if counts > 0 then + -- observe Tags coming from MarkerOps + if Target.menuname then + enemysets.menuname = Target.menuname + if Target.freetext then + enemysets.freetext = Target.freetext + end + end + self:AddTarget(enemysets) + end + return self + end + + if self.UseWhiteList then + if not self:_CheckTaskTypeAllowed(type) then + return self + end + end + + if self.UseBlackList then + if self:_CheckTaskTypeDisallowed(type) then + return self + end + end + + local task = PLAYERTASK:New(type,Target,self.repeatonfailed,self.repeattimes,ttstype) + + -- observe Tags coming from MarkerOps + if Target.menuname then + task:SetMenuName(Target.menuname) + if Target.freetext then + task:AddFreetext(Target.freetext) + end + end + + task.coalition = self.Coalition + task.TypeName = Target.TypeName + + if type == AUFTRAG.Type.BOMBRUNWAY then + -- task to handle event shot + task:HandleEvent(EVENTS.Shot) + function task:OnEventShot(EventData) + local data = EventData -- Core.Event#EVENTDATA EventData + local wcat = Object.getCategory(data.Weapon) -- cat 2 or 3 + local coord = data.IniUnit:GetCoordinate() or data.IniGroup:GetCoordinate() + local vec2 = coord:GetVec2() or {x=0, y=0} + local coal = data.IniCoalition + local afbzone = AIRBASE:FindByName(Target:GetName()):GetZone() + local runways = AIRBASE:FindByName(Target:GetName()):GetRunways() or {} + local inrunwayzone = false + for _,_runway in pairs(runways) do + local runway = _runway -- Wrapper.Airbase#AIRBASE.Runway + if runway.zone:IsVec2InZone(vec2) then + inrunwayzone = true + end + end + local inzone = afbzone:IsVec2InZone(vec2) + if coal == task.coalition and (wcat == 2 or wcat == 3) and (inrunwayzone or inzone) then + -- bombing/rockets inside target AFB zone - well done! + task:__Success(-20) + end + end + end + + task:_SetController(self) + self.TaskQueue:Push(task) + self:__TaskAdded(10,task) + + return self +end + +--- [User] Add a PLAYERTASK object to the list of (open) tasks +-- @param #PLAYERTASKCONTROLLER self +-- @param Ops.PlayerTask#PLAYERTASK PlayerTask +-- @param #boolean Silent If true, make no "has new task" announcement +-- @param #boolean TaskFilter If true, apply the white/black-list task filters here, also +-- @return #PLAYERTASKCONTROLLER self +-- @usage +-- Example to create a PLAYERTASK of type CTLD and give Players 10 minutes to complete: +-- +-- local newtask = PLAYERTASK:New(AUFTRAG.Type.CTLD,ZONE:Find("Unloading"),false,0,"Combat Transport") +-- newtask.Time0 = timer.getAbsTime() -- inject a timestamp for T0 +-- newtask:AddFreetext("Transport crates to the drop zone and build a vehicle in the next 10 minutes!") +-- +-- -- add a condition for failure - fail after 10 minutes +-- newtask:AddConditionFailure( +-- function() +-- local Time = timer.getAbsTime() +-- if Time - newtask.Time0 > 600 then +-- return true +-- end +-- return false +-- end +-- ) +-- +-- taskmanager:AddPlayerTaskToQueue(PlayerTask) +function PLAYERTASKCONTROLLER:AddPlayerTaskToQueue(PlayerTask,Silent,TaskFilter) + self:T(self.lid.."AddPlayerTaskToQueue") + if PlayerTask and PlayerTask.ClassName and PlayerTask.ClassName == "PLAYERTASK" then + if TaskFilter then + if self.UseWhiteList and (not self:_CheckTaskTypeAllowed(PlayerTask.Type)) then + return self + end + if self.UseBlackList and self:_CheckTaskTypeDisallowed(PlayerTask.Type) then + return self + end + end + PlayerTask:_SetController(self) + PlayerTask:SetCoalition(self.Coalition) + self.TaskQueue:Push(PlayerTask) + if not Silent then + self:__TaskAdded(10,PlayerTask) + end + else + self:E(self.lid.."***** NO valid PAYERTASK object sent!") + end + return self +end + +--- [User] Override this function in order to implement custom logic if a player can join a task or not. +-- @param #PLAYERTASKCONTROLLER self +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #boolean Outcome True if player can join the task, false if not +function PLAYERTASKCONTROLLER:CanJoinTask(Task, Group, Client) + return true +end + +--- [Internal] Join a player to a task +-- @param #PLAYERTASKCONTROLLER self +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @param #boolean Force Assign task even if client already has one +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_JoinTask(Task, Force, Group, Client) + self:T({Force, Group, Client}) + self:T(self.lid.."_JoinTask") + + if not self:CanJoinTask(Task, Group, Client) then + return self + end + + if not Task:CanJoinTask(Group, Client) then + return self + end + + local force = false + if type(Force) == "boolean" then + force = Force + end + local playername, ttsplayername = self:_GetPlayerName(Client) + if self.TasksPerPlayer:HasUniqueID(playername) and not force then + -- Player already has a task + if not self.NoScreenOutput then + local text = self.gettext:GetEntry("HAVEACTIVETASK",self.locale) + local m=MESSAGE:New(text,"10","Tasking"):ToClient(Client) + end + return self + end + local taskstate = Task:GetState() + if not Task:IsDone() then + if taskstate ~= "Executing" then + Task:__Requested(-1) + Task:__Executing(-2) + end + Task:AddClient(Client) + local joined = self.gettext:GetEntry("PILOTJOINEDTASK",self.locale) + -- PILOTJOINEDTASK = "%s, %s. You have been assigned %s task %03d", + --self:I(string.format("Task %s | TaskType %s | Number %s | Type %s",self.MenuName or self.Name, Task.TTSType, tonumber(Task.PlayerTaskNr),type(Task.PlayerTaskNr))) + local text = string.format(joined,ttsplayername, self.MenuName or self.Name, Task.TTSType, Task.PlayerTaskNr) + self:T(self.lid..text) + if not self.NoScreenOutput then + self:_SendMessageToClients(text) + --local m=MESSAGE:New(text,"10","Tasking"):ToAll() + end + if self.UseSRS then + self:T(self.lid..text) + self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2) + end + self.TasksPerPlayer:Push(Task,playername) + self:__PlayerJoinedTask(1, Group, Client, Task) + -- clear menu + self:_SwitchMenuForClient(Client,"Active",1) + end + if Task.Type == AUFTRAG.Type.PRECISIONBOMBING then + if not self.PrecisionTasks:HasUniqueID(Task.PlayerTaskNr) then + self.PrecisionTasks:Push(Task,Task.PlayerTaskNr) + end + end + return self +end + +--- [Internal] Switch flashing info for a client +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_SwitchFlashing(Group, Client) + self:T(self.lid.."_SwitchFlashing") + local playername, ttsplayername = self:_GetPlayerName(Client) + if (not self.FlashPlayer[playername]) or (self.FlashPlayer[playername] == false) then + -- Switch on + self.FlashPlayer[playername] = Client + local flashtext = self.gettext:GetEntry("FLASHON",self.locale) + local text = string.format(flashtext,ttsplayername) + local m = MESSAGE:New(text,10,"Tasking"):ToClient(Client) + else + -- Switch off + self.FlashPlayer[playername] = false + local flashtext = self.gettext:GetEntry("FLASHOFF",self.locale) + local text = string.format(flashtext,ttsplayername) + local m = MESSAGE:New(text,10,"Tasking"):ToClient(Client) + end + return self +end + +function PLAYERTASKCONTROLLER:_ShowRadioInfo(Group, Client) + self:T(self.lid.."_ShowRadioInfo") + local playername, ttsplayername = self:_GetPlayerName(Client) + + if self.UseSRS then + local frequency = self.Frequency + local freqtext = "" + if type(frequency) == "table" then + freqtext = self.gettext:GetEntry("FREQUENCIES",self.locale) + freqtext = freqtext..table.concat(frequency,", ") + else + local freqt = self.gettext:GetEntry("FREQUENCY",self.locale) + freqtext = string.format(freqt,frequency) + end + + local switchtext = self.gettext:GetEntry("BROADCAST",self.locale) + + playername = ttsplayername or self:_GetTextForSpeech(playername) + --local text = string.format("%s, %s, switch to %s for task assignment!",EventData.IniPlayerName,self.MenuName or self.Name,freqtext) + local text = string.format(switchtext,playername,self.MenuName or self.Name,freqtext) + self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2,{Group},text,30,self.BCFrequency,self.BCModulation) + end + + return self +end + +--- [Internal] Flashing directional info for a client +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_FlashInfo() + self:T(self.lid.."_FlashInfo") + for _playername,_client in pairs(self.FlashPlayer) do + if _client and _client:IsAlive() then + if self.TasksPerPlayer:HasUniqueID(_playername) then + local task = self.TasksPerPlayer:ReadByID(_playername) -- Ops.PlayerTask#PLAYERTASK + local Coordinate = task.Target:GetCoordinate() + local CoordText = "" + if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A then + CoordText = Coordinate:ToStringA2G(_client, nil, self.ShowMagnetic) + else + CoordText = Coordinate:ToStringA2A(_client, nil, self.ShowMagnetic) + end + local targettxt = self.gettext:GetEntry("TARGET",self.locale) + local text = "Target: "..CoordText + local m = MESSAGE:New(text,10,"Tasking"):ToClient(_client) + end + end + end + return self +end + +--- [Internal] Find matching drone for precision bombing task, if any is assigned. +-- @param #PLAYERTASKCONTROLLER self +-- @param #number ID Task ID to look for +-- @return Ops.OpsGroup#OPSGROUP Drone +function PLAYERTASKCONTROLLER:_FindLasingDroneForTaskID(ID) + local drone = nil + self.LasingDroneSet:ForEachGroup( + function(grp) + if grp and grp:IsAlive() and grp.playertask and grp.playertask.id and grp.playertask.id == ID then + drone = grp + end + end + ) + return drone +end + +--- [Internal] Show active task info +-- @param #PLAYERTASKCONTROLLER self +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_ActiveTaskInfo(Task, Group, Client) + self:T(self.lid.."_ActiveTaskInfo") + local playername, ttsplayername = self:_GetPlayerName(Client) + local text = "" + local textTTS = "" + local task = nil + if type(Task) ~= "string" then + task = Task + end + if self.TasksPerPlayer:HasUniqueID(playername) or task then + -- NODO: Show multiple? + -- Details + local task = task or self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK + local tname = self.gettext:GetEntry("TASKNAME",self.locale) + local ttsname = self.gettext:GetEntry("TASKNAMETTS",self.locale) + local taskname = string.format(tname,task.Type,task.PlayerTaskNr) + local ttstaskname = string.format(ttsname,task.TTSType,task.PlayerTaskNr) + local Coordinate = task.Target:GetCoordinate() or COORDINATE:New(0,0,0) -- Core.Point#COORDINATE + local Elevation = Coordinate:GetLandHeight() or 0 -- meters + local CoordText = "" + local CoordTextLLDM = nil + local ShowThreatInfo = task.ShowThreatDetails + local LasingDrone = self:_FindLasingDroneForTaskID(task.PlayerTaskNr) + if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A then + CoordText = Coordinate:ToStringA2G(Client,nil,self.ShowMagnetic) + else + CoordText = Coordinate:ToStringA2A(Client,nil,self.ShowMagnetic) + end + --self:I("CoordText = "..CoordText) + -- Threat Level + local ThreatLevel = task.Target:GetThreatLevelMax() + --local ThreatLevelText = "high" + local ThreatLevelText = self.gettext:GetEntry("THREATHIGH",self.locale) + if ThreatLevel > 3 and ThreatLevel < 8 then + --ThreatLevelText = "medium" + ThreatLevelText = self.gettext:GetEntry("THREATMEDIUM",self.locale) + elseif ThreatLevel <= 3 then + --ThreatLevelText = "low" + ThreatLevelText = self.gettext:GetEntry("THREATLOW",self.locale) + end + -- Targetno and Threat + local targets = task.Target:CountTargets() or 0 + local clientlist, clientcount = task:GetClients() + local ThreatGraph = "[" .. string.rep( "■", ThreatLevel ) .. string.rep( "□", 10 - ThreatLevel ) .. "]: "..ThreatLevel + local ThreatLocaleText = self.gettext:GetEntry("THREATTEXT",self.locale) + if ShowThreatInfo == true then + text = string.format(ThreatLocaleText, taskname, ThreatGraph, targets, CoordText) + else + ThreatLocaleText = self.gettext:GetEntry("NOTHREATTEXT",self.locale) + text = string.format(ThreatLocaleText, taskname) + end + local settings = _DATABASE:GetPlayerSettings(playername) or _SETTINGS -- Core.Settings#SETTINGS + local elevationmeasure = self.gettext:GetEntry("FEET",self.locale) + if settings:IsMetric() then + elevationmeasure = self.gettext:GetEntry("METER",self.locale) + --Elevation = math.floor(UTILS.MetersToFeet(Elevation)) + else + Elevation = math.floor(UTILS.MetersToFeet(Elevation)) + end + -- ELEVATION = "\nTarget Elevation: %s %s", + local elev = self.gettext:GetEntry("ELEVATION",self.locale) + text = text .. string.format(elev,tostring(math.floor(Elevation)),elevationmeasure) + -- Prec bombing + if task.Type == AUFTRAG.Type.PRECISIONBOMBING and self.precisionbombing then + if LasingDrone and LasingDrone.playertask then + local yes = self.gettext:GetEntry("YES",self.locale) + local no = self.gettext:GetEntry("NO",self.locale) + local inreach = LasingDrone.playertask.inreach == true and yes or no + local islasing = LasingDrone:IsLasing() == true and yes or no + local prectext = self.gettext:GetEntry("POINTERTARGETREPORT",self.locale) + prectext = string.format(prectext,inreach,islasing) + text = text .. prectext.." ("..LasingDrone.playertask.lasercode..")" + end + end + -- Buddylasing + if task.Type == AUFTRAG.Type.PRECISIONBOMBING and self.buddylasing then + if self.PlayerRecce then + local yes = self.gettext:GetEntry("YES",self.locale) + local no = self.gettext:GetEntry("NO",self.locale) + -- TODO make dist dependent on PlayerRecce Object + local reachdist = 8000 + local inreach = false + -- someone close enough? + local pset = self.PlayerRecce.PlayerSet:GetAliveSet() + for _,_player in pairs(pset) do + local player = _player -- Wrapper.Client#CLIENT + local pcoord = player:GetCoordinate() + if pcoord:Get2DDistance(Coordinate) <= reachdist then + inreach = true + local callsign = player:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs) + local playername = player:GetPlayerName() + local islasing = no + if self.PlayerRecce.CanLase[player:GetTypeName()] and self.PlayerRecce.AutoLase[playername] then + -- TODO - maybe compare Spot target + islasing = yes + end + local inrtext = inreach == true and yes or no + local prectext = self.gettext:GetEntry("RECCETARGETREPORT",self.locale) + -- RECCETARGETREPORT = "\nSpäher % im Zielbereich: %s\nLasing: %s", + prectext = string.format(prectext,callsign,inrtext,islasing) + text = text .. prectext + end + end + end + -- Transport + elseif task.Type == AUFTRAG.Type.CTLD or task.Type == AUFTRAG.Type.CSAR then + -- THREATTEXT = "%s\nThreat: %s\nTargets left: %d\nCoord: %s", + -- THREATTEXTTTS = "%s, %s. Target information for %s. Threat level %s. Targets left %d. Target location %s.", + text = taskname + textTTS = taskname + local detail = task:GetFreetext() + local detailTTS = task:GetFreetextTTS() + local brieftxt = self.gettext:GetEntry("BRIEFING",self.locale) + local locatxt = self.gettext:GetEntry("TARGETLOCATION",self.locale) + text = text .. string.format("\n%s: %s\n%s %s",brieftxt,detail,locatxt,CoordText) + --text = text .. "\nBriefing: "..detail.."\nTarget location "..CoordText + --textTTS = textTTS .. "; Briefing: "..detailTTS.."\nTarget location "..CoordText + textTTS = textTTS .. string.format("; %s: %s; %s %s",brieftxt,detailTTS,locatxt,CoordText) + end + + -- Pilots + local clienttxt = self.gettext:GetEntry("PILOTS",self.locale) + if clientcount > 0 then + for _,_name in pairs(clientlist) do + if self.customcallsigns[_name] then + _name = self.customcallsigns[_name] + _name = string.gsub(_name, "(%d) ","%1") + end + clienttxt = clienttxt .. _name .. ", " + end + clienttxt=string.gsub(clienttxt,", $",".") + else + local keine = self.gettext:GetEntry("NONE",self.locale) + clienttxt = clienttxt .. keine + end + text = text .. clienttxt + textTTS = textTTS .. clienttxt + -- Task Report + if self.InfoHasCoordinate then + if self.InfoHasLLDDM then + CoordTextLLDM = Coordinate:ToStringLLDDM() + else + CoordTextLLDM = Coordinate:ToStringLLDMS() + end + -- TARGETLOCATION + local locatxt = self.gettext:GetEntry("COORDINATE",self.locale) + text = string.format("%s\n%s: %s",text,locatxt,CoordTextLLDM) + end + if task:HasFreetext() and not ( task.Type == AUFTRAG.Type.CTLD or task.Type == AUFTRAG.Type.CSAR) then + local brieftxt = self.gettext:GetEntry("BRIEFING",self.locale) + text = text .. string.format("\n%s: ",brieftxt)..task:GetFreetext() + end + + if self.UseSRS then + if string.find(CoordText," BR, ") then + CoordText = string.gsub(CoordText," BR, "," Bee, Arr; ") + end + if self.ShowMagnetic then + text=string.gsub(text,"°M|","° magnetic; ") + end + if string.find(CoordText,"MGRS") then + local Text = string.gsub(CoordText,"MGRS ","") + Text = string.gsub(Text,"%s+","") + Text = string.gsub(Text,"([%a%d])","%1;") -- "0 5 1 " + Text = string.gsub(Text,"0","zero") + Text = string.gsub(Text,"9","niner") + CoordText = "MGRS;"..Text + if self.PathToGoogleKey then + --CoordText = string.format("%s",CoordText) + --doesn't seem to work any longer + end + --self:I(self.lid.." | ".. CoordText) + end + local ttstext + local ThreatLocaleTextTTS = self.gettext:GetEntry("THREATTEXTTTS",self.locale) + -- THREATTEXT = "%s\nThreat: %s\nTargets left: %d\nCoord: %s", + -- THREATTEXTTTS = "%s, %s. Target information for %s. Threat level %s. Targets left %d. Target location %s.", + if ShowThreatInfo == true then + ttstext = string.format(ThreatLocaleTextTTS,ttsplayername,self.MenuName or self.Name,ttstaskname,ThreatLevelText, targets, CoordText) + else + ThreatLocaleTextTTS = self.gettext:GetEntry("NOTHREATTEXTTTS",self.locale) + ttstext = string.format(ThreatLocaleTextTTS,ttsplayername,self.MenuName or self.Name) + end + + -- POINTERTARGETLASINGTTS = ". Pointer over target and lasing." + + if task.Type == AUFTRAG.Type.PRECISIONBOMBING and self.precisionbombing then + if LasingDrone and LasingDrone.playertask.inreach and LasingDrone:IsLasing() then + local lasingtext = self.gettext:GetEntry("POINTERTARGETLASINGTTS",self.locale) + ttstext = ttstext .. lasingtext + end + elseif task.Type == AUFTRAG.Type.CTLD or task.Type == AUFTRAG.Type.CSAR then + ttstext = textTTS + if string.find(ttstext," BR, ") then + CoordText = string.gsub(ttstext," BR, "," Bee, Arr, ") + end + elseif task:HasFreetext() then + + -- add tts freetext + local brieftxt = self.gettext:GetEntry("BRIEFING",self.locale) + ttstext = ttstext .. string.format("; %s: ",brieftxt)..task:GetFreetextTTS() + end + --self:I("**** TTS Text ****\n"..ttstext.."\n*****") + self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,2) + end + else + text = self.gettext:GetEntry("NOACTIVETASK",self.locale) + end + if not self.NoScreenOutput then + local m=MESSAGE:New(text,self.TaskInfoDuration or 30,"Tasking"):ToClient(Client) + end + return self +end + +--- [Internal] Mark task on F10 map +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_MarkTask(Group, Client) + self:T(self.lid.."_MarkTask") + local playername, ttsplayername = self:_GetPlayerName(Client) + local text = "" + if self.TasksPerPlayer:HasUniqueID(playername) then + local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK + text = string.format("Task ID #%03d | Type: %s | Threat: %d",task.PlayerTaskNr,task.Type,task.Target:GetThreatLevelMax()) + task:MarkTargetOnF10Map(text,self.Coalition,self.MarkerReadOnly) + local textmark = self.gettext:GetEntry("MARKTASK",self.locale) + --text = string.format("%s, copy pilot %s, task %03d location marked on map!", self.MenuName or self.Name, playername, task.PlayerTaskNr) + text = string.format(textmark, ttsplayername, self.MenuName or self.Name, task.PlayerTaskNr) + self:T(self.lid..text) + if self.UseSRS then + self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2) + end + else + text = self.gettext:GetEntry("NOACTIVETASK",self.locale) + end + if not self.NoScreenOutput then + local m=MESSAGE:New(text,"10","Tasking"):ToClient(Client) + end + return self +end + +--- [Internal] Smoke task location +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_SmokeTask(Group, Client) + self:T(self.lid.."_SmokeTask") + local playername, ttsplayername = self:_GetPlayerName(Client) + local text = "" + if self.TasksPerPlayer:HasUniqueID(playername) then + local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK + if task.CanSmoke == true then + task:SmokeTarget() + local textmark = self.gettext:GetEntry("SMOKETASK",self.locale) + text = string.format(textmark, ttsplayername, self.MenuName or self.Name, task.PlayerTaskNr) + self:T(self.lid..text) + --local m=MESSAGE:New(text,"10","Tasking"):ToAll() + if self.UseSRS then + self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2) + end + self:__TaskTargetSmoked(5,task) + else + local textmark = self.gettext:GetEntry("NOSMOKETASK",self.locale) + text = string.format(textmark, ttsplayername, self.MenuName or self.Name, task.PlayerTaskNr) + self:T(self.lid..text) + --local m=MESSAGE:New(text,"10","Tasking"):ToAll() + if self.UseSRS then + self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2) + end + end + else + text = self.gettext:GetEntry("NOACTIVETASK",self.locale) + end + if not self.NoScreenOutput then + local m=MESSAGE:New(text,15,"Tasking"):ToClient(Client) + end + return self +end + +--- [Internal] Flare task location +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_FlareTask(Group, Client) + self:T(self.lid.."_FlareTask") + local playername, ttsplayername = self:_GetPlayerName(Client) + local text = "" + if self.TasksPerPlayer:HasUniqueID(playername) then + local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK + task:FlareTarget() + local textmark = self.gettext:GetEntry("FLARETASK",self.locale) + text = string.format(textmark, ttsplayername, self.MenuName or self.Name, task.PlayerTaskNr) + self:T(self.lid..text) + --local m=MESSAGE:New(text,"10","Tasking"):ToAll() + if self.UseSRS then + self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2) + end + self:__TaskTargetFlared(5,task) + else + text = self.gettext:GetEntry("NOACTIVETASK",self.locale) + end + if not self.NoScreenOutput then + local m=MESSAGE:New(text,15,"Tasking"):ToClient(Client) + end + return self +end + +--- [Internal] Illuminate task location +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_IlluminateTask(Group, Client) + self:T(self.lid.."_IlluminateTask") + local playername, ttsplayername = self:_GetPlayerName(Client) + local text = "" + if self.TasksPerPlayer:HasUniqueID(playername) then + local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK + task:FlareTarget() + local textmark = self.gettext:GetEntry("FLARETASK",self.locale) + text = string.format(textmark, ttsplayername, self.MenuName or self.Name, task.PlayerTaskNr) + self:T(self.lid..text) + --local m=MESSAGE:New(text,"10","Tasking"):ToAll() + if self.UseSRS then + self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2) + end + self:__TaskTargetIlluminated(5,task) + else + text = self.gettext:GetEntry("NOACTIVETASK",self.locale) + end + if not self.NoScreenOutput then + local m=MESSAGE:New(text,15,"Tasking"):ToClient(Client) + end + return self +end + +--- [Internal] Abort Task +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_AbortTask(Group, Client) + self:T(self.lid.."_AbortTask") + local playername, ttsplayername = self:_GetPlayerName(Client) + local text = "" + if self.TasksPerPlayer:HasUniqueID(playername) then + local task = self.TasksPerPlayer:PullByID(playername) -- Ops.PlayerTask#PLAYERTASK + task:ClientAbort(Client) + local textmark = self.gettext:GetEntry("ABORTTASK",self.locale) + -- ABORTTASK = "%s, to all stations, %s has aborted %s task %03d!", + text = string.format(textmark, self.MenuName or self.Name, ttsplayername, task.TTSType, task.PlayerTaskNr) + self:T(self.lid..text) + --local m=MESSAGE:New(text,"10","Tasking"):ToAll() + if self.UseSRS then + self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2) + end + self:__PlayerAbortedTask(1,Group, Client,task) + else + text = self.gettext:GetEntry("NOACTIVETASK",self.locale) + end + if not self.NoScreenOutput then + local m=MESSAGE:New(text,15,"Tasking"):ToClient(Client) + end + self:_SwitchMenuForClient(Client,"Info",1) + return self +end + + +-- TODO - New Menu Manager +--- [Internal] _UpdateJoinMenuTemplate +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_UpdateJoinMenuTemplate() + self:T("_UpdateJoinMenuTemplate") + if self.TaskQueue:Count() > 0 then + local taskpertype = self:_GetTasksPerType() + local JoinMenu = self.JoinMenu -- Core.ClientMenu#CLIENTMENU + --self:I(JoinMenu.UUID) + local controller = self.JoinTaskMenuTemplate -- Core.ClientMenu#CLIENTMENUMANAGER + local actcontroller = self.ActiveTaskMenuTemplate -- Core.ClientMenu#CLIENTMENUMANAGER + local actinfomenu = self.ActiveInfoMenu + --local entrynumbers = {} + --local existingentries = {} + + if self.TaskQueue:Count() == 0 and self.MenuNoTask == nil then + local menunotasks = self.gettext:GetEntry("MENUNOTASKS",self.locale) + self.MenuNoTask = controller:NewEntry(menunotasks,self.JoinMenu) + controller:AddEntry(self.MenuNoTask) + end + + if self.TaskQueue:Count() > 0 and self.MenuNoTask ~= nil then + controller:DeleteGenericEntry(self.MenuNoTask) + controller:DeleteF10Entry(self.MenuNoTask) + self.MenuNoTask = nil + end + + local maxn = self.menuitemlimit + -- Generate task type menu items + for _type,_ in pairs(taskpertype) do + local found = controller:FindEntriesByText(_type) + --self:I({found}) + if #found == 0 then + local newentry = controller:NewEntry(_type,JoinMenu) + controller:AddEntry(newentry) + if self.JoinInfoMenu then + local newentry = controller:NewEntry(_type,self.JoinInfoMenu) + controller:AddEntry(newentry) + end + if actinfomenu then + local newentry = actcontroller:NewEntry(_type,self.ActiveInfoMenu) + actcontroller:AddEntry(newentry) + end + end + end + + local typelist = self:_GetAvailableTaskTypes() + -- Slot in Tasks + for _tasktype,_data in pairs(typelist) do + self:T("**** Building for TaskType: ".._tasktype) + --local tasks = taskpertype[_tasktype] or {} + for _,_task in pairs(taskpertype[_tasktype]) do + _task = _task -- Ops.PlayerTask#PLAYERTASK + self:T("**** Building for Task: ".._task.Target:GetName()) + if _task.InMenu then + self:T("**** Task already in Menu ".._task.Target:GetName()) + else + local menutaskno = self.gettext:GetEntry("MENUTASKNO",self.locale) + --local text = string.format("%s %03d [%d%s",menutaskno,_task.PlayerTaskNr,pilotcount,newtext) + local text = string.format("%s %03d",menutaskno,_task.PlayerTaskNr) + if self.UseGroupNames then + local name = _task.Target:GetName() + if name ~= "Unknown" then + --text = string.format("%s (%03d) [%d%s",name,_task.PlayerTaskNr,pilotcount,newtext) + text = string.format("%s (%03d)",name,_task.PlayerTaskNr) + end + end + local parenttable, number = controller:FindEntriesByText(_tasktype,JoinMenu) + if number > 0 then + local Parent = parenttable[1] + local matches, count = controller:FindEntriesByParent(Parent) + self:T("***** Join Menu ".._tasktype.. " # of entries: "..count) + if count < self.menuitemlimit then + local taskentry = controller:NewEntry(text,Parent,self._JoinTask,self,_task,"false") + controller:AddEntry(taskentry) + _task.InMenu = true + if not _task.UUIDS then _task.UUIDS = {} end + table.insert(_task.UUIDS,taskentry.UUID) + end + end + if self.JoinInfoMenu then + local parenttable, number = controller:FindEntriesByText(_tasktype,self.JoinInfoMenu) + if number > 0 then + local Parent = parenttable[1] + local matches, count = controller:FindEntriesByParent(Parent) + self:T("***** Join Info Menu ".._tasktype.. " # of entries: "..count) + if count < self.menuitemlimit then + local taskentry = controller:NewEntry(text,Parent,self._ActiveTaskInfo,self,_task) + controller:AddEntry(taskentry) + _task.InMenu = true + if not _task.UUIDS then _task.UUIDS = {} end + table.insert(_task.UUIDS,taskentry.UUID) + end + end + end + if actinfomenu then + local parenttable, number = actcontroller:FindEntriesByText(_tasktype,self.ActiveInfoMenu) + if number > 0 then + local Parent = parenttable[1] + local matches, count = actcontroller:FindEntriesByParent(Parent) + self:T("***** Active Info Menu ".._tasktype.. " # of entries: "..count) + if count < self.menuitemlimit then + local taskentry = actcontroller:NewEntry(text,Parent,self._ActiveTaskInfo,self,_task) + actcontroller:AddEntry(taskentry) + _task.InMenu = true + if not _task.AUUIDS then _task.AUUIDS = {} end + table.insert(_task.AUUIDS,taskentry.UUID) + end + end + end + end + end + end + end + return self +end + +--- [Internal] _RemoveMenuEntriesForTask +-- @param #PLAYERTASKCONTROLLER self +-- @param #PLAYERTASK Task +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_RemoveMenuEntriesForTask(Task,Client) + self:T("_RemoveMenuEntriesForTask") + --self:I("Task name: "..Task.Target:GetName()) + --self:I("Client: "..Client:GetPlayerName()) + if Task then + if Task.UUIDS and self.JoinTaskMenuTemplate then + --self:I("***** JoinTaskMenuTemplate") + --UTILS.PrintTableToLog(Task.UUIDS) + local controller = self.JoinTaskMenuTemplate + for _,_uuid in pairs(Task.UUIDS) do + local Entry = controller:FindEntryByUUID(_uuid) + if Entry then + controller:DeleteF10Entry(Entry,Client) + controller:DeleteGenericEntry(Entry) + --UTILS.PrintTableToLog(controller.menutree) + end + end + end + + if Task.AUUIDS and self.ActiveTaskMenuTemplate then + --self:I("***** ActiveTaskMenuTemplate") + --UTILS.PrintTableToLog(Task.AUUIDS) + for _,_uuid in pairs(Task.AUUIDS) do + local controller = self.ActiveTaskMenuTemplate + local Entry = controller:FindEntryByUUID(_uuid) + if Entry then + controller:DeleteF10Entry(Entry,Client) + controller:DeleteGenericEntry(Entry) + --UTILS.PrintTableToLog(controller.menutree) + end + end + end + + Task.UUIDS = nil + Task.AUUIDS = nil + end + return self +end + +--- [Internal] _CreateJoinMenuTemplate +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_CreateJoinMenuTemplate() + self:T("_CreateActiveTaskMenuTemplate") + + local menujoin = self.gettext:GetEntry("MENUJOIN",self.locale) + local menunotasks = self.gettext:GetEntry("MENUNOTASKS",self.locale) + local flashtext = self.gettext:GetEntry("FLASHMENU",self.locale) + + local JoinTaskMenuTemplate = CLIENTMENUMANAGER:New(self.ClientSet,"JoinTask") + + if not self.JoinTopMenu then + local taskings = self.gettext:GetEntry("MENUTASKING",self.locale) + local longname = self.Name..taskings..self.Type + local menuname = self.MenuName or longname + self.JoinTopMenu = JoinTaskMenuTemplate:NewEntry(menuname,self.MenuParent) + end + + if self.AllowFlash then + JoinTaskMenuTemplate:NewEntry(flashtext,self.JoinTopMenu,self._SwitchFlashing,self) + end + + self.JoinMenu = JoinTaskMenuTemplate:NewEntry(menujoin,self.JoinTopMenu) + + if self.taskinfomenu then + local menutaskinfo = self.gettext:GetEntry("MENUTASKINFO",self.locale) + self.JoinInfoMenu = JoinTaskMenuTemplate:NewEntry(menutaskinfo,self.JoinTopMenu) + end + + if self.TaskQueue:Count() == 0 and self.MenuNoTask == nil then + self.MenuNoTask = JoinTaskMenuTemplate:NewEntry(menunotasks,self.JoinMenu) + end + + if self.TaskQueue:Count() > 0 and self.MenuNoTask ~= nil then + JoinTaskMenuTemplate:DeleteGenericEntry(self.MenuNoTask) + self.MenuNoTask = nil + end + + if self.InformationMenu then + local radioinfo = self.gettext:GetEntry("RADIOS",self.locale) + JoinTaskMenuTemplate:NewEntry(radioinfo,self.JoinTopMenu,self._ShowRadioInfo,self) + end + + self.JoinTaskMenuTemplate = JoinTaskMenuTemplate + + return self +end + +--- [Internal] _CreateActiveTaskMenuTemplate +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_CreateActiveTaskMenuTemplate() + self:T("_CreateActiveTaskMenuTemplate") + + local menuactive = self.gettext:GetEntry("MENUACTIVE",self.locale) + local menuinfo = self.gettext:GetEntry("MENUINFO",self.locale) + local menumark = self.gettext:GetEntry("MENUMARK",self.locale) + local menusmoke = self.gettext:GetEntry("MENUSMOKE",self.locale) + local menuflare = self.gettext:GetEntry("MENUFLARE",self.locale) + local menuillu = self.gettext:GetEntry("MENUILLU",self.locale) + local menuabort = self.gettext:GetEntry("MENUABORT",self.locale) + + local ActiveTaskMenuTemplate = CLIENTMENUMANAGER:New(self.ActiveClientSet,"ActiveTask") + + if not self.ActiveTopMenu then + local taskings = self.gettext:GetEntry("MENUTASKING",self.locale) + local longname = self.Name..taskings..self.Type + local menuname = self.MenuName or longname + self.ActiveTopMenu = ActiveTaskMenuTemplate:NewEntry(menuname,self.MenuParent) + end + + if self.AllowFlash then + local flashtext = self.gettext:GetEntry("FLASHMENU",self.locale) + ActiveTaskMenuTemplate:NewEntry(flashtext,self.ActiveTopMenu,self._SwitchFlashing,self) + end + + local active = ActiveTaskMenuTemplate:NewEntry(menuactive,self.ActiveTopMenu) + ActiveTaskMenuTemplate:NewEntry(menuinfo,active,self._ActiveTaskInfo,self,"NONE") + ActiveTaskMenuTemplate:NewEntry(menumark,active,self._MarkTask,self) + + if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A and self.noflaresmokemenu ~= true then + ActiveTaskMenuTemplate:NewEntry(menusmoke,active,self._SmokeTask,self) + ActiveTaskMenuTemplate:NewEntry(menuflare,active,self._FlareTask,self) + + if self.illumenu then + ActiveTaskMenuTemplate:NewEntry(menuillu,active,self._IlluminateTask,self) + end + + end + + ActiveTaskMenuTemplate:NewEntry(menuabort,active,self._AbortTask,self) + self.ActiveTaskMenuTemplate = ActiveTaskMenuTemplate + + if self.taskinfomenu and self.activehasinfomenu then + local menutaskinfo = self.gettext:GetEntry("MENUTASKINFO",self.locale) + self.ActiveInfoMenu = ActiveTaskMenuTemplate:NewEntry(menutaskinfo,self.ActiveTopMenu) + end + + return self +end + +--- [Internal] _SwitchMenuForClient +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Client#CLIENT Client The client +-- @param #string MenuType +-- @param #number Delay +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_SwitchMenuForClient(Client,MenuType,Delay) + self:T(self.lid.."_SwitchMenuForClient") + if Delay then + self:ScheduleOnce(Delay,self._SwitchMenuForClient,self,Client,MenuType) + return self + end + if MenuType == "Info" then + self.ClientSet:AddClientsByName(Client:GetName()) + self.ActiveClientSet:Remove(Client:GetName(),true) + self.ActiveTaskMenuTemplate:ResetMenu(Client) + self.JoinTaskMenuTemplate:ResetMenu(Client) + self.JoinTaskMenuTemplate:Propagate(Client) + elseif MenuType == "Active" then + self.ActiveClientSet:AddClientsByName(Client:GetName()) + self.ClientSet:Remove(Client:GetName(),true) + self.ActiveTaskMenuTemplate:ResetMenu(Client) + self.JoinTaskMenuTemplate:ResetMenu(Client) + self.ActiveTaskMenuTemplate:Propagate(Client) + else + self:E(self.lid .."Unknown menu type in _SwitchMenuForClient:"..tostring(MenuType)) + end + return self +end + +--- [User] Add agent group to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this. +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Recce Group of agents. Can also be an @{Ops.OpsGroup#OPSGROUP} object. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:AddAgent(Recce) + self:T(self.lid.."AddAgent") + if self.Intel then + self.Intel:AddAgent(Recce) + else + self:E(self.lid.."*****NO detection has been set up (yet)!") + end + return self +end + +--- [User] Add agent SET_GROUP to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this. +-- @param #PLAYERTASKCONTROLLER self +-- @param Core.Set#SET_GROUP RecceSet SET_GROUP of agents. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:AddAgentSet(RecceSet) + self:T(self.lid.."AddAgentSet") + if self.Intel then + local Set = RecceSet:GetAliveSet() + for _,_Recce in pairs(Set) do + self.Intel:AddAgent(_Recce) + end + else + self:E(self.lid.."*****NO detection has been set up (yet)!") + end + return self +end + +--- [User] Set up detection of STATIC objects. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this. +-- @param #PLAYERTASKCONTROLLER self +-- @param #boolean OnOff Set to `true`for on and `false`for off. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SwitchDetectStatics(OnOff) + self:T(self.lid.."SwitchDetectStatics") + if self.Intel then + self.Intel:SetDetectStatics(OnOff) + else + self:E(self.lid.."***** NO detection has been set up (yet)!") + end + return self +end + +--- [User] Add an accept zone to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this. +-- @param #PLAYERTASKCONTROLLER self +-- @param Core.Zone#ZONE AcceptZone Add a zone to the accept zone set. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:AddAcceptZone(AcceptZone) + self:T(self.lid.."AddAcceptZone") + if self.Intel then + self.Intel:AddAcceptZone(AcceptZone) + else + self:E(self.lid.."*****NO detection has been set up (yet)!") + end + return self +end + +--- [User] Add an accept SET_ZONE to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this. +-- @param #PLAYERTASKCONTROLLER self +-- @param Core.Set#SET_ZONE AcceptZoneSet Add a SET_ZONE to the accept zone set. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:AddAcceptZoneSet(AcceptZoneSet) + self:T(self.lid.."AddAcceptZoneSet") + if self.Intel then + self.Intel.acceptzoneset:AddSet(AcceptZoneSet) + else + self:E(self.lid.."*****NO detection has been set up (yet)!") + end + return self +end + +--- [User] Add a reject zone to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this. +-- @param #PLAYERTASKCONTROLLER self +-- @param Core.Zone#ZONE RejectZone Add a zone to the reject zone set. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:AddRejectZone(RejectZone) + self:T(self.lid.."AddRejectZone") + if self.Intel then + self.Intel:AddRejectZone(RejectZone) + else + self:E(self.lid.."*****NO detection has been set up (yet)!") + end + return self +end + +--- [User] Add a reject SET_ZONE to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this. +-- @param #PLAYERTASKCONTROLLER self +-- @param Core.Set#SET_ZONE RejectZoneSet Add a zone to the reject zone set. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:AddRejectZoneSet(RejectZoneSet) + self:T(self.lid.."AddRejectZoneSet") + if self.Intel then + self.Intel.rejectzoneset:AddSet(RejectZoneSet) + else + self:E(self.lid.."*****NO detection has been set up (yet)!") + end + return self +end + +--- [User] Add a conflict zone to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this. +-- @param #PLAYERTASKCONTROLLER self +-- @param Core.Zone#ZONE ConflictZone Add a zone to the conflict zone set. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:AddConflictZone(ConflictZone) + self:T(self.lid.."AddConflictZone") + if self.Intel then + self.Intel:AddConflictZone(ConflictZone) + else + self:E(self.lid.."*****NO detection has been set up (yet)!") + end + return self +end + +--- [User] Add a conflict SET_ZONE to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this. +-- @param #PLAYERTASKCONTROLLER self +-- @param Core.Set#SET_ZONE ConflictZoneSet Add a zone to the conflict zone set. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:AddConflictZoneSet(ConflictZoneSet) + self:T(self.lid.."AddConflictZoneSet") + if self.Intel then + self.Intel.conflictzoneset:AddSet(ConflictZoneSet) + else + self:E(self.lid.."*****NO detection has been set up (yet)!") + end + return self +end + +--- [User] Remove an accept zone from INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this. +-- @param #PLAYERTASKCONTROLLER self +-- @param Core.Zone#ZONE AcceptZone Remove this zone from the accept zone set. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:RemoveAcceptZone(AcceptZone) + self:T(self.lid.."RemoveAcceptZone") + if self.Intel then + self.Intel:RemoveAcceptZone(AcceptZone) + else + self:E(self.lid.."*****NO detection has been set up (yet)!") + end + return self +end + +--- [User] Remove a reject zone from INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this. +-- @param #PLAYERTASKCONTROLLER self +-- @param Core.Zone#ZONE RejectZone Remove this zone from the reject zone set. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:RemoveRejectZone(RejectZone) + self:T(self.lid.."RemoveRejectZone") + if self.Intel then + self.Intel:RemoveRejectZone(RejectZone) + else + self:E(self.lid.."*****NO detection has been set up (yet)!") + end + return self +end + +--- [User] Remove a conflict zone from INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this. +-- @param #PLAYERTASKCONTROLLER self +-- @param Core.Zone#ZONE ConflictZone Remove this zone from the conflict zone set. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:RemoveConflictZone(ConflictZone) + self:T(self.lid.."RemoveConflictZone") + if self.Intel then + self.Intel:RemoveConflictZone(ConflictZone) + else + self:E(self.lid.."*****NO detection has been set up (yet)!") + end + return self +end + +--- [User] Set the top menu name to a custom string. +-- @param #PLAYERTASKCONTROLLER self +-- @param #string Name The name to use as the top menu designation. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetMenuName(Name) + self:T(self.lid.."SetMenuName: "..Name) + self.MenuName = Name + return self +end + +--- [User] Set the top menu to be a sub-menu of another MENU entry. +-- @param #PLAYERTASKCONTROLLER self +-- @param Core.Menu#MENU_MISSION Menu +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetParentMenu(Menu) + self:T(self.lid.."SetParentMenu") + --self.MenuParent = Menu + return self +end + +--- [User] Set up INTEL detection +-- @param #PLAYERTASKCONTROLLER self +-- @param #string RecceName This name will be used to build a detection group set. All groups with this string somewhere in their group name will be added as Recce. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetupIntel(RecceName) + self:T(self.lid.."SetupIntel") + self.RecceSet = SET_GROUP:New():FilterCoalitions(self.CoalitionName):FilterPrefixes(RecceName):FilterStart() + self.Intel = INTEL:New(self.RecceSet,self.Coalition,self.Name.."-Intel") + self.Intel:SetClusterAnalysis(true,false,false) + self.Intel:SetClusterRadius(self.ClusterRadius or 0.5) + self.Intel.statusupdate = 25 + self.Intel:SetAcceptZones() + self.Intel:SetRejectZones() + --if self.verbose then + --self.Intel:SetDetectionTypes(true,true,false,true,true,true) + --end + if self.Type == PLAYERTASKCONTROLLER.Type.A2G or self.Type == PLAYERTASKCONTROLLER.Type.A2GS then + self.Intel:SetDetectStatics(true) + end + self.Intel:__Start(2) + + local function NewCluster(Cluster) + if not self.usecluster then return self end + local cluster = Cluster -- Ops.Intel#INTEL.Cluster + local type = cluster.ctype + self:T({type,self.Type}) + if (type == INTEL.Ctype.AIRCRAFT and self.Type == PLAYERTASKCONTROLLER.Type.A2A) or (type == INTEL.Ctype.NAVAL and (self.Type == PLAYERTASKCONTROLLER.Type.A2S or self.Type == PLAYERTASKCONTROLLER.Type.A2GS)) then + self:T("A2A or A2S") + local contacts = cluster.Contacts -- #table of GROUP + local targetset = SET_GROUP:New() + for _,_object in pairs(contacts) do + local contact = _object -- Ops.Intel#INTEL.Contact + self:T("Adding group: "..contact.groupname) + targetset:AddGroup(contact.group,true) + end + self:AddTarget(targetset) + elseif (type == INTEL.Ctype.GROUND or type == INTEL.Ctype.STRUCTURE) and (self.Type == PLAYERTASKCONTROLLER.Type.A2G or self.Type == PLAYERTASKCONTROLLER.Type.A2GS) then + self:T("A2G") + local contacts = cluster.Contacts -- #table of GROUP or STATIC + local targetset = nil -- Core.Set#SET_BASE + if type == INTEL.Ctype.GROUND then + targetset = SET_GROUP:New() + for _,_object in pairs(contacts) do + local contact = _object -- Ops.Intel#INTEL.Contact + self:T("Adding group: "..contact.groupname) + targetset:AddGroup(contact.group,true) + end + elseif type == INTEL.Ctype.STRUCTURE then + targetset = SET_STATIC:New() + for _,_object in pairs(contacts) do + local contact = _object -- Ops.Intel#INTEL.Contact + self:T("Adding static: "..contact.groupname) + targetset:AddStatic(contact.group) + end + end + if targetset then + self:AddTarget(targetset) + end + end + end + + local function NewContact(Contact) + if self.usecluster then return self end + local contact = Contact -- Ops.Intel#INTEL.Contact + local type = contact.ctype + self:T({type,self.Type}) + if (type == INTEL.Ctype.AIRCRAFT and self.Type == PLAYERTASKCONTROLLER.Type.A2A) or (type == INTEL.Ctype.NAVAL and (self.Type == PLAYERTASKCONTROLLER.Type.A2S or self.Type == PLAYERTASKCONTROLLER.Type.A2GS)) then + self:T("A2A or A2S") + self:T("Adding group: "..contact.groupname) + self:AddTarget(contact.group) + elseif (type == INTEL.Ctype.GROUND or type == INTEL.Ctype.STRUCTURE) and (self.Type == PLAYERTASKCONTROLLER.Type.A2G or self.Type == PLAYERTASKCONTROLLER.Type.A2GS) then + self:T("A2G") + self:T("Adding group: "..contact.groupname) + self:AddTarget(contact.group) + end + end + + function self.Intel:OnAfterNewCluster(From,Event,To,Cluster) + NewCluster(Cluster) + end + + function self.Intel:OnAfterNewContact(From,Event,To,Contact) + NewContact(Contact) + end + + return self +end + +--- [User] Set SRS TTS details - see @{Sound.SRS} for details.`SetSRS()` will try to use as many attributes configured with @{Sound.SRS#MSRS.LoadConfigFile}() as possible. +-- @param #PLAYERTASKCONTROLLER self +-- @param #number Frequency Frequency to be used. Can also be given as a table of multiple frequencies, e.g. 271 or {127,251}. There needs to be exactly the same number of modulations! +-- @param #number Modulation Modulation to be used. Can also be given as a table of multiple modulations, e.g. radio.modulation.AM or {radio.modulation.FM,radio.modulation.AM}. There needs to be exactly the same number of frequencies! +-- @param #string PathToSRS Defaults to "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" +-- @param #string Gender (Optional) Defaults to "male" +-- @param #string Culture (Optional) Defaults to "en-US" +-- @param #number Port (Optional) Defaults to 5002 +-- @param #string Voice (Optional) Use a specifc voice with the @{Sound.SRS#SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. +-- Note that this must be installed on your windows system. Can also be Google voice types, if you are using Google TTS. +-- @param #number Volume (Optional) Volume - between 0.0 (silent) and 1.0 (loudest) +-- @param #string PathToGoogleKey (Optional) Path to your google key if you want to use google TTS; if you use a config file for MSRS, hand in nil here. +-- @param #string AccessKey (Optional) Your Google API access key. This is necessary if DCS-gRPC is used as backend; if you use a config file for MSRS, hand in nil here. +-- @param Core.Point#COORDINATE Coordinate Coordinate from which the controller radio is sending +-- @param #string Backend (Optional) MSRS Backend to be used, can be MSRS.Backend.SRSEXE or MSRS.Backend.GRPC; if you use a config file for MSRS, hand in nil here. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetSRS(Frequency,Modulation,PathToSRS,Gender,Culture,Port,Voice,Volume,PathToGoogleKey,AccessKey,Coordinate,Backend) + self:T(self.lid.."SetSRS") + self.PathToSRS = PathToSRS or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" -- + self.Gender = Gender or MSRS.gender or "male" -- + self.Culture = Culture or MSRS.culture or "en-US" -- + self.Port = Port or MSRS.port or 5002 -- + self.Voice = Voice or MSRS.voice + self.PathToGoogleKey = PathToGoogleKey -- + self.AccessKey = AccessKey + self.Volume = Volume or 1.0 -- + self.UseSRS = true + self.Frequency = Frequency or {127,251} -- + self.BCFrequency = self.Frequency + self.Modulation = Modulation or {radio.modulation.FM,radio.modulation.AM} -- + self.BCModulation = self.Modulation + -- set up SRS + self.SRS=MSRS:New(self.PathToSRS,self.Frequency,self.Modulation,Backend) + self.SRS:SetCoalition(self.Coalition) + self.SRS:SetLabel(self.MenuName or self.Name) + self.SRS:SetGender(self.Gender) + self.SRS:SetCulture(self.Culture) + self.SRS:SetPort(self.Port) + self.SRS:SetVolume(self.Volume) + if self.PathToGoogleKey then + --self.SRS:SetGoogle(self.PathToGoogleKey) + self.SRS:SetProviderOptionsGoogle(self.PathToGoogleKey,self.AccessKey) + self.SRS:SetProvider(MSRS.Provider.GOOGLE) + end + -- Pre-configured Google? + if (not PathToGoogleKey) and self.SRS:GetProvider() == MSRS.Provider.GOOGLE then + self.PathToGoogleKey = MSRS.poptions.gcloud.credentials + self.Voice = Voice or MSRS.poptions.gcloud.voice + self.AccessKey = AccessKey or MSRS.poptions.gcloud.key + end + if Coordinate then + self.SRS:SetCoordinate(Coordinate) + end + self.SRS:SetVoice(self.Voice) + self.SRSQueue = MSRSQUEUE:New(self.MenuName or self.Name) + self.SRSQueue:SetTransmitOnlyWithPlayers(self.TransmitOnlyWithPlayers) + return self +end + +--- [User] Set SRS Broadcast - for the announcement to joining players which SRS frequency, modulation to use. Use in case you want to set this differently to the standard SRS. +-- @param #PLAYERTASKCONTROLLER self +-- @param #number Frequency Frequency to be used. Can also be given as a table of multiple frequencies, e.g. 271 or {127,251}. There needs to be exactly the same number of modulations! +-- @param #number Modulation Modulation to be used. Can also be given as a table of multiple modulations, e.g. radio.modulation.AM or {radio.modulation.FM,radio.modulation.AM}. There needs to be exactly the same number of frequencies! +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetSRSBroadcast(Frequency,Modulation) + self:T(self.lid.."SetSRSBroadcast") + if self.SRS then + self.BCFrequency = Frequency + self.BCModulation = Modulation + end + return self +end + +------------------------------------------------------------------------------------------------------------------- +-- FSM Functions PLAYERTASKCONTROLLER +-- TODO: FSM Functions PLAYERTASKCONTROLLER +------------------------------------------------------------------------------------------------------------------- + +--- [Internal] On after start call +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:T(self.lid.."onafterStart") + self:_CreateJoinMenuTemplate() + self:_CreateActiveTaskMenuTemplate() + -- Player Events + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler) + self:HandleEvent(EVENTS.Ejection, self._EventHandler) + self:HandleEvent(EVENTS.Crash, self._EventHandler) + self:HandleEvent(EVENTS.PilotDead, self._EventHandler) + self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler) + self:HandleEvent(EVENTS.UnitLost, self._EventHandler) + self:SetEventPriority(5) + return self +end + +--- [Internal] On after Status call +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterStatus(From, Event, To) + self:T({From, Event, To}) + + self:_CheckTargetQueue() + self:_CheckTaskQueue() + self:_CheckPrecisionTasks() + if self.AllowFlash then + self:_FlashInfo() + end + + local targetcount = self.TargetQueue:Count() + local taskcount = self.TaskQueue:Count() + local playercount = self.ClientSet:CountAlive() + local assignedtasks = self.TasksPerPlayer:Count() + local enforcedmenu = false + + if taskcount ~= self.lasttaskcount then + self.lasttaskcount = taskcount + if taskcount < self.menuitemlimit then + enforcedmenu = true + end + end + + self:_UpdateJoinMenuTemplate() + + if self.verbose then + local text = string.format("%s | New Targets: %02d | Active Tasks: %02d | Active Players: %02d | Assigned Tasks: %02d",self.MenuName, targetcount,taskcount,playercount,assignedtasks) + self:I(text) + end + + if self:GetState() ~= "Stopped" then + self:__Status(-30) + end + return self +end + +--- [Internal] On after task done +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskDone(From, Event, To, Task) + self:T({From, Event, To}) + self:T(self.lid.."TaskDone") + return self +end + +--- [Internal] On after task cancelled +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @param #boolean Silent If true, suppress message output on cancel. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskCancelled(From, Event, To, Task, Silent) + self:T({From, Event, To}) + self:T(self.lid.."TaskCancelled") + if Silent ~= true then + local canceltxt = self.gettext:GetEntry("TASKCANCELLED",self.locale) + local canceltxttts = self.gettext:GetEntry("TASKCANCELLEDTTS",self.locale) + local taskname = string.format(canceltxt, Task.PlayerTaskNr, tostring(Task.Type)) + + if self.NoScreenOutput ~= true then + self:_SendMessageToClients(taskname,15) + --local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition) + end + + if self.UseSRS then + taskname = string.format(canceltxttts, self.MenuName or self.Name, Task.PlayerTaskNr, tostring(Task.TTSType)) + self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2) + end + + end + local clients=Task:GetClientObjects() + for _,client in pairs(clients) do + self:_RemoveMenuEntriesForTask(Task,client) + --self:_SwitchMenuForClient(client,"Info") + end + for _,client in pairs(clients) do + --self:_RemoveMenuEntriesForTask(Task,client) + self:_SwitchMenuForClient(client,"Info",5) + end + return self +end + +--- [Internal] On after task success +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskSuccess(From, Event, To, Task) + self:T({From, Event, To}) + self:T(self.lid.."TaskSuccess") + local succtxt = self.gettext:GetEntry("TASKSUCCESS",self.locale) + local succtxttts = self.gettext:GetEntry("TASKSUCCESSTTS",self.locale) + local taskname = string.format(succtxt, Task.PlayerTaskNr, tostring(Task.Type)) + if not self.NoScreenOutput then + self:_SendMessageToClients(taskname,15) + --local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition) + end + if self.UseSRS then + taskname = string.format(succtxttts, self.MenuName or self.Name, Task.PlayerTaskNr, tostring(Task.TTSType)) + self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2) + end + + local clients=Task:GetClientObjects() + for _,client in pairs(clients) do + self:_RemoveMenuEntriesForTask(Task,client) + --self:_SwitchMenuForClient(client,"Info") + end + for _,client in pairs(clients) do + -- self:_RemoveMenuEntriesForTask(Task,client) + self:_SwitchMenuForClient(client,"Info",5) + end + return self +end + +--- [Internal] On after task failed +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskFailed(From, Event, To, Task) + self:T({From, Event, To}) + self:T(self.lid.."TaskFailed") + local failtxt = self.gettext:GetEntry("TASKFAILED",self.locale) + local failtxttts = self.gettext:GetEntry("TASKFAILEDTTS",self.locale) + local taskname = string.format(failtxt, Task.PlayerTaskNr, tostring(Task.Type)) + if not self.NoScreenOutput then + self:_SendMessageToClients(taskname,15) + --local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition) + end + if self.UseSRS then + taskname = string.format(failtxttts, self.MenuName or self.Name, Task.PlayerTaskNr, tostring(Task.TTSType)) + self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2) + end + local clients=Task:GetClientObjects() + for _,client in pairs(clients) do + self:_RemoveMenuEntriesForTask(Task,client) + --self:_SwitchMenuForClient(client,"Info") + end + for _,client in pairs(clients) do + -- self:_RemoveMenuEntriesForTask(Task,client) + self:_SwitchMenuForClient(client,"Info",5) + end + return self +end + +--- [Internal] On after task failed, repeat planned +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskRepeatOnFailed(From, Event, To, Task) + self:T({From, Event, To}) + self:T(self.lid.."RepeatOnFailed") + local repfailtxt = self.gettext:GetEntry("TASKFAILEDREPLAN",self.locale) + local repfailtxttts = self.gettext:GetEntry("TASKFAILEDREPLANTTS",self.locale) + local taskname = string.format(repfailtxt, Task.PlayerTaskNr, tostring(Task.Type)) + if not self.NoScreenOutput then + self:_SendMessageToClients(taskname,15) + --local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition) + end + if self.UseSRS then + taskname = string.format(repfailtxttts, self.MenuName or self.Name, Task.PlayerTaskNr, tostring(Task.TTSType)) + self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2) + end + return self +end + +--- [Internal] On after task added +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskAdded(From, Event, To, Task) + self:T({From, Event, To}) + self:T(self.lid.."TaskAdded") + local addtxt = self.gettext:GetEntry("TASKADDED",self.locale) + local taskname = string.format(addtxt, self.MenuName or self.Name, tostring(Task.Type)) + if not self.NoScreenOutput then + self:_SendMessageToClients(taskname,15) + --local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition) + end + if self.UseSRS then + taskname = string.format(addtxt, self.MenuName or self.Name, tostring(Task.TTSType)) + self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2) + end + return self +end + +--- [Internal] On after Stop call +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterStop(From, Event, To) + self:T({From, Event, To}) + self:T(self.lid.."Stopped.") + -- Player leaves + self:UnHandleEvent(EVENTS.PlayerLeaveUnit) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.PilotDead) + self:UnHandleEvent(EVENTS.PlayerEnterAircraft) + return self +end +------- +-- END PLAYERTASKCONTROLLER +----- +end diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua index 70a1e95f4..65190c11d 100644 --- a/Moose Development/Moose/Ops/RescueHelo.lua +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -20,7 +20,6 @@ -- === -- -- ### Author: **funkyfranky** --- ### Contributions: Flightcontrol (@{AI.AI_Formation} class being used here) -- -- @module Ops.RescueHelo -- @image Ops_RescueHelo.png @@ -37,7 +36,7 @@ -- @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 Functional.Formation#FORMATION formation 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. @@ -235,7 +234,7 @@ _RESCUEHELOID=0 --- Class version. -- @field #string version -RESCUEHELO.version="1.1.0" +RESCUEHELO.version="1.2.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -248,6 +247,7 @@ RESCUEHELO.version="1.1.0" -- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. -- DONE: Add rescue event when aircraft crashes. -- DONE: Make offset input parameter. +-- DONE: Make useable for AUFTRAG ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -337,6 +337,9 @@ function RESCUEHELO:New(carrierunit, helogroupname) self:AddTransition("*", "Status", "*") self:AddTransition("*", "Stop", "Stopped") + + self:I(self.lid.."Started.") + --- Triggers the FSM event "Start" that starts the rescue helo. Initializes parameters and starts event handlers. -- @function [parent=#RESCUEHELO] Start @@ -871,15 +874,25 @@ function RESCUEHELO:onafterStart(From, Event, To) -- 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) - + local UsesAliveGroup=false + local Spawn = GROUP:FindByName(self.helogroupname) + if Spawn and Spawn:IsAlive() then + self.helo=Spawn + UsesAliveGroup = true + delay = 1 + else + + -- Spawn helo. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. + Spawn=SPAWN:NewWithAlias(self.helogroupname, self.alias) + + -- Set modex for spawn. + Spawn:InitModex(self.modex) + + end + -- Spawn in air or at airbase. - if self.takeoff==SPAWN.Takeoff.Air then + if UsesAliveGroup==false and self.takeoff==SPAWN.Takeoff.Air then -- Carrier heading local hdg=self.carrier:GetHeading() @@ -899,7 +912,7 @@ function RESCUEHELO:onafterStart(From, Event, To) -- Start formation in 1 seconds delay=1 - else + elseif UsesAliveGroup==false and self.uncontrolledac then -- Check if an uncontrolled helo group was requested. if self.uncontrolledac then @@ -919,9 +932,9 @@ function RESCUEHELO:onafterStart(From, Event, To) -- 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 + end + end + elseif UsesAliveGroup==false then -- Spawn at airbase. self.helo=Spawn:SpawnAtAirbase(self.airbase, self.takeoff, nil, AIRBASE.TerminalType.HelicopterUsable) @@ -935,9 +948,8 @@ function RESCUEHELO:onafterStart(From, Event, To) delay=60 end - end - end + -- Set of group(s) to follow Mother. self.followset=SET_GROUP:New() @@ -947,17 +959,13 @@ function RESCUEHELO:onafterStart(From, Event, To) 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.") - + self.formation=FORMATION:New(self.carrier, self.followset, "Helo Formation with Carrier") -- 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) diff --git a/Moose Development/Moose/Ops/Squadron.lua b/Moose Development/Moose/Ops/Squadron.lua new file mode 100644 index 000000000..15d5768ef --- /dev/null +++ b/Moose Development/Moose/Ops/Squadron.lua @@ -0,0 +1,324 @@ +--- **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 #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 #string takeoffType Take of type. +-- @field #table parkingIDs Parking IDs for this squadron. +-- @field #boolean despawnAfterLanding Aircraft are despawned after landing. +-- @field #boolean despawnAfterHolding Aircraft are despawned after holding. +-- @extends Ops.Cohort#COHORT + +--- *It is unbelievable what a squadron of twelve aircraft did to tip the balance* -- Adolf Galland +-- +-- === +-- +-- # The SQUADRON Concept +-- +-- A SQUADRON is essential part of an @{Ops.Airwing#AIRWING} and consists of **one** type of aircraft. +-- +-- +-- +-- @field #SQUADRON +SQUADRON = { + ClassName = "SQUADRON", + verbose = 0, + modex = nil, + modexcounter = 0, + callsignName = nil, + callsigncounter= 11, + tankerSystem = nil, + refuelSystem = nil, +} + +--- SQUADRON class version. +-- @field #string version +SQUADRON.version="0.8.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: 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". Must be **unique**! +-- @return #SQUADRON self +function SQUADRON:New(TemplateGroupName, Ngroups, SquadronName) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, COHORT:New(TemplateGroupName, Ngroups, SquadronName)) -- #SQUADRON + + -- Everyone can ORBIT. + self:AddMissionCapability(AUFTRAG.Type.ORBIT) + + -- Is air. + self.isAir=true + + -- Refueling system. + self.refuelSystem=select(2, self.templategroup:GetUnit(1):IsRefuelable()) + self.tankerSystem=select(2, self.templategroup:GetUnit(1):IsTanker()) + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + -- See COHORT class + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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 valid parking spot IDs. Assets of this squad are only allowed to be spawned at these parking spots. **Note** that the IDs are different from the ones displayed in the mission editor! +-- @param #SQUADRON self +-- @param #table ParkingIDs Table of parking ID numbers or a single `#number`. +-- @return #SQUADRON self +function SQUADRON:SetParkingIDs(ParkingIDs) + if type(ParkingIDs)~="table" then + ParkingIDs={ParkingIDs} + end + self.parkingIDs=ParkingIDs + 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 or "Air" for spawning in air. +-- @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 + elseif TakeoffType:lower()=="air" then + self.takeoffType=COORDINATE.WaypointType.TurningPoint + else + self.takeoffType=COORDINATE.WaypointType.TakeOffParking + end + return self +end + +--- Set takeoff type cold (default). All assets of this squadron will be spawned with engines off (cold). +-- @param #SQUADRON self +-- @return #SQUADRON self +function SQUADRON:SetTakeoffCold() + self:SetTakeoffType("Cold") + return self +end + +--- Set takeoff type hot. All assets of this squadron will be spawned with engines on (hot). +-- @param #SQUADRON self +-- @return #SQUADRON self +function SQUADRON:SetTakeoffHot() + self:SetTakeoffType("Hot") + return self +end + +--- Set takeoff type air. All assets of this squadron will be spawned in air above the airbase. +-- @param #SQUADRON self +-- @return #SQUADRON self +function SQUADRON:SetTakeoffAir() + self:SetTakeoffType("Air") + return self +end + +--- Set despawn after landing. Aircraft will be despawned after the landing event. +-- Can help to avoid DCS AI taxiing issues. +-- @param #SQUADRON self +-- @param #boolean Switch If `true` (default), activate despawn after landing. +-- @return #SQUADRON self +function SQUADRON:SetDespawnAfterLanding(Switch) + if Switch then + self.despawnAfterLanding=Switch + else + self.despawnAfterLanding=true + end + return self +end + +--- Set despawn after holding. Aircraft will be despawned when they arrive at their holding position at the airbase. +-- Can help to avoid DCS AI taxiing issues. +-- @param #SQUADRON self +-- @param #boolean Switch If `true` (default), activate despawn after holding. +-- @return #SQUADRON self +function SQUADRON:SetDespawnAfterHolding(Switch) + if Switch then + self.despawnAfterHolding=Switch + else + self.despawnAfterHolding=true + end + 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.legion=Airwing + return self +end + +--- Get airwing. +-- @param #SQUADRON self +-- @return Ops.Airwing#AIRWING The airwing. +function SQUADRON:GetAirwing(Airwing) + return self.legion +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:CountAssets(true) + local NassetsQP=0 ; local NassetsP=0 ; local NassetsQ=0 + if self.legion then + NassetsQP, NassetsP, NassetsQ=self.legion: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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + diff --git a/Moose Development/Moose/Ops/Target.lua b/Moose Development/Moose/Ops/Target.lua new file mode 100644 index 000000000..5a3b75464 --- /dev/null +++ b/Moose Development/Moose/Ops/Target.lua @@ -0,0 +1,2158 @@ +--- **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, SCENERY, AIRBASE, COORDINATE, ZONE, SET_GROUP, SET_UNIT, SET_STATIC, SET_SCENERY, SET_ZONE +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Additions: **applevangelist** +-- +-- @module Ops.Target +-- @image OPS_Target.png + + +--- TARGET class. +-- @type TARGET +-- @field #string ClassName Name of the class. +-- @field #number verbose Verbosity level. +-- @field #number uid Unique ID of the target. +-- @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. +-- @field #number prio Priority. +-- @field #number importance Importance. +-- @field Ops.Auftrag#AUFTRAG mission Mission attached to this target. +-- @field Ops.Intel#INTEL.Contact contact Contact attached to this target. +-- @field #boolean isDestroyed If true, target objects were destroyed. +-- @field #table resources Resource list. +-- @field #table conditionStart Start condition functions. +-- @field Ops.Operation#OPERATION operation Operation this target is part of. +-- @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 +-- +-- === +-- +-- # 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, + conditionStart = {}, + TStatus = 30, +} + + +--- 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. +-- @field #string OPSZONE Target is an OPSZONE object. +TARGET.ObjectType={ + GROUP="Group", + UNIT="Unit", + STATIC="Static", + SCENERY="Scenery", + COORDINATE="Coordinate", + AIRBASE="Airbase", + ZONE="Zone", + OPSZONE="OpsZone" +} + + +--- 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. +-- @field #string DAMAGED Object is damaged. +TARGET.ObjectStatus={ + ALIVE="Alive", + DEAD="Dead", + DAMAGED="Damaged", +} + +--- Resource. +-- @type TARGET.Resource +-- @field #string MissionType Mission type, e.g. `AUFTRAG.Type.BAI`. +-- @field #number Nmin Min number of assets. +-- @field #number Nmax Max number of assets. +-- @field #table Attributes Generalized attribute, e.g. `{GROUP.Attribute.GROUND_INFANTRY}`. +-- @field #table Properties Properties ([DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes)), e.g. `"Attack helicopters"` or `"Mobile AAA"`. +-- @field Ops.Auftrag#AUFTRAG mission Attached mission. + +--- 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.7.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Had cases where target life was 0 but target was not dead. Need to figure out why! <== This is due to delayed dead event. +-- DONE: Add pseudo functions. +-- DONE: Initial object can be nil. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new TARGET object and start the FSM. +-- @param #TARGET self +-- @param #table TargetObject Target object. Can be a: UNIT, GROUP, STATIC, SCENERY, AIRBASE, COORDINATE, ZONE, SET_GROUP, SET_UNIT, SET_STATIC, SET_SCENERY, SET_ZONE +-- @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 + + -- Set UID. + self.uid=_TARGETID + + if TargetObject then + + -- Add object. + self:AddObject(TargetObject) + + end + + -- Defaults. + self:SetPriority() + self:SetImportance() + self.TStatus = 30 + + -- Log ID. + self.lid=string.format("TARGET #%03d | ", _TARGETID) + + -- 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", "Damaged") -- Target was damaged. + self:AddTransition("*", "Destroyed", "Dead") -- Target was completely destroyed. + self:AddTransition("*", "Dead", "Dead") -- Target is dead. Could be destroyed or despawned. + + ------------------------ + --- 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. + + + --- Triggers the FSM event "ObjectDamaged". + -- @function [parent=#TARGET] ObjectDamaged + -- @param #TARGET self + -- @param #TARGET.Object Target Target object. + + --- Triggers the FSM event "ObjectDestroyed". + -- @function [parent=#TARGET] ObjectDestroyed + -- @param #TARGET self + -- @param #TARGET.Object Target Target object. + + --- Triggers the FSM event "ObjectDead". + -- @function [parent=#TARGET] ObjectDead + -- @param #TARGET self + -- @param #TARGET.Object Target Target object. + + + --- Triggers the FSM event "Damaged". + -- @function [parent=#TARGET] Damaged + -- @param #TARGET self + + --- Triggers the FSM event "Destroyed". + -- @function [parent=#TARGET] Destroyed + -- @param #TARGET self + + --- Triggers the FSM event "Dead". + -- @function [parent=#TARGET] Dead + -- @param #TARGET self + + + --- On After "ObjectDamaged" event. A (sub-) target object has been damaged, e.g. a UNIT of a GROUP, or an object of a SET + -- @function [parent=#TARGET] OnAfterObjectDamaged + -- @param #TARGET self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #TARGET.Object Target Target object. + + --- On After "ObjectDestroyed" event. A (sub-) target object has been destroyed, e.g. a UNIT of a GROUP, or an object of a SET + -- @function [parent=#TARGET] OnAfterObjectDestroyed + -- @param #TARGET self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #TARGET.Object Target Target object. + + --- On After "ObjectDead" event. A (sub-) target object is dead, e.g. a UNIT of a GROUP, or an object of a SET + -- @function [parent=#TARGET] OnAfterObjectDead + -- @param #TARGET self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #TARGET.Object Target Target object. + + + --- On After "Damaged" event. Any of the target objects has been damaged. + -- @function [parent=#TARGET] OnAfterDamaged + -- @param #TARGET self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "Destroyed" event. All target objects have been destroyed. + -- @function [parent=#TARGET] OnAfterDestroyed + -- @param #TARGET self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- On After "Dead" event. All target objects are dead. + -- @function [parent=#TARGET] OnAfterDead + -- @param #TARGET self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + -- Start. + self:__Start(-0.1) + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create target data from a given object. Valid objects are: +-- +-- * GROUP +-- * UNIT +-- * STATIC +-- * SCENERY +-- * AIRBASE +-- * COORDINATE +-- * ZONE +-- * SET_GROUP +-- * SET_UNIT +-- * SET_STATIC +-- * SET_SCENERY +-- * SET_OPSGROUP +-- * SET_ZONE +-- * SET_OPSZONE +-- +-- @param #TARGET self +-- @param Wrapper.Positionable#POSITIONABLE Object The target UNIT, GROUP, STATIC, SCENERY, AIRBASE, COORDINATE, ZONE, SET_GROUP, SET_UNIT, SET_STATIC, SET_SCENERY, SET_ZONE +function TARGET:AddObject(Object) + + if Object:IsInstanceOf("SET_GROUP") or + Object:IsInstanceOf("SET_UNIT") or + Object:IsInstanceOf("SET_STATIC") or + Object:IsInstanceOf("SET_SCENERY") or + Object:IsInstanceOf("SET_OPSGROUP") or + Object:IsInstanceOf("SET_OPSZONE") then + + --- + -- Sets + --- + + local set=Object --Core.Set#SET_GROUP + + for _,object in pairs(set.Set) do + self:AddObject(object) + end + + elseif Object:IsInstanceOf("SET_ZONE") then + + local set=Object --Core.Set#SET_ZONE + + set:SortByName() + + for index,ZoneName in pairs(set.Index) do + local zone=set.Set[ZoneName] --Core.Zone#ZONE + self:_AddObject(zone) + end + + else + + --- + -- Groups, Units, Statics, Airbases, Coordinates + --- + + if Object:IsInstanceOf("OPSGROUP") then + self:_AddObject(Object:GetGroup()) -- We add the MOOSE GROUP object not the OPSGROUP object. + --elseif Object:IsInstanceOf("OPSZONE") then + --self:_AddObject(Object:GetZone()) + else + self:_AddObject(Object) + end + + end + + return self +end + +--- Set priority of the target. +-- @param #TARGET self +-- @param #number Priority Priority of the target. Default 50. +-- @return #TARGET self +function TARGET:SetPriority(Priority) + self.prio=Priority or 50 + return self +end + +--- Set importance of the target. +-- @param #TARGET self +-- @param #number Importance Importance of the target. Default `nil`. +-- @return #TARGET self +function TARGET:SetImportance(Importance) + self.importance=Importance + return self +end + +--- Add start condition. +-- @param #TARGET 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 #TARGET self +function TARGET:AddConditionStart(ConditionFunction, ...) + + local condition={} --Ops.Auftrag#AUFTRAG.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionStart, condition) + + return self +end + +--- Add stop condition. +-- @param #TARGET 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 #TARGET self +function TARGET:AddConditionStop(ConditionFunction, ...) + + local condition={} --Ops.Auftrag#AUFTRAG.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionStop, condition) + + return self +end + +--- Check if all given condition are true. +-- @param #TARGET 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 TARGET:EvalConditionsAll(Conditions) + + -- Any stop condition must be true. + for _,_condition in pairs(Conditions or {}) do + local condition=_condition --Ops.Auftrag#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 #TARGET self +-- @param #table Conditions Table of conditions. +-- @return #boolean If true, at least one condition is true. +function TARGET:EvalConditionsAny(Conditions) + + -- Any stop condition must be true. + for _,_condition in pairs(Conditions or {}) do + local condition=_condition --Ops.Auftrag#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 + +--- Add mission type and number of required assets to resource. +-- @param #TARGET self +-- @param #string MissionType Mission Type. +-- @param #number Nmin Min number of required assets. +-- @param #number Nmax Max number of requried assets. +-- @param #table Attributes Generalized attribute(s). +-- @param #table Properties DCS attribute(s). Default `nil`. +-- @return #TARGET.Resource The resource table. +function TARGET:AddResource(MissionType, Nmin, Nmax, Attributes, Properties) + + -- Ensure table. + if Attributes and type(Attributes)~="table" then + Attributes={Attributes} + end + + -- Ensure table. + if Properties and type(Properties)~="table" then + Properties={Properties} + end + + -- Create new resource table. + local resource={} --#TARGET.Resource + resource.MissionType=MissionType + resource.Nmin=Nmin or 1 + resource.Nmax=Nmax or 1 + resource.Attributes=Attributes or {} + resource.Properties=Properties or {} + + -- Init resource table. + self.resources=self.resources or {} + + -- Add to table. + table.insert(self.resources, resource) + + -- Debug output. + if self.verbose>10 then + local text="Resource:" + for _,_r in pairs(self.resources) do + local r=_r --#TARGET.Resource + text=text..string.format("\nmission=%s, Nmin=%d, Nmax=%d, attribute=%s, properties=%s", r.MissionType, r.Nmin, r.Nmax, tostring(r.Attributes[1]), tostring(r.Properties[1])) + end + self:I(self.lid..text) + end + + return resource +end + +--- Check if TARGET is alive. +-- @param #TARGET self +-- @return #boolean If true, target is alive. +function TARGET:IsAlive() + + for _,_target in pairs(self.targets) do + local target=_target --Ops.Target#TARGET.Object + if target.Status~=TARGET.ObjectStatus.DEAD then + if self.isDestroyed then + self:E(self.lid..string.format("ERROR: target is DESTROYED but target object status is not DEAD but %s for object %s", target.Status, target.Name)) + elseif self:IsDead() then + self:E(self.lid..string.format("ERROR: target is DEAD but target object status is not DEAD but %s for object %s", target.Status, target.Name)) + end + return true + end + end + + return false +end + +--- Check if TARGET is destroyed. +-- @param #TARGET self +-- @return #boolean If true, target is destroyed. +function TARGET:IsDestroyed() + return self.isDestroyed +end + + +--- Check if TARGET is dead. +-- @param #TARGET self +-- @return #boolean If true, target is dead. +function TARGET:IsDead() + local is=self:Is("Dead") + return is +end + +--- Check if target object is dead. +-- @param #TARGET self +-- @param #TARGET.Object TargetObject The target object. +-- @return #boolean If true, target is dead. +function TARGET:IsTargetDead(TargetObject) + local isDead=TargetObject.Status==TARGET.ObjectStatus.DEAD + return isDead +end + +--- Check if target object is alive. +-- @param #TARGET self +-- @param #TARGET.Object TargetObject The target object. +-- @return #boolean If true, target is dead. +function TARGET:IsTargetAlive(TargetObject) + local isAlive=TargetObject.Status==TARGET.ObjectStatus.ALIVE + return isAlive +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) + self:T({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) + return self +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) + --self:T({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 + + -- old life + local life=target.Life + + -- curr life + target.Life=self:GetTargetLife(target) + + -- TODO: special case ED bug > life **increases** after hits on SCENERY + if target.Life > target.Life0 then + local delta = 2*(target.Life-target.Life0) + target.Life0 = target.Life0 + delta + life = target.Life0 + self.life0 = self.life0+delta + end + + -- Check if life decreased ==> damaged + if target.Life object dead now for target object %s!", tostring(target.Name))) + self:ObjectDead(target) + damaged = true + end + + end + + -- Target was damaged. + if damaged then + self:Damaged() + end + + -- Log output verbose=1. + if self.verbose>=1 then + local text=string.format("%s: Targets=%d/%d [%d, %d], Life=%.1f/%.1f, Damage=%.1f", + fsmstate, self:CountTargets(), self.N0, self.Ndestroyed, self.Ndead, self:GetLife(), self:GetLife0(), self:GetDamage()) + if self:CountTargets() == 0 or self:GetDamage() >= 100 then + text=text.." - Dead!" + elseif 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, N0=%d, Ndestroyed=%d, Ndead=%d", + i, target.Type, target.Name, target.Status, target.Life, target.Life0, damage, target.N0, target.Ndestroyed, target.Ndead) + end + self:I(self.lid..text) + end + + -- Consitency check if target is still alive but all target objects are dead + if self:IsAlive() and (self:CountTargets()==0 or self:GetDamage()>=100) then + self:Dead() + end + + -- Quick sanity check + for i,_target in pairs(self.targets) do + local target=_target --#TARGET.Object + if target.Ndestroyed>target.N0 then + self:E(self.lid..string.format("ERROR: Number of destroyed target objects greater than number of initial target objects: %d>%d!", target.Ndestroyed, target.N0)) + end + if target.Ndestroyed>target.N0 then + self:E(self.lid..string.format("ERROR: Number of dead target objects greater than number of initial target objects: %d>%d!", target.Ndead, target.N0)) + end + end + + -- Update status again in 30 sec. + if self:IsAlive() then + self:__Status(-self.TStatus) + else + self:I(self.lid..string.format("Target is not alive any more ==> no further status updates are carried out")) + end + + return self +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) + self:T({From, Event, To}) + -- Debug info. + self:T(self.lid..string.format("Object %s damaged", Target.Name)) + + return self +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) + self:T({From, Event, To}) + -- Debug message. + self:T(self.lid..string.format("Object %s destroyed", Target.Name)) + + -- Increase destroyed counter. + self.Ndestroyed=self.Ndestroyed+1 + + Target.Ndestroyed=Target.Ndestroyed+1 + + Target.Life=0 + + -- Call object dead event. + self:ObjectDead(Target) + + return self +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) + self:T({From, Event, To}) + -- Debug message. + self:T(self.lid..string.format("Object %s dead", Target.Name)) + + -- Set target status. + Target.Status=TARGET.ObjectStatus.DEAD + + -- Increase dead object counter + Target.Ndead=Target.Ndead+1 + + -- Set target object life to 0. + Target.Life=0 + + -- 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 + break -- break the loop because we now we are not dead + end + end + + -- All dead ==> Trigger destroyed event. + if dead then + + if self.Ndestroyed==self.Ntargets0 then + + self.isDestroyed=true + + self:Destroyed() + + else + + self:Dead() + + end + else + self:Damaged() + end + + return self +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({From, Event, To}) + + self:T(self.lid..string.format("TARGET damaged")) + + return self +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({From, Event, To}) + + self:T(self.lid..string.format("TARGET destroyed")) + + self:Dead() + + return self +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({From, Event, To}) + + self:T(self.lid..string.format("TARGET dead")) + + return self +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:T(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 + + local Ndead=target.Ndead + local Ndestroyed=target.Ndestroyed + if EventData.id==EVENTS.RemoveUnit then + Ndead=Ndead+1 + else + Ndestroyed=Ndestroyed+1 + Ndead=Ndead+1 + end + + + -- Check if ALL objects are dead + if Ndead==target.N0 then + + if 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))) + + target.Life = 0 + + -- Trigger object destroyed event. This sets the Life to zero and increases Ndestroyed + self:ObjectDestroyed(target) + + else + + -- Debug message. + self:T2(self.lid..string.format("EVENT ID=%d: target %s removed ==> dead", EventData.id, tostring(target.Name))) + + target.Life = 0 + + -- Trigger object dead event. This sets the Life to zero and increases Ndead counter + self:ObjectDead(target) + + end + + end + + end -- Event belongs to this TARGET + + end + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Adding and Removing Targets +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create target data from a given object. +-- @param #TARGET self +-- @param Wrapper.Positionable#POSITIONABLE Object The target UNIT, GROUP, STATIC, SCENERY, AIRBASE, COORDINATE, ZONE, SET_GROUP, SET_UNIT, SET_STATIC, SET_SCENERY, SET_ZONE +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=static:GetLife0() + target.Life=static:GetLife() + 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=scenery:GetLife0() + + if target.Life0==0 then target.Life0 = 1 end + + target.Life=scenery:GetLife() + + 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 + + target.N0=target.N0+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 + + target.N0=target.N0+1 + + elseif Object:IsInstanceOf("OPSZONE") then + + + local zone=Object --Ops.OpsZone#OPSZONE + Object=zone + + target.Type=TARGET.ObjectType.OPSZONE + target.Name=zone:GetName() + + target.Coordinate=zone:GetCoordinate() + + target.N0=target.N0+1 + + 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) + + if self.name==nil then + self.name=self:GetTargetName(target) + end + if self.category==nil then + self.category=self:GetTargetCategory(target) + end + + return self +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 + local life=Target.Object:GetLife() + return life + --return 1 + else + return 0 + end + + elseif Target.Type==TARGET.ObjectType.SCENERY then + + if Target.Object and Target.Object:IsAlive(25) then + local life = Target.Object:GetLife() + return life + 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 or Target.Type==TARGET.ObjectType.OPSZONE then + + return 1 + + else + self:E("ERROR: unknown target object type in GetTargetLife!") + end + + return self +end + +--- Get current total life points. This is the sum of all target objects. +-- @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 threat level +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return #number Threat level of target. +function TARGET:GetTargetThreatLevelMax(Target) + + if Target.Type==TARGET.ObjectType.GROUP then + + local group=Target.Object --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + local tl=group:GetThreatLevel() + + return tl + 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:GetThreatLevel() + return life + else + return 0 + end + + elseif Target.Type==TARGET.ObjectType.STATIC then + + return 0 + + elseif Target.Type==TARGET.ObjectType.SCENERY then + + return 0 + + elseif Target.Type==TARGET.ObjectType.AIRBASE then + + return 0 + + elseif Target.Type==TARGET.ObjectType.COORDINATE then + + return 0 + + elseif Target.Type==TARGET.ObjectType.ZONE then + + local zone = Target.Object -- Core.Zone#ZONE_RADIUS + local foundunits = {} + if zone:IsInstanceOf("ZONE_RADIUS") or zone:IsInstanceOf("ZONE_POLYGON") then + zone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT,Unit.Category.SHIP}) + foundunits = zone:GetScannedSetUnit() + else + foundunits = SET_UNIT:New():FilterZones({zone}):FilterOnce() + end + local ThreatMax = foundunits:GetThreatLevelMax() or 0 + return ThreatMax + + elseif Target.Type==TARGET.ObjectType.OPSZONE then + + local unitset = Target.Object:GetScannedUnitSet() -- Core.Set#SET_UNIT + local ThreatMax = unitset:GetThreatLevelMax() + return ThreatMax + + else + self:E("ERROR: unknown target object type in GetTargetThreatLevel!") + return 0 + end + + return self +end + + +--- Get threat level. +-- @param #TARGET self +-- @return #number Threat level. +function TARGET:GetThreatLevelMax() + + local N=0 + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + local n=self:GetTargetThreatLevelMax(Target) + + if n>N then + N=n + end + + end + + return N +end + +--- Get target 2D position vector. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return DCS#Vec2 Vector with x,y components. +function TARGET:GetTargetVec2(Target) + + local vec3=self:GetTargetVec3(Target) + + if vec3 then + return {x=vec3.x, y=vec3.z} + end + + return nil +end + +--- Get target 3D position vector. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @param #boolean Average +-- @return DCS#Vec3 Vector with x,y,z components. +function TARGET:GetTargetVec3(Target, Average) + + if Target.Type==TARGET.ObjectType.GROUP then + + local object=Target.Object --Wrapper.Group#GROUP + + if object and object:IsAlive() then + local vec3=object:GetVec3() + if Average then + vec3=object:GetAverageVec3() + end + + if vec3 then + return vec3 + else + return nil + end + 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 + + elseif Target.Type==TARGET.ObjectType.OPSZONE then + + local object=Target.Object --Ops.OpsZone#OPSZONE + + local vec3=object:GetZone():GetVec3() + return vec3 + + end + + self:E(self.lid.."ERROR: Unknown TARGET type! Cannot get Vec3") +end + +--- Get heading of the target. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return #number Heading in degrees. +function TARGET:GetTargetHeading(Target) + + if Target.Type==TARGET.ObjectType.GROUP then + + local object=Target.Object --Wrapper.Group#GROUP + + if object and object:IsAlive() then + local heading=object:GetHeading() + + if heading then + return heading + else + return nil + end + 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 heading=object:GetHeading() + return heading + 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 heading=object:GetHeading() + return heading + else + return nil + end + + elseif Target.Type==TARGET.ObjectType.SCENERY then + + local object=Target.Object --Wrapper.Scenery#SCENERY + + if object then + local heading=object:GetHeading() + return heading + else + return nil + end + + elseif Target.Type==TARGET.ObjectType.AIRBASE then + + local object=Target.Object --Wrapper.Airbase#AIRBASE + + -- Airbase has no real heading. Return 0. Maybe take the runway heading? + return 0 + + elseif Target.Type==TARGET.ObjectType.COORDINATE then + + local object=Target.Object --Core.Point#COORDINATE + + -- A coordinate has no heading. Return 0. + return 0 + + elseif Target.Type==TARGET.ObjectType.ZONE or Target.Type==TARGET.ObjectType.OPSZONE then + + local object=Target.Object --Core.Zone#ZONE + + -- A zone has no heading. Return 0. + return 0 + + end + + self:E(self.lid.."ERROR: Unknown TARGET type! Cannot get heading") +end + + +--- Get target coordinate. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @param #boolean Average +-- @return Core.Point#COORDINATE Coordinate of the target. +function TARGET:GetTargetCoordinate(Target, Average) + + 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, Average) + + -- 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() + + elseif Target.Type==TARGET.ObjectType.ZONE then + + local Zone=Target.Object --Core.Zone#ZONE + + return Zone:GetName() + + elseif Target.Type==TARGET.ObjectType.SCENERY then + + local Zone=Target.Object --Core.Zone#ZONE + + return Zone:GetName() + end + + return "Unknown" +end + +--- Get name. +-- @param #TARGET self +-- @return #string Name of the target usually the first object. +function TARGET:GetName() + local name=self.name or "Unknown" + return name +end + +--- Get 2D vector. +-- @param #TARGET self +-- @return DCS#Vec2 2D vector of the target. +function TARGET:GetVec2() + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + local coordinate=self:GetTargetVec2(Target) + + if coordinate then + return coordinate + end + + end + + self:E(self.lid..string.format("ERROR: Cannot get Vec2 of target %s", self.name)) + return nil +end + +--- Get 3D vector. +-- @param #TARGET self +-- @return DCS#Vec3 3D vector of the target. +function TARGET:GetVec3() + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + local coordinate=self:GetTargetVec3(Target) + + if coordinate then + return coordinate + end + + end + + self:E(self.lid..string.format("ERROR: Cannot get Vec3 of target %s", self.name)) + return nil +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", tostring(self.name))) + return nil +end + +--- Get average coordinate. +-- @param #TARGET self +-- @return Core.Point#COORDINATE Coordinate of the target. +function TARGET:GetAverageCoordinate() + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + local coordinate=self:GetTargetCoordinate(Target, true) + + if coordinate then + return coordinate + end + + end + + self:E(self.lid..string.format("ERROR: Cannot get average coordinate of target %s", tostring(self.name))) + return nil +end + + +--- Get coordinates of all targets. (e.g. for a SET_STATIC) +-- @param #TARGET self +-- @return #table Table with coordinates of all targets. +function TARGET:GetCoordinates() + local coordinates={} + + for _,_target in pairs(self.targets) do + local target=_target --#TARGET.Object + + local coordinate=self:GetTargetCoordinate(target) + if coordinate then + table.insert(coordinates, coordinate) + end + + end + + return coordinates +end + +--- Get heading of target. +-- @param #TARGET self +-- @return #number Heading of the target in degrees. +function TARGET:GetHeading() + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + local heading=self:GetTargetHeading(Target) + + if heading then + return heading + end + + end + + self:E(self.lid..string.format("ERROR: Cannot get heading of target %s", tostring(self.name))) + return nil +end + +--- Get category. +-- @param #TARGET self +-- @return #string Target category. See `TARGET.Category.X`, where `X=AIRCRAFT, GROUND`. +function TARGET:GetCategory() + return self.category +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 + + elseif Target.Type==TARGET.ObjectType.OPSZONE then + + return TARGET.Category.OPSZONE + + else + self:E("ERROR: unknown target category!") + end + + return category +end + + +--- Get coalition of target object. If an object has no coalition (*e.g.* a coordinate) it is returned as neutral. +-- @param #TARGET self +-- @param #TARGET.Object Target Target object. +-- @return #number Coalition number. +function TARGET:GetTargetCoalition(Target) + + + -- We take neutral for objects that do not have a coalition. + local coal=coalition.side.NEUTRAL + + + if Target.Type==TARGET.ObjectType.GROUP then + + if Target.Object and Target.Object:IsAlive()~=nil then + local object=Target.Object --Wrapper.Group#GROUP + + coal=object:GetCoalition() + + end + + elseif Target.Type==TARGET.ObjectType.UNIT then + + if Target.Object and Target.Object:IsAlive()~=nil then + local object=Target.Object --Wrapper.Unit#UNIT + + coal=object:GetCoalition() + + end + + elseif Target.Type==TARGET.ObjectType.STATIC then + local object=Target.Object --Wrapper.Static#STATIC + + coal=object:GetCoalition() + + elseif Target.Type==TARGET.ObjectType.SCENERY then + + -- Scenery has no coalition. + + elseif Target.Type==TARGET.ObjectType.AIRBASE then + local object=Target.Object --Wrapper.Airbase#AIRBASE + + coal=object:GetCoalition() + + elseif Target.Type==TARGET.ObjectType.COORDINATE then + + -- Coordinate has no coalition. + + elseif Target.Type==TARGET.ObjectType.ZONE then + + -- Zone has no coalition. + + elseif Target.Type==TARGET.ObjectType.OPSZONE then + local object=Target.Object --Ops.OpsZone#OPSZONE + + coal=object:GetOwner() + + else + self:E("ERROR: unknown target category!") + end + + + return coal +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 +-- @param Core.Point#COORDINATE RefCoordinate (Optional) Reference coordinate to determine the closest target objective. +-- @param #table Coalitions (Optional) Only consider targets of the given coalition(s). +-- @return #TARGET.Object The target objective. +function TARGET:GetObjective(RefCoordinate, Coalitions) + + if RefCoordinate then + + local dmin=math.huge + local tmin=nil --#TARGET.Object + + for _,_target in pairs(self.targets) do + local target=_target --#TARGET.Object + + if target.Status~=TARGET.ObjectStatus.DEAD and (Coalitions==nil or UTILS.IsInTable(UTILS.EnsureTable(Coalitions), self:GetTargetCoalition(target))) then + + local vec3=self:GetTargetVec3(target) + + local d=UTILS.VecDist3D(vec3, RefCoordinate) + + if d1 then + if Coalitions==nil or UTILS.IsInTable(UTILS.EnsureTable(Coalitions), unit:GetCoalition()) then + N=N+1 + end + 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 + if Coalitions==nil or UTILS.IsInTable(Coalitions, target:GetCoalition()) then + N=N+1 + end + end + + elseif Target.Type==TARGET.ObjectType.STATIC then + + local target=Target.Object --Wrapper.Static#STATIC + + if target and target:IsAlive() then + if Coalitions==nil or UTILS.IsInTable(Coalitions, target:GetCoalition()) then + N=N+1 + end + end + + elseif Target.Type==TARGET.ObjectType.SCENERY then + + if Target.Status~=TARGET.ObjectStatus.DEAD then + N=N+1 + end + + elseif Target.Type==TARGET.ObjectType.AIRBASE then + + local target=Target.Object --Wrapper.Airbase#AIRBASE + + if Target.Status==TARGET.ObjectStatus.ALIVE then + if Coalitions==nil or UTILS.IsInTable(Coalitions, target:GetCoalition()) then + N=N+1 + end + end + + elseif Target.Type==TARGET.ObjectType.COORDINATE then + + -- No target, where we can check the alive status, so we assume it is alive. Changed this because otherwise target count is 0 if we pass a coordinate. + -- This is also more consitent with the life and is alive status. + if not OnlyReallyAlive then + N=N+1 + end + + elseif Target.Type==TARGET.ObjectType.ZONE then + + -- No target, where we can check the alive status, so we assume it is alive. Changed this because otherwise target count is 0 if we pass a coordinate. + -- This is also more consitent with the life and is alive status. + if not OnlyReallyAlive then + N=N+1 + end + + elseif Target.Type==TARGET.ObjectType.OPSZONE then + + local target=Target.Object --Ops.OpsZone#OPSZONE + + if Coalitions==nil or UTILS.IsInTable(Coalitions, target:GetOwner()) then + N=N+1 + end + + else + self:E(self.lid.."ERROR: Unknown target type! Cannot count targets") + end + + return N +end + +--- Count alive targets. +-- @param #TARGET self +-- @param #table Coalitions (Optional) Only count targets of the given coalition(s). +-- @param #boolean OnlyReallyAlive (Optional) If `true`, count only really alive targets (units, groups) but not coordinates or zones. +-- @return #number Number of alive target objects. +function TARGET:CountTargets(Coalitions, OnlyReallyAlive) + + local N=0 + + for _,_target in pairs(self.targets) do + local Target=_target --#TARGET.Object + + N=N+self:CountObjectives(Target, Coalitions, OnlyReallyAlive) + + 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 tostring(name)==tostring(Name) then + return true + end + end + + return false +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Tasking/CommandCenter.lua b/Moose Development/Moose/Tasking/CommandCenter.lua index 30d285682..68da47300 100644 --- a/Moose Development/Moose/Tasking/CommandCenter.lua +++ b/Moose Development/Moose/Tasking/CommandCenter.lua @@ -183,7 +183,8 @@ COMMANDCENTER = { } ---- @type COMMANDCENTER.AutoAssignMethods +--- +-- @type COMMANDCENTER.AutoAssignMethods COMMANDCENTER.AutoAssignMethods = { ["Random"] = 1, ["Distance"] = 2, @@ -212,7 +213,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) self:SetMessageDuration(10) self:HandleEvent( EVENTS.Birth, - --- @param #COMMANDCENTER self + -- @param #COMMANDCENTER self -- @param Core.Event#EVENTDATA EventData function( self, EventData ) if EventData.IniObjectCategory == 1 then @@ -243,7 +244,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) -- -- - 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 #COMMANDCENTER self -- -- @param Core.Event#EVENTDATA EventData -- function( self, EventData ) -- local PlayerUnit = EventData.IniUnit @@ -260,7 +261,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) -- 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 #TASK self -- @param Core.Event#EVENTDATA EventData function( self, EventData ) local PlayerUnit = EventData.IniUnit @@ -275,7 +276,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) -- 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 #TASK self -- @param Core.Event#EVENTDATA EventData function( self, EventData ) local PlayerUnit = EventData.IniUnit @@ -292,7 +293,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) -- 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 #TASK self -- @param Core.Event#EVENTDATA EventData function( self, EventData ) local PlayerUnit = EventData.IniUnit diff --git a/Moose Development/Moose/Tasking/DetectionManager.lua b/Moose Development/Moose/Tasking/DetectionManager.lua index a1c4505d5..16025282a 100644 --- a/Moose Development/Moose/Tasking/DetectionManager.lua +++ b/Moose Development/Moose/Tasking/DetectionManager.lua @@ -50,7 +50,7 @@ do -- DETECTION MANAGER - --- @type 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. @@ -64,7 +64,7 @@ do -- DETECTION MANAGER Detection = nil, } - --- @field Tasking.CommandCenter#COMMANDCENTER + -- @field Tasking.CommandCenter#COMMANDCENTER DETECTION_MANAGER.CC = nil --- FAC constructor. diff --git a/Moose Development/Moose/Tasking/Mission.lua b/Moose Development/Moose/Tasking/Mission.lua index 0c5b76ac3..8cd08f087 100644 --- a/Moose Development/Moose/Tasking/Mission.lua +++ b/Moose Development/Moose/Tasking/Mission.lua @@ -19,7 +19,8 @@ -- @module Tasking.Mission -- @image Task_Mission.JPG ---- @type MISSION +--- +-- @type MISSION -- @field #MISSION.Clients _Clients -- @field Core.Menu#MENU_COALITION MissionMenu -- @field #string MissionBriefing @@ -792,7 +793,7 @@ function MISSION:HasGroup( TaskGroup ) return Has end ---- @param #MISSION self +-- @param #MISSION self -- @return #number function MISSION:GetTasksRemaining() -- Determine how many tasks are remaining. @@ -807,7 +808,7 @@ function MISSION:GetTasksRemaining() return TasksRemaining end ---- @param #MISSION self +-- @param #MISSION self -- @return #number function MISSION:GetTaskTypes() -- Determine how many tasks are remaining. @@ -867,7 +868,7 @@ end ---- - Aborted Tasks (xp) ---- - Failed Tasks (xp) ---- ----- @param #MISSION self +-- @param #MISSION self ---- @return #string --function MISSION:ReportSummary() -- @@ -1177,7 +1178,7 @@ end ---- @param #MISSION self +-- @param #MISSION self -- @param #string TaskStatus The status -- @param Wrapper.Group#GROUP ReportGroup function MISSION:MenuReportTasksPerStatus( ReportGroup, TaskStatus ) @@ -1188,7 +1189,7 @@ function MISSION:MenuReportTasksPerStatus( ReportGroup, TaskStatus ) end ---- @param #MISSION self +-- @param #MISSION self -- @param Wrapper.Group#GROUP ReportGroup function MISSION:MenuReportPlayersPerTask( ReportGroup ) @@ -1197,7 +1198,7 @@ function MISSION:MenuReportPlayersPerTask( ReportGroup ) self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Overview ) end ---- @param #MISSION self +-- @param #MISSION self -- @param Wrapper.Group#GROUP ReportGroup function MISSION:MenuReportPlayersProgress( ReportGroup ) diff --git a/Moose Development/Moose/Tasking/Task.lua b/Moose Development/Moose/Tasking/Task.lua index be3e86f2d..04d6a4598 100644 --- a/Moose Development/Moose/Tasking/Task.lua +++ b/Moose Development/Moose/Tasking/Task.lua @@ -222,7 +222,8 @@ -- @module Tasking.Task -- @image MOOSE.JPG ---- @type TASK +--- +-- @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 @@ -728,7 +729,7 @@ function TASK:AddGroups( GroupSet ) GroupSet = GroupSet or SET_GROUP:New() self.SetGroup:ForEachGroup( - --- @param Wrapper.Group#GROUP GroupSet + -- @param Wrapper.Group#GROUP GroupSet function( GroupItem ) GroupSet:Add( GroupItem:GetName(), GroupItem) end @@ -821,7 +822,7 @@ end do -- Group Assignment - --- @param #TASK self + -- @param #TASK self -- @param Actions.Act_Assign#ACT_ASSIGN AcceptClass function TASK:SetAssignMethod( AcceptClass ) @@ -1201,7 +1202,7 @@ function TASK:RemoveAssignedMenuForGroup( TaskGroup ) end ---- @param #TASK self +-- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup function TASK:MenuAssignToGroup( TaskGroup ) @@ -1210,7 +1211,7 @@ function TASK:MenuAssignToGroup( TaskGroup ) self:AssignToGroup( TaskGroup ) end ---- @param #TASK self +-- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup function TASK:MenuMarkToGroup( TaskGroup ) self:F() diff --git a/Moose Development/Moose/Tasking/TaskInfo.lua b/Moose Development/Moose/Tasking/TaskInfo.lua index effcdf698..203b94e01 100644 --- a/Moose Development/Moose/Tasking/TaskInfo.lua +++ b/Moose Development/Moose/Tasking/TaskInfo.lua @@ -11,7 +11,8 @@ -- @module Tasking.TaskInfo -- @image MOOSE.JPG ---- @type TASKINFO +--- +-- @type TASKINFO -- @extends Core.Base#BASE --- @@ -31,7 +32,8 @@ TASKINFO = { ClassName = "TASKINFO", } ---- @type TASKINFO.Detail #string A string that flags to document which level of detail needs to be shown in the report. +--- +-- @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. @@ -281,7 +283,7 @@ function TASKINFO:AddCargoSet( SetCargo, Order, Detail, Keep ) local CargoReport = REPORT:New() CargoReport:Add( "" ) SetCargo:ForEachCargo( - --- @param Cargo.Cargo#CARGO Cargo + -- @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 diff --git a/Moose Development/Moose/Tasking/Task_A2A.lua b/Moose Development/Moose/Tasking/Task_A2A.lua index 6ba36b054..43d6e1f5a 100644 --- a/Moose Development/Moose/Tasking/Task_A2A.lua +++ b/Moose Development/Moose/Tasking/Task_A2A.lua @@ -176,19 +176,19 @@ do -- TASK_A2A end - --- @param #TASK_A2A self + -- @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 + -- @param #TASK_A2A self function TASK_A2A:GetPlannedMenuText() return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" end - --- @param #TASK_A2A self + -- @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 @@ -201,7 +201,7 @@ do -- TASK_A2A ActRouteRendezVous:SetRange( RendezVousRange ) end - --- @param #TASK_A2A self + -- @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. @@ -213,7 +213,7 @@ do -- TASK_A2A return ActRouteRendezVous:GetCoordinate(), ActRouteRendezVous:GetRange() end - --- @param #TASK_A2A self + -- @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 ) @@ -224,7 +224,7 @@ do -- TASK_A2A ActRouteRendezVous:SetZone( RendezVousZone ) end - --- @param #TASK_A2A self + -- @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 ) @@ -235,7 +235,7 @@ do -- TASK_A2A return ActRouteRendezVous:GetZone() end - --- @param #TASK_A2A self + -- @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 ) @@ -246,7 +246,7 @@ do -- TASK_A2A ActRouteTarget:SetCoordinate( TargetCoordinate ) end - --- @param #TASK_A2A self + -- @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 ) @@ -257,7 +257,7 @@ do -- TASK_A2A return ActRouteTarget:GetCoordinate() end - --- @param #TASK_A2A self + -- @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 ) @@ -268,7 +268,7 @@ do -- TASK_A2A ActRouteTarget:SetZone( TargetZone, Altitude, Heading ) end - --- @param #TASK_A2A self + -- @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 ) @@ -312,7 +312,7 @@ do -- TASK_A2A self:__Goal( -10 ) end - --- @param #TASK_A2A self + -- @param #TASK_A2A self function TASK_A2A:UpdateTaskInfo( DetectedItem ) if self:IsStatePlanned() or self:IsStateAssigned() then @@ -504,7 +504,7 @@ do -- TASK_A2A_SWEEP return self end - --- @param #TASK_A2A_SWEEP self + -- @param #TASK_A2A_SWEEP self function TASK_A2A_SWEEP:onafterGoal( TaskUnit, From, Event, To ) local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT diff --git a/Moose Development/Moose/Tasking/Task_A2A_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_A2A_Dispatcher.lua index 69c29d731..b3c4b0568 100644 --- a/Moose Development/Moose/Tasking/Task_A2A_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_A2A_Dispatcher.lua @@ -150,7 +150,7 @@ do -- TASK_A2A_DISPATCHER -- -- TaskDispatcher = TASK_A2A_DISPATCHER:New( ... ) -- - -- --- @param #TaskDispatcher self + -- -- @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. diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua index 526a508d4..6bfcfffa1 100644 --- a/Moose Development/Moose/Tasking/Task_CARGO.lua +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -411,7 +411,7 @@ do -- TASK_CARGO - --- @type TASK_CARGO + -- @type TASK_CARGO -- @extends Tasking.Task#TASK --- Model tasks for players to transport Cargo. @@ -622,7 +622,7 @@ do -- TASK_CARGO Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) Fsm:AddTransition( "Failed", "Fail", "Failed" ) - ---- @param #FSM_PROCESS self + -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param #TASK_CARGO Task function Fsm:OnAfterAssigned( TaskUnit, Task ) @@ -645,7 +645,7 @@ do -- TASK_CARGO Task.SetCargo:ForEachCargo( - --- @param Cargo.Cargo#CARGO Cargo + -- @param Cargo.Cargo#CARGO Cargo function( Cargo ) if Cargo:IsAlive() then @@ -839,7 +839,7 @@ do -- TASK_CARGO --#Wrapper.Unit#UNIT - --- @param #FSM_PROCESS self + -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_Cargo#TASK_CARGO Task -- @param From @@ -859,7 +859,7 @@ do -- TASK_CARGO - --- @param #FSM_PROCESS self + -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_Cargo#TASK_CARGO Task function Fsm:onafterArriveAtPickup( TaskUnit, Task ) @@ -875,7 +875,7 @@ do -- TASK_CARGO end - --- @param #FSM_PROCESS self + -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_Cargo#TASK_CARGO Task function Fsm:onafterCancelRouteToPickup( TaskUnit, Task ) @@ -886,7 +886,7 @@ do -- TASK_CARGO end - --- @param #FSM_PROCESS self + -- @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() } ) @@ -898,7 +898,7 @@ do -- TASK_CARGO end - --- @param #FSM_PROCESS self + -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_Cargo#TASK_CARGO Task function Fsm:onafterArriveAtDeploy( TaskUnit, Task ) @@ -913,7 +913,7 @@ do -- TASK_CARGO end - --- @param #FSM_PROCESS self + -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_Cargo#TASK_CARGO Task function Fsm:onafterCancelRouteToDeploy( TaskUnit, Task ) @@ -925,7 +925,7 @@ do -- TASK_CARGO - --- @param #FSM_PROCESS self + -- @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 ) @@ -960,7 +960,7 @@ do -- TASK_CARGO end end - --- @param #FSM_PROCESS self + -- @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 ) @@ -993,7 +993,7 @@ do -- TASK_CARGO end end - --- @param #FSM_PROCESS self + -- @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 ) @@ -1005,7 +1005,7 @@ do -- TASK_CARGO end - --- @param #FSM_PROCESS self + -- @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 ) @@ -1033,7 +1033,7 @@ do -- TASK_CARGO end - --- @param #FSM_PROCESS self + -- @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 ) @@ -1048,7 +1048,7 @@ do -- TASK_CARGO end - --- @param #FSM_PROCESS self + -- @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 ) @@ -1197,26 +1197,26 @@ do -- TASK_CARGO return self.SmokeColor end - --- @param #TASK_CARGO self + -- @param #TASK_CARGO self function TASK_CARGO:GetPlannedMenuText() return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" end - --- @param #TASK_CARGO self + -- @param #TASK_CARGO self -- @return Core.Set#SET_CARGO The Cargo Set. function TASK_CARGO:GetCargoSet() return self.SetCargo end - --- @param #TASK_CARGO self + -- @param #TASK_CARGO self -- @return #list The Deployment Zones. function TASK_CARGO:GetDeployZones() return self.DeployZones end - --- @param #TASK_CARGO self + -- @param #TASK_CARGO self -- @param AI.AI_Cargo#AI_CARGO Cargo The cargo. -- @param Wrapper.Unit#UNIT TaskUnit -- @return #TASK_CARGO @@ -1239,7 +1239,7 @@ do -- TASK_CARGO end - --- @param #TASK_CARGO self + -- @param #TASK_CARGO self -- @param Core.Zone#ZONE DeployZone -- @param Wrapper.Unit#UNIT TaskUnit -- @return #TASK_CARGO @@ -1260,7 +1260,7 @@ do -- TASK_CARGO end - --- @param #TASK_CARGO self + -- @param #TASK_CARGO self -- @param Core.Zone#ZONE DeployZone -- @param Wrapper.Unit#UNIT TaskUnit -- @return #TASK_CARGO @@ -1271,7 +1271,7 @@ do -- TASK_CARGO return self end - --- @param #TASK_CARGO self + -- @param #TASK_CARGO self -- @param Core.Zone#ZONE DeployZone -- @param Wrapper.Unit#UNIT TaskUnit -- @return #TASK_CARGO @@ -1282,7 +1282,7 @@ do -- TASK_CARGO return self end - --- @param #TASK_CARGO self + -- @param #TASK_CARGO self -- @param #list DeployZones -- @param Wrapper.Unit#UNIT TaskUnit -- @return #TASK_CARGO @@ -1297,7 +1297,7 @@ do -- TASK_CARGO - --- @param #TASK_CARGO self + -- @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 ) @@ -1366,7 +1366,7 @@ do -- TASK_CARGO return self.GoalTotal end - --- @param #TASK_CARGO self + -- @param #TASK_CARGO self function TASK_CARGO:UpdateTaskInfo() if self:IsStatePlanned() or self:IsStateAssigned() then diff --git a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua index 5ede27694..fa334dcf4 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua @@ -69,7 +69,7 @@ do -- TASK_CAPTURE_DISPATCHER -- @extends Tasking.Task_Manager#TASK_MANAGER -- @field TASK_CAPTURE_DISPATCHER.ZONE ZONE - --- @type TASK_CAPTURE_DISPATCHER.CSAR + -- @type TASK_CAPTURE_DISPATCHER.CSAR -- @field Wrapper.Unit#UNIT PilotUnit -- @field Tasking.Task#TASK Task diff --git a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua index 82265c8ae..4c2372e8d 100644 --- a/Moose Development/Moose/Tasking/Task_Capture_Zone.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -121,7 +121,7 @@ do -- TASK_ZONE_GOAL end - --- @param #TASK_ZONE_GOAL self + -- @param #TASK_ZONE_GOAL self -- @param Functional.ZoneGoal#ZONE_GOAL ZoneGoal The ZoneGoal Engine. function TASK_ZONE_GOAL:SetProtect( ZoneGoal ) @@ -130,13 +130,13 @@ do -- TASK_ZONE_GOAL - --- @param #TASK_ZONE_GOAL self + -- @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 #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 ) @@ -148,7 +148,7 @@ do -- TASK_ZONE_GOAL end - --- @param #TASK_ZONE_GOAL self + -- @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 ) @@ -288,7 +288,7 @@ do -- TASK_CAPTURE_ZONE end - --- @param #TASK_CAPTURE_ZONE self + -- @param #TASK_CAPTURE_ZONE self -- @param Wrapper.Unit#UNIT TaskUnit function TASK_CAPTURE_ZONE:OnAfterGoal( From, Event, To, PlayerUnit, PlayerName ) diff --git a/Moose Development/Moose/Tasking/Task_Cargo_CSAR.lua b/Moose Development/Moose/Tasking/Task_Cargo_CSAR.lua index e216c6ea2..ca71d64ef 100644 --- a/Moose Development/Moose/Tasking/Task_Cargo_CSAR.lua +++ b/Moose Development/Moose/Tasking/Task_Cargo_CSAR.lua @@ -74,8 +74,8 @@ do -- TASK_CARGO_CSAR - --- @type TASK_CARGO_CSAR - -- @extends Tasking.Task_CARGO#TASK_CARGO + -- @type TASK_CARGO_CSAR + -- @extends Tasking.Task_Cargo#TASK_CARGO --- Orchestrates the task for players to execute CSAR for downed pilots. -- diff --git a/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua index e50dbbfeb..b79a098bd 100644 --- a/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua @@ -79,7 +79,7 @@ do -- TASK_CARGO_DISPATCHER -- @field TASK_CARGO_DISPATCHER.CSAR CSAR -- @field Core.Set#SET_ZONE SetZonesCSAR - --- @type TASK_CARGO_DISPATCHER.CSAR + -- @type TASK_CARGO_DISPATCHER.CSAR -- @field Wrapper.Unit#UNIT PilotUnit -- @field Tasking.Task#TASK Task diff --git a/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua b/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua index 5f6fb6e51..3a30eaf22 100644 --- a/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua +++ b/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua @@ -287,7 +287,7 @@ do -- TASK_CARGO_TRANSPORT local CargoReport = REPORT:New( "Transport Cargo. The following cargo needs to be transported including initial positions:") SetCargo:ForEachCargo( - --- @param Core.Cargo#CARGO Cargo + -- @param Core.Cargo#CARGO Cargo function( Cargo ) local CargoType = Cargo:GetType() local CargoName = Cargo:GetName() @@ -350,7 +350,7 @@ do -- TASK_CARGO_TRANSPORT return CargoDeployed end - --- @param #TASK_CARGO_TRANSPORT self + -- @param #TASK_CARGO_TRANSPORT self function TASK_CARGO_TRANSPORT:onafterGoal( TaskUnit, From, Event, To ) local CargoSet = self.CargoSet diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index d2245001c..2bc937ac9 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -1841,3 +1841,28 @@ ENUMS.FARPObjectTypeNamesAndShape ={ [ENUMS.FARPType.PADSINGLE] = { TypeName="FARP_SINGLE_01", ShapeName="FARP_SINGLE_01"}, } +--- Radio frequency bands (HF, VHF, UHF) +-- @type ENUMS.FrequencyBand +-- @field #number HF High frequency +-- @field #number VHF_LOW Very high frequency +-- @field #number VHF_HI Very high frequency +-- @field #number UHF Ultra high frequency +ENUMS.FrequencyBand = { + HF = 0, + VHF_LOW = 1, + VHF_HI = 2, + UHF = 3, +} + +--- Radio modulation types (AM, FM) +-- @type ENUMS.ModulationType +-- @field #number AM Amplitude modulation +-- @field #number FM Frequency modulation +-- @field #number AMFM Amplitude and frequency modulation +-- @field #number DISCARD Discard modulation +ENUMS.ModulationType = { + AM = 0, + FM = 1, + AMFM = 2, + DISCARD = -1, +} diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 9a5409bdf..e9e629a0b 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -409,7 +409,7 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime ) 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. + -- 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 } ) diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index b6c0c54d1..d4c93960f 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -1164,6 +1164,23 @@ function GROUP:GetVec3() return nil end +--- Returns the current {@Core.Vector#VECTOR} of the first Unit in the GROUP. +-- @param #GROUP self +-- @return Core.Vector#VECTOR Vector of the first Unit of the GROUP or nil if cannot be found. +function GROUP:GetVector() + + -- Get first unit. + local unit=self:GetUnit(1) + + if unit then + local vector=unit:GetVector() + return vector + end + + self:E("ERROR: Cannot get Vector of group "..tostring(self.GroupName)) + return nil +end + --- Returns the average Vec3 vector of the Units in the GROUP. -- @param #GROUP self -- @return DCS#Vec3 Current Vec3 of the GROUP or nil if cannot be found. diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 7be7ed135..69c949429 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -219,6 +219,25 @@ function POSITIONABLE:GetOrientationZ() end end +--- Returns the vectors of the orientation of the object: +-- X is the orientation parallel to the movement of the object, Z perpendicular and Y vertical orientation. +-- @param #POSITIONABLE self +-- @return Core.Vector#VECTOR X orientation, i.e. parallel to the direction of movement. +-- @return Core.Vector#VECTOR Y orientation, i.e. vertical. +-- @return Core.Vector#VECTOR Z orientation, i.e. perpendicular to the direction of movement. +function POSITIONABLE:GetOrientationVectors() + local position = self:GetPosition() + if position then + local vecx=VECTOR:NewFromVec(position.x) + local vecy=VECTOR:NewFromVec(position.y) + local vecz=VECTOR:NewFromVec(position.z) + return vecx, vecy, vecz + else + BASE:E( { "Cannot GetOrientation", Positionable = self, Alive = self:IsAlive() } ) + return nil, nil, nil + end +end + --- Returns the @{DCS#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. -- @param #POSITIONABLE self -- @return DCS#Position The 3D position vectors of the POSITIONABLE. @@ -286,6 +305,27 @@ function POSITIONABLE:GetVec2() return nil end +--- Returns the @{Core.Vector#VECTOR} indicating the 3D position of the object on the map. +-- @param #POSITIONABLE self +-- @return Core.Vector#VECTOR The 3D vector. +function POSITIONABLE:GetVector() + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + + local Vec3 = DCSPositionable:getPoint() -- DCS#Vec3 + + local vector=VECTOR:NewFromVec(Vec3) + + return vector + end + + self:E( { "Cannot GetVec2", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + --- Returns a COORDINATE object indicating the point in 2D of the POSITIONABLE within the mission. -- @param #POSITIONABLE self -- @return Core.Point#COORDINATE The 3D point vector of the POSITIONABLE. @@ -914,6 +954,24 @@ function POSITIONABLE:GetVelocityVec3() return nil end +--- Returns the velocity vector. +-- @param #POSITIONABLE self +-- @return Core.Vector#VECTOR The velocity vector or `nil` if the object does not exist. +function POSITIONABLE:GetVelocityVector() + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable and DCSPositionable:isExist() then + local vec3 = DCSPositionable:getVelocity() + local vector=VECTOR:NewFromVec(vec3) + return vector + end + + BASE:E( { "Cannot GetVelocityVector", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + --- Get relative velocity with respect to another POSITIONABLE. -- @param #POSITIONABLE self -- @param #POSITIONABLE Positionable Other POSITIONABLE. 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.files b/Moose Setup/Moose.files index 6f79e6933..f7c8be377 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -16,23 +16,22 @@ Core/Event.lua Core/Settings.lua Core/Menu.lua Core/Zone.lua -Core/Zone_Detection.lua Core/Database.lua Core/Set.lua Core/Point.lua -Core/Velocity.lua Core/Message.lua Core/Fsm.lua Core/Spawn.lua Core/SpawnStatic.lua Core/Timer.lua -Core/Goal.lua Core/Spot.lua Core/MarkerOps_Base.lua Core/Astar.lua Core/Condition.lua -Core/TextAndSound.lua Core/Pathline.lua +Core/ClientMenu.lua +Core/Astar.lua +Core/MarkerOps_Base.lua Wrapper/Object.lua Wrapper/Identifiable.lua @@ -50,26 +49,17 @@ Wrapper/Net.lua Wrapper/Storage.lua Wrapper/DynamicCargo.lua -Cargo/Cargo.lua -Cargo/CargoUnit.lua -Cargo/CargoSlingload.lua -Cargo/CargoCrate.lua -Cargo/CargoGroup.lua - Functional/Scoring.lua Functional/CleanUp.lua Functional/Movement.lua Functional/Sead.lua Functional/Escort.lua -Functional/MissileTrainer.lua Functional/ATC_Ground.lua Functional/Detection.lua Functional/DetectionZones.lua Functional/Designate.lua Functional/RAT.lua Functional/Range.lua -Functional/ZoneGoal.lua -Functional/ZoneGoalCoalition.lua Functional/ZoneCaptureCoalition.lua Functional/Artillery.lua Functional/Suppression.lua @@ -78,51 +68,46 @@ Functional/Warehouse.lua Functional/Fox.lua Functional/Mantis.lua Functional/Shorad.lua +Functional/Autolase.lua +Functional/AICSAR.lua +Functional/AmmoTruck.lua +Functional/ZoneGoalCargo.lua +Functional/Tiresias.lua +Functional/Stratego.lua Functional/ClientWatch.lua +Functional/Formation.lua Ops/Airboss.lua Ops/RecoveryTanker.lua Ops/RescueHelo.lua Ops/ATIS.lua -Ops/CTLD.lua +Ops/Auftrag.lua +Ops/OpsGroup.lua +Ops/FlightGroup.lua +Ops/NavyGroup.lua +Ops/Cohort.lua +Ops/Squadron.lua +Ops/Platoon.lua +Ops/Flotilla.lua +Ops/Legion.lua +Ops/AirWing.lua +Ops/Brigade.lua +Ops/Fleet.lua +Ops/Intelligence.lua +Ops/Commander.lua +Ops/Chief.lua Ops/CSAR.lua - -AI/AI_Balancer.lua -AI/AI_Air.lua -AI/AI_Air_Patrol.lua -AI/AI_Air_Engage.lua -AI/AI_A2A_Patrol.lua -AI/AI_A2A_Cap.lua -AI/AI_A2A_Gci.lua -AI/AI_A2A_Dispatcher.lua -AI/AI_A2G_BAI.lua -AI/AI_A2G_CAS.lua -AI/AI_A2G_SEAD.lua -AI/AI_A2G_Dispatcher.lua -AI/AI_Patrol.lua -AI/AI_CAP.lua -AI/AI_CAS.lua -AI/AI_BAI.lua -AI/AI_Formation.lua -AI/AI_Escort.lua -AI/AI_Escort_Request.lua -AI/AI_Escort_Dispatcher.lua -AI/AI_Escort_Dispatcher_Request.lua -AI/AI_Cargo.lua -AI/AI_Cargo_APC.lua -AI/AI_Cargo_Helicopter.lua -AI/AI_Cargo_Airplane.lua -AI/AI_Cargo_Ship.lua -AI/AI_Cargo_Dispatcher.lua -AI/AI_Cargo_Dispatcher_APC.lua -AI/AI_Cargo_Dispatcher_Helicopter.lua -AI/AI_Cargo_Dispatcher_Airplane.lua -AI/AI_Cargo_Dispatcher_Ship.lua - -Actions/Act_Assign.lua -Actions/Act_Route.lua -Actions/Act_Account.lua -Actions/Act_Assist.lua +Ops/CTLD.lua +Ops/Awacs.lua +Ops/Operation.lua +Ops/FlightControl.lua +Ops/PlayerTask.lua +Ops/PlayerRecce.lua +Ops/EasyGCICAP.lua +Ops/OpsZone.lua +Ops/ArmyGroup.lua +Ops/OpsTransport.lua +Ops/Target.lua Sound/UserSound.lua Sound/SoundOutput.lua @@ -131,21 +116,4 @@ Sound/RadioQueue.lua Sound/RadioSpeech.lua Sound/SRS.lua -Tasking/CommandCenter.lua -Tasking/Mission.lua -Tasking/Task.lua -Tasking/TaskInfo.lua -Tasking/Task_Manager.lua -Tasking/DetectionManager.lua -Tasking/Task_A2G_Dispatcher.lua -Tasking/Task_A2G.lua -Tasking/Task_A2A_Dispatcher.lua -Tasking/Task_A2A.lua -Tasking/Task_CARGO.lua -Tasking/Task_Cargo_Transport.lua -Tasking/Task_Cargo_CSAR.lua -Tasking/Task_Cargo_Dispatcher.lua -Tasking/Task_Capture_Zone.lua -Tasking/Task_Capture_Dispatcher.lua - Globals.lua diff --git a/Moose Setup/Moose_Create.lua b/Moose Setup/Moose_Create.lua index a69a34a18..e060d8238 100644 --- a/Moose Setup/Moose_Create.lua +++ b/Moose Setup/Moose_Create.lua @@ -1,4 +1,4 @@ --- This routine is called from the LDT environment to create the Moose.lua file stub for use in .miz files. +--- This routine is called from the LDT environment to create the Moose.lua file stub for use in .miz files. local MooseDynamicStatic = arg[1] local MooseCommitHash = arg[2] diff --git a/lua54/lua54.dll b/lua54/lua54.dll new file mode 100644 index 000000000..003fc57d2 Binary files /dev/null and b/lua54/lua54.dll differ diff --git a/lua54/lua54.exe b/lua54/lua54.exe new file mode 100644 index 000000000..46f296c60 Binary files /dev/null and b/lua54/lua54.exe differ