diff --git a/.gitignore b/.gitignore index 135102f04..5e42c0c93 100644 --- a/.gitignore +++ b/.gitignore @@ -221,3 +221,8 @@ _gsdata_/ .gitattributes .gitignore Moose Test Missions/MOOSE_Test_Template.miz +Moose Development/Moose/.vscode/launch.json +MooseCodeWS.code-workspace +.gitignore +.gitignore +/.gitignore diff --git a/.scannerwork/.sonar_lock b/.scannerwork/.sonar_lock new file mode 100644 index 000000000..e69de29bb diff --git a/.scannerwork/report-task.txt b/.scannerwork/report-task.txt new file mode 100644 index 000000000..cc70ad160 --- /dev/null +++ b/.scannerwork/report-task.txt @@ -0,0 +1,6 @@ +projectKey=Test +serverUrl=http://localhost:9000 +serverVersion=8.1.0.31237 +dashboardUrl=http://localhost:9000/dashboard?id=Test +ceTaskId=AXAlUJO97YLjwz1VUDXR +ceTaskUrl=http://localhost:9000/api/ce/task?id=AXAlUJO97YLjwz1VUDXR diff --git a/Moose Development/Moose/AI/AI_A2A_Cap.lua b/Moose Development/Moose/AI/AI_A2A_Cap.lua index de9e184da..08062cc11 100644 --- a/Moose Development/Moose/AI/AI_A2A_Cap.lua +++ b/Moose Development/Moose/AI/AI_A2A_Cap.lua @@ -1,66 +1,67 @@ --- **AI** -- (R2.2) - Models the process of Combat Air Patrol (CAP) for airplanes. -- -- === --- +-- -- ### Author: **FlightControl** --- --- === +-- +-- === -- -- @module AI.AI_A2A_Cap -- @image AI_Combat_Air_Patrol.JPG --- @type AI_A2A_CAP --- @extends AI.AI_A2A_Patrol#AI_A2A_PATROL +-- @extends AI.AI_Air_Patrol#AI_AIR_PATROL +-- @extends AI.AI_Air_Engage#AI_AIR_ENGAGE ---- The AI_A2A_CAP class implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} +--- The AI_A2A_CAP class implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} -- and automatically engage any airborne enemies that are within a certain range or within a certain zone. --- +-- -- ![Process](..\Presentations\AI_CAP\Dia3.JPG) --- +-- -- The AI_A2A_CAP is assigned a @{Wrapper.Group} and this must be done before the AI_A2A_CAP process can be started using the **Start** event. --- +-- -- ![Process](..\Presentations\AI_CAP\Dia4.JPG) --- +-- -- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. -- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- +-- -- ![Process](..\Presentations\AI_CAP\Dia5.JPG) --- +-- -- This cycle will continue. --- +-- -- ![Process](..\Presentations\AI_CAP\Dia6.JPG) --- +-- -- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. -- -- ![Process](..\Presentations\AI_CAP\Dia9.JPG) --- +-- -- When enemies are detected, the AI will automatically engage the enemy. --- +-- -- ![Process](..\Presentations\AI_CAP\Dia10.JPG) --- +-- -- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. -- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- +-- -- ![Process](..\Presentations\AI_CAP\Dia13.JPG) --- +-- -- ## 1. AI_A2A_CAP constructor --- +-- -- * @{#AI_A2A_CAP.New}(): Creates a new AI_A2A_CAP object. --- +-- -- ## 2. AI_A2A_CAP is a FSM --- +-- -- ![Process](..\Presentations\AI_CAP\Dia2.JPG) --- +-- -- ### 2.1 AI_A2A_CAP States --- +-- -- * **None** ( Group ): The process is not started yet. -- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. -- * **Engaging** ( Group ): The AI is engaging the bogeys. -- * **Returning** ( Group ): The AI is returning to Base.. --- +-- -- ### 2.2 AI_A2A_CAP Events --- +-- -- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. -- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. -- * **@{#AI_A2A_CAP.Engage}**: Let the AI engage the bogeys. @@ -73,30 +74,61 @@ -- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. -- -- ## 3. Set the Range of Engagement --- +-- -- ![Range](..\Presentations\AI_CAP\Dia11.JPG) --- --- An optional range can be set in meters, +-- +-- An optional range can be set in meters, -- that will define when the AI will engage with the detected airborne enemy targets. -- The range can be beyond or smaller than the range of the Patrol Zone. -- The range is applied at the position of the AI. -- Use the method @{AI.AI_CAP#AI_A2A_CAP.SetEngageRange}() to define that range. -- -- ## 4. Set the Zone of Engagement --- +-- -- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) --- --- An optional @{Zone} can be set, +-- +-- An optional @{Zone} can be set, -- that will define when the AI will engage with the detected airborne enemy targets. -- Use the method @{AI.AI_Cap#AI_A2A_CAP.SetEngageZone}() to define that Zone. --- +-- -- === --- +-- -- @field #AI_A2A_CAP AI_A2A_CAP = { ClassName = "AI_A2A_CAP", } +--- Creates a new AI_A2A_CAP object +-- @param #AI_A2A_CAP self +-- @param Wrapper.Group#GROUP AICap +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @return #AI_A2A_CAP +function AI_A2A_CAP:New2( AICap, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType ) + + -- Multiple inheritance ... :-) + local AI_Air = AI_AIR:New( AICap ) + local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- #AI_AIR_PATROL + local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AICap, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + local self = BASE:Inherit( self, AI_Air_Engage ) --#AI_A2A_CAP + + self:SetFuelThreshold( .2, 60 ) + self:SetDamageThreshold( 0.4 ) + self:SetDisengageRadius( 70000 ) + + + return self +end + --- Creates a new AI_A2A_CAP object -- @param #AI_A2A_CAP self -- @param Wrapper.Group#GROUP AICap @@ -111,174 +143,8 @@ AI_A2A_CAP = { -- @return #AI_A2A_CAP function AI_A2A_CAP:New( AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, PatrolAltType ) - -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2A_PATROL:New( AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2A_CAP + return self:New2( AICap, EngageMinSpeed, EngageMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolZone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, PatrolAltType ) - self.Accomplished = false - self.Engaging = false - - self.EngageMinSpeed = EngageMinSpeed - self.EngageMaxSpeed = EngageMaxSpeed - - self:AddTransition( { "Patrolling", "Engaging", "Returning", "Airborne" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_A2A_CAP] OnBeforeEngage - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Engage. - -- @function [parent=#AI_A2A_CAP] OnAfterEngage - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2A_CAP] Engage - -- @param #AI_A2A_CAP self - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2A_CAP] __Engage - -- @param #AI_A2A_CAP self - -- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_A2A_CAP] OnLeaveEngaging --- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_A2A_CAP] OnEnterEngaging --- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_A2A_CAP] OnBeforeFired - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_A2A_CAP] OnAfterFired - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2A_CAP] Fired - -- @param #AI_A2A_CAP self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2A_CAP] __Fired - -- @param #AI_A2A_CAP self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_A2A_CAP] OnBeforeDestroy - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_A2A_CAP] OnAfterDestroy - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2A_CAP] Destroy - -- @param #AI_A2A_CAP self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2A_CAP] __Destroy - -- @param #AI_A2A_CAP self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_A2A_CAP] OnBeforeAbort - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_A2A_CAP] OnAfterAbort - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2A_CAP] Abort - -- @param #AI_A2A_CAP self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2A_CAP] __Abort - -- @param #AI_A2A_CAP self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2A_CAP] OnBeforeAccomplish - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2A_CAP] OnAfterAccomplish - -- @param #AI_A2A_CAP self - -- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2A_CAP] Accomplish - -- @param #AI_A2A_CAP self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2A_CAP] __Accomplish - -- @param #AI_A2A_CAP self - -- @param #number Delay The delay in seconds. - - return self end --- onafter State Transition for Event Patrol. @@ -289,199 +155,58 @@ end -- @param #string To The To State string. function AI_A2A_CAP:onafterStart( AICap, From, Event, To ) - self:GetParent( self ).onafterStart( self, AICap, From, Event, To ) + self:GetParent( self, AI_A2A_CAP ).onafterStart( self, AICap, From, Event, To ) AICap:HandleEvent( EVENTS.Takeoff, nil, self ) end ---- Set the Engage Zone which defines where the AI will engage bogies. +--- Set the Engage Zone which defines where the AI will engage bogies. -- @param #AI_A2A_CAP self -- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP. -- @return #AI_A2A_CAP self function AI_A2A_CAP:SetEngageZone( EngageZone ) self:F2() - if EngageZone then + if EngageZone then self.EngageZone = EngageZone else self.EngageZone = nil end end ---- Set the Engage Range when the AI will engage with airborne enemies. +--- Set the Engage Range when the AI will engage with airborne enemies. -- @param #AI_A2A_CAP self -- @param #number EngageRange The Engage Range. -- @return #AI_A2A_CAP self function AI_A2A_CAP:SetEngageRange( EngageRange ) self:F2() - if EngageRange then + if EngageRange then self.EngageRange = EngageRange else self.EngageRange = nil end end ---- onafter State Transition for Event Patrol. +--- Evaluate the attack and create an AttackUnitTask list. -- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_CAP:onafterPatrol( AICap, From, Event, To ) +-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. +-- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param #number EngageAltitude The altitude to engage the targets. +-- @return #AI_A2A_CAP self +function AI_A2A_CAP:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) - -- Call the parent Start event handler - self:GetParent(self).onafterPatrol( self, AICap, From, Event, To ) - self:HandleEvent( EVENTS.Dead ) + local AttackUnitTasks = {} -end - --- todo: need to fix this global function - ---- @param Wrapper.Group#GROUP AICap -function AI_A2A_CAP.AttackRoute( AICap, Fsm ) - - AICap:F( { "AI_A2A_CAP.AttackRoute:", AICap:GetName() } ) - - if AICap:IsAlive() then - Fsm:__Engage( 0.5 ) - end -end - ---- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_CAP:onbeforeEngage( AICap, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_CAP:onafterAbort( AICap, From, Event, To ) - AICap:ClearTasks() - self:__Route( 0.5 ) -end - - ---- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The AICap Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_CAP:onafterEngage( AICap, From, Event, To, AttackSetUnit ) - - self:F( { AICap, From, Event, To, AttackSetUnit} ) - - self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT - - local FirstAttackUnit = self.AttackSetUnit:GetFirst() -- Wrapper.Unit#UNIT - - if FirstAttackUnit and FirstAttackUnit:IsAlive() then -- If there is no attacker anymore, stop the engagement. - - if AICap:IsAlive() then - - local EngageRoute = {} - - --- Calculate the target route point. - local CurrentCoord = AICap:GetCoordinate() - local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() - local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) - local ToInterceptAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) - - --- Create a route point of type air. - local ToPatrolRoutePoint = CurrentCoord:Translate( 5000, ToInterceptAngle ):WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - self:F( { Angle = ToInterceptAngle, ToTargetSpeed = ToTargetSpeed } ) - self:T2( { self.MinSpeed, self.MaxSpeed, ToTargetSpeed } ) - - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - - local AttackTasks = {} - - for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do - local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT - self:T( { "Attacking Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) - if AttackUnit:IsAlive() and AttackUnit:IsAir() then - AttackTasks[#AttackTasks+1] = AICap:TaskAttackUnit( AttackUnit ) - end - end - - if #AttackTasks == 0 then - self:E("No targets found -> Going back to Patrolling") - self:__Abort( 0.5 ) - else - AICap:OptionROEOpenFire() - AICap:OptionROTEvadeFire() - - AttackTasks[#AttackTasks+1] = AICap:TaskFunction( "AI_A2A_CAP.AttackRoute", self ) - EngageRoute[#EngageRoute].task = AICap:TaskCombo( AttackTasks ) - end - - AICap:Route( EngageRoute, 0.5 ) + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT + if AttackUnit and AttackUnit:IsAlive() and AttackUnit:IsAir() then + -- TODO: Add coalition check? Only attack units of if AttackUnit:GetCoalition()~=AICap:GetCoalition() + -- Maybe the detected set also contains + self:T( { "Attacking Task:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) end - else - self:E("No targets found -> Going back to Patrolling") - self:__Abort( 0.5 ) end -end - ---- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_CAP:onafterAccomplish( AICap, From, Event, To ) - self.Accomplished = true - self:SetDetectionOff() -end - ---- @param #AI_A2A_CAP self --- @param Wrapper.Group#GROUP AICap The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param Core.Event#EVENTDATA EventData -function AI_A2A_CAP:onafterDestroy( AICap, From, Event, To, EventData ) - - if EventData.IniUnit then - self.AttackUnits[EventData.IniUnit] = nil - end -end - ---- @param #AI_A2A_CAP self --- @param Core.Event#EVENTDATA EventData -function AI_A2A_CAP:OnEventDead( EventData ) - self:F( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then - self:__Destroy( 1, EventData ) - end - end -end - ---- @param Wrapper.Group#GROUP AICap -function AI_A2A_CAP.Resume( AICap, Fsm ) - - AICap:I( { "AI_A2A_CAP.Resume:", AICap:GetName() } ) - if AICap:IsAlive() then - Fsm:__Reset( 1 ) - Fsm:__Route( 5 ) - end - + + return AttackUnitTasks end diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index a884daf88..cdf8d52ad 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -1,9 +1,9 @@ --- **AI** - (R2.2) - Manages the process of an automatic A2A defense system based on an EWR network targets and coordinating CAP and GCI. --- +-- -- === --- +-- -- Features: --- +-- -- * Setup quickly an A2A defense system for a coalition. -- * Setup (CAP) Control Air Patrols at defined zones to enhance your A2A defenses. -- * Setup (GCI) Ground Control Intercept at defined airbases to enhance your A2A defenses. @@ -18,170 +18,170 @@ -- * Setup specific settings for specific squadrons. -- * Quickly setup an A2A defense system using @{#AI_A2A_GCICAP}. -- * Setup a more advanced defense system using @{#AI_A2A_DISPATCHER}. --- +-- -- === --- +-- -- ## Missions: --- +-- -- [AID-A2A - AI A2A Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2A%20-%20AI%20A2A%20Dispatching) --- +-- -- === --- +-- -- ## YouTube Channel: --- +-- -- [DCS WORLD - MOOSE - A2A GCICAP - Build an automatic A2A Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) --- +-- -- === --- +-- -- # QUICK START GUIDE --- +-- -- There are basically two classes available to model an A2A defense system. --- +-- -- AI\_A2A\_DISPATCHER is the main A2A defense class that models the A2A defense system. -- AI\_A2A\_GCICAP derives or inherits from AI\_A2A\_DISPATCHER and is a more **noob** user friendly class, but is less flexible. --- +-- -- Before you start using the AI\_A2A\_DISPATCHER or AI\_A2A\_GCICAP ask youself the following questions. --- +-- -- ## 0. Do I need AI\_A2A\_DISPATCHER or do I need AI\_A2A\_GCICAP? --- +-- -- AI\_A2A\_GCICAP, automates a lot of the below questions using the mission editor and requires minimal lua scripting. -- But the AI\_A2A\_GCICAP provides less flexibility and a lot of options are defaulted. -- With AI\_A2A\_DISPATCHER you can setup a much more **fine grained** A2A defense mechanism, but some more (easy) lua scripting is required. --- +-- -- ## 1. Which Coalition am I modeling an A2A defense system for? blue or red? --- +-- -- One AI\_A2A\_DISPATCHER object can create a defense system for **one coalition**, which is blue or red. -- If you want to create a **mutual defense system**, for both blue and red, then you need to create **two** AI\_A2A\_DISPATCHER **objects**, -- each governing their defense system. --- --- +-- +-- -- ## 2. Which type of EWR will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow). --- +-- -- The MOOSE framework leverages the @{Detection} classes to perform the EWR detection. -- Several types of @{Detection} classes exist, and the most common characteristics of these classes is that they: --- +-- -- * Perform detections from multiple FACs as one co-operating entity. -- * Communicate with a Head Quarters, which consolidates each detection. -- * Groups detections based on a method (per area, per type or per unit). -- * Communicates detections. --- +-- -- ## 3. Which EWR units will be used as part of the detection system? Only Ground or also Airborne? --- --- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. +-- +-- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). -- Additionally, ANY other radar capable unit can be part of the EWR network! Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. --- The position of these units is very important as they need to provide enough coverage +-- The position of these units is very important as they need to provide enough coverage -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. --- +-- -- ## 4. Is a border required? --- --- Is this a cold car or a hot war situation? In case of a cold war situation, a border can be set that will only trigger defenses +-- +-- Is this a cold war or a hot war situation? In case of a cold war situation, a border can be set that will only trigger defenses -- if the border is crossed by enemy units. --- +-- -- ## 5. What maximum range needs to be checked to allow defenses to engage any attacker? --- +-- -- A good functioning defense will have a "maximum range" evaluated to the enemy when CAP will be engaged or GCI will be spawned. --- +-- -- ## 6. Which Airbases, Carrier Ships, Farps will take part in the defense system for the Coalition? --- +-- -- Carefully plan which airbases will take part in the coalition. Color each airbase in the color of the coalition. --- +-- -- ## 7. Which Squadrons will I create and which name will I give each Squadron? --- +-- -- The defense system works with Squadrons. Each Squadron must be given a unique name, that forms the **key** to the defense system. -- Several options and activities can be set per Squadron. --- +-- -- ## 8. Where will the Squadrons be located? On Airbases? On Carrier Ships? On Farps? --- +-- -- Squadrons are placed as the "home base" on an airfield, carrier or farp. -- Carefully plan where each Squadron will be located as part of the defense system. --- +-- -- ## 9. Which plane models will I assign for each Squadron? Do I need one plane model or more plane models per squadron? --- +-- -- Per Squadron, one or multiple plane models can be allocated as **Templates**. -- These are late activated groups with one airplane or helicopter that start with a specific name, called the **template prefix**. -- The A2A defense system will select from the given templates a random template to spawn a new plane (group). --- +-- -- ## 10. Which payloads, skills and skins will these plane models have? --- --- Per Squadron, even if you have one plane model, you can still allocate multiple templates of one plane model, --- each having different payloads, skills and skins. +-- +-- Per Squadron, even if you have one plane model, you can still allocate multiple templates of one plane model, +-- each having different payloads, skills and skins. -- The A2A defense system will select from the given templates a random template to spawn a new plane (group). --- +-- -- ## 11. For each Squadron, which will perform CAP? --- +-- -- Per Squadron, evaluate which Squadrons will perform CAP. -- Not all Squadrons need to perform CAP. --- +-- -- ## 12. For each Squadron doing CAP, in which ZONE(s) will the CAP be performed? --- +-- -- Per CAP, evaluate **where** the CAP will be performed, in other words, define the **zone**. -- Near the border or a bit further away? --- +-- -- ## 13. For each Squadron doing CAP, which zone types will I create? --- +-- -- Per CAP zone, evaluate whether you want: --- +-- -- * simple trigger zones -- * polygon zones -- * moving zones --- +-- -- Depending on the type of zone selected, a different @{Zone} object needs to be created from a ZONE_ class. --- +-- -- ## 14. For each Squadron doing CAP, what are the time intervals and CAP amounts to be performed? --- +-- -- For each CAP: --- +-- -- * **How many** CAP you want to have airborne at the same time? -- * **How frequent** you want the defense mechanism to check whether to start a new CAP? --- +-- -- ## 15. For each Squadron, which will perform GCI? --- +-- -- For each Squadron, evaluate which Squadrons will perform GCI? -- Not all Squadrons need to perform GCI. --- +-- -- ## 16. For each Squadron, which takeoff method will I use? --- +-- -- For each Squadron, evaluate which takeoff method will be used: --- +-- -- * Straight from the air -- * From the runway -- * From a parking spot with running engines -- * From a parking spot with cold engines --- +-- -- **The default takeoff method is staight in the air.** --- +-- -- ## 17. For each Squadron, which landing method will I use? --- +-- -- For each Squadron, evaluate which landing method will be used: --- +-- -- * Despawn near the airbase when returning -- * Despawn after landing on the runway -- * Despawn after engine shutdown after landing --- +-- -- **The default landing method is despawn when near the airbase when returning.** --- +-- -- ## 18. For each Squadron, which overhead will I use? --- +-- -- For each Squadron, depending on the airplane type (modern, old) and payload, which overhead is required to provide any defense? -- In other words, if **X** attacker airplanes are detected, how many **Y** defense airplanes need to be spawned per squadron? -- The **Y** is dependent on the type of airplane (era), payload, fuel levels, skills etc. -- The overhead is a **factor** that will calculate dynamically how many **Y** defenses will be required based on **X** attackers detected. --- +-- -- **The default overhead is 1. A value greater than 1, like 1.5 will increase the overhead with 50%, a value smaller than 1, like 0.5 will decrease the overhead with 50%.** --- +-- -- ## 19. For each Squadron, which grouping will I use? --- +-- -- When multiple targets are detected, how will defense airplanes be grouped when multiple defense airplanes are spawned for multiple attackers? -- Per one, two, three, four? --- +-- -- **The default grouping is 1. That means, that each spawned defender will act individually.** --- +-- -- === --- +-- -- ### Authors: **FlightControl** rework of GCICAP + introduction of new concepts (squadrons). -- ### Authors: **Stonehouse**, **SNAFU** in terms of the advice, documentation, and the original GCICAP script. --- +-- -- @module AI.AI_A2A_Dispatcher -- @image AI_Air_To_Air_Dispatching.JPG @@ -193,638 +193,657 @@ do -- AI_A2A_DISPATCHER -- @type AI_A2A_DISPATCHER -- @extends Tasking.DetectionManager#DETECTION_MANAGER - --- Create an automatic air defence system for a coalition. - -- + --- Create an automatic air defence system for a coalition. + -- -- === - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia3.JPG) - -- - -- It includes automatic spawning of Combat Air Patrol aircraft (CAP) and Ground Controlled Intercept aircraft (GCI) in response to enemy air movements that are detected by a ground based radar network. + -- + -- It includes automatic spawning of Combat Air Patrol aircraft (CAP) and Ground Controlled Intercept aircraft (GCI) in response to enemy air movements that are detected by a ground based radar network. -- CAP flights will take off and proceed to designated CAP zones where they will remain on station until the ground radars direct them to intercept detected enemy aircraft or they run short of fuel and must return to base (RTB). When a CAP flight leaves their zone to perform an interception or return to base a new CAP flight will spawn to take their place. -- If all CAP flights are engaged or RTB then additional GCI interceptors will scramble to intercept unengaged enemy aircraft under ground radar control. - -- With a little time and with a little work it provides the mission designer with a convincing and completely automatic air defence system. + -- With a little time and with a little work it provides the mission designer with a convincing and completely automatic air defence system. -- In short it is a plug in very flexible and configurable air defence module for DCS World. - -- + -- -- Note that in order to create a two way A2A defense system, two AI\_A2A\_DISPATCHER defense system may need to be created, for each coalition one. -- This is a good implementation, because maybe in the future, more coalitions may become available in DCS world. - -- + -- -- === -- -- # USAGE GUIDE - -- + -- -- ## 1. AI\_A2A\_DISPATCHER constructor: - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_1.JPG) - -- - -- + -- + -- -- The @{#AI_A2A_DISPATCHER.New}() method creates a new AI\_A2A\_DISPATCHER instance. - -- + -- -- ### 1.1. Define the **EWR network**: - -- + -- -- As part of the AI\_A2A\_DISPATCHER :New() constructor, an EWR network must be given as the first parameter. -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia5.JPG) - -- - -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. + -- + -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). -- Additionally, ANY other radar capable unit can be part of the EWR network! Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. - -- The position of these units is very important as they need to provide enough coverage + -- The position of these units is very important as they need to provide enough coverage -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia7.JPG) - -- - -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. - -- For example if they are a long way forward and can detect enemy planes on the ground and taking off - -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. - -- Having the radars further back will mean a slower escalation because fewer targets will be detected and - -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. - -- It all depends on what the desired effect is. - -- + -- + -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. + -- For example if they are a long way forward and can detect enemy planes on the ground and taking off + -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. + -- Having the radars further back will mean a slower escalation because fewer targets will be detected and + -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. + -- It all depends on what the desired effect is. + -- -- EWR networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection#DETECTION_BASE} object that is given as the input parameter of the AI\_A2A\_DISPATCHER class. - -- By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, + -- By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, -- increasing or decreasing the radar coverage of the Early Warning System. - -- + -- -- See the following example to setup an EWR network containing EWR stations and AWACS. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_2.JPG) -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_3.JPG) - -- + -- -- -- Define a SET_GROUP object that builds a collection of groups that define the EWR network. -- -- Here we build the network with all the groups that have a name starting with DF CCCP AWACS and DF CCCP EWR. -- DetectionSetGroup = SET_GROUP:New() -- DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } ) -- DetectionSetGroup:FilterStart() - -- + -- -- -- Setup the detection and group targets to a 30km range! -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) -- -- -- Setup the A2A dispatcher, and initialize it. -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **DetectionSetGroup**. -- **DetectionSetGroup** is then being configured to filter all active groups with a group name starting with **DF CCCP AWACS** or **DF CCCP EWR** to be included in the Set. -- **DetectionSetGroup** is then being ordered to start the dynamic filtering. Note that any destroy or new spawn of a group with the above names will be removed or added to the Set. - -- + -- -- Then a new Detection object is created from the class DETECTION_AREAS. A grouping radius of 30000 is choosen, which is 30km. -- The **Detection** object is then passed to the @{#AI_A2A_DISPATCHER.New}() method to indicate the EWR network configuration and setup the A2A defense detection mechanism. - -- + -- -- You could build a **mutual defense system** like this: - -- + -- -- A2ADispatcher_Red = AI_A2A_DISPATCHER:New( EWR_Red ) -- A2ADispatcher_Blue = AI_A2A_DISPATCHER:New( EWR_Blue ) - -- - -- ### 2. Define the detected **target grouping radius**: - -- + -- + -- ### 1.2. Define the detected **target grouping radius**: + -- -- The target grouping radius is a property of the Detection object, that was passed to the AI\_A2A\_DISPATCHER object, but can be changed. -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. - -- Fast planes like in the 80s, need a larger radius than WWII planes. + -- Fast planes like in the 80s, need a larger radius than WWII planes. -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft. - -- + -- -- Note that detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate -- group being detected. This may result in additional GCI being started by the dispatcher! So don't make this value too small! - -- + -- -- ## 3. Set the **Engage Radius**: - -- - -- Define the **Engage Radius** to **engage any target by airborne friendlies**, + -- + -- Define the **Engage Radius** to **engage any target by airborne friendlies**, -- which are executing **cap** or **returning** from an intercept mission. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia10.JPG) - -- - -- If there is a target area detected and reported, - -- then any friendlies that are airborne near this target area, + -- + -- If there is a target area detected and reported, + -- then any friendlies that are airborne near this target area, -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). - -- - -- For example, if **50000** or **50km** is given as a value, then any friendly that is airborne within **50km** from the detected target, + -- + -- For example, if **50000** or **50km** is given as a value, then any friendly that is airborne within **50km** from the detected target, -- will be considered to receive the command to engage that target area. - -- + -- -- You need to evaluate the value of this parameter carefully: - -- + -- -- * If too small, more intercept missions may be triggered upon detected target areas. -- * If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far. - -- - -- The **default** Engage Radius is defined as **100000** or **100km**. + -- + -- The **default** Engage Radius is defined as **100000** or **100km**. -- Use the method @{#AI_A2A_DISPATCHER.SetEngageRadius}() to set a specific Engage Radius. -- **The Engage Radius is defined for ALL squadrons which are operational.** - -- + -- -- Demonstration Mission: [AID-019 - AI_A2A - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-019%20-%20AI_A2A%20-%20Engage%20Range%20Test) - -- + -- -- In this example an Engage Radius is set to various values. - -- + -- -- -- Set 50km as the radius to engage any target by airborne friendlies. -- A2ADispatcher:SetEngageRadius( 50000 ) - -- + -- -- -- Set 100km as the radius to engage any target by airborne friendlies. -- A2ADispatcher:SetEngageRadius() -- 100000 is the default value. - -- - -- + -- + -- -- ## 4. Set the **Ground Controlled Intercept Radius** or **Gci radius**: - -- + -- -- When targets are detected that are still really far off, you don't want the AI_A2A_DISPATCHER to launch intercepts just yet. - -- You want it to wait until a certain Gci range is reached, which is the **distance of the closest airbase to target** + -- You want it to wait until a certain Gci range is reached, which is the **distance of the closest airbase to target** -- being **smaller** than the **Ground Controlled Intercept radius** or **Gci radius**. - -- - -- The **default** Gci radius is defined as **200000** or **200km**. Override the default Gci radius when the era of the warfare is early, or, + -- + -- The **default** Gci radius is defined as **200000** or **200km**. Override the default Gci radius when the era of the warfare is early, or, -- when you don't want to let the AI_A2A_DISPATCHER react immediately when a certain border or area is not being crossed. - -- + -- -- Use the method @{#AI_A2A_DISPATCHER.SetGciRadius}() to set a specific controlled ground intercept radius. -- **The Ground Controlled Intercept radius is defined for ALL squadrons which are operational.** - -- + -- -- Demonstration Mission: [AID-013 - AI_A2A - Intercept Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-013%20-%20AI_A2A%20-%20Intercept%20Test) - -- + -- -- In these examples, the Gci Radius is set to various values: - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Set 100km as the radius to ground control intercept detected targets from the nearest airbase. -- A2ADispatcher:SetGciRadius( 100000 ) - -- + -- -- -- Set 200km as the radius to ground control intercept. -- A2ADispatcher:SetGciRadius() -- 200000 is the default value. - -- + -- -- ## 5. Set the **borders**: - -- - -- According to the tactical and strategic design of the mission broadly decide the shape and extent of red and blue territories. + -- + -- According to the tactical and strategic design of the mission broadly decide the shape and extent of red and blue territories. -- They should be laid out such that a border area is created between the two coalitions. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia4.JPG) - -- + -- -- **Define a border area to simulate a cold war scenario.** -- Use the method @{#AI_A2A_DISPATCHER.SetBorderZone}() to create a border zone for the dispatcher. - -- + -- -- A **cold war** is one where CAP aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. -- A **hot war** is one where CAP aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send CAP and GCI aircraft to attack it. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia9.JPG) - -- + -- -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Core.Zone#ZONE_BASE}. - -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than - -- it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. + -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than + -- it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. -- In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. - -- + -- -- Demonstration Mission: [AID-009 - AI_A2A - Border Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-009 - AI_A2A - Border Test) - -- + -- -- In this example a border is set for the CCCP A2A dispatcher: - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_4.JPG) - -- + -- -- -- Setup the A2A dispatcher, and initialize it. -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- -- -- Setup the border. - -- -- Initialize the dispatcher, setting up a border zone. This is a polygon, + -- -- Initialize the dispatcher, setting up a border zone. This is a polygon, -- -- which takes the waypoints of a late activated group with the name CCCP Border as the boundaries of the border area. -- -- Any enemy crossing this border will be engaged. - -- + -- -- CCCPBorderZone = ZONE_POLYGON:New( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) -- A2ADispatcher:SetBorderZone( CCCPBorderZone ) - -- - -- ## 6. Squadrons: - -- + -- + -- ## 6. Squadrons: + -- -- The AI\_A2A\_DISPATCHER works with **Squadrons**, that need to be defined using the different methods available. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, -- while defining which plane types are being used by the squadron and how many resources are available. - -- + -- -- Squadrons: - -- + -- -- * Have name (string) that is the identifier or key of the squadron. -- * Have specific plane types. -- * Are located at one airbase. -- * Optionally have a limited set of resources. The default is that squadrons have **unlimited resources**. - -- + -- -- The name of the squadron given acts as the **squadron key** in the AI\_A2A\_DISPATCHER:Squadron...() methods. - -- + -- -- Additionally, squadrons have specific configuration options to: - -- + -- -- * Control how new aircraft are taking off from the airfield (in the air, cold, hot, at the runway). -- * Control how returning aircraft are landing at the airfield (in the air near the airbase, after landing, after engine shutdown). -- * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped. -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned. - -- + -- -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft. - -- + -- -- This example defines a couple of squadrons. Note the templates defined within the Mission Editor. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_5.JPG) -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_6.JPG) - -- + -- -- -- Setup the squadrons. -- A2ADispatcher:SetSquadron( "Mineralnye", AIRBASE.Caucasus.Mineralnye_Vody, { "SQ CCCP SU-27" }, 20 ) -- A2ADispatcher:SetSquadron( "Maykop", AIRBASE.Caucasus.Maykop_Khanskaya, { "SQ CCCP MIG-31" }, 20 ) -- A2ADispatcher:SetSquadron( "Mozdok", AIRBASE.Caucasus.Mozdok, { "SQ CCCP MIG-31" }, 20 ) -- A2ADispatcher:SetSquadron( "Sochi", AIRBASE.Caucasus.Sochi_Adler, { "SQ CCCP SU-27" }, 20 ) -- A2ADispatcher:SetSquadron( "Novo", AIRBASE.Caucasus.Novorossiysk, { "SQ CCCP SU-27" }, 20 ) - -- + -- -- ### 6.1. Set squadron take-off methods - -- + -- -- Use the various SetSquadronTakeoff... methods to control how squadrons are taking-off from the airfield: - -- + -- -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details. -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield. - -- + -- -- **The default landing method is to spawn new aircraft directly in the air.** - -- + -- -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency. -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that: - -- + -- -- * aircraft will stop waiting for each other or for a landing aircraft before takeoff. -- * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other. -- * aircraft may collide at the airbase. -- * aircraft may be awaiting the landing of a plane currently in the air, but never lands ... - -- + -- -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs. -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues! - -- + -- -- This example sets the default takeoff method to be from the runway. -- And for a couple of squadrons overrides this default method. - -- + -- -- -- Setup the Takeoff methods - -- + -- -- -- The default takeoff -- A2ADispatcher:SetDefaultTakeOffFromRunway() - -- + -- -- -- The individual takeoff per squadron -- A2ADispatcher:SetSquadronTakeoff( "Mineralnye", AI_A2A_DISPATCHER.Takeoff.Air ) -- A2ADispatcher:SetSquadronTakeoffInAir( "Sochi" ) -- A2ADispatcher:SetSquadronTakeoffFromRunway( "Mozdok" ) -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "Maykop" ) - -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "Novo" ) - -- - -- + -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "Novo" ) + -- + -- -- ### 6.1. Set Squadron takeoff altitude when spawning new aircraft in the air. - -- + -- -- In the case of the @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() there is also an other parameter that can be applied. -- That is modifying or setting the **altitude** from where planes spawn in the air. -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAirAltitude}() to set the altitude for a specific squadron. -- The default takeoff altitude can be modified or set using the method @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAirAltitude}(). -- As part of the method @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() a parameter can be specified to set the takeoff altitude. -- If this parameter is not specified, then the default altitude will be used for the squadron. - -- + -- -- ### 6.2. Set squadron landing methods - -- + -- -- In analogy with takeoff, the landing methods are to control how squadrons land at the airfield: - -- + -- -- * @{#AI_A2A_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. - -- + -- -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the -- A2A defense system, as no new CAP or GCI planes can takeoff. -- Note that the method @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control. - -- + -- -- This example defines the default landing method to be at the runway. -- And for a couple of squadrons overrides this default method. - -- + -- -- -- Setup the Landing methods - -- + -- -- -- The default landing method -- A2ADispatcher:SetDefaultLandingAtRunway() - -- + -- -- -- The individual landing per squadron -- A2ADispatcher:SetSquadronLandingAtRunway( "Mineralnye" ) -- A2ADispatcher:SetSquadronLandingNearAirbase( "Sochi" ) -- A2ADispatcher:SetSquadronLandingAtEngineShutdown( "Mozdok" ) -- A2ADispatcher:SetSquadronLandingNearAirbase( "Maykop" ) -- A2ADispatcher:SetSquadronLanding( "Novo", AI_A2A_DISPATCHER.Landing.AtRunway ) - -- - -- + -- + -- -- ### 6.3. Set squadron grouping - -- + -- -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronGrouping}() to set the grouping of CAP or GCI flights that will take-off when spawned. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia12.JPG) - -- + -- -- In the case of GCI, the @{#AI_A2A_DISPATCHER.SetSquadronGrouping}() method has additional behaviour. When there aren't enough CAP flights airborne, a GCI will be initiated for the remaining - -- targets to be engaged. Depending on the grouping parameter, the spawned flights for GCI are grouped into this setting. - -- For example with a group setting of 2, if 3 targets are detected and cannot be engaged by CAP or any airborne flight, + -- targets to be engaged. Depending on the grouping parameter, the spawned flights for GCI are grouped into this setting. + -- For example with a group setting of 2, if 3 targets are detected and cannot be engaged by CAP or any airborne flight, -- a GCI needs to be started, the GCI flights will be grouped as follows: Group 1 of 2 flights and Group 2 of one flight! - -- + -- -- Even more ... If one target has been detected, and the overhead is 1.5, grouping is 1, then two groups of planes will be spawned, with one unit each! - -- + -- -- The **grouping value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense flights grouping when the tactical situation changes. - -- + -- -- ### 6.4. Overhead and Balance the effectiveness of the air defenses in case of GCI. - -- + -- -- The effectiveness can be set with the **overhead parameter**. This is a number that is used to calculate the amount of Units that dispatching command will allocate to GCI in surplus of detected amount of units. - -- The **default value** of the overhead parameter is 1.0, which means **equal balance**. - -- + -- The **default value** of the overhead parameter is 1.0, which means **equal balance**. + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia11.JPG) - -- + -- -- However, depending on the (type of) aircraft (strength and payload) in the squadron and the amount of resources available, this parameter can be changed. - -- + -- -- The @{#AI_A2A_DISPATCHER.SetSquadronOverhead}() method can be used to tweak the defense strength, - -- taking into account the plane types of the squadron. - -- + -- taking into account the plane types of the squadron. + -- -- For example, a MIG-31 with full long-distance A2A missiles payload, may still be less effective than a F-15C with short missiles... -- So in this case, one may want to use the @{#AI_A2A_DISPATCHER.SetOverhead}() method to allocate more defending planes as the amount of detected attacking planes. - -- The overhead must be given as a decimal value with 1 as the neutral value, which means that overhead values: - -- + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that overhead values: + -- -- * Higher than 1.0, for example 1.5, will increase the defense unit amounts. For 4 planes detected, 6 planes will be spawned. -- * Lower than 1, for example 0.75, will decrease the defense unit amounts. For 4 planes detected, only 3 planes will be spawned. - -- - -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group - -- multiplied by the Overhead and rounded up to the smallest integer. - -- + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- -- For example ... If one target has been detected, and the overhead is 1.5, grouping is 1, then two groups of planes will be spawned, with one unit each! - -- + -- -- The **overhead value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense overhead when the tactical situation changes. -- -- ## 6.5. Squadron fuel treshold. - -- + -- -- When an airplane gets **out of fuel** to a certain %-tage, which is by default **15% (0.15)**, there are two possible actions that can be taken: -- - The defender will go RTB, and will be replaced with a new defender if possible. -- - The defender will refuel at a tanker, if a tanker has been specified for the squadron. - -- + -- -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronFuelThreshold}() to set the **squadron fuel treshold** of spawned airplanes for all squadrons. - -- + -- -- ## 7. Setup a squadron for CAP - -- + -- -- ### 7.1. Set the CAP zones - -- + -- -- CAP zones are patrol areas where Combat Air Patrol (CAP) flights loiter until they either return to base due to low fuel or are assigned an interception task by ground control. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia6.JPG) - -- - -- * As the CAP flights wander around within the zone waiting to be tasked, these zones need to be large enough that the aircraft are not constantly turning + -- + -- * As the CAP flights wander around within the zone waiting to be tasked, these zones need to be large enough that the aircraft are not constantly turning -- but do not have to be big and numerous enough to completely cover a border. - -- + -- -- * CAP zones can be of any type, and are derived from the @{Core.Zone#ZONE_BASE} class. Zones can be @{Core.Zone#ZONE}, @{Core.Zone#ZONE_POLYGON}, @{Core.Zone#ZONE_UNIT}, @{Core.Zone#ZONE_GROUP}, etc. -- This allows to setup **static, moving and/or complex zones** wherein aircraft will perform the CAP. - -- - -- * Typically 20000-50000 metres width is used and they are spaced so that aircraft in the zone waiting for tasks don't have to far to travel to protect their coalitions important targets. - -- These targets are chosen as part of the mission design and might be an important airfield or town etc. - -- Zone size is also determined somewhat by territory size, plane types + -- + -- * Typically 20000-50000 metres width is used and they are spaced so that aircraft in the zone waiting for tasks don't have to far to travel to protect their coalitions important targets. + -- These targets are chosen as part of the mission design and might be an important airfield or town etc. + -- Zone size is also determined somewhat by territory size, plane types -- (eg WW2 aircraft might mean smaller zones or more zones because they are slower and take longer to intercept enemy aircraft). - -- - -- * In a **cold war** it is important to make sure a CAP zone doesn't intrude into enemy territory as otherwise CAP flights will likely cross borders + -- + -- * In a **cold war** it is important to make sure a CAP zone doesn't intrude into enemy territory as otherwise CAP flights will likely cross borders -- and spark a full scale conflict which will escalate rapidly. - -- - -- * CAP flights do not need to be in the CAP zone before they are "on station" and ready for tasking. - -- + -- + -- * CAP flights do not need to be in the CAP zone before they are "on station" and ready for tasking. + -- -- * Typically if a CAP flight is tasked and therefore leaves their zone empty while they go off and intercept their target another CAP flight will spawn to take their place. - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia7.JPG) - -- + -- -- The following example illustrates how CAP zones are coded: - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_8.JPG) - -- + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_8.JPG) + -- -- -- CAP Squadron execution. -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_7.JPG) - -- + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_7.JPG) + -- -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_9.JPG) - -- + -- + -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_9.JPG) + -- -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- + -- -- Note the different @{Zone} MOOSE classes being used to create zones of different types. Please click the @{Zone} link for more information about the different zone types. -- Zones can be circles, can be setup in the mission editor using trigger zones, but can also be setup in the mission editor as polygons and in this case GROUP objects are being used! - -- + -- -- ## 7.2. Set the squadron to execute CAP: - -- + -- -- The method @{#AI_A2A_DISPATCHER.SetSquadronCap}() defines a CAP execution for a squadron. - -- + -- -- Setting-up a CAP zone also requires specific parameters: - -- + -- -- * The minimum and maximum altitude -- * The minimum speed and maximum patrol speed -- * The minimum and maximum engage speed -- * The type of altitude measurement - -- - -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. - -- + -- + -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. + -- -- The @{#AI_A2A_DISPATCHER.SetSquadronCapInterval}() method specifies **how much** and **when** CAP flights will takeoff. - -- - -- It is recommended not to overload the air defense with CAP flights, as these will decrease the performance of the overall system. - -- + -- + -- It is recommended not to overload the air defense with CAP flights, as these will decrease the performance of the overall system. + -- -- For example, the following setup will create a CAP for squadron "Sochi": - -- + -- -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- + -- -- ## 7.3. Squadron tanker to refuel when executing CAP and defender is out of fuel. - -- + -- -- Instead of sending CAP to RTB when out of fuel, you can let CAP refuel in mid air using a tanker. -- This greatly increases the efficiency of your CAP operations. - -- + -- -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected. -- Then, use the method @{#AI_A2A_DISPATCHER.SetDefaultTanker}() to set the default tanker for the refuelling. -- You can also specify a specific tanker for refuelling for a squadron by using the method @{#AI_A2A_DISPATCHER.SetSquadronTanker}(). - -- + -- -- When the tanker specified is alive and in the air, the tanker will be used for refuelling. - -- + -- -- For example, the following setup will create a CAP for squadron "Gelend" with a refuel task for the squadron: - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_10.JPG) - -- + -- -- -- Define the CAP -- A2ADispatcher:SetSquadron( "Gelend", AIRBASE.Caucasus.Gelendzhik, { "SQ CCCP SU-30" }, 20 ) -- A2ADispatcher:SetSquadronCap( "Gelend", ZONE:New( "PatrolZoneGelend" ), 4000, 8000, 600, 800, 1000, 1300 ) - -- A2ADispatcher:SetSquadronCapInterval( "Gelend", 2, 30, 600, 1 ) + -- A2ADispatcher:SetSquadronCapInterval( "Gelend", 2, 30, 600, 1 ) -- A2ADispatcher:SetSquadronGci( "Gelend", 900, 1200 ) - -- + -- -- -- Setup the Refuelling for squadron "Gelend", at tanker (group) "TankerGelend" when the fuel in the tank of the CAP defenders is less than 80%. -- A2ADispatcher:SetSquadronFuelThreshold( "Gelend", 0.8 ) -- A2ADispatcher:SetSquadronTanker( "Gelend", "TankerGelend" ) - -- + -- + -- ## 7.4 Set up race track pattern + -- + -- By default, flights patrol randomly within the CAP zone. It is also possible to let them fly a race track pattern using the + -- @{#AI_A2A_DISPATCHER.SetDefaultCapRacetrack}(*LeglengthMin*, *LeglengthMax*, *HeadingMin*, *HeadingMax*, *DurationMin*, *DurationMax*) or + -- @{#AI_A2A_DISPATCHER.SetSquadronCapRacetrack}(*SquadronName*, *LeglengthMin*, *LeglengthMax*, *HeadingMin*, *HeadingMax*, *DurationMin*, *DurationMax*) functions. + -- The first function enables this for all squadrons, the latter only for specific squadrons. For example, + -- + -- -- Enable race track pattern for CAP squadron "Mineralnye". + -- A2ADispatcher:SetSquadronCapRacetrack("Mineralnye", 10000, 20000, 90, 180, 10*60, 20*60) + -- + -- In this case the squadron "Mineralnye" will a race track pattern at a random point in the CAP zone. The leg length will be randomly selected between 10,000 and 20,000 meters. The heading + -- of the race track will randomly selected between 90 (West to East) and 180 (North to South) degrees. + -- After a random duration between 10 and 20 minutes, the flight will get a new random orbit location. + -- + -- Note that all parameters except the squadron name are optional. If not specified, default values are taken. Speed and altitude are taken from the + -- + -- Also note that the center of the race track pattern is chosen randomly within the patrol zone and can be close the the boarder of the zone. Hence, it cannot be guaranteed that the + -- whole pattern lies within the patrol zone. + -- -- ## 8. Setup a squadron for GCI: - -- + -- -- The method @{#AI_A2A_DISPATCHER.SetSquadronGci}() defines a GCI execution for a squadron. - -- + -- -- Setting-up a GCI readiness also requires specific parameters: - -- + -- -- * The minimum speed and maximum patrol speed - -- + -- -- Essentially this controls how many flights of GCI aircraft can be active at any time. -- Note allowing large numbers of active GCI flights can adversely impact mission performance on low or medium specification hosts/servers. - -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders, + -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders, -- too short will mean that the intruders may have alraedy passed the ideal interception point! - -- + -- -- For example, the following setup will create a GCI for squadron "Sochi": - -- + -- -- A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) - -- + -- -- ## 9. Other configuration options - -- + -- -- ### 9.1. Set a tactical display panel: - -- + -- -- Every 30 seconds, a tactical display panel can be shown that illustrates what the status is of the different groups controlled by AI\_A2A\_DISPATCHER. -- Use the method @{#AI_A2A_DISPATCHER.SetTacticalDisplay}() to switch on the tactical display panel. The default will not show this panel. -- Note that there may be some performance impact if this panel is shown. - -- + -- -- ## 10. Defaults settings. - -- + -- -- This provides a good overview of the different parameters that are setup or hardcoded by default. -- For some default settings, a method is available that allows you to tweak the defaults. - -- + -- -- ## 10.1. Default takeoff method. - -- + -- -- The default **takeoff method** is set to **in the air**, which means that new spawned airplanes will be spawned directly in the air above the airbase by default. - -- + -- -- **The default takeoff method can be set for ALL squadrons that don't have an individual takeoff method configured.** - -- + -- -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoff}() is the generic configuration method to control takeoff by default from the air, hot, cold or from the runway. See the method for further details. -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffInAir}() will spawn by default new aircraft from the squadron directly in the air. -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffFromParkingCold}() will spawn by default new aircraft in without running engines at a parking spot at the airfield. -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffFromParkingHot}() will spawn by default new aircraft in with running engines at a parking spot at the airfield. -- * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffFromRunway}() will spawn by default new aircraft at the runway at the airfield. - -- + -- -- ## 10.2. Default landing method. - -- + -- -- The default **landing method** is set to **near the airbase**, which means that returning airplanes will be despawned directly in the air by default. - -- + -- -- The default landing method can be set for ALL squadrons that don't have an individual landing method configured. - -- + -- -- * @{#AI_A2A_DISPATCHER.SetDefaultLanding}() is the generic configuration method to control by default landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. -- * @{#AI_A2A_DISPATCHER.SetDefaultLandingNearAirbase}() will despawn by default the returning aircraft in the air when near the airfield. -- * @{#AI_A2A_DISPATCHER.SetDefaultLandingAtRunway}() will despawn by default the returning aircraft directly after landing at the runway. -- * @{#AI_A2A_DISPATCHER.SetDefaultLandingAtEngineShutdown}() will despawn by default the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. - -- + -- -- ## 10.3. Default overhead. - -- + -- -- The default **overhead** is set to **1**. That essentially means that there isn't any overhead set by default. - -- + -- -- The default overhead value can be set for ALL squadrons that don't have an individual overhead value configured. -- -- Use the @{#AI_A2A_DISPATCHER.SetDefaultOverhead}() method can be used to set the default overhead or defense strength for ALL squadrons. -- -- ## 10.4. Default grouping. - -- + -- -- The default **grouping** is set to **one airplane**. That essentially means that there won't be any grouping applied by default. - -- + -- -- The default grouping value can be set for ALL squadrons that don't have an individual grouping value configured. - -- + -- -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultGrouping}() to set the **default grouping** of spawned airplanes for all squadrons. - -- + -- -- ## 10.5. Default RTB fuel treshold. - -- + -- -- When an airplane gets **out of fuel** to a certain %-tage, which is **15% (0.15)**, it will go RTB, and will be replaced with a new airplane when applicable. - -- + -- -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultFuelThreshold}() to set the **default fuel treshold** of spawned airplanes for all squadrons. - -- + -- -- ## 10.6. Default RTB damage treshold. - -- + -- -- When an airplane is **damaged** to a certain %-tage, which is **40% (0.40)**, it will go RTB, and will be replaced with a new airplane when applicable. - -- + -- -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultDamageThreshold}() to set the **default damage treshold** of spawned airplanes for all squadrons. - -- + -- -- ## 10.7. Default settings for CAP. - -- + -- -- ### 10.7.1. Default CAP Time Interval. - -- + -- -- CAP is time driven, and will evaluate in random time intervals if a new CAP needs to be spawned. -- The **default CAP time interval** is between **180** and **600** seconds. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultCapTimeInterval}() to set the **default CAP time interval** of spawned airplanes for all squadrons. + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultCapTimeInterval}() to set the **default CAP time interval** of spawned airplanes for all squadrons. -- Note that you can still change the CAP limit and CAP time intervals for each CAP individually using the @{#AI_A2A_DISPATCHER.SetSquadronCapTimeInterval}() method. - -- + -- -- ### 10.7.2. Default CAP limit. - -- + -- -- Multiple CAP can be airborne at the same time for one squadron, which is controlled by the **CAP limit**. -- The **default CAP limit** is 1 CAP per squadron to be airborne at the same time. -- Note that the default CAP limit is used when a Squadron CAP is defined, and cannot be changed afterwards. -- So, ensure that you set the default CAP limit **before** you spawn the Squadron CAP. - -- - -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultCapTimeInterval}() to set the **default CAP time interval** of spawned airplanes for all squadrons. + -- + -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultCapTimeInterval}() to set the **default CAP time interval** of spawned airplanes for all squadrons. -- Note that you can still change the CAP limit and CAP time intervals for each CAP individually using the @{#AI_A2A_DISPATCHER.SetSquadronCapTimeInterval}() method. - -- + -- -- ## 10.7.3. Default tanker for refuelling when executing CAP. - -- + -- -- Instead of sending CAP to RTB when out of fuel, you can let CAP refuel in mid air using a tanker. -- This greatly increases the efficiency of your CAP operations. - -- + -- -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected. -- Then, use the method @{#AI_A2A_DISPATCHER.SetDefaultTanker}() to set the tanker for the dispatcher. -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultFuelThreshold}() to set the %-tage left in the defender airplane tanks when a refuel action is needed. - -- + -- -- When the tanker specified is alive and in the air, the tanker will be used for refuelling. - -- + -- -- For example, the following setup will set the default refuel tanker to "Tanker": - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_11.JPG) - -- + -- -- -- Define the CAP -- A2ADispatcher:SetSquadron( "Sochi", AIRBASE.Caucasus.Sochi_Adler, { "SQ CCCP SU-34" }, 20 ) -- A2ADispatcher:SetSquadronCap( "Sochi", ZONE:New( "PatrolZone" ), 4000, 8000, 600, 800, 1000, 1300 ) - -- A2ADispatcher:SetSquadronCapInterval("Sochi", 2, 30, 600, 1 ) + -- A2ADispatcher:SetSquadronCapInterval("Sochi", 2, 30, 600, 1 ) -- A2ADispatcher:SetSquadronGci( "Sochi", 900, 1200 ) - -- + -- -- -- Set the default tanker for refuelling to "Tanker", when the default fuel treshold has reached 90% fuel left. -- A2ADispatcher:SetDefaultFuelThreshold( 0.9 ) -- A2ADispatcher:SetDefaultTanker( "Tanker" ) - -- + -- -- ## 10.8. Default settings for GCI. - -- + -- -- ## 10.8.1. Optimal intercept point calculation. - -- - -- When intruders are detected, the intrusion path of the attackers can be monitored by the EWR. + -- + -- When intruders are detected, the intrusion path of the attackers can be monitored by the EWR. -- Although defender planes might be on standby at the airbase, it can still take some time to get the defenses up in the air if there aren't any defenses airborne. -- This time can easily take 2 to 3 minutes, and even then the defenders still need to fly towards the target, which takes also time. - -- + -- -- Therefore, an optimal **intercept point** is calculated which takes a couple of parameters: - -- + -- -- * The average bearing of the intruders for an amount of seconds. -- * The average speed of the intruders for an amount of seconds. -- * An assumed time it takes to get planes operational at the airbase. - -- + -- -- The **intercept point** will determine: - -- + -- -- * If there are any friendlies close to engage the target. These can be defenders performing CAP or defenders in RTB. -- * The optimal airbase from where defenders will takeoff for GCI. - -- + -- -- Use the method @{#AI_A2A_DISPATCHER.SetIntercept}() to modify the assumed intercept delay time to calculate a valid interception. - -- + -- -- ## 10.8.2. Default Disengage Radius. - -- + -- -- The radius to **disengage any target** when the **distance** of the defender to the **home base** is larger than the specified meters. -- The default Disengage Radius is **300km** (300000 meters). Note that the Disengage Radius is applicable to ALL squadrons! - -- + -- -- Use the method @{#AI_A2A_DISPATCHER.SetDisengageRadius}() to modify the default Disengage Radius to another distance setting. - -- + -- -- ## 11. Airbase capture: - -- + -- -- Different squadrons can be located at one airbase. -- If the airbase gets captured, that is, when there is an enemy unit near the airbase, and there aren't anymore friendlies at the airbase, the airbase will change coalition ownership. -- As a result, the GCI and CAP will stop! -- However, the squadron will still stay alive. Any airplane that is airborne will continue its operations until all airborne airplanes -- of the squadron will be destroyed. This to keep consistency of air operations not to confuse the players. - -- + -- -- ## 12. Q & A: - -- + -- -- ### 12.1. Which countries will be selected for each coalition? - -- - -- Which countries are assigned to a coalition influences which units are available to the coalition. - -- For example because the mission calls for a EWR radar on the blue side the Ukraine might be chosen as a blue country - -- so that the 55G6 EWR radar unit is available to blue. - -- Some countries assign different tasking to aircraft, for example Germany assigns the CAP task to F-4E Phantoms but the USA does not. - -- Therefore if F4s are wanted as a coalition's CAP or GCI aircraft Germany will need to be assigned to that coalition. - -- + -- + -- Which countries are assigned to a coalition influences which units are available to the coalition. + -- For example because the mission calls for a EWR radar on the blue side the Ukraine might be chosen as a blue country + -- so that the 55G6 EWR radar unit is available to blue. + -- Some countries assign different tasking to aircraft, for example Germany assigns the CAP task to F-4E Phantoms but the USA does not. + -- Therefore if F4s are wanted as a coalition's CAP or GCI aircraft Germany will need to be assigned to that coalition. + -- -- ### 12.2. Country, type, load out, skill and skins for CAP and GCI aircraft? - -- + -- -- * Note these can be from any countries within the coalition but must be an aircraft with one of the main tasks being "CAP". -- * Obviously skins which are selected must be available to all players that join the mission otherwise they will see a default skin. - -- * Load outs should be appropriate to a CAP mission eg perhaps drop tanks for CAP flights and extra missiles for GCI flights. + -- * Load outs should be appropriate to a CAP mission eg perhaps drop tanks for CAP flights and extra missiles for GCI flights. -- * These decisions will eventually lead to template aircraft units being placed as late activation units that the script will use as templates for spawning CAP and GCI flights. Up to 4 different aircraft configurations can be chosen for each coalition. The spawned aircraft will inherit the characteristics of the template aircraft. - -- * The selected aircraft type must be able to perform the CAP tasking for the chosen country. - -- - -- + -- * The selected aircraft type must be able to perform the CAP tasking for the chosen country. + -- + -- -- @field #AI_A2A_DISPATCHER AI_A2A_DISPATCHER = { ClassName = "AI_A2A_DISPATCHER", @@ -832,13 +851,41 @@ do -- AI_A2A_DISPATCHER } + --- Squadron data structure. + -- @type AI_A2A_DISPATCHER.Squadron + -- @field #string Name Name of the squadron. + -- @field #number ResourceCount Number of resources. + -- @field #string AirbaseName Name of the home airbase. + -- @field Wrapper.Airbase#AIRBASE Airbase The home airbase of the squadron. + -- @field #boolean Captured If true, airbase of the squadron was captured. + -- @field #table Resources Flight group resources Resources[TemplateID][GroupName] = SpawnGroup. + -- @field #boolean Uncontrolled If true, flight groups are spawned uncontrolled and later activated. + -- @field #table Gci GCI. + -- @field #number Overhead Squadron overhead. + -- @field #number Grouping Squadron flight group size. + -- @field #number Takeoff Takeoff type. + -- @field #number TakeoffAltitude Altitude in meters for spawn in air. + -- @field #number Landing Landing type. + -- @field #number FuelThreshold Fuel threshold [0,1] for RTB. + -- @field #string TankerName Name of the refuelling tanker. + -- @field #table Table of template group names of the squadron. + -- @field #table Spawn Table of spaws Core.Spawn#SPAWN. + -- @field #table TemplatePrefixes + -- @field #boolean Racetrack If true, CAP flights will perform a racetrack pattern rather than randomly patrolling the zone. + -- @field #number RacetrackLengthMin Min Length of race track in meters. Default 10,000 m. + -- @field #number RacetrackLengthMax Max Length of race track in meters. Default 15,000 m. + -- @field #number RacetrackHeadingMin Min heading of race track in degrees. Default 0 deg, i.e. from South to North. + -- @field #number RacetrackHeadingMax Max heading of race track in degrees. Default 180 deg, i.e. from North to South. + -- @field #number RacetrackDurationMin Min duration in seconds before the CAP flight changes its orbit position. Default never. + -- @field #number RacetrackDurationMax Max duration in seconds before the CAP flight changes its orbit position. Default never. + --- Enumerator for spawns at airbases -- @type AI_A2A_DISPATCHER.Takeoff -- @extends Wrapper.Group#GROUP.Takeoff - + --- @field #AI_A2A_DISPATCHER.Takeoff Takeoff AI_A2A_DISPATCHER.Takeoff = GROUP.Takeoff - + --- Defnes Landing location. -- @field Landing AI_A2A_DISPATCHER.Landing = { @@ -846,7 +893,7 @@ do -- AI_A2A_DISPATCHER AtRunway = 2, AtEngineShutdown = 3, } - + --- AI_A2A_DISPATCHER constructor. -- This is defining the A2A DISPATCHER for one coaliton. -- The Dispatcher works with a @{Functional.Detection#DETECTION_BASE} object that is taking of the detection of targets using the EWR units. @@ -855,33 +902,33 @@ do -- AI_A2A_DISPATCHER -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network. -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Setup the Detection, using DETECTION_AREAS. -- -- First define the SET of GROUPs that are defining the EWR network. -- -- Here with prefixes DF CCCP AWACS, DF CCCP EWR. -- DetectionSetGroup = SET_GROUP:New() -- DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } ) -- DetectionSetGroup:FilterStart() - -- + -- -- -- Define the DETECTION_AREAS, using the DetectionSetGroup, with a 30km grouping radius. -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- + -- function AI_A2A_DISPATCHER:New( Detection ) -- Inherits from DETECTION_MANAGER local self = BASE:Inherit( self, DETECTION_MANAGER:New( nil, Detection ) ) -- #AI_A2A_DISPATCHER - + self.Detection = Detection -- Functional.Detection#DETECTION_AREAS - + -- This table models the DefenderSquadron templates. self.DefenderSquadrons = {} -- The Defender Squadrons. self.DefenderSpawns = {} self.DefenderTasks = {} -- The Defenders Tasks. self.DefenderDefault = {} -- The Defender Default Settings over all Squadrons. - + -- TODO: Check detection through radar. self.Detection:FilterCategories( { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) --self.Detection:InitDetectRadar( true ) @@ -891,7 +938,7 @@ do -- AI_A2A_DISPATCHER self:SetGciRadius() self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds. self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km. - + self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Air ) self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above the ground. self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.NearAirbase ) @@ -901,10 +948,10 @@ do -- AI_A2A_DISPATCHER self:SetDefaultDamageThreshold( 0.4 ) -- When 40% of damage, go RTB. self:SetDefaultCapTimeInterval( 180, 600 ) -- Between 180 and 600 seconds. self:SetDefaultCapLimit( 1 ) -- Maximum one CAP per squadron. - - + + self:AddTransition( "Started", "Assign", "Started" ) - + --- OnAfter Transition Handler for Event Assign. -- @function [parent=#AI_A2A_DISPATCHER] OnAfterAssign -- @param #AI_A2A_DISPATCHER self @@ -914,7 +961,7 @@ do -- AI_A2A_DISPATCHER -- @param Tasking.Task_A2A#AI_A2A Task -- @param Wrapper.Unit#UNIT TaskUnit -- @param #string PlayerName - + self:AddTransition( "*", "CAP", "*" ) --- CAP Handler OnBefore for AI_A2A_DISPATCHER @@ -924,23 +971,23 @@ do -- AI_A2A_DISPATCHER -- @param #string Event -- @param #string To -- @return #boolean - + --- CAP Handler OnAfter for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] OnAfterCAP -- @param #AI_A2A_DISPATCHER self -- @param #string From -- @param #string Event -- @param #string To - + --- CAP Trigger for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] CAP -- @param #AI_A2A_DISPATCHER self - + --- CAP Asynchronous Trigger for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] __CAP -- @param #AI_A2A_DISPATCHER self -- @param #number Delay - + self:AddTransition( "*", "GCI", "*" ) --- GCI Handler OnBefore for AI_A2A_DISPATCHER @@ -950,85 +997,104 @@ do -- AI_A2A_DISPATCHER -- @param #string Event -- @param #string To -- @return #boolean - + --- GCI Handler OnAfter for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] OnAfterGCI -- @param #AI_A2A_DISPATCHER self -- @param #string From -- @param #string Event -- @param #string To - + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #number DefendersMissing Number of missing defenders. + -- @param #table DefenderFriendlies Friendly defenders. + --- GCI Trigger for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] GCI -- @param #AI_A2A_DISPATCHER self - + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #number DefendersMissing Number of missing defenders. + -- @param #table DefenderFriendlies Friendly defenders. + --- GCI Asynchronous Trigger for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] __GCI -- @param #AI_A2A_DISPATCHER self -- @param #number Delay - + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #number DefendersMissing Number of missing defenders. + -- @param #table DefenderFriendlies Friendly defenders. + self:AddTransition( "*", "ENGAGE", "*" ) - + --- ENGAGE Handler OnBefore for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] OnBeforeENGAGE -- @param #AI_A2A_DISPATCHER self -- @param #string From -- @param #string Event -- @param #string To + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #table Defenders Defenders table. -- @return #boolean - + --- ENGAGE Handler OnAfter for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] OnAfterENGAGE -- @param #AI_A2A_DISPATCHER self -- @param #string From -- @param #string Event -- @param #string To - + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #table Defenders Defenders table. + --- ENGAGE Trigger for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] ENGAGE -- @param #AI_A2A_DISPATCHER self - + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #table Defenders Defenders table. + --- ENGAGE Asynchronous Trigger for AI_A2A_DISPATCHER -- @function [parent=#AI_A2A_DISPATCHER] __ENGAGE -- @param #AI_A2A_DISPATCHER self -- @param #number Delay - - + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #table Defenders Defenders table. + + -- Subscribe to the CRASH event so that when planes are shot -- by a Unit from the dispatcher, they will be removed from the detection... -- This will avoid the detection to still "know" the shot unit until the next detection. -- Otherwise, a new intercept or engage may happen for an already shot plane! - - + + self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) - - + + self:HandleEvent( EVENTS.Land ) self:HandleEvent( EVENTS.EngineShutdown ) - + -- Handle the situation where the airbases are captured. self:HandleEvent( EVENTS.BaseCaptured ) - + self:SetTacticalDisplay( false ) - + self.DefenderCAPIndex = 0 - + self:__Start( 5 ) - + return self end - --- @param #AI_A2A_DISPATCHER self + --- On after "Start" event. + -- @param #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:onafterStart( From, Event, To ) self:GetParent( self, AI_A2A_DISPATCHER ).onafterStart( self, From, Event, To ) -- Spawn the resources. - for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do - DefenderSquadron.Resource = {} + for SquadronName,_DefenderSquadron in pairs( self.DefenderSquadrons ) do + local DefenderSquadron=_DefenderSquadron --#AI_A2A_DISPATCHER.Squadron + DefenderSquadron.Resources = {} if DefenderSquadron.ResourceCount then for Resource = 1, DefenderSquadron.ResourceCount do self:ParkDefender( DefenderSquadron ) @@ -1036,33 +1102,57 @@ do -- AI_A2A_DISPATCHER end end end - - --- @param #AI_A2A_DISPATCHER self + + --- Park defender. + -- @param #AI_A2A_DISPATCHER self + -- @param #AI_A2A_DISPATCHER.Squadron DefenderSquadron The squadron. function AI_A2A_DISPATCHER:ParkDefender( DefenderSquadron ) + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) + local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN + Spawn:InitGrouping( 1 ) + local SpawnGroup + if self:IsSquadronVisible( DefenderSquadron.Name ) then + + local Grouping=DefenderSquadron.Grouping or self.DefenderDefault.Grouping + + Grouping=1 + + Spawn:InitGrouping(Grouping) + SpawnGroup = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, SPAWN.Takeoff.Cold ) + local GroupName = SpawnGroup:GetName() + DefenderSquadron.Resources = DefenderSquadron.Resources or {} + DefenderSquadron.Resources[TemplateID] = DefenderSquadron.Resources[TemplateID] or {} DefenderSquadron.Resources[TemplateID][GroupName] = {} DefenderSquadron.Resources[TemplateID][GroupName] = SpawnGroup + + self.uncontrolled=self.uncontrolled or {} + self.uncontrolled[DefenderSquadron.Name]=self.uncontrolled[DefenderSquadron.Name] or {} + + table.insert(self.uncontrolled[DefenderSquadron.Name], {group=SpawnGroup, name=GroupName, grouping=Grouping}) end + end - --- @param #AI_A2A_DISPATCHER self + --- Event base captured. + -- @param #AI_A2A_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2A_DISPATCHER:OnEventBaseCaptured( EventData ) local AirbaseName = EventData.PlaceName -- The name of the airbase that was captured. - + self:I( "Captured " .. AirbaseName ) - + -- Now search for all squadrons located at the airbase, and sanatize them. for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do if Squadron.AirbaseName == AirbaseName then @@ -1073,13 +1163,15 @@ do -- AI_A2A_DISPATCHER end end - --- @param #AI_A2A_DISPATCHER self + --- Event dead or crash. + -- @param #AI_A2A_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2A_DISPATCHER:OnEventCrashOrDead( EventData ) - self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) + self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) end - --- @param #AI_A2A_DISPATCHER self + --- Event land. + -- @param #AI_A2A_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2A_DISPATCHER:OnEventLand( EventData ) self:F( "Landed" ) @@ -1095,7 +1187,7 @@ do -- AI_A2A_DISPATCHER self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() - self:ParkDefender( Squadron, Defender ) + self:ParkDefender( Squadron ) return end if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then @@ -1103,10 +1195,11 @@ do -- AI_A2A_DISPATCHER DefenderUnit:Destroy() return end - end + end end - - --- @param #AI_A2A_DISPATCHER self + + --- Event engine shutdown. + -- @param #AI_A2A_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2A_DISPATCHER:OnEventEngineShutdown( EventData ) local DefenderUnit = EventData.IniUnit @@ -1122,101 +1215,101 @@ do -- AI_A2A_DISPATCHER self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() - self:ParkDefender( Squadron, Defender ) + self:ParkDefender( Squadron ) end - end + end end - + --- Define the radius to engage any target by airborne friendlies, which are executing cap or returning from an intercept mission. - -- If there is a target area detected and reported, then any friendlies that are airborne near this target area, + -- If there is a target area detected and reported, then any friendlies that are airborne near this target area, -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). - -- - -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, + -- + -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, -- will be considered to receive the command to engage that target area. - -- + -- -- You need to evaluate the value of this parameter carefully: - -- + -- -- * If too small, more intercept missions may be triggered upon detected target areas. -- * If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far. - -- + -- -- **Use the method @{#AI_A2A_DISPATCHER.SetEngageRadius}() to modify the default Engage Radius for ALL squadrons.** - -- + -- -- Demonstration Mission: [AID-019 - AI_A2A - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-019%20-%20AI_A2A%20-%20Engage%20Range%20Test) - -- + -- -- @param #AI_A2A_DISPATCHER self -- @param #number EngageRadius (Optional, Default = 100000) The radius to report friendlies near the target. -- @return #AI_A2A_DISPATCHER -- @usage - -- + -- -- -- Set 50km as the radius to engage any target by airborne friendlies. -- A2ADispatcher:SetEngageRadius( 50000 ) - -- + -- -- -- Set 100km as the radius to engage any target by airborne friendlies. -- A2ADispatcher:SetEngageRadius() -- 100000 is the default value. - -- + -- function AI_A2A_DISPATCHER:SetEngageRadius( EngageRadius ) self.Detection:SetFriendliesRange( EngageRadius or 100000 ) - + return self end --- Define the radius to disengage any target when the distance to the home base is larger than the specified meters. -- @param #AI_A2A_DISPATCHER self - -- @param #number DisengageRadius (Optional, Default = 300000) The radius to disengage a target when too far from the home base. + -- @param #number DisengageRadius (Optional, Default = 300000) The radius in meters to disengage a target when too far from the home base. -- @return #AI_A2A_DISPATCHER -- @usage - -- + -- -- -- Set 50km as the Disengage Radius. -- A2ADispatcher:SetDisengageRadius( 50000 ) - -- + -- -- -- Set 100km as the Disengage Radius. -- A2ADispatcher:SetDisngageRadius() -- 300000 is the default value. - -- + -- function AI_A2A_DISPATCHER:SetDisengageRadius( DisengageRadius ) self.DisengageRadius = DisengageRadius or 300000 - + return self end - - + + --- Define the radius to check if a target can be engaged by an ground controlled intercept. -- When targets are detected that are still really far off, you don't want the AI_A2A_DISPATCHER to launch intercepts just yet. - -- You want it to wait until a certain Gci range is reached, which is the **distance of the closest airbase to target** + -- You want it to wait until a certain Gci range is reached, which is the **distance of the closest airbase to target** -- being **smaller** than the **Ground Controlled Intercept radius** or **Gci radius**. - -- - -- The **default** Gci radius is defined as **200000** or **200km**. Override the default Gci radius when the era of the warfare is early, or, + -- + -- The **default** Gci radius is defined as **200000** or **200km**. Override the default Gci radius when the era of the warfare is early, or, -- when you don't want to let the AI_A2A_DISPATCHER react immediately when a certain border or area is not being crossed. - -- + -- -- Use the method @{#AI_A2A_DISPATCHER.SetGciRadius}() to set a specific controlled ground intercept radius. -- **The Ground Controlled Intercept radius is defined for ALL squadrons which are operational.** - -- + -- -- Demonstration Mission: [AID-013 - AI_A2A - Intercept Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-013%20-%20AI_A2A%20-%20Intercept%20Test) - -- + -- -- @param #AI_A2A_DISPATCHER self -- @param #number GciRadius (Optional, Default = 200000) The radius to ground control intercept detected targets from the nearest airbase. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Set 100km as the radius to ground control intercept detected targets from the nearest airbase. -- A2ADispatcher:SetGciRadius( 100000 ) - -- + -- -- -- Set 200km as the radius to ground control intercept. -- A2ADispatcher:SetGciRadius() -- 200000 is the default value. - -- + -- function AI_A2A_DISPATCHER:SetGciRadius( GciRadius ) - self.GciRadius = GciRadius or 200000 - + self.GciRadius = GciRadius or 200000 + return self end - - - + + + --- Define a border area to simulate a **cold war** scenario. -- A **cold war** is one where CAP aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. -- A **hot war** is one where CAP aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send CAP and GCI aircraft to attack it. @@ -1224,31 +1317,31 @@ do -- AI_A2A_DISPATCHER -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. Set the noborders parameter to 1 -- @param #AI_A2A_DISPATCHER self -- @param Core.Zone#ZONE_BASE BorderZone An object derived from ZONE_BASE, or a list of objects derived from ZONE_BASE. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Set one ZONE_POLYGON object as the border for the A2A dispatcher. -- local BorderZone = ZONE_POLYGON( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) -- The GROUP object is a late activate helicopter unit. -- A2ADispatcher:SetBorderZone( BorderZone ) - -- + -- -- or - -- + -- -- -- Set two ZONE_POLYGON objects as the border for the A2A dispatcher. -- local BorderZone1 = ZONE_POLYGON( "CCCP Border1", GROUP:FindByName( "CCCP Border1" ) ) -- The GROUP object is a late activate helicopter unit. -- local BorderZone2 = ZONE_POLYGON( "CCCP Border2", GROUP:FindByName( "CCCP Border2" ) ) -- The GROUP object is a late activate helicopter unit. -- A2ADispatcher:SetBorderZone( { BorderZone1, BorderZone2 } ) - -- - -- + -- + -- function AI_A2A_DISPATCHER:SetBorderZone( BorderZone ) self.Detection:SetAcceptZones( BorderZone ) return self end - + --- Display a tactical report every 30 seconds about which aircraft are: -- * Patrolling -- * Engaging @@ -1258,42 +1351,42 @@ do -- AI_A2A_DISPATCHER -- * ... -- @param #AI_A2A_DISPATCHER self -- @param #boolean TacticalDisplay Provide a value of **true** to display every 30 seconds a tactical overview. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the Tactical Display for debug mode. -- A2ADispatcher:SetTacticalDisplay( true ) - -- + -- function AI_A2A_DISPATCHER:SetTacticalDisplay( TacticalDisplay ) - + self.TacticalDisplay = TacticalDisplay - + return self - end + end --- Set the default damage treshold when defenders will RTB. -- The default damage treshold is by default set to 40%, which means that when the airplane is 40% damaged, it will go RTB. -- @param #AI_A2A_DISPATCHER self -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the %-tage of the damage treshold before going RTB. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the default damage treshold. -- A2ADispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged. - -- + -- function AI_A2A_DISPATCHER:SetDefaultDamageThreshold( DamageThreshold ) - + self.DefenderDefault.DamageThreshold = DamageThreshold - + return self - end + end --- Set the default CAP time interval for squadrons, which will be used to determine a random CAP timing. @@ -1301,20 +1394,20 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_DISPATCHER self -- @param #number CapMinSeconds The minimum amount of seconds for the random time interval. -- @param #number CapMaxSeconds The maximum amount of seconds for the random time interval. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the default CAP time interval. -- A2ADispatcher:SetDefaultCapTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds. - -- + -- function AI_A2A_DISPATCHER:SetDefaultCapTimeInterval( CapMinSeconds, CapMaxSeconds ) - + self.DefenderDefault.CapMinSeconds = CapMinSeconds self.DefenderDefault.CapMaxSeconds = CapMaxSeconds - + return self end @@ -1323,82 +1416,95 @@ do -- AI_A2A_DISPATCHER -- The default CAP limit is 1 CAP, which means one CAP group being spawned. -- @param #AI_A2A_DISPATCHER self -- @param #number CapLimit The maximum amount of CAP that can be airborne at the same time for the squadron. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the default CAP limit. -- A2ADispatcher:SetDefaultCapLimit( 2 ) -- Maximum 2 CAP per squadron. - -- + -- function AI_A2A_DISPATCHER:SetDefaultCapLimit( CapLimit ) - + self.DefenderDefault.CapLimit = CapLimit - + return self - end - + end + --- Set intercept. + -- @param #AI_A2A_DISPATCHER self + -- @param #number InterceptDelay Delay in seconds before intercept. + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetIntercept( InterceptDelay ) - + self.DefenderDefault.InterceptDelay = InterceptDelay - + local Detection = self.Detection -- Functional.Detection#DETECTION_AREAS Detection:SetIntercept( true, InterceptDelay ) - + return self - end + end --- Calculates which AI friendlies are nearby the area -- @param #AI_A2A_DISPATCHER self - -- @param DetectedItem + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem -- @return #table A list of the friendlies nearby. function AI_A2A_DISPATCHER:GetAIFriendliesNearBy( DetectedItem ) - + local FriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem ) - + return FriendliesNearBy end - --- + --- Return the defender tasks table. -- @param #AI_A2A_DISPATCHER self + -- @return #table Defender tasks as table. function AI_A2A_DISPATCHER:GetDefenderTasks() return self.DefenderTasks or {} end - - --- + + --- Get defender task. -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @return #table Defender task. function AI_A2A_DISPATCHER:GetDefenderTask( Defender ) return self.DefenderTasks[Defender] end - --- + --- Get defender task FSM. -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @return Core.Fsm#FSM The FSM. function AI_A2A_DISPATCHER:GetDefenderTaskFsm( Defender ) return self:GetDefenderTask( Defender ).Fsm end - - --- + + --- Get target of defender. -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @return Target function AI_A2A_DISPATCHER:GetDefenderTaskTarget( Defender ) return self:GetDefenderTask( Defender ).Target end - + --- -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @return #string Squadron name of the defender task. function AI_A2A_DISPATCHER:GetDefenderTaskSquadronName( Defender ) return self:GetDefenderTask( Defender ).SquadronName end --- -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. function AI_A2A_DISPATCHER:ClearDefenderTask( Defender ) - if Defender:IsAlive() and self.DefenderTasks[Defender] then + if Defender and Defender:IsAlive() and self.DefenderTasks[Defender] then local Target = self.DefenderTasks[Defender].Target - local Message = "Clearing (" .. self.DefenderTasks[Defender].Type .. ") " - Message = Message .. Defender:GetName() + local Message = "Clearing (" .. self.DefenderTasks[Defender].Type .. ") " + Message = Message .. Defender:GetName() if Target then Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" end @@ -1410,14 +1516,15 @@ do -- AI_A2A_DISPATCHER --- -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. function AI_A2A_DISPATCHER:ClearDefenderTaskTarget( Defender ) - + local DefenderTask = self:GetDefenderTask( Defender ) - - if Defender:IsAlive() and DefenderTask then + + if Defender and Defender:IsAlive() and DefenderTask then local Target = DefenderTask.Target - local Message = "Clearing (" .. DefenderTask.Type .. ") " - Message = Message .. Defender:GetName() + local Message = "Clearing (" .. DefenderTask.Type .. ") " + Message = Message .. Defender:GetName() if Target then Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" end @@ -1427,8 +1534,8 @@ do -- AI_A2A_DISPATCHER DefenderTask.Target = nil end -- if Defender and DefenderTask then --- if DefenderTask.Fsm:Is( "Fuel" ) --- or DefenderTask.Fsm:Is( "LostControl") +-- if DefenderTask.Fsm:Is( "Fuel" ) +-- or DefenderTask.Fsm:Is( "LostControl") -- or DefenderTask.Fsm:Is( "Damaged" ) then -- self:ClearDefenderTask( Defender ) -- end @@ -1436,13 +1543,19 @@ do -- AI_A2A_DISPATCHER return self end - - --- + + --- Set defender task. -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName Name of the squadron. + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @param #table Type Type of the defender task + -- @param Core.Fsm#FSM Fsm The defender task FSM. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem Target The defender detected item. + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target ) - - self:F( { SquadronName = SquadronName, Defender = Defender:GetName() } ) - + + self:F( { SquadronName = SquadronName, Defender = Defender:GetName(), Type=Type, Target=Target } ) + self.DefenderTasks[Defender] = self.DefenderTasks[Defender] or {} self.DefenderTasks[Defender].Type = Type self.DefenderTasks[Defender].Fsm = Fsm @@ -1453,15 +1566,17 @@ do -- AI_A2A_DISPATCHER end return self end - - - --- + + + --- Set defender task target. -- @param #AI_A2A_DISPATCHER self - -- @param Wrapper.Group#GROUP AIGroup + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection The detection object. + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefenderTaskTarget( Defender, AttackerDetection ) - - local Message = "(" .. self.DefenderTasks[Defender].Type .. ") " - Message = Message .. Defender:GetName() + + local Message = "(" .. self.DefenderTasks[Defender].Type .. ") " + Message = Message .. Defender:GetName() Message = Message .. ( AttackerDetection and ( " target " .. AttackerDetection.Index .. " [" .. AttackerDetection.Set:Count() .. "]" ) ) or "" self:F( { AttackerDetection = Message } ) if AttackerDetection then @@ -1471,90 +1586,90 @@ do -- AI_A2A_DISPATCHER end - --- This is the main method to define Squadrons programmatically. + --- This is the main method to define Squadrons programmatically. -- Squadrons: - -- + -- -- * Have a **name or key** that is the identifier or key of the squadron. -- * Have **specific plane types** defined by **templates**. -- * Are **located at one specific airbase**. Multiple squadrons can be located at one airbase through. -- * Optionally have a limited set of **resources**. The default is that squadrons have unlimited resources. - -- + -- -- The name of the squadron given acts as the **squadron key** in the AI\_A2A\_DISPATCHER:Squadron...() methods. - -- + -- -- Additionally, squadrons have specific configuration options to: - -- + -- -- * Control how new aircraft are **taking off** from the airfield (in the air, cold, hot, at the runway). -- * Control how returning aircraft are **landing** at the airfield (in the air near the airbase, after landing, after engine shutdown). -- * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped. -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned. - -- + -- -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft. - -- + -- -- @param #AI_A2A_DISPATCHER self - -- - -- @param #string SquadronName A string (text) that defines the squadron identifier or the key of the Squadron. - -- It can be any name, for example `"104th Squadron"` or `"SQ SQUADRON1"`, whatever. - -- As long as you remember that this name becomes the identifier of your squadron you have defined. + -- + -- @param #string SquadronName A string (text) that defines the squadron identifier or the key of the Squadron. + -- It can be any name, for example `"104th Squadron"` or `"SQ SQUADRON1"`, whatever. + -- As long as you remember that this name becomes the identifier of your squadron you have defined. -- You need to use this name in other methods too! - -- - -- @param #string AirbaseName The airbase name where you want to have the squadron located. - -- You need to specify here EXACTLY the name of the airbase as you see it in the mission editor. - -- Examples are `"Batumi"` or `"Tbilisi-Lochini"`. + -- + -- @param #string AirbaseName The airbase name where you want to have the squadron located. + -- You need to specify here EXACTLY the name of the airbase as you see it in the mission editor. + -- Examples are `"Batumi"` or `"Tbilisi-Lochini"`. -- EXACTLY the airbase name, between quotes `""`. -- To ease the airbase naming when using the LDT editor and IntelliSense, the @{Wrapper.Airbase#AIRBASE} class contains enumerations of the airbases of each map. - -- + -- -- * Caucasus: @{Wrapper.Airbase#AIRBASE.Caucaus} -- * Nevada or NTTR: @{Wrapper.Airbase#AIRBASE.Nevada} -- * Normandy: @{Wrapper.Airbase#AIRBASE.Normandy} - -- - -- @param #string TemplatePrefixes A string or an array of strings specifying the **prefix names of the templates** (not going to explain what is templates here again). - -- Examples are `{ "104th", "105th" }` or `"104th"` or `"Template 1"` or `"BLUE PLANES"`. + -- + -- @param #string TemplatePrefixes A string or an array of strings specifying the **prefix names of the templates** (not going to explain what is templates here again). + -- Examples are `{ "104th", "105th" }` or `"104th"` or `"Template 1"` or `"BLUE PLANES"`. -- Just remember that your template (groups late activated) need to start with the prefix you have specified in your code. -- If you have only one prefix name for a squadron, you don't need to use the `{ }`, otherwise you need to use the brackets. - -- + -- -- @param #number ResourceCount (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available. - -- + -- -- @usage -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- @usage - -- -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock... + -- -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock... -- A2ADispatcher:SetSquadron( "Squadron1", "Batumi", "SQ1", 40 ) - -- + -- -- @usage -- -- This will create squadron "Sq 1" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" and has 20 planes in stock... -- -- Note that in this implementation, the A2A dispatcher will select a random plane type when a new plane (group) needs to be spawned for defenses. -- -- Note the usage of the {} for the airplane templates list. -- A2ADispatcher:SetSquadron( "Sq 1", "Batumi", { "Mig-29", "Su-27" }, 40 ) - -- + -- -- @usage -- -- This will create 2 squadrons "104th" and "23th" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" respectively and each squadron has 10 planes in stock... -- A2ADispatcher:SetSquadron( "104th", "Batumi", "Mig-29", 10 ) -- A2ADispatcher:SetSquadron( "23th", "Batumi", "Su-27", 10 ) - -- + -- -- @usage -- -- This is an example like the previous, but now with infinite resources. -- -- The ResourceCount parameter is not given in the SetSquadron method. -- A2ADispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) -- A2ADispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) - -- - -- - -- @return #AI_A2A_DISPATCHER + -- + -- + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) - - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - local DefenderSquadron = self.DefenderSquadrons[SquadronName] - + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] --#AI_A2A_DISPATCHER.Squadron + DefenderSquadron.Name = SquadronName DefenderSquadron.Airbase = AIRBASE:FindByName( AirbaseName ) DefenderSquadron.AirbaseName = DefenderSquadron.Airbase:GetName() if not DefenderSquadron.Airbase then error( "Cannot find airbase with name:" .. AirbaseName ) end - + DefenderSquadron.Spawn = {} if type( TemplatePrefixes ) == "string" then local SpawnTemplate = TemplatePrefixes @@ -1570,46 +1685,63 @@ do -- AI_A2A_DISPATCHER DefenderSquadron.TemplatePrefixes = TemplatePrefixes DefenderSquadron.Captured = false -- Not captured. This flag will be set to true, when the airbase where the squadron is located, is captured. + self:SetSquadronLanguage( SquadronName, "EN" ) -- Squadrons speak English by default. + self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) - + return self end - + --- Get an item from the Squadron table. -- @param #AI_A2A_DISPATCHER self - -- @return #table + -- @param #string SquadronName Name of the squadron. + -- @return #AI_A2A_DISPATCHER.Squadron Defender squadron table. function AI_A2A_DISPATCHER:GetSquadron( SquadronName ) local DefenderSquadron = self.DefenderSquadrons[SquadronName] - + if not DefenderSquadron then error( "Unknown Squadron:" .. SquadronName ) end - + return DefenderSquadron end - + --- Set the Squadron visible before startup of the dispatcher. -- All planes will be spawned as uncontrolled on the parking spot. -- They will lock the parking spot. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Set the Squadron visible before startup of dispatcher. -- A2ADispatcher:SetSquadronVisible( "Mineralnye" ) - -- + -- function AI_A2A_DISPATCHER:SetSquadronVisible( SquadronName ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - - local DefenderSquadron = self:GetSquadron( SquadronName ) - + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) --#AI_A2A_DISPATCHER.Squadron + DefenderSquadron.Uncontrolled = true - for SpawnTemplate, DefenderSpawn in pairs( self.DefenderSpawns ) do - DefenderSpawn:InitUnControlled() + -- For now, grouping is forced to 1 due to other parts of the class which would not work well with grouping>1. + DefenderSquadron.Grouping=1 + + -- Get free parking for fighter aircraft. + local nfreeparking=DefenderSquadron.Airbase:GetFreeParkingSpotsNumber(AIRBASE.TerminalType.FighterAircraft, true) + + -- Take number of free parking spots if no resource count was specifed. + DefenderSquadron.ResourceCount=DefenderSquadron.ResourceCount or nfreeparking + + -- Check that resource count is not larger than free parking spots. + DefenderSquadron.ResourceCount=math.min(DefenderSquadron.ResourceCount, nfreeparking) + + -- Set uncontrolled spawning option. + for SpawnTemplate,_DefenderSpawn in pairs( self.DefenderSpawns ) do + local DefenderSpawn=_DefenderSpawn --Core.Spawn#SPAWN + DefenderSpawn:InitUnControlled(true) end end @@ -1617,32 +1749,104 @@ do -- AI_A2A_DISPATCHER --- Check if the Squadron is visible before startup of the dispatcher. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @return #bool true if visible. + -- @return #boolean true if visible. -- @usage - -- + -- -- -- Set the Squadron visible before startup of dispatcher. -- local IsVisible = A2ADispatcher:IsSquadronVisible( "Mineralnye" ) - -- + -- function AI_A2A_DISPATCHER:IsSquadronVisible( SquadronName ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - - local DefenderSquadron = self:GetSquadron( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) --#AI_A2A_DISPATCHER.Squadron if DefenderSquadron then return DefenderSquadron.Uncontrolled == true end - + return nil - + + end + + --- Set a CAP for a Squadron. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed. + -- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. + -- @param #number EngageAltType The altitude type to engage, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed. + -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. + -- @param #number PatrolFloorAltitude The minimum altitude at which the cap can be executed. + -- @param #number PatrolCeilingAltitude the maximum altitude at which the cap can be executed. + -- @param #number PatrolAltType The altitude type to patrol, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2A_DISPATCHER + -- @usage + -- + -- -- CAP Squadron execution. + -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) + -- -- Setup a CAP, engaging between 800 and 900 km/h, altitude 30 (above the sea), radio altitude measurement, + -- -- patrolling speed between 500 and 600 km/h, altitude between 4000 and 10000 meters, barometric altitude measurement. + -- A2ADispatcher:SetSquadronCapV2( "Mineralnye", 800, 900, 30, 30, "RADIO", CAPZoneEast, 500, 600, 4000, 10000, "BARO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) + -- -- Setup a CAP, engaging between 800 and 1200 km/h, altitude between 4000 and 10000 meters, radio altitude measurement, + -- -- patrolling speed between 600 and 800 km/h, altitude between 4000 and 8000, barometric altitude measurement. + -- A2ADispatcher:SetSquadronCapV2( "Sochi", 800, 1200, 2000, 3000, "RADIO", CAPZoneWest, 600, 800, 4000, 8000, "BARO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) + -- + -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") + -- -- Setup a CAP, engaging between 800 and 1200 km/h, altitude between 5000 and 8000 meters, barometric altitude measurement, + -- -- patrolling speed between 600 and 800 km/h, altitude between 4000 and 8000, radio altitude. + -- A2ADispatcher:SetSquadronCapV2( "Maykop", 800, 1200, 5000, 8000, "BARO", CAPZoneMiddle, 600, 800, 4000, 8000, "RADIO" ) + -- A2ADispatcher:SetSquadronCapInterval( "Maykop", 2, 30, 120, 1 ) + -- + function AI_A2A_DISPATCHER:SetSquadronCap2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Cap = self.DefenderSquadrons[SquadronName].Cap + Cap.Name = SquadronName + Cap.EngageMinSpeed = EngageMinSpeed + Cap.EngageMaxSpeed = EngageMaxSpeed + Cap.EngageFloorAltitude = EngageFloorAltitude + Cap.EngageCeilingAltitude = EngageCeilingAltitude + Cap.Zone = Zone + Cap.PatrolMinSpeed = PatrolMinSpeed + Cap.PatrolMaxSpeed = PatrolMaxSpeed + Cap.PatrolFloorAltitude = PatrolFloorAltitude + Cap.PatrolCeilingAltitude = PatrolCeilingAltitude + Cap.PatrolAltType = PatrolAltType + Cap.EngageAltType = EngageAltType + + self:SetSquadronCapInterval( SquadronName, self.DefenderDefault.CapLimit, self.DefenderDefault.CapMinSeconds, self.DefenderDefault.CapMaxSeconds, 1 ) + + self:I( { CAP = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageAltType } } ) + + -- Add the CAP to the EWR network. + + local RecceSet = self.Detection:GetDetectionSet() + RecceSet:FilterPrefixes( DefenderSquadron.TemplatePrefixes ) + RecceSet:FilterStart() + + self.Detection:SetFriendlyPrefixes( DefenderSquadron.TemplatePrefixes ) + + return self end --- Set a CAP for a Squadron. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed. - -- @param #number FloorAltitude The minimum altitude at which the cap can be executed. - -- @param #number CeilingAltitude the maximum altitude at which the cap can be executed. + -- @param #number PatrolFloorAltitude The minimum altitude at which the cap can be executed. + -- @param #number PatrolCeilingAltitude the maximum altitude at which the cap can be executed. -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed. -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed. -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed. @@ -1650,54 +1854,26 @@ do -- AI_A2A_DISPATCHER -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. -- @return #AI_A2A_DISPATCHER -- @usage - -- + -- -- -- CAP Squadron execution. -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) - -- + -- -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- + -- -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- - function AI_A2A_DISPATCHER:SetSquadronCap( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} - self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} - - local DefenderSquadron = self:GetSquadron( SquadronName ) + -- + function AI_A2A_DISPATCHER:SetSquadronCap( SquadronName, Zone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) - local Cap = self.DefenderSquadrons[SquadronName].Cap - Cap.Name = SquadronName - Cap.Zone = Zone - Cap.FloorAltitude = FloorAltitude - Cap.CeilingAltitude = CeilingAltitude - Cap.PatrolMinSpeed = PatrolMinSpeed - Cap.PatrolMaxSpeed = PatrolMaxSpeed - Cap.EngageMinSpeed = EngageMinSpeed - Cap.EngageMaxSpeed = EngageMaxSpeed - Cap.AltType = AltType - - self:SetSquadronCapInterval( SquadronName, self.DefenderDefault.CapLimit, self.DefenderDefault.CapMinSeconds, self.DefenderDefault.CapMaxSeconds, 1 ) - - self:F( { CAP = { SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType } } ) - - -- Add the CAP to the EWR network. - - local RecceSet = self.Detection:GetDetectionSetGroup() - RecceSet:FilterPrefixes( DefenderSquadron.TemplatePrefixes ) - RecceSet:FilterStart() - - self.Detection:SetFriendlyPrefixes( DefenderSquadron.TemplatePrefixes ) - - return self + return self:SetSquadronCap2( SquadronName, EngageMinSpeed, EngageMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, AltType, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, AltType ) end - - --- Set the squadron CAP parameters. + + --- Set the squadron CAP parameters. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. -- @param #number CapLimit (optional) The maximum amount of CAP groups to be spawned. Note that a CAP is a group, so can consist out of 1 to 4 airplanes. The default is 1 CAP group. @@ -1706,23 +1882,23 @@ do -- AI_A2A_DISPATCHER -- @param #number Probability Is not in use, you can skip this parameter. -- @return #AI_A2A_DISPATCHER -- @usage - -- + -- -- -- CAP Squadron execution. -- CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) ) -- A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 ) -- A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 ) - -- + -- -- CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) ) -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- + -- -- CAPZoneMiddle = ZONE:New( "CAP Zone Middle") -- A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- + -- function AI_A2A_DISPATCHER:SetSquadronCapInterval( SquadronName, CapLimit, LowInterval, HighInterval, Probability ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -1733,32 +1909,32 @@ do -- AI_A2A_DISPATCHER Cap.HighInterval = HighInterval or 600 Cap.Probability = Probability or 1 Cap.CapLimit = CapLimit or 1 - Cap.Scheduler = Cap.Scheduler or SCHEDULER:New( self ) + Cap.Scheduler = Cap.Scheduler or SCHEDULER:New( self ) local Scheduler = Cap.Scheduler -- Core.Scheduler#SCHEDULER local ScheduleID = Cap.ScheduleID local Variance = ( Cap.HighInterval - Cap.LowInterval ) / 2 local Repeat = Cap.LowInterval + Variance local Randomization = Variance / Repeat local Start = math.random( 1, Cap.HighInterval ) - + if ScheduleID then Scheduler:Stop( ScheduleID ) end - + Cap.ScheduleID = Scheduler:Schedule( self, self.SchedulerCAP, { SquadronName }, Start, Repeat, Randomization ) else error( "This squadron does not exist:" .. SquadronName ) end end - + --- -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:GetCAPDelay( SquadronName ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} local DefenderSquadron = self:GetSquadron( SquadronName ) @@ -1771,22 +1947,22 @@ do -- AI_A2A_DISPATCHER end end - --- + --- Check if squadron can do CAP. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @return #table DefenderSquadron + -- @return #AI_A2A_DISPATCHER.Squadron DefenderSquadron function AI_A2A_DISPATCHER:CanCAP( SquadronName ) self:F({SquadronName = SquadronName}) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} local DefenderSquadron = self:GetSquadron( SquadronName ) if DefenderSquadron.Captured == false then -- We can only spawn new CAP if the base has not been captured. - + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. - + local Cap = DefenderSquadron.Cap if Cap then local CapCount = self:CountCapAirborne( SquadronName ) @@ -1804,20 +1980,74 @@ do -- AI_A2A_DISPATCHER end - --- + --- Set race track pattern as default when any squadron is performing CAP. + -- @param #AI_A2A_DISPATCHER self + -- @param #number LeglengthMin Min length of the race track leg in meters. Default 10,000 m. + -- @param #number LeglengthMax Max length of the race track leg in meters. Default 15,000 m. + -- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. counter clockwise from South to North. + -- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. counter clockwise from North to South. + -- @param #number DurationMin (Optional) Min duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. + -- @param #number DurationMax (Optional) Max duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. + -- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetDefaultCapRacetrack(LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) + + self.DefenderDefault.Racetrack=true + self.DefenderDefault.RacetrackLengthMin=LeglengthMin + self.DefenderDefault.RacetrackLengthMax=LeglengthMax + self.DefenderDefault.RacetrackHeadingMin=HeadingMin + self.DefenderDefault.RacetrackHeadingMax=HeadingMax + self.DefenderDefault.RacetrackDurationMin=DurationMin + self.DefenderDefault.RacetrackDurationMax=DurationMax + self.DefenderDefault.RacetrackCoordinates=CapCoordinates + + return self + end + + --- Set race track pattern when squadron is performing CAP. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName Name of the squadron. + -- @param #number LeglengthMin Min length of the race track leg in meters. Default 10,000 m. + -- @param #number LeglengthMax Max length of the race track leg in meters. Default 15,000 m. + -- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. from South to North. + -- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. from North to South. + -- @param #number DurationMin (Optional) Min duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. + -- @param #number DurationMax (Optional) Max duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage. + -- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. + -- @return #AI_A2A_DISPATCHER self + function AI_A2A_DISPATCHER:SetSquadronCapRacetrack(SquadronName, LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron then + DefenderSquadron.Racetrack=true + DefenderSquadron.RacetrackLengthMin=LeglengthMin + DefenderSquadron.RacetrackLengthMax=LeglengthMax + DefenderSquadron.RacetrackHeadingMin=HeadingMin + DefenderSquadron.RacetrackHeadingMax=HeadingMax + DefenderSquadron.RacetrackDurationMin=DurationMin + DefenderSquadron.RacetrackDurationMax=DurationMax + DefenderSquadron.RacetrackCoordinates=CapCoordinates + end + + return self + end + + + --- Check if squadron can do GCI. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. -- @return #table DefenderSquadron function AI_A2A_DISPATCHER:CanGCI( SquadronName ) self:F({SquadronName = SquadronName}) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci or {} local DefenderSquadron = self:GetSquadron( SquadronName ) if DefenderSquadron.Captured == false then -- We can only spawn new CAP if the base has not been captured. - + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. local Gci = DefenderSquadron.Gci if Gci then @@ -1828,67 +2058,98 @@ do -- AI_A2A_DISPATCHER return nil end - - --- + --- Set squadron GCI. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. - -- @param #number EngageMinSpeed The minimum speed at which the gci can be executed. - -- @param #number EngageMaxSpeed The maximum speed at which the gci can be executed. - -- @usage - -- + -- @param #number EngageMinSpeed The minimum speed [km/h] at which the GCI can be executed. + -- @param #number EngageMaxSpeed The maximum speed [km/h] at which the GCI can be executed. + -- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. + -- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". + -- @usage + -- -- -- GCI Squadron execution. - -- A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) - -- A2ADispatcher:SetSquadronGci( "Novo", 900, 2100 ) - -- A2ADispatcher:SetSquadronGci( "Maykop", 900, 1200 ) - -- + -- A2ADispatcher:SetSquadronGci2( "Mozdok", 900, 1200, 5000, 5000, "BARO" ) + -- A2ADispatcher:SetSquadronGci2( "Novo", 900, 2100, 30, 30, "RADIO" ) + -- A2ADispatcher:SetSquadronGci2( "Maykop", 900, 1200, 100, 300, "RADIO" ) + -- -- @return #AI_A2A_DISPATCHER - function AI_A2A_DISPATCHER:SetSquadronGci( SquadronName, EngageMinSpeed, EngageMaxSpeed ) - - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + function AI_A2A_DISPATCHER:SetSquadronGci2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci or {} - + local Intercept = self.DefenderSquadrons[SquadronName].Gci Intercept.Name = SquadronName Intercept.EngageMinSpeed = EngageMinSpeed Intercept.EngageMaxSpeed = EngageMaxSpeed - + Intercept.EngageFloorAltitude = EngageFloorAltitude + Intercept.EngageCeilingAltitude = EngageCeilingAltitude + Intercept.EngageAltType = EngageAltType + + self:I( { GCI = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + end + + --- Set squadron GCI. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed The minimum speed [km/h] at which the GCI can be executed. + -- @param #number EngageMaxSpeed The maximum speed [km/h] at which the GCI can be executed. + -- @usage + -- + -- -- GCI Squadron execution. + -- A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) + -- A2ADispatcher:SetSquadronGci( "Novo", 900, 2100 ) + -- A2ADispatcher:SetSquadronGci( "Maykop", 900, 1200 ) + -- + -- @return #AI_A2A_DISPATCHER + function AI_A2A_DISPATCHER:SetSquadronGci( SquadronName, EngageMinSpeed, EngageMaxSpeed ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci or {} + + local Intercept = self.DefenderSquadrons[SquadronName].Gci + Intercept.Name = SquadronName + Intercept.EngageMinSpeed = EngageMinSpeed + Intercept.EngageMaxSpeed = EngageMaxSpeed + self:F( { GCI = { SquadronName, EngageMinSpeed, EngageMaxSpeed } } ) end - + --- Defines the default amount of extra planes that will take-off as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. -- The default overhead is 1, so equal balance. The @{#AI_A2A_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2A missiles payload, may still be less effective than a F-15C with short missiles... -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. - -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: - -- + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- -- * Higher than 1, will increase the defense unit amounts. -- * Lower than 1, will decrease the defense unit amounts. - -- - -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group - -- multiplied by the Overhead and rounded up to the smallest integer. - -- + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. - -- + -- -- See example below. - -- + -- -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. - -- + -- -- A2ADispatcher:SetDefaultOverhead( 1.5 ) - -- + -- -- @return #AI_A2A_DISPATCHER function AI_A2A_DISPATCHER:SetDefaultOverhead( Overhead ) self.DefenderDefault.Overhead = Overhead - + return self end @@ -1900,35 +2161,35 @@ do -- AI_A2A_DISPATCHER -- The default overhead is 1, so equal balance. The @{#AI_A2A_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2A missiles payload, may still be less effective than a F-15C with short missiles... -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. - -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: - -- + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- -- * Higher than 1, will increase the defense unit amounts. -- * Lower than 1, will decrease the defense unit amounts. - -- - -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group - -- multiplied by the Overhead and rounded up to the smallest integer. - -- + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. - -- + -- -- See example below. - -- + -- -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. - -- + -- -- A2ADispatcher:SetSquadronOverhead( "SquadronName", 1.5 ) - -- - -- @return #AI_A2A_DISPATCHER + -- + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronOverhead( SquadronName, Overhead ) local DefenderSquadron = self:GetSquadron( SquadronName ) DefenderSquadron.Overhead = Overhead - + return self end @@ -1936,20 +2197,20 @@ do -- AI_A2A_DISPATCHER --- Sets the default grouping of new airplanes spawned. -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. -- @param #AI_A2A_DISPATCHER self - -- @param #number Grouping The level of grouping that will be applied of the CAP or GCI defenders. + -- @param #number Grouping The level of grouping that will be applied of the CAP or GCI defenders. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Set a grouping by default per 2 airplanes. -- A2ADispatcher:SetDefaultGrouping( 2 ) - -- - -- - -- @return #AI_A2A_DISPATCHER + -- + -- + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultGrouping( Grouping ) - + self.DefenderDefault.Grouping = Grouping - + return self end @@ -1958,21 +2219,21 @@ do -- AI_A2A_DISPATCHER -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. - -- @param #number Grouping The level of grouping that will be applied of the CAP or GCI defenders. + -- @param #number Grouping The level of grouping that will be applied of the CAP or GCI defenders. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Set a grouping per 2 airplanes. -- A2ADispatcher:SetSquadronGrouping( "SquadronName", 2 ) - -- - -- - -- @return #AI_A2A_DISPATCHER + -- + -- + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronGrouping( SquadronName, Grouping ) - + local DefenderSquadron = self:GetSquadron( SquadronName ) DefenderSquadron.Grouping = Grouping - + return self end @@ -1981,28 +2242,28 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_DISPATCHER self -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights by default take-off in the air. -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Air ) - -- + -- -- -- Let new flights by default take-off from the runway. -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Runway ) - -- + -- -- -- Let new flights by default take-off from the airbase hot. -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Hot ) - -- + -- -- -- Let new flights by default take-off from the airbase cold. -- A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Cold ) - -- - -- - -- @return #AI_A2A_DISPATCHER - -- + -- + -- + -- @return #AI_A2A_DISPATCHER self + -- function AI_A2A_DISPATCHER:SetDefaultTakeoff( Takeoff ) self.DefenderDefault.Takeoff = Takeoff - + return self end @@ -2011,112 +2272,112 @@ do -- AI_A2A_DISPATCHER -- @param #string SquadronName The name of the squadron. -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights take-off in the air. -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Air ) - -- + -- -- -- Let new flights take-off from the runway. -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Runway ) - -- + -- -- -- Let new flights take-off from the airbase hot. -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Hot ) - -- + -- -- -- Let new flights take-off from the airbase cold. -- A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Cold ) - -- - -- - -- @return #AI_A2A_DISPATCHER - -- + -- + -- + -- @return #AI_A2A_DISPATCHER self + -- function AI_A2A_DISPATCHER:SetSquadronTakeoff( SquadronName, Takeoff ) local DefenderSquadron = self:GetSquadron( SquadronName ) DefenderSquadron.Takeoff = Takeoff - + return self end - + --- Gets the default method at which new flights will spawn and take-off as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights by default take-off in the air. -- local TakeoffMethod = A2ADispatcher:GetDefaultTakeoff() -- if TakeOffMethod == , AI_A2A_Dispatcher.Takeoff.InAir then -- ... -- end - -- + -- function AI_A2A_DISPATCHER:GetDefaultTakeoff( ) return self.DefenderDefault.Takeoff end - + --- Gets the method at which new flights will spawn and take-off as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights take-off in the air. -- local TakeoffMethod = A2ADispatcher:GetSquadronTakeoff( "SquadronName" ) -- if TakeOffMethod == , AI_A2A_Dispatcher.Takeoff.InAir then -- ... -- end - -- + -- function AI_A2A_DISPATCHER:GetSquadronTakeoff( SquadronName ) local DefenderSquadron = self:GetSquadron( SquadronName ) return DefenderSquadron.Takeoff or self.DefenderDefault.Takeoff end - + --- Sets flights to default take-off in the air, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights by default take-off in the air. -- A2ADispatcher:SetDefaultTakeoffInAir() - -- - -- @return #AI_A2A_DISPATCHER - -- + -- + -- @return #AI_A2A_DISPATCHER self + -- function AI_A2A_DISPATCHER:SetDefaultTakeoffInAir() self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Air ) - + return self end - + --- Sets flights to take-off in the air, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @param #number TakeoffAltitude (optional) The altitude in meters above the ground. If not given, the default takeoff altitude will be used. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights take-off in the air. -- A2ADispatcher:SetSquadronTakeoffInAir( "SquadronName" ) - -- - -- @return #AI_A2A_DISPATCHER - -- + -- + -- @return #AI_A2A_DISPATCHER self + -- function AI_A2A_DISPATCHER:SetSquadronTakeoffInAir( SquadronName, TakeoffAltitude ) self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Air ) - + if TakeoffAltitude then self:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) end - + return self end @@ -2124,57 +2385,57 @@ do -- AI_A2A_DISPATCHER --- Sets flights by default to take-off from the runway, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights by default take-off from the runway. -- A2ADispatcher:SetDefaultTakeoffFromRunway() - -- - -- @return #AI_A2A_DISPATCHER - -- + -- + -- @return #AI_A2A_DISPATCHER self + -- function AI_A2A_DISPATCHER:SetDefaultTakeoffFromRunway() self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Runway ) - + return self end - + --- Sets flights to take-off from the runway, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights take-off from the runway. -- A2ADispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) - -- - -- @return #AI_A2A_DISPATCHER - -- + -- + -- @return #AI_A2A_DISPATCHER self + -- function AI_A2A_DISPATCHER:SetSquadronTakeoffFromRunway( SquadronName ) self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Runway ) - + return self end - + --- Sets flights by default to take-off from the airbase at a hot location, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights by default take-off at a hot parking spot. -- A2ADispatcher:SetDefaultTakeoffFromParkingHot() - -- - -- @return #AI_A2A_DISPATCHER - -- + -- + -- @return #AI_A2A_DISPATCHER self + -- function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingHot() self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Hot ) - + return self end @@ -2182,77 +2443,77 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights take-off in the air. -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) - -- - -- @return #AI_A2A_DISPATCHER - -- + -- + -- @return #AI_A2A_DISPATCHER self + -- function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingHot( SquadronName ) self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Hot ) - + return self end - - + + --- Sets flights to by default take-off from the airbase at a cold location, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights take-off from a cold parking spot. -- A2ADispatcher:SetDefaultTakeoffFromParkingCold() - -- - -- @return #AI_A2A_DISPATCHER - -- + -- + -- @return #AI_A2A_DISPATCHER self + -- function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingCold() self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Cold ) - + return self end - + --- Sets flights to take-off from the airbase at a cold location, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights take-off from a cold parking spot. -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) - -- - -- @return #AI_A2A_DISPATCHER - -- + -- + -- @return #AI_A2A_DISPATCHER self + -- function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingCold( SquadronName ) self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Cold ) - + return self end - + --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. -- @param #AI_A2A_DISPATCHER self -- @param #number TakeoffAltitude The altitude in meters above the ground. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Set the default takeoff altitude when taking off in the air. -- A2ADispatcher:SetDefaultTakeoffInAirAltitude( 2000 ) -- This makes planes start at 2000 meters above the ground. - -- - -- @return #AI_A2A_DISPATCHER - -- + -- + -- @return #AI_A2A_DISPATCHER self + -- function AI_A2A_DISPATCHER:SetDefaultTakeoffInAirAltitude( TakeoffAltitude ) self.DefenderDefault.TakeoffAltitude = TakeoffAltitude - + return self end @@ -2261,244 +2522,244 @@ do -- AI_A2A_DISPATCHER -- @param #string SquadronName The name of the squadron. -- @param #number TakeoffAltitude The altitude in meters above the ground. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Set the default takeoff altitude when taking off in the air. -- A2ADispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes planes start at 2000 meters above the ground. - -- - -- @return #AI_A2A_DISPATCHER - -- + -- + -- @return #AI_A2A_DISPATCHER self + -- function AI_A2A_DISPATCHER:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) local DefenderSquadron = self:GetSquadron( SquadronName ) DefenderSquadron.TakeoffAltitude = TakeoffAltitude - + return self end - + --- Defines the default method at which flights will land and despawn as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights by default despawn near the airbase when returning. -- A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.NearAirbase ) - -- + -- -- -- Let new flights by default despawn after landing land at the runway. -- A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.AtRunway ) - -- + -- -- -- Let new flights by default despawn after landing and parking, and after engine shutdown. -- A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.AtEngineShutdown ) - -- - -- @return #AI_A2A_DISPATCHER + -- + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultLanding( Landing ) self.DefenderDefault.Landing = Landing - + return self end - + --- Defines the method at which flights will land and despawn as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights despawn near the airbase when returning. -- A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.NearAirbase ) - -- + -- -- -- Let new flights despawn after landing land at the runway. -- A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.AtRunway ) - -- + -- -- -- Let new flights despawn after landing and parking, and after engine shutdown. -- A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.AtEngineShutdown ) - -- - -- @return #AI_A2A_DISPATCHER + -- + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronLanding( SquadronName, Landing ) local DefenderSquadron = self:GetSquadron( SquadronName ) DefenderSquadron.Landing = Landing - + return self end - + --- Gets the default method at which flights will land and despawn as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights by default despawn near the airbase when returning. -- local LandingMethod = A2ADispatcher:GetDefaultLanding( AI_A2A_Dispatcher.Landing.NearAirbase ) -- if LandingMethod == AI_A2A_Dispatcher.Landing.NearAirbase then -- ... -- end - -- + -- function AI_A2A_DISPATCHER:GetDefaultLanding() return self.DefenderDefault.Landing end - + --- Gets the method at which flights will land and despawn as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let new flights despawn near the airbase when returning. -- local LandingMethod = A2ADispatcher:GetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.NearAirbase ) -- if LandingMethod == AI_A2A_Dispatcher.Landing.NearAirbase then -- ... -- end - -- + -- function AI_A2A_DISPATCHER:GetSquadronLanding( SquadronName ) local DefenderSquadron = self:GetSquadron( SquadronName ) return DefenderSquadron.Landing or self.DefenderDefault.Landing end - + --- Sets flights by default to land and despawn near the airbase in the air, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let flights by default to land near the airbase and despawn. -- A2ADispatcher:SetDefaultLandingNearAirbase() - -- - -- @return #AI_A2A_DISPATCHER + -- + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultLandingNearAirbase() self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.NearAirbase ) - + return self end - + --- Sets flights to land and despawn near the airbase in the air, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let flights to land near the airbase and despawn. -- A2ADispatcher:SetSquadronLandingNearAirbase( "SquadronName" ) - -- - -- @return #AI_A2A_DISPATCHER + -- + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName ) self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.NearAirbase ) - + return self end - + --- Sets flights by default to land and despawn at the runway, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let flights by default land at the runway and despawn. -- A2ADispatcher:SetDefaultLandingAtRunway() - -- - -- @return #AI_A2A_DISPATCHER + -- + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultLandingAtRunway() self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.AtRunway ) - + return self end - + --- Sets flights to land and despawn at the runway, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let flights land at the runway and despawn. -- A2ADispatcher:SetSquadronLandingAtRunway( "SquadronName" ) - -- - -- @return #AI_A2A_DISPATCHER + -- + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronLandingAtRunway( SquadronName ) self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.AtRunway ) - + return self end - + --- Sets flights by default to land and despawn at engine shutdown, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let flights by default land and despawn at engine shutdown. -- A2ADispatcher:SetDefaultLandingAtEngineShutdown() - -- - -- @return #AI_A2A_DISPATCHER + -- + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetDefaultLandingAtEngineShutdown() self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.AtEngineShutdown ) - + return self end - + --- Sets flights to land and despawn at engine shutdown, as part of the defense system. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @usage: - -- + -- -- local A2ADispatcher = AI_A2A_DISPATCHER:New( ... ) - -- + -- -- -- Let flights land and despawn at engine shutdown. -- A2ADispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" ) - -- - -- @return #AI_A2A_DISPATCHER + -- + -- @return #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName ) self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.AtEngineShutdown ) - + return self end - + --- Set the default fuel treshold when defenders will RTB or Refuel in the air. -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. -- @param #AI_A2A_DISPATCHER self -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the default fuel treshold. -- A2ADispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- + -- function AI_A2A_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold ) - + self.DefenderDefault.FuelThreshold = FuelThreshold - + return self - end + end --- Set the fuel treshold for the squadron when defenders will RTB or Refuel in the air. @@ -2506,43 +2767,43 @@ do -- AI_A2A_DISPATCHER -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the default fuel treshold. -- A2ADispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- + -- function AI_A2A_DISPATCHER:SetSquadronFuelThreshold( SquadronName, FuelThreshold ) - + local DefenderSquadron = self:GetSquadron( SquadronName ) DefenderSquadron.FuelThreshold = FuelThreshold - + return self - end + end --- Set the default tanker where defenders will Refuel in the air. -- @param #AI_A2A_DISPATCHER self -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. - -- @return #AI_A2A_DISPATCHER + -- @return #AI_A2A_DISPATCHER self -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the default fuel treshold. -- A2ADispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- + -- -- -- Now Setup the default tanker. -- A2ADispatcher:SetDefaultTanker( "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. function AI_A2A_DISPATCHER:SetDefaultTanker( TankerName ) - + self.DefenderDefault.TankerName = TankerName - + return self - end + end --- Set the squadron tanker where defenders will Refuel in the air. @@ -2551,27 +2812,83 @@ do -- AI_A2A_DISPATCHER -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. -- @return #AI_A2A_DISPATCHER -- @usage - -- + -- -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. - -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) - -- + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- -- -- Now Setup the squadron fuel treshold. -- A2ADispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. - -- + -- -- -- Now Setup the squadron tanker. -- A2ADispatcher:SetSquadronTanker( "SquadronName", "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. function AI_A2A_DISPATCHER:SetSquadronTanker( SquadronName, TankerName ) - + local DefenderSquadron = self:GetSquadron( SquadronName ) DefenderSquadron.TankerName = TankerName - + return self - end + end + --- Set the squadron language. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #string Language A string defining the language to be embedded within the miz file. + -- @return #AI_A2A_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2A dispatcher, and initialize it using the Detection object. + -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) + -- + -- -- Set for English. + -- A2ADispatcher:SetSquadronLanguage( "SquadronName", "EN" ) -- This squadron speaks English. + -- + -- -- Set for Russian. + -- A2ADispatcher:SetSquadronLanguage( "SquadronName", "RU" ) -- This squadron speaks Russian. + function AI_A2A_DISPATCHER:SetSquadronLanguage( SquadronName, Language ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Language = Language + + if DefenderSquadron.RadioQueue then + DefenderSquadron.RadioQueue:SetLanguage( Language ) + end + + return self + end - --- @param #AI_A2A_DISPATCHER self + --- Set the frequency of communication and the mode of communication for voice overs. + -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number RadioFrequency The frequency of communication. + -- @param #number RadioModulation The modulation of communication. + -- @param #number RadioPower The power in Watts of communication. + function AI_A2A_DISPATCHER:SetSquadronRadioFrequency( SquadronName, RadioFrequency, RadioModulation, RadioPower ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.RadioFrequency = RadioFrequency + DefenderSquadron.RadioModulation = RadioModulation or radio.modulation.AM + DefenderSquadron.RadioPower = RadioPower or 100 + + if DefenderSquadron.RadioQueue then + DefenderSquadron.RadioQueue:Stop() + end + + DefenderSquadron.RadioQueue = nil + + DefenderSquadron.RadioQueue = RADIOSPEECH:New( DefenderSquadron.RadioFrequency, DefenderSquadron.RadioModulation ) + DefenderSquadron.RadioQueue.power = DefenderSquadron.RadioPower + DefenderSquadron.RadioQueue:Start( 0.5 ) + + DefenderSquadron.RadioQueue:SetLanguage( DefenderSquadron.Language ) + end + + --- Add defender to squadron. Resource count will get smaller. + -- @param #AI_A2A_DISPATCHER self + -- @param #AI_A2A_DISPATCHER.Squadron Squadron The squadron. + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @param #number Size Size of the group. function AI_A2A_DISPATCHER:AddDefenderToSquadron( Squadron, Defender, Size ) self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() @@ -2582,7 +2899,10 @@ do -- AI_A2A_DISPATCHER self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) end - --- @param #AI_A2A_DISPATCHER self + --- Remove defender from squadron. Resource count will increase. + -- @param #AI_A2A_DISPATCHER self + -- @param #AI_A2A_DISPATCHER.Squadron Squadron The squadron. + -- @param Wrapper.Group#GROUP Defender The defender group. function AI_A2A_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() @@ -2592,23 +2912,26 @@ do -- AI_A2A_DISPATCHER self.Defenders[ DefenderName ] = nil self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) end - + + --- Get squadron from defender. + -- @param #AI_A2A_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender The defender group. + -- @return #AI_A2A_DISPATCHER.Squadron Squadron The squadron. function AI_A2A_DISPATCHER:GetSquadronFromDefender( Defender ) self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() self:F( { DefenderName = DefenderName } ) - return self.Defenders[ DefenderName ] + return self.Defenders[ DefenderName ] end - + --- Creates an SWEEP task when there are targets for it. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. function AI_A2A_DISPATCHER:EvaluateSWEEP( DetectedItem ) self:F( { DetectedItem.ItemID } ) - + local DetectedSet = DetectedItem.Set local DetectedZone = DetectedItem.Zone @@ -2619,29 +2942,32 @@ do -- AI_A2A_DISPATCHER local TargetSetUnit = SET_UNIT:New() TargetSetUnit:SetDatabase( DetectedSet ) TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection. - + return TargetSetUnit end - + return nil end - --- + --- Count number of airborne CAP flights. -- @param #AI_A2A_DISPATCHER self + -- @param #string SquadronName Name of the squadron. + -- @return #number Number of defender CAP groups. function AI_A2A_DISPATCHER:CountCapAirborne( SquadronName ) local CapCount = 0 - + local DefenderSquadron = self.DefenderSquadrons[SquadronName] if DefenderSquadron then for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do if DefenderTask.SquadronName == SquadronName then if DefenderTask.Type == "CAP" then - if AIGroup:IsAlive() then + if AIGroup and AIGroup:IsAlive() then -- Check if the CAP is patrolling or engaging. If not, this is not a valid CAP, even if it is alive! -- The CAP could be damaged, lost control, or out of fuel! - if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" ) - or DefenderTask.Fsm:Is( "Started" ) then + --env.info("FF fsm state "..tostring(DefenderTask.Fsm:GetState())) + if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" ) or DefenderTask.Fsm:Is( "Started" ) then + --env.info("FF capcount "..CapCount) CapCount = CapCount + 1 end end @@ -2652,28 +2978,31 @@ do -- AI_A2A_DISPATCHER return CapCount end - - - --- + + + --- Count number of engaging defender groups. -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detection object. + -- @return #number Number of defender groups engaging. function AI_A2A_DISPATCHER:CountDefendersEngaged( AttackerDetection ) -- First, count the active AIGroups Units, targetting the DetectedSet local DefenderCount = 0 - + local DetectedSet = AttackerDetection.Set --DetectedSet:Flush() - + local DefenderTasks = self:GetDefenderTasks() + for DefenderGroup, DefenderTask in pairs( DefenderTasks ) do local Defender = DefenderGroup -- Wrapper.Group#GROUP - local DefenderTaskTarget = DefenderTask.Target + local DefenderTaskTarget = DefenderTask.Target --Functional.Detection#DETECTION_BASE.DetectedItem local DefenderSquadronName = DefenderTask.SquadronName - + if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then local Squadron = self:GetSquadron( DefenderSquadronName ) local SquadronOverhead = Squadron.Overhead or self.DefenderDefault.Overhead - + local DefenderSize = Defender:GetInitialSize() if DefenderSize then DefenderCount = DefenderCount + DefenderSize / SquadronOverhead @@ -2688,21 +3017,24 @@ do -- AI_A2A_DISPATCHER return DefenderCount end - - --- + + --- Count defenders to be engaged if number of attackers larger than number of defenders. -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #number DefenderCount Number of defenders. + -- @return #table Table of friendly groups. function AI_A2A_DISPATCHER:CountDefendersToBeEngaged( AttackerDetection, DefenderCount ) - + local Friendlies = nil local AttackerSet = AttackerDetection.Set local AttackerCount = AttackerSet:Count() local DefenderFriendlies = self:GetAIFriendliesNearBy( AttackerDetection ) - + for FriendlyDistance, AIFriendly in UTILS.spairs( DefenderFriendlies or {} ) do -- We only allow to ENGAGE targets as long as the Units on both sides are balanced. - if AttackerCount > DefenderCount then + if AttackerCount > DefenderCount then local Friendly = AIFriendly:GetGroup() -- Wrapper.Group#GROUP if Friendly and Friendly:IsAlive() then -- Ok, so we have a friendly near the potential target. @@ -2713,8 +3045,7 @@ do -- AI_A2A_DISPATCHER if DefenderTask.Type == "CAP" or DefenderTask.Type == "GCI" then -- If there is no target, then add the AIGroup to the ResultAIGroups for Engagement to the AttackerSet if DefenderTask.Target == nil then - if DefenderTask.Fsm:Is( "Returning" ) - or DefenderTask.Fsm:Is( "Patrolling" ) then + if DefenderTask.Fsm:Is( "Returning" ) or DefenderTask.Fsm:Is( "Patrolling" ) then Friendlies = Friendlies or {} Friendlies[Friendly] = Friendly DefenderCount = DefenderCount + Friendly:GetSize() @@ -2722,7 +3053,7 @@ do -- AI_A2A_DISPATCHER end end end - end + end end else break @@ -2733,24 +3064,60 @@ do -- AI_A2A_DISPATCHER end - --- + --- Activate resource. -- @param #AI_A2A_DISPATCHER self + -- @param #AI_A2A_DISPATCHER.Squadron DefenderSquadron The defender squadron. + -- @param #number DefendersNeeded Number of defenders needed. Default 4. + -- @return Wrapper.Group#GROUP The defender group. + -- @return #boolean Grouping. function AI_A2A_DISPATCHER:ResourceActivate( DefenderSquadron, DefendersNeeded ) - + local SquadronName = DefenderSquadron.Name + DefendersNeeded = DefendersNeeded or 4 + local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping + DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded - + + --env.info(string.format("FF resource activate: Squadron=%s grouping=%d needed=%d visible=%s", SquadronName, DefenderGrouping, DefendersNeeded, tostring(self:IsSquadronVisible( SquadronName )))) + if self:IsSquadronVisible( SquadronName ) then - + + local n=#self.uncontrolled[SquadronName] + + if n>0 then + -- Random number 1,...n + local id=math.random(n) + + -- Pick a random defender group. + local Defender=self.uncontrolled[SquadronName][id].group --Wrapper.Group#GROUP + + -- Start uncontrolled group. + Defender:StartUncontrolled() + + -- Get grouping. + DefenderGrouping=self.uncontrolled[SquadronName][id].grouping + + -- Add defender to squadron. + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + + -- Remove defender from uncontrolled table. + table.remove(self.uncontrolled[SquadronName], id) + + return Defender, DefenderGrouping + else + return nil,0 + end + -- Here we CAP the new planes. -- The Resources table is filled in advance. local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) -- Choose the template. - + + --[[ -- We determine the grouping based on the parameters set. self:F( { DefenderGrouping = DefenderGrouping } ) - + -- New we will form the group to spawn in. -- We search for the first free resource matching the template. local DefenderUnitIndex = 1 @@ -2774,9 +3141,9 @@ do -- AI_A2A_DISPATCHER if DefenderUnitIndex > DefenderGrouping then break end - - end - + + end + if DefenderCAPTemplate then local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) local SpawnGroup = GROUP:Register( DefenderName ) @@ -2786,153 +3153,225 @@ do -- AI_A2A_DISPATCHER DefenderCAPTemplate.route.points[1].type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type DefenderCAPTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action local Defender = _DATABASE:Spawn( DefenderCAPTemplate ) - + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) return Defender, DefenderGrouping end + ]] + else + + ---------------------------- + --- Squadron not visible --- + ---------------------------- + local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN + if DefenderGrouping then Spawn:InitGrouping( DefenderGrouping ) else Spawn:InitGrouping() end - + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local Defender = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + return Defender, DefenderGrouping end return nil, nil end - - --- + + --- On after "CAP" event. -- @param #AI_A2A_DISPATCHER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string SquadronName Name of the squadron. function AI_A2A_DISPATCHER:onafterCAP( From, Event, To, SquadronName ) - + self:F({SquadronName = SquadronName}) - self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {} - + local DefenderSquadron = self:CanCAP( SquadronName ) - + if DefenderSquadron then - + local Cap = DefenderSquadron.Cap - + if Cap then - local DefenderCAP, DefenderGrouping = self:ResourceActivate( DefenderSquadron ) - + local DefenderCAP, DefenderGrouping = self:ResourceActivate( DefenderSquadron ) + if DefenderCAP then - - local Fsm = AI_A2A_CAP:New( DefenderCAP, Cap.Zone, Cap.FloorAltitude, Cap.CeilingAltitude, Cap.PatrolMinSpeed, Cap.PatrolMaxSpeed, Cap.EngageMinSpeed, Cap.EngageMaxSpeed, Cap.AltType ) - Fsm:SetDispatcher( self ) - Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) - Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) - Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) - Fsm:SetDisengageRadius( self.DisengageRadius ) - Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) - Fsm:Start() - - self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", Fsm ) - function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"CAP Birth", Defender:GetName()}) - --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - - local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local AI_A2A_Fsm = AI_A2A_CAP:New2( DefenderCAP, Cap.EngageMinSpeed, Cap.EngageMaxSpeed, Cap.EngageFloorAltitude, Cap.EngageCeilingAltitude, Cap.EngageAltType, Cap.Zone, Cap.PatrolMinSpeed, Cap.PatrolMaxSpeed, Cap.PatrolFloorAltitude, Cap.PatrolCeilingAltitude, Cap.PatrolAltType ) + AI_A2A_Fsm:SetDispatcher( self ) + AI_A2A_Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + AI_A2A_Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + AI_A2A_Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + AI_A2A_Fsm:SetDisengageRadius( self.DisengageRadius ) + AI_A2A_Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) + if DefenderSquadron.Racetrack or self.DefenderDefault.Racetrack then + AI_A2A_Fsm:SetRaceTrackPattern(DefenderSquadron.RacetrackLengthMin or self.DefenderDefault.RacetrackLengthMin, + DefenderSquadron.RacetrackLengthMax or self.DefenderDefault.RacetrackLengthMax, + DefenderSquadron.RacetrackHeadingMin or self.DefenderDefault.RacetrackHeadingMin, + DefenderSquadron.RacetrackHeadingMax or self.DefenderDefault.RacetrackHeadingMax, + DefenderSquadron.RacetrackDurationMin or self.DefenderDefault.RacetrackDurationMin, + DefenderSquadron.RacetrackDurationMax or self.DefenderDefault.RacetrackDurationMax, + DefenderSquadron.RacetrackCoordinates or self.DefenderDefault.RacetrackCoordinates) + end + AI_A2A_Fsm:Start() - if Squadron then - Fsm:__Patrol( 2 ) -- Start Patrolling + self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", AI_A2A_Fsm ) + + function AI_A2A_Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) + -- Issue GetCallsign() returns nil, see https://github.com/FlightControl-Master/MOOSE/issues/1228 + if DefenderGroup and DefenderGroup:IsAlive() then + self:F({"CAP Takeoff", DefenderGroup:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2A_Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " Wheels up.", DefenderGroup ) + AI_A2A_Fsm:__Patrol( 2 ) -- Start Patrolling + end end end - - function Fsm:onafterRTB( Defender, From, Event, To ) - self:F({"CAP RTB", Defender:GetName()}) - self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) - local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER - Dispatcher:ClearDefenderTaskTarget( Defender ) + + function AI_A2A_Fsm:onafterPatrolRoute( DefenderGroup, From, Event, To ) + if DefenderGroup and DefenderGroup:IsAlive() then + self:F({"CAP PatrolRoute", DefenderGroup:GetName()}) + self:GetParent(self).onafterPatrolRoute( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + if Squadron then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", patrolling.", DefenderGroup ) + end + + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) + end end - + + function AI_A2A_Fsm:onafterRTB( DefenderGroup, From, Event, To ) + if DefenderGroup and DefenderGroup:IsAlive() then + self:F({"CAP RTB", DefenderGroup:GetName()}) + + self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + if Squadron then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) + end + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) + end + end + --- @param #AI_A2A_DISPATCHER self - function Fsm:onafterHome( Defender, From, Event, To, Action ) - self:F({"CAP Home", Defender:GetName()}) - self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - - local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - - if Action and Action == "Destroy" then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - end - - if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - self:ParkDefender( Squadron, Defender ) + function AI_A2A_Fsm:onafterHome( Defender, From, Event, To, Action ) + if Defender and Defender:IsAlive() then + self:F({"CAP Home", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + + if Action and Action == "Destroy" then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + end + + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + Dispatcher:ParkDefender( Squadron ) + end end end end end end - + end - --- + --- On after "ENGAGE" event. -- @param #AI_A2A_DISPATCHER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #table Defenders Defenders table. function AI_A2A_DISPATCHER:onafterENGAGE( From, Event, To, AttackerDetection, Defenders ) - + self:F("ENGAGING Detection ID="..tostring(AttackerDetection.ID)) + if Defenders then for DefenderID, Defender in pairs( Defenders ) do local Fsm = self:GetDefenderTaskFsm( Defender ) - Fsm:__Engage( 1, AttackerDetection.Set ) -- Engage on the TargetSetUnit - + + Fsm:EngageRoute( AttackerDetection.Set ) -- Engage on the TargetSetUnit + self:SetDefenderTaskTarget( Defender, AttackerDetection ) end + end end - --- + --- On after "GCI" event. -- @param #AI_A2A_DISPATCHER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item. + -- @param #number DefendersMissing Number of missing defenders. + -- @param #table DefenderFriendlies Friendly defenders. function AI_A2A_DISPATCHER:onafterGCI( From, Event, To, AttackerDetection, DefendersMissing, DefenderFriendlies ) + self:F("GCI Detection ID="..tostring(AttackerDetection.ID)) + self:F( { From, Event, To, AttackerDetection.Index, DefendersMissing, DefenderFriendlies } ) local AttackerSet = AttackerDetection.Set local AttackerUnit = AttackerSet:GetFirst() - + if AttackerUnit and AttackerUnit:IsAlive() then local AttackerCount = AttackerSet:Count() local DefenderCount = 0 - + for DefenderID, DefenderGroup in pairs( DefenderFriendlies or {} ) do - + local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) - Fsm:__Engage( 1, AttackerSet ) -- Engage on the TargetSetUnit - + Fsm:__EngageRoute( 0.1, AttackerSet ) -- Engage on the TargetSetUnit + self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) - + DefenderCount = DefenderCount + DefenderGroup:GetSize() end - + self:F( { DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) DefenderCount = DefendersMissing - + local ClosestDistance = 0 local ClosestDefenderSquadronName = nil - + local BreakLoop = false - + while( DefenderCount > 0 and not BreakLoop ) do - + self:F( { DefenderSquadrons = self.DefenderSquadrons } ) for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons or {} ) do @@ -2940,7 +3379,7 @@ do -- AI_A2A_DISPATCHER self:F( { GCI = DefenderSquadron.Gci } ) for InterceptID, Intercept in pairs( DefenderSquadron.Gci or {} ) do - + self:F( { DefenderSquadron } ) local SpawnCoord = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE local AttackerCoord = AttackerUnit:GetCoordinate() @@ -2950,9 +3389,9 @@ do -- AI_A2A_DISPATCHER local InterceptDistance = SpawnCoord:Get2DDistance( InterceptCoord ) local AirbaseDistance = SpawnCoord:Get2DDistance( AttackerCoord ) self:F( { InterceptDistance = InterceptDistance, AirbaseDistance = AirbaseDistance, InterceptCoord = InterceptCoord } ) - + if ClosestDistance == 0 or InterceptDistance < ClosestDistance then - + -- Only intercept if the distance to target is smaller or equal to the GciRadius limit. if AirbaseDistance <= self.GciRadius then ClosestDistance = InterceptDistance @@ -2962,80 +3401,139 @@ do -- AI_A2A_DISPATCHER end end end - + if ClosestDefenderSquadronName then - + local DefenderSquadron = self:CanGCI( ClosestDefenderSquadronName ) - + if DefenderSquadron then - + local Gci = self.DefenderSquadrons[ClosestDefenderSquadronName].Gci - + if Gci then - + local DefenderOverhead = DefenderSquadron.Overhead or self.DefenderDefault.Overhead local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping local DefendersNeeded = math.ceil( DefenderCount * DefenderOverhead ) - + self:F( { Overhead = DefenderOverhead, SquadronOverhead = DefenderSquadron.Overhead , DefaultOverhead = self.DefenderDefault.Overhead } ) self:F( { Grouping = DefenderGrouping, SquadronGrouping = DefenderSquadron.Grouping, DefaultGrouping = self.DefenderDefault.Grouping } ) self:F( { DefendersCount = DefenderCount, DefendersNeeded = DefendersNeeded } ) - + -- DefenderSquadron.ResourceCount can have the value nil, which expresses unlimited resources. -- DefendersNeeded cannot exceed DefenderSquadron.ResourceCount! if DefenderSquadron.ResourceCount and DefendersNeeded > DefenderSquadron.ResourceCount then DefendersNeeded = DefenderSquadron.ResourceCount BreakLoop = true end - + while ( DefendersNeeded > 0 ) do - - local DefenderGCI, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) - + + local DefenderGCI, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) + DefendersNeeded = DefendersNeeded - DefenderGrouping - + if DefenderGCI then - + DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead - - local Fsm = AI_A2A_GCI:New( DefenderGCI, Gci.EngageMinSpeed, Gci.EngageMaxSpeed ) + + local Fsm = AI_A2A_GCI:New2( DefenderGCI, Gci.EngageMinSpeed, Gci.EngageMaxSpeed, Gci.EngageFloorAltitude, Gci.EngageCeilingAltitude, Gci.EngageAltType ) Fsm:SetDispatcher( self ) Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) Fsm:SetDisengageRadius( self.DisengageRadius ) Fsm:Start() - - + + self:SetDefenderTask( ClosestDefenderSquadronName, DefenderGCI, "GCI", Fsm, AttackerDetection ) - - - function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"GCI Birth", Defender:GetName()}) + + + function Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) + self:F({"GCI Birth", DefenderGroup:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) - + + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender ) - + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + local DefenderTarget = Dispatcher:GetDefenderTaskTarget( DefenderGroup ) + if DefenderTarget then - Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit + if Squadron.Language == "EN" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " wheels up.", DefenderGroup ) + elseif Squadron.Language == "RU" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " колеÑа вверх.", DefenderGroup ) + end + --Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit + Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit end end - - function Fsm:onafterRTB( Defender, From, Event, To ) - self:F({"GCI RTB", Defender:GetName()}) - self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) - - local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER - Dispatcher:ClearDefenderTaskTarget( Defender ) + + function Fsm:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"GCI Route", DefenderGroup:GetName()}) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron and AttackSetUnit:Count() > 0 then + local FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE + + if Squadron.Language == "EN" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", intercepting bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + elseif Squadron.Language == "RU" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", перехват Ñамолетов в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + elseif Squadron.Language == "DE" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", Eindringlinge abfangen bei" .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + end + end + self:GetParent( Fsm ).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) end - + + function Fsm:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"GCI Engage", DefenderGroup:GetName()}) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron and AttackSetUnit:Count() > 0 then + local FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE + + if Squadron.Language == "EN" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + elseif Squadron.Language == "RU" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", захватывающие Ñамолеты в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup ) + end + end + self:GetParent( Fsm ).onafterEngage( self, DefenderGroup, From, Event, To, AttackSetUnit ) + end + + function Fsm:onafterRTB( DefenderGroup, From, Event, To ) + self:F({"GCI RTB", DefenderGroup:GetName()}) + self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron then + if Squadron.Language == "EN" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " returning to base.", DefenderGroup ) + elseif Squadron.Language == "RU" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", возвращаÑÑÑŒ на базу.", DefenderGroup ) + end + end + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) + end + --- @param #AI_A2A_DISPATCHER self function Fsm:onafterLostControl( Defender, From, Event, To ) self:F({"GCI LostControl", Defender:GetName()}) self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) if Defender:IsAboveRunway() then @@ -3043,26 +3541,34 @@ do -- AI_A2A_DISPATCHER Defender:Destroy() end end - + --- @param #AI_A2A_DISPATCHER self - function Fsm:onafterHome( Defender, From, Event, To, Action ) - self:F({"GCI Home", Defender:GetName()}) - self:GetParent(self).onafterHome( self, Defender, From, Event, To ) - + function Fsm:onafterHome( DefenderGroup, From, Event, To, Action ) + self:F({"GCI Home", DefenderGroup:GetName()}) + self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER - local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) - + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron.Language == "EN" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. " landing at base.", DefenderGroup ) + elseif Squadron.Language == "RU" then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", захватывающие Ñамолеты в поÑадка на базу.", DefenderGroup ) + end + if Action and Action == "Destroy" then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() end - + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then - Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) - Defender:Destroy() - self:ParkDefender( Squadron, Defender ) + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + Dispatcher:ParkDefender( Squadron ) end end + end -- if DefenderGCI then end -- while ( DefendersNeeded > 0 ) do end @@ -3085,38 +3591,36 @@ do -- AI_A2A_DISPATCHER --- Creates an ENGAGE task when there are human friendlies airborne near the targets. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. - -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units or nil. function AI_A2A_DISPATCHER:EvaluateENGAGE( DetectedItem ) self:F( { DetectedItem.ItemID } ) - + -- First, count the active AIGroups Units, targetting the DetectedSet local DefenderCount = self:CountDefendersEngaged( DetectedItem ) local DefenderGroups = self:CountDefendersToBeEngaged( DetectedItem, DefenderCount ) self:F( { DefenderCount = DefenderCount } ) - + -- Only allow ENGAGE when: -- 1. There are friendly units near the detected attackers. -- 2. There is sufficient fuel -- 3. There is sufficient ammo -- 4. The plane is not damaged if DefenderGroups and DetectedItem.IsDetected == true then - return DefenderGroups end - - return nil, nil + + return nil end - + --- Creates an GCI task when there are targets for it. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. - -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. - -- @return #nil If there are no targets to be set. + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units or nil if there are no targets to be set. + -- @return #table Table of friendly groups. function AI_A2A_DISPATCHER:EvaluateGCI( DetectedItem ) self:F( { DetectedItem.ItemID } ) - + local AttackerSet = DetectedItem.Set local AttackerCount = AttackerSet:Count() @@ -3128,27 +3632,149 @@ do -- AI_A2A_DISPATCHER local Friendlies = self:CountDefendersToBeEngaged( DetectedItem, DefenderCount ) if DetectedItem.IsDetected == true then - + return DefendersMissing, Friendlies end - + return nil, nil end + --- Assigns A2G AI Tasks in relation to the detected items. + -- @param #AI_A2G_DISPATCHER self + function AI_A2A_DISPATCHER:Order( DetectedItem ) + + local detection=self.Detection -- Functional.Detection#DETECTION_AREAS + + local ShortestDistance = 999999999 + + -- Get coordinate (or nil). + local AttackCoordinate = detection:GetDetectedItemCoordinate( DetectedItem ) + + -- Issue https://github.com/FlightControl-Master/MOOSE/issues/1232 + if AttackCoordinate then + + for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + + self:T( { DefenderSquadron = DefenderSquadron.Name } ) + + local Airbase = DefenderSquadron.Airbase + local AirbaseCoordinate = Airbase:GetCoordinate() + + local EvaluateDistance = AttackCoordinate:Get2DDistance( AirbaseCoordinate ) + + if EvaluateDistance <= ShortestDistance then + ShortestDistance = EvaluateDistance + end + end + + end + + return ShortestDistance + end + + + --- Shows the tactical display. + -- @param #AI_A2A_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. + function AI_A2A_DISPATCHER:ShowTacticalDisplay( Detection ) + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local TaskReport = REPORT:New() + + local Report = REPORT:New( "Tactical Overview:" ) + + local DefenderGroupCount = 0 + + -- Now that all obsolete tasks are removed, loop through the detected targets. + --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do + + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT + local DetectedCount = DetectedSet:Count() + local DetectedZone = DetectedItem.Zone + + self:F( { "Target ID", DetectedItem.ItemID } ) + DetectedSet:Flush( self ) + + local DetectedID = DetectedItem.ID + local DetectionIndex = DetectedItem.Index + local DetectedItemChanged = DetectedItem.Changed + + -- Show tactical situation + Report:Add( string.format( "\n- Target %s (%s): (#%d) %s" , DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Set:GetObjectNames() ) ) + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + local Defender = Defender -- Wrapper.Group#GROUP + if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then + if Defender and Defender:IsAlive() then + DefenderGroupCount = DefenderGroupCount + 1 + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", + Defender:GetName(), + Defender:GetSize(), + Defender:GetInitialSize(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + end + + Report:Add( "\n- No Targets:") + local TaskCount = 0 + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + TaskCount = TaskCount + 1 + local Defender = Defender -- Wrapper.Group#GROUP + if not DefenderTask.Target then + if Defender:IsAlive() then + local DefenderHasTask = Defender:HasTask() + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + DefenderGroupCount = DefenderGroupCount + 1 + Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s", + Defender:GetName(), + Defender:GetSize(), + Defender:GetInitialSize(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + Report:Add( string.format( "\n- %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) + + self:F( Report:Text( "\n" ) ) + trigger.action.outText( Report:Text( "\n" ), 25 ) + + return true + + end + --- Assigns A2A AI Tasks in relation to the detected items. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. function AI_A2A_DISPATCHER:ProcessDetected( Detection ) - + local AreaMsg = {} local TaskMsg = {} local ChangeMsg = {} - + local TaskReport = REPORT:New() - + for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do local AIGroup = AIGroup -- Wrapper.Group#GROUP if not AIGroup:IsAlive() then @@ -3176,13 +3802,15 @@ do -- AI_A2A_DISPATCHER end end - local Report = REPORT:New( "\nTactical Overview" ) + local Report = REPORT:New( "Tactical Overviews" ) local DefenderGroupCount = 0 -- Now that all obsolete tasks are removed, loop through the detected targets. - for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do - + -- Closest detected targets to be considered first! + --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT local DetectedCount = DetectedSet:Count() @@ -3194,8 +3822,8 @@ do -- AI_A2A_DISPATCHER local DetectedID = DetectedItem.ID local DetectionIndex = DetectedItem.Index local DetectedItemChanged = DetectedItem.Changed - - do + + do local Friendlies = self:EvaluateENGAGE( DetectedItem ) -- Returns a SetUnit if there are targets to be GCIed... if Friendlies then self:F( { AIGroups = Friendlies } ) @@ -3210,60 +3838,12 @@ do -- AI_A2A_DISPATCHER self:GCI( DetectedItem, DefendersMissing, Friendlies ) end end - - if self.TacticalDisplay then - -- Show tactical situation - Report:Add( string.format( "\n - Target %s ( %s ): ( #%d ) %s" , DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Set:GetObjectNames() ) ) - for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do - local Defender = Defender -- Wrapper.Group#GROUP - if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then - if Defender:IsAlive() then - DefenderGroupCount = DefenderGroupCount + 1 - local Fuel = Defender:GetFuelMin() * 100 - local Damage = Defender:GetLife() / Defender:GetLife0() * 100 - Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", - Defender:GetName(), - DefenderTask.Type, - DefenderTask.Fsm:GetState(), - Defender:GetSize(), - Fuel, - Damage, - Defender:HasTask() == true and "Executing" or "Idle" ) ) - end - end - end - end end if self.TacticalDisplay then - Report:Add( "\n - No Targets:") - local TaskCount = 0 - for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do - TaskCount = TaskCount + 1 - local Defender = Defender -- Wrapper.Group#GROUP - if not DefenderTask.Target then - if Defender:IsAlive() then - local DefenderHasTask = Defender:HasTask() - local Fuel = Defender:GetFuelMin() * 100 - local Damage = Defender:GetLife() / Defender:GetLife0() * 100 - DefenderGroupCount = DefenderGroupCount + 1 - Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", - Defender:GetName(), - DefenderTask.Type, - DefenderTask.Fsm:GetState(), - Defender:GetSize(), - Fuel, - Damage, - Defender:HasTask() == true and "Executing" or "Idle" ) ) - end - end - end - Report:Add( string.format( "\n - %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) - - self:F( Report:Text( "\n" ) ) - trigger.action.outText( Report:Text( "\n" ), 25 ) + self:ShowTacticalDisplay( Detection ) end - + return true end @@ -3273,13 +3853,13 @@ do --- Calculates which HUMAN friendlies are nearby the area. -- @param #AI_A2A_DISPATCHER self - -- @param DetectedItem The detected item. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. function AI_A2A_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem ) - + local DetectedSet = DetectedItem.Set local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem ) - + local PlayerTypes = {} local PlayersCount = 0 @@ -3298,13 +3878,13 @@ do end end end - + end --self:F( { PlayersCount = PlayersCount } ) - + local PlayerTypesReport = REPORT:New() - + if PlayersCount > 0 then for PlayerName, PlayerType in pairs( PlayerTypes ) do PlayerTypesReport:Add( string.format('"%s" in %s', PlayerName, PlayerType ) ) @@ -3312,20 +3892,20 @@ do else PlayerTypesReport:Add( "-" ) end - - + + return PlayersCount, PlayerTypesReport end --- Calculates which friendlies are nearby the area. -- @param #AI_A2A_DISPATCHER self - -- @param DetectedItem The detected item. + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. function AI_A2A_DISPATCHER:GetFriendliesNearBy( DetectedItem ) - + local DetectedSet = DetectedItem.Set local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem ) - + local FriendlyTypes = {} local FriendliesCount = 0 @@ -3342,13 +3922,13 @@ do end end end - + end --self:F( { FriendliesCount = FriendliesCount } ) - + local FriendlyTypesReport = REPORT:New() - + if FriendliesCount > 0 then for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do FriendlyTypesReport:Add( string.format("%d of %s", FriendlyTypeCount, FriendlyType ) ) @@ -3356,8 +3936,8 @@ do else FriendlyTypesReport:Add( "-" ) end - - + + return FriendliesCount, FriendlyTypesReport end @@ -3375,256 +3955,256 @@ do --- @type AI_A2A_GCICAP -- @extends #AI_A2A_DISPATCHER - --- Create an automatic air defence system for a coalition setting up GCI and CAP air defenses. + --- Create an automatic air defence system for a coalition setting up GCI and CAP air defenses. -- The class derives from @{#AI_A2A_DISPATCHER} and thus, all the methods that are defined in the @{#AI_A2A_DISPATCHER} class, can be used also in AI\_A2A\_GCICAP. - -- + -- -- === - -- + -- -- # Demo Missions - -- + -- -- ### [AI\_A2A\_GCICAP for Caucasus](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-200%20-%20AI_A2A%20-%20GCICAP%20Demonstration) -- ### [AI\_A2A\_GCICAP for NTTR](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-210%20-%20NTTR%20AI_A2A_GCICAP%20Demonstration) -- ### [AI\_A2A\_GCICAP for Normandy](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-220%20-%20NORMANDY%20AI_A2A_GCICAP%20Demonstration) - -- + -- -- ### [AI\_A2A\_GCICAP for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching) -- -- === - -- + -- -- # YouTube Channel - -- + -- -- ### [DCS WORLD - MOOSE - A2A GCICAP - Build an automatic A2A Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) - -- + -- -- === - -- + -- -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia3.JPG) - -- - -- AI\_A2A\_GCICAP includes automatic spawning of Combat Air Patrol aircraft (CAP) and Ground Controlled Intercept aircraft (GCI) in response to enemy - -- air movements that are detected by an airborne or ground based radar network. - -- + -- + -- AI\_A2A\_GCICAP includes automatic spawning of Combat Air Patrol aircraft (CAP) and Ground Controlled Intercept aircraft (GCI) in response to enemy + -- air movements that are detected by an airborne or ground based radar network. + -- -- With a little time and with a little work it provides the mission designer with a convincing and completely automatic air defence system. - -- - -- The AI_A2A_GCICAP provides a lightweight configuration method using the mission editor. Within a very short time, and with very little coding, - -- the mission designer is able to configure a complete A2A defense system for a coalition using the DCS Mission Editor available functions. - -- Using the DCS Mission Editor, you define borders of the coalition which are guarded by GCICAP, + -- + -- The AI_A2A_GCICAP provides a lightweight configuration method using the mission editor. Within a very short time, and with very little coding, + -- the mission designer is able to configure a complete A2A defense system for a coalition using the DCS Mission Editor available functions. + -- Using the DCS Mission Editor, you define borders of the coalition which are guarded by GCICAP, -- configure airbases to belong to the coalition, define squadrons flying certain types of planes or payloads per airbase, and define CAP zones. - -- **Very little lua needs to be applied, a one liner**, which is fully explained below, which can be embedded - -- right in a DO SCRIPT trigger action or in a larger DO SCRIPT FILE trigger action. - -- - -- CAP flights will take off and proceed to designated CAP zones where they will remain on station until the ground radars direct them to intercept - -- detected enemy aircraft or they run short of fuel and must return to base (RTB). - -- + -- **Very little lua needs to be applied, a one liner**, which is fully explained below, which can be embedded + -- right in a DO SCRIPT trigger action or in a larger DO SCRIPT FILE trigger action. + -- + -- CAP flights will take off and proceed to designated CAP zones where they will remain on station until the ground radars direct them to intercept + -- detected enemy aircraft or they run short of fuel and must return to base (RTB). + -- -- When a CAP flight leaves their zone to perform a GCI or return to base a new CAP flight will spawn to take its place. -- If all CAP flights are engaged or RTB then additional GCI interceptors will scramble to intercept unengaged enemy aircraft under ground radar control. - -- + -- -- In short it is a plug in very flexible and configurable air defence module for DCS World. - -- + -- -- === - -- + -- -- # The following actions need to be followed when using AI\_A2A\_GCICAP in your mission: - -- - -- ## 1) Configure a working AI\_A2A\_GCICAP defense system for ONE coalition. - -- - -- ### 1.1) Define which airbases are for which coalition. - -- + -- + -- ## 1) Configure a working AI\_A2A\_GCICAP defense system for ONE coalition. + -- + -- ### 1.1) Define which airbases are for which coalition. + -- -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_1.JPG) - -- + -- -- Color the airbases red or blue. You can do this by selecting the airbase on the map, and select the coalition blue or red. - -- - -- ### 1.2) Place groups of units given a name starting with a **EWR prefix** of your choice to build your EWR network. - -- + -- + -- ### 1.2) Place groups of units given a name starting with a **EWR prefix** of your choice to build your EWR network. + -- -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_2.JPG) - -- - -- **All EWR groups starting with the EWR prefix (text) will be included in the detection system.** - -- + -- + -- **All EWR groups starting with the EWR prefix (text) will be included in the detection system.** + -- -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy. - -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. + -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). - -- Additionally, ANY other radar capable unit can be part of the EWR network! + -- Additionally, ANY other radar capable unit can be part of the EWR network! -- Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. - -- The position of these units is very important as they need to provide enough coverage + -- The position of these units is very important as they need to provide enough coverage -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. - -- - -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. - -- For example if they are a long way forward and can detect enemy planes on the ground and taking off - -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. - -- Having the radars further back will mean a slower escalation because fewer targets will be detected and - -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. - -- It all depends on what the desired effect is. - -- - -- EWR networks are **dynamically maintained**. By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, + -- + -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. + -- For example if they are a long way forward and can detect enemy planes on the ground and taking off + -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. + -- Having the radars further back will mean a slower escalation because fewer targets will be detected and + -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. + -- It all depends on what the desired effect is. + -- + -- EWR networks are **dynamically maintained**. By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, -- increasing or decreasing the radar coverage of the Early Warning System. - -- - -- ### 1.3) Place Airplane or Helicopter Groups with late activation switched on above the airbases to define Squadrons. - -- + -- + -- ### 1.3) Place Airplane or Helicopter Groups with late activation switched on above the airbases to define Squadrons. + -- -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_3.JPG) - -- - -- These are **templates**, with a given name starting with a **Template prefix** above each airbase that you wanna have a squadron. - -- These **templates** need to be within 1.5km from the airbase center. They don't need to have a slot at the airplane, they can just be positioned above the airbase, + -- + -- These are **templates**, with a given name starting with a **Template prefix** above each airbase that you wanna have a squadron. + -- These **templates** need to be within 1.5km from the airbase center. They don't need to have a slot at the airplane, they can just be positioned above the airbase, -- without a route, and should only have ONE unit. - -- + -- -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_4.JPG) - -- - -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** - -- - -- ### 1.4) Place floating helicopters to create the CAP zones defined by its route points. - -- + -- + -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** + -- + -- ### 1.4) Place floating helicopters to create the CAP zones defined by its route points. + -- -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_5.JPG) - -- - -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** - -- - -- The helicopter indicates the start of the CAP zone. - -- The route points define the form of the CAP zone polygon. - -- + -- + -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** + -- + -- The helicopter indicates the start of the CAP zone. + -- The route points define the form of the CAP zone polygon. + -- -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_6.JPG) - -- + -- -- **The place of the helicopter is important, as the airbase closest to the helicopter will be the airbase from where the CAP planes will take off for CAP.** - -- + -- -- ## 2) There are a lot of defaults set, which can be further modified using the methods in @{#AI_A2A_DISPATCHER}: - -- + -- -- ### 2.1) Planes are taking off in the air from the airbases. - -- + -- -- This prevents airbases to get cluttered with airplanes taking off, it also reduces the risk of human players colliding with taxiiing airplanes, -- resulting in the airbase to halt operations. - -- + -- -- You can change the way how planes take off by using the inherited methods from AI\_A2A\_DISPATCHER: - -- + -- -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details. -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. -- * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield. - -- + -- -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency. -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that: - -- + -- -- * aircraft will stop waiting for each other or for a landing aircraft before takeoff. -- * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other. -- * aircraft may collide at the airbase. -- * aircraft may be awaiting the landing of a plane currently in the air, but never lands ... - -- + -- -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs. -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues! - -- + -- -- ### 2.2) Planes return near the airbase or will land if damaged. - -- + -- -- When damaged airplanes return to the airbase, they will be routed and will dissapear in the air when they are near the airbase. -- There are exceptions to this rule, airplanes that aren't "listening" anymore due to damage or out of fuel, will return to the airbase and land. - -- + -- -- You can change the way how planes land by using the inherited methods from AI\_A2A\_DISPATCHER: - -- + -- -- * @{#AI_A2A_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. -- * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. - -- + -- -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the -- A2A defense system, as no new CAP or GCI planes can takeoff. -- Note that the method @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control. - -- - -- ### 2.3) CAP operations setup for specific airbases, will be executed with the following parameters: - -- - -- * The altitude will range between 6000 and 10000 meters. - -- * The CAP speed will vary between 500 and 800 km/h. + -- + -- ### 2.3) CAP operations setup for specific airbases, will be executed with the following parameters: + -- + -- * The altitude will range between 6000 and 10000 meters. + -- * The CAP speed will vary between 500 and 800 km/h. -- * The engage speed between 800 and 1200 km/h. - -- + -- -- You can change or add a CAP zone by using the inherited methods from AI\_A2A\_DISPATCHER: - -- + -- -- The method @{#AI_A2A_DISPATCHER.SetSquadronCap}() defines a CAP execution for a squadron. - -- + -- -- Setting-up a CAP zone also requires specific parameters: - -- + -- -- * The minimum and maximum altitude -- * The minimum speed and maximum patrol speed -- * The minimum and maximum engage speed -- * The type of altitude measurement - -- - -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. - -- + -- + -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. + -- -- The @{#AI_A2A_DISPATCHER.SetSquadronCapInterval}() method specifies **how much** and **when** CAP flights will takeoff. - -- - -- It is recommended not to overload the air defense with CAP flights, as these will decrease the performance of the overall system. - -- + -- + -- It is recommended not to overload the air defense with CAP flights, as these will decrease the performance of the overall system. + -- -- For example, the following setup will create a CAP for squadron "Sochi": - -- + -- -- A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) -- A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 ) - -- + -- -- ### 2.4) Each airbase will perform GCI when required, with the following parameters: - -- + -- -- * The engage speed is between 800 and 1200 km/h. - -- + -- -- You can change or add a GCI parameters by using the inherited methods from AI\_A2A\_DISPATCHER: - -- + -- -- The method @{#AI_A2A_DISPATCHER.SetSquadronGci}() defines a GCI execution for a squadron. - -- + -- -- Setting-up a GCI readiness also requires specific parameters: - -- + -- -- * The minimum speed and maximum patrol speed - -- + -- -- Essentially this controls how many flights of GCI aircraft can be active at any time. -- Note allowing large numbers of active GCI flights can adversely impact mission performance on low or medium specification hosts/servers. - -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders, + -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders, -- too short will mean that the intruders may have alraedy passed the ideal interception point! - -- + -- -- For example, the following setup will create a GCI for squadron "Sochi": - -- + -- -- A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) - -- + -- -- ### 2.5) Grouping or detected targets. - -- + -- -- Detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate -- group being detected. - -- + -- -- Targets will be grouped within a radius of 30km by default. - -- + -- -- The radius indicates that detected targets need to be grouped within a radius of 30km. -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. - -- Fast planes like in the 80s, need a larger radius than WWII planes. + -- Fast planes like in the 80s, need a larger radius than WWII planes. -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft. - -- + -- -- ## 3) Additional notes: - -- + -- -- In order to create a two way A2A defense system, **two AI\_A2A\_GCICAP defense systems must need to be created**, for each coalition one. -- Each defense system needs its own EWR network setup, airplane templates and CAP configurations. - -- + -- -- This is a good implementation, because maybe in the future, more coalitions may become available in DCS world. - -- + -- -- ## 4) Coding examples how to use the AI\_A2A\_GCICAP class: - -- + -- -- ### 4.1) An easy setup: - -- + -- -- -- Setup the AI_A2A_GCICAP dispatcher for one coalition, and initialize it. -- GCI_Red = AI_A2A_GCICAP:New( "EWR CCCP", "SQUADRON CCCP", "CAP CCCP", 2 ) - -- -- + -- -- -- The following parameters were given to the :New method of AI_A2A_GCICAP, and mean the following: - -- + -- -- * `"EWR CCCP"`: Groups of the blue coalition are placed that define the EWR network. These groups start with the name `EWR CCCP`. -- * `"SQUADRON CCCP"`: Late activated Groups objects of the red coalition are placed above the relevant airbases that will contain these templates in the squadron. -- These late activated Groups start with the name `SQUADRON CCCP`. Each Group object contains only one Unit, and defines the weapon payload, skin and skill level. -- * `"CAP CCCP"`: CAP Zones are defined using floating, late activated Helicopter Group objects, where the route points define the route of the polygon of the CAP Zone. -- These Helicopter Group objects start with the name `CAP CCCP`, and will be the locations wherein CAP will be performed. - -- * `2` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. - -- - -- + -- * `2` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. + -- + -- -- ### 4.2) A more advanced setup: - -- + -- -- -- Setup the AI_A2A_GCICAP dispatcher for the blue coalition. - -- - -- A2A_GCICAP_Blue = AI_A2A_GCICAP:New( { "BLUE EWR" }, { "104th", "105th", "106th" }, { "104th CAP" }, 4 ) - -- + -- + -- A2A_GCICAP_Blue = AI_A2A_GCICAP:New( { "BLUE EWR" }, { "104th", "105th", "106th" }, { "104th CAP" }, 4 ) + -- -- The following parameters for the :New method have the following meaning: - -- + -- -- * `{ "BLUE EWR" }`: An array of the group name prefixes of the groups of the blue coalition are placed that define the EWR network. These groups start with the name `BLUE EWR`. - -- * `{ "104th", "105th", "106th" } `: An array of the group name prefixes of the Late activated Groups objects of the blue coalition are + -- * `{ "104th", "105th", "106th" } `: An array of the group name prefixes of the Late activated Groups objects of the blue coalition are -- placed above the relevant airbases that will contain these templates in the squadron. - -- These late activated Groups start with the name `104th` or `105th` or `106th`. - -- * `{ "104th CAP" }`: An array of the names of the CAP zones are defined using floating, late activated helicopter group objects, + -- These late activated Groups start with the name `104th` or `105th` or `106th`. + -- * `{ "104th CAP" }`: An array of the names of the CAP zones are defined using floating, late activated helicopter group objects, -- where the route points define the route of the polygon of the CAP Zone. -- These Helicopter Group objects start with the name `104th CAP`, and will be the locations wherein CAP will be performed. - -- * `4` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. - -- + -- * `4` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. + -- -- @field #AI_A2A_GCICAP AI_A2A_GCICAP = { ClassName = "AI_A2A_GCICAP", @@ -3638,73 +4218,73 @@ do -- @param #string TemplatePrefixes A list of template prefixes. -- @param #string CapPrefixes A list of CAP zone prefixes (polygon zones). -- @param #number CapLimit A number of how many CAP maximum will be spawned. - -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. + -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. -- @return #AI_A2A_GCICAP -- @usage - -- + -- -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. -- -- The CAP Zone prefix is "CAP Zone". -- -- The CAP Limit is 2. - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2 ) - -- + -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2 ) + -- -- @usage - -- + -- -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. -- -- The CAP Zone prefix is "CAP Zone". -- -- The CAP Limit is 2. -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000 ) - -- + -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000 ) + -- -- @usage - -- + -- -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. -- -- The CAP Zone prefix is "CAP Zone". -- -- The CAP Limit is 2. -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, -- -- will be considered a defense task if the target is within 60km from the defender. - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000 ) - -- + -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000 ) + -- -- @usage - -- + -- -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. -- -- The EWR network group prefix is DF CCCP. All groups starting with DF CCCP will be part of the EWR network. -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. -- -- The CAP Zone prefix is "CAP Zone". -- -- The CAP Limit is 2. -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, -- -- will be considered a defense task if the target is within 60km from the defender. -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000 ) - -- + -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000 ) + -- -- @usage - -- + -- -- -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources. -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. -- -- The CAP Zone prefix is "CAP Zone". -- -- The CAP Limit is 2. -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, -- -- will be considered a defense task if the target is within 60km from the defender. -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. - -- - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) - -- + -- + -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) + -- -- @usage - -- + -- -- -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources. -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. @@ -3714,9 +4294,9 @@ do -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. - -- - -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, nil, nil, nil, nil, nil, 30 ) - -- + -- + -- A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, nil, nil, nil, nil, nil, 30 ) + -- function AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) local EWRSetGroup = SET_GROUP:New() @@ -3726,14 +4306,14 @@ do local Detection = DETECTION_AREAS:New( EWRSetGroup, GroupingRadius or 30000 ) local self = BASE:Inherit( self, AI_A2A_DISPATCHER:New( Detection ) ) -- #AI_A2A_GCICAP - + self:SetEngageRadius( EngageRadius ) self:SetGciRadius( GciRadius ) -- Determine the coalition of the EWRNetwork, this will be the coalition of the GCICAP. local EWRFirst = EWRSetGroup:GetFirst() -- Wrapper.Group#GROUP local EWRCoalition = EWRFirst:GetCoalition() - + -- Determine the airbases belonging to the coalition. local AirbaseNames = {} -- #list<#string> for AirbaseID, AirbaseData in pairs( _DATABASE.AIRBASES ) do @@ -3742,25 +4322,25 @@ do if Airbase:GetCoalition() == EWRCoalition then table.insert( AirbaseNames, AirbaseName ) end - end - + end + self.Templates = SET_GROUP :New() :FilterPrefixes( TemplatePrefixes ) :FilterOnce() -- Setup squadrons - + self:I( { Airbases = AirbaseNames } ) - self:I( "Defining Templates for Airbases ..." ) + self:I( "Defining Templates for Airbases ..." ) for AirbaseID, AirbaseName in pairs( AirbaseNames ) do local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE local AirbaseName = Airbase:GetName() local AirbaseCoord = Airbase:GetCoordinate() local AirbaseZone = ZONE_RADIUS:New( "Airbase", AirbaseCoord:GetVec2(), 3000 ) local Templates = nil - self:I( { Airbase = AirbaseName } ) + self:I( { Airbase = AirbaseName } ) for TemplateID, Template in pairs( self.Templates:GetSet() ) do local Template = Template -- Wrapper.Group#GROUP local TemplateCoord = Template:GetCoordinate() @@ -3776,20 +4356,20 @@ do end -- Setup CAP. - -- Find for each CAP the nearest airbase to the (start or center) of the zone. + -- Find for each CAP the nearest airbase to the (start or center) of the zone. -- CAP will be launched from there. - + self.CAPTemplates = SET_GROUP:New() self.CAPTemplates:FilterPrefixes( CapPrefixes ) self.CAPTemplates:FilterOnce() - - self:I( "Setting up CAP ..." ) + + self:I( "Setting up CAP ..." ) for CAPID, CAPTemplate in pairs( self.CAPTemplates:GetSet() ) do local CAPZone = ZONE_POLYGON:New( CAPTemplate:GetName(), CAPTemplate ) -- Now find the closest airbase from the ZONE (start or center) local AirbaseDistance = 99999999 local AirbaseClosest = nil -- Wrapper.Airbase#AIRBASE - self:I( { CAPZoneGroup = CAPID } ) + self:I( { CAPZoneGroup = CAPID } ) for AirbaseID, AirbaseName in pairs( AirbaseNames ) do local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE local AirbaseName = Airbase:GetName() @@ -3797,7 +4377,7 @@ do local Squadron = self.DefenderSquadrons[AirbaseName] if Squadron then local Distance = AirbaseCoord:Get2DDistance( CAPZone:GetCoordinate() ) - self:I( { AirbaseDistance = Distance } ) + self:I( { AirbaseDistance = Distance } ) if Distance < AirbaseDistance then AirbaseDistance = Distance AirbaseClosest = Airbase @@ -3805,35 +4385,35 @@ do end end if AirbaseClosest then - self:I( { CAPAirbase = AirbaseClosest:GetName() } ) + self:I( { CAPAirbase = AirbaseClosest:GetName() } ) self:SetSquadronCap( AirbaseClosest:GetName(), CAPZone, 6000, 10000, 500, 800, 800, 1200, "RADIO" ) self:SetSquadronCapInterval( AirbaseClosest:GetName(), CapLimit, 300, 600, 1 ) - end - end + end + end -- Setup GCI. -- GCI is setup for all Squadrons. - self:I( "Setting up GCI ..." ) + self:I( "Setting up GCI ..." ) for AirbaseID, AirbaseName in pairs( AirbaseNames ) do local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE local AirbaseName = Airbase:GetName() local Squadron = self.DefenderSquadrons[AirbaseName] - self:F( { Airbase = AirbaseName } ) + self:F( { Airbase = AirbaseName } ) if Squadron then - self:I( { GCIAirbase = AirbaseName } ) + self:I( { GCIAirbase = AirbaseName } ) self:SetSquadronGci( AirbaseName, 800, 1200 ) end end - + self:__Start( 5 ) - + self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) - + self:HandleEvent( EVENTS.Land ) self:HandleEvent( EVENTS.EngineShutdown ) - + return self end @@ -3844,24 +4424,24 @@ do -- @param #string BorderPrefix A Border Zone Prefix. -- @param #string CapPrefixes A list of CAP zone prefixes (polygon zones). -- @param #number CapLimit A number of how many CAP maximum will be spawned. - -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. + -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. -- @return #AI_A2A_GCICAP -- @usage - -- + -- -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. -- -- The CAP Zone prefix is "CAP Zone". -- -- The CAP Limit is 2. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2 ) - -- + -- + -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2 ) + -- -- @usage - -- + -- -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. @@ -3869,11 +4449,11 @@ do -- -- The CAP Zone prefix is "CAP Zone". -- -- The CAP Limit is 2. -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000 ) - -- + -- + -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000 ) + -- -- @usage - -- + -- -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. @@ -3881,13 +4461,13 @@ do -- -- The CAP Zone prefix is "CAP Zone". -- -- The CAP Limit is 2. -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, -- -- will be considered a defense task if the target is within 60km from the defender. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000 ) - -- + -- + -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000 ) + -- -- @usage - -- + -- -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. @@ -3895,14 +4475,14 @@ do -- -- The CAP Zone prefix is "CAP Zone". -- -- The CAP Limit is 2. -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, -- -- will be considered a defense task if the target is within 60km from the defender. -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000 ) - -- + -- + -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000 ) + -- -- @usage - -- + -- -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources. -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. @@ -3910,15 +4490,15 @@ do -- -- The CAP Zone prefix is "CAP Zone". -- -- The CAP Limit is 2. -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. - -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, -- -- will be considered a defense task if the target is within 60km from the defender. -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) - -- + -- + -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) + -- -- @usage - -- + -- -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources. -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. @@ -3929,9 +4509,9 @@ do -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. - -- - -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", nil, nil, nil, nil, nil, 30 ) - -- + -- + -- A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", nil, nil, nil, nil, nil, 30 ) + -- function AI_A2A_GCICAP:NewWithBorder( EWRPrefixes, TemplatePrefixes, BorderPrefix, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) local self = AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) @@ -3939,10 +4519,9 @@ do if BorderPrefix then self:SetBorderZone( ZONE_POLYGON:New( BorderPrefix, GROUP:FindByName( BorderPrefix ) ) ) end - + return self end end - diff --git a/Moose Development/Moose/AI/AI_A2A_Gci.lua b/Moose Development/Moose/AI/AI_A2A_Gci.lua index 53b7141ab..4a3f4570b 100644 --- a/Moose Development/Moose/AI/AI_A2A_Gci.lua +++ b/Moose Development/Moose/AI/AI_A2A_Gci.lua @@ -1,12 +1,12 @@ --- **AI** -- (R2.2) - Models the process of Ground Controlled Interception (GCI) for airplanes. -- -- This is a class used in the @{AI_A2A_Dispatcher}. --- +-- -- === --- +-- -- ### Author: **FlightControl** --- --- === +-- +-- === -- -- @module AI.AI_A2A_GCI -- @image AI_Ground_Control_Intercept.JPG @@ -18,52 +18,52 @@ --- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. --- +-- -- ![Process](..\Presentations\AI_GCI\Dia3.JPG) --- +-- -- The AI_A2A_GCI is assigned a @{Wrapper.Group} and this must be done before the AI_A2A_GCI process can be started using the **Start** event. --- +-- -- ![Process](..\Presentations\AI_GCI\Dia4.JPG) --- +-- -- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. -- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. --- +-- -- ![Process](..\Presentations\AI_GCI\Dia5.JPG) --- +-- -- This cycle will continue. --- +-- -- ![Process](..\Presentations\AI_GCI\Dia6.JPG) --- +-- -- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. -- -- ![Process](..\Presentations\AI_GCI\Dia9.JPG) --- +-- -- When enemies are detected, the AI will automatically engage the enemy. --- +-- -- ![Process](..\Presentations\AI_GCI\Dia10.JPG) --- +-- -- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. -- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. --- +-- -- ![Process](..\Presentations\AI_GCI\Dia13.JPG) --- +-- -- ## 1. AI_A2A_GCI constructor --- +-- -- * @{#AI_A2A_GCI.New}(): Creates a new AI_A2A_GCI object. --- +-- -- ## 2. AI_A2A_GCI is a FSM --- +-- -- ![Process](..\Presentations\AI_GCI\Dia2.JPG) --- +-- -- ### 2.1 AI_A2A_GCI States --- +-- -- * **None** ( Group ): The process is not started yet. -- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. -- * **Engaging** ( Group ): The AI is engaging the bogeys. -- * **Returning** ( Group ): The AI is returning to Base.. --- +-- -- ### 2.2 AI_A2A_GCI Events --- +-- -- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. -- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. -- * **@{#AI_A2A_GCI.Engage}**: Let the AI engage the bogeys. @@ -76,25 +76,25 @@ -- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. -- -- ## 3. Set the Range of Engagement --- +-- -- ![Range](..\Presentations\AI_GCI\Dia11.JPG) --- --- An optional range can be set in meters, +-- +-- An optional range can be set in meters, -- that will define when the AI will engage with the detected airborne enemy targets. -- The range can be beyond or smaller than the range of the Patrol Zone. -- The range is applied at the position of the AI. -- Use the method @{AI.AI_GCI#AI_A2A_GCI.SetEngageRange}() to define that range. -- -- ## 4. Set the Zone of Engagement --- +-- -- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) --- --- An optional @{Zone} can be set, +-- +-- An optional @{Zone} can be set, -- that will define when the AI will engage with the detected airborne enemy targets. -- Use the method @{AI.AI_Cap#AI_A2A_GCI.SetEngageZone}() to define that Zone. --- +-- -- === --- +-- -- @field #AI_A2A_GCI AI_A2A_GCI = { ClassName = "AI_A2A_GCI", @@ -105,183 +105,39 @@ AI_A2A_GCI = { --- Creates a new AI_A2A_GCI object -- @param #AI_A2A_GCI self -- @param Wrapper.Group#GROUP AIIntercept +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". -- @return #AI_A2A_GCI -function AI_A2A_GCI:New( AIIntercept, EngageMinSpeed, EngageMaxSpeed ) +function AI_A2A_GCI:New2( AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) - -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2A:New( AIIntercept ) ) -- #AI_A2A_GCI + local AI_Air = AI_AIR:New( AIIntercept ) + local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air, AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + local self = BASE:Inherit( self, AI_Air_Engage ) -- #AI_A2A_GCI - self.Accomplished = false - self.Engaging = false - - self.EngageMinSpeed = EngageMinSpeed - self.EngageMaxSpeed = EngageMaxSpeed - self.PatrolMinSpeed = EngageMinSpeed - self.PatrolMaxSpeed = EngageMaxSpeed - - self.PatrolAltType = "RADIO" - - self:AddTransition( { "Started", "Engaging", "Returning", "Airborne" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI. - - --- OnBefore Transition Handler for Event Engage. - -- @function [parent=#AI_A2A_GCI] OnBeforeEngage - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Engage. - -- @function [parent=#AI_A2A_GCI] OnAfterEngage - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2A_GCI] Engage - -- @param #AI_A2A_GCI self - - --- Asynchronous Event Trigger for Event Engage. - -- @function [parent=#AI_A2A_GCI] __Engage - -- @param #AI_A2A_GCI self - -- @param #number Delay The delay in seconds. - ---- OnLeave Transition Handler for State Engaging. --- @function [parent=#AI_A2A_GCI] OnLeaveEngaging --- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @return #boolean Return false to cancel Transition. - ---- OnEnter Transition Handler for State Engaging. --- @function [parent=#AI_A2A_GCI] OnEnterEngaging --- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. - - self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI. - - --- OnBefore Transition Handler for Event Fired. - -- @function [parent=#AI_A2A_GCI] OnBeforeFired - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Fired. - -- @function [parent=#AI_A2A_GCI] OnAfterFired - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2A_GCI] Fired - -- @param #AI_A2A_GCI self - - --- Asynchronous Event Trigger for Event Fired. - -- @function [parent=#AI_A2A_GCI] __Fired - -- @param #AI_A2A_GCI self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI. - - --- OnBefore Transition Handler for Event Destroy. - -- @function [parent=#AI_A2A_GCI] OnBeforeDestroy - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Destroy. - -- @function [parent=#AI_A2A_GCI] OnAfterDestroy - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2A_GCI] Destroy - -- @param #AI_A2A_GCI self - - --- Asynchronous Event Trigger for Event Destroy. - -- @function [parent=#AI_A2A_GCI] __Destroy - -- @param #AI_A2A_GCI self - -- @param #number Delay The delay in seconds. - - - self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI. - - --- OnBefore Transition Handler for Event Abort. - -- @function [parent=#AI_A2A_GCI] OnBeforeAbort - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Abort. - -- @function [parent=#AI_A2A_GCI] OnAfterAbort - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2A_GCI] Abort - -- @param #AI_A2A_GCI self - - --- Asynchronous Event Trigger for Event Abort. - -- @function [parent=#AI_A2A_GCI] __Abort - -- @param #AI_A2A_GCI self - -- @param #number Delay The delay in seconds. - - self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI. - - --- OnBefore Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2A_GCI] OnBeforeAccomplish - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event Accomplish. - -- @function [parent=#AI_A2A_GCI] OnAfterAccomplish - -- @param #AI_A2A_GCI self - -- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - - --- Synchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2A_GCI] Accomplish - -- @param #AI_A2A_GCI self - - --- Asynchronous Event Trigger for Event Accomplish. - -- @function [parent=#AI_A2A_GCI] __Accomplish - -- @param #AI_A2A_GCI self - -- @param #number Delay The delay in seconds. + self:SetFuelThreshold( .2, 60 ) + self:SetDamageThreshold( 0.4 ) + self:SetDisengageRadius( 70000 ) return self end +--- Creates a new AI_A2A_GCI object +-- @param #AI_A2A_GCI self +-- @param Wrapper.Group#GROUP AIIntercept +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @return #AI_A2A_GCI +function AI_A2A_GCI:New( AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + return self:New2( AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) +end + --- onafter State Transition for Event Patrol. -- @param #AI_A2A_GCI self -- @param Wrapper.Group#GROUP AIIntercept The AI Group managed by the FSM. @@ -290,173 +146,29 @@ end -- @param #string To The To State string. function AI_A2A_GCI:onafterStart( AIIntercept, From, Event, To ) - self:GetParent( self ).onafterStart( self, AIIntercept, From, Event, To ) - AIIntercept:HandleEvent( EVENTS.Takeoff, nil, self ) - + self:GetParent( self, AI_A2A_GCI ).onafterStart( self, AIIntercept, From, Event, To ) end - ---- onafter State Transition for Event Patrol. +--- Evaluate the attack and create an AttackUnitTask list. -- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_GCI:onafterEngage( AIIntercept, From, Event, To ) +-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. +-- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param #number EngageAltitude The altitude to engage the targets. +-- @return #AI_A2A_GCI self +function AI_A2A_GCI:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) - self:HandleEvent( EVENTS.Dead ) + local AttackUnitTasks = {} -end - --- todo: need to fix this global function - ---- @param Wrapper.Group#GROUP AIControllable -function AI_A2A_GCI.InterceptRoute( AIIntercept, Fsm ) - - AIIntercept:F( { "AI_A2A_GCI.InterceptRoute:", AIIntercept:GetName() } ) - - if AIIntercept:IsAlive() then - Fsm:__Engage( 0.5 ) - - --local Task = AIIntercept:TaskOrbitCircle( 4000, 400 ) - --AIIntercept:SetTask( Task ) - end -end - ---- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_GCI:onbeforeEngage( AIIntercept, From, Event, To ) - - if self.Accomplished == true then - return false - end -end - ---- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The AI Group managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_GCI:onafterAbort( AIIntercept, From, Event, To ) - AIIntercept:ClearTasks() - self:Return() - self:__RTB( 0.5 ) -end - - ---- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The GroupGroup managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_GCI:onafterEngage( AIIntercept, From, Event, To, AttackSetUnit ) - - self:F( { AIIntercept, From, Event, To, AttackSetUnit} ) - - self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT - - local FirstAttackUnit = self.AttackSetUnit:GetFirst() - - if FirstAttackUnit and FirstAttackUnit:IsAlive() then - - if AIIntercept:IsAlive() then - - local EngageRoute = {} - - local CurrentCoord = AIIntercept:GetCoordinate() - - --- Calculate the target route point. - - local CurrentCoord = AIIntercept:GetCoordinate() - - local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate() - self:SetTargetDistance( ToTargetCoord ) -- For RTB status check - - local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) - local ToInterceptAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) - - --- Create a route point of type air. - local ToPatrolRoutePoint = CurrentCoord:Translate( 15000, ToInterceptAngle ):WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) - - self:F( { Angle = ToInterceptAngle, ToTargetSpeed = ToTargetSpeed } ) - self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) - - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint - - local AttackTasks = {} - - for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do - local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT - if AttackUnit:IsAlive() and AttackUnit:IsAir() then - self:T( { "Intercepting Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) - AttackTasks[#AttackTasks+1] = AIIntercept:TaskAttackUnit( AttackUnit ) - end - end - - if #AttackTasks == 0 then - self:E("No targets found -> Going RTB") - self:Return() - self:__RTB( 0.5 ) - else - AIIntercept:OptionROEOpenFire() - AIIntercept:OptionROTEvadeFire() - - AttackTasks[#AttackTasks+1] = AIIntercept:TaskFunction( "AI_A2A_GCI.InterceptRoute", self ) - EngageRoute[#EngageRoute].task = AIIntercept:TaskCombo( AttackTasks ) - end - - AIIntercept:Route( EngageRoute, 0.5 ) - + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT + self:T( { "Attacking Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } ) + if AttackUnit:IsAlive() and AttackUnit:IsAir() then + -- TODO: Add coalition check? Only attack units of if AttackUnit:GetCoalition()~=AICap:GetCoalition() + -- Maybe the detected set also contains + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) end - else - self:E("No targets found -> Going RTB") - self:Return() - self:__RTB( 0.5 ) end -end - ---- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. -function AI_A2A_GCI:onafterAccomplish( AIIntercept, From, Event, To ) - self.Accomplished = true - self:SetDetectionOff() -end - ---- @param #AI_A2A_GCI self --- @param Wrapper.Group#GROUP AIIntercept The Group Object managed by the FSM. --- @param #string From The From State string. --- @param #string Event The Event string. --- @param #string To The To State string. --- @param Core.Event#EVENTDATA EventData -function AI_A2A_GCI:onafterDestroy( AIIntercept, From, Event, To, EventData ) - - if EventData.IniUnit then - self.AttackUnits[EventData.IniUnit] = nil - end -end - ---- @param #AI_A2A_GCI self --- @param Core.Event#EVENTDATA EventData -function AI_A2A_GCI:OnEventDead( EventData ) - self:F( { "EventDead", EventData } ) - - if EventData.IniDCSUnit then - if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then - self:__Destroy( 1, EventData ) - end - end + + return AttackUnitTasks end diff --git a/Moose Development/Moose/AI/AI_A2A_Patrol.lua b/Moose Development/Moose/AI/AI_A2A_Patrol.lua index 7b44c14fb..d0a01bdd6 100644 --- a/Moose Development/Moose/AI/AI_A2A_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2A_Patrol.lua @@ -121,13 +121,13 @@ AI_A2A_PATROL = { --- Creates a new AI_A2A_PATROL object -- @param #AI_A2A_PATROL self --- @param Wrapper.Group#GROUP AIPatrol +-- @param Wrapper.Group#GROUP AIPatrol The patrol group object. -- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. -- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. --- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to BARO -- @return #AI_A2A_PATROL self -- @usage -- -- Define a new AI_A2A_PATROL Object. This PatrolArea will patrol a Group within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h. @@ -136,8 +136,14 @@ AI_A2A_PATROL = { -- PatrolArea = AI_A2A_PATROL:New( PatrolZone, 3000, 6000, 600, 900 ) function AI_A2A_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) - -- Inherits from BASE - local self = BASE:Inherit( self, AI_A2A:New( AIPatrol ) ) -- #AI_A2A_PATROL + local AI_Air = AI_AIR:New( AIPatrol ) + local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + local self = BASE:Inherit( self, AI_Air_Patrol ) -- #AI_A2A_PATROL + + self:SetFuelThreshold( .2, 60 ) + self:SetDamageThreshold( 0.4 ) + self:SetDisengageRadius( 70000 ) + self.PatrolZone = PatrolZone self.PatrolFloorAltitude = PatrolFloorAltitude @@ -145,8 +151,8 @@ function AI_A2A_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCei self.PatrolMinSpeed = PatrolMinSpeed self.PatrolMaxSpeed = PatrolMaxSpeed - -- defafult PatrolAltType to "RADIO" if not specified - self.PatrolAltType = PatrolAltType or "RADIO" + -- defafult PatrolAltType to "BARO" if not specified + self.PatrolAltType = PatrolAltType or "BARO" self:AddTransition( { "Started", "Airborne", "Refuelling" }, "Patrol", "Patrolling" ) @@ -281,15 +287,15 @@ function AI_A2A_PATROL:onafterPatrol( AIPatrol, From, Event, To ) end - ---- @param Wrapper.Group#GROUP AIPatrol --- This statis method is called from the route path within the last task at the last waaypoint of the AIPatrol. +--- This statis method is called from the route path within the last task at the last waaypoint of the AIPatrol. -- Note that this method is required, as triggers the next route when patrolling for the AIPatrol. +-- @param Wrapper.Group#GROUP AIPatrol The AI group. +-- @param #AI_A2A_PATROL Fsm The FSM. function AI_A2A_PATROL.PatrolRoute( AIPatrol, Fsm ) AIPatrol:F( { "AI_A2A_PATROL.PatrolRoute:", AIPatrol:GetName() } ) - if AIPatrol:IsAlive() then + if AIPatrol and AIPatrol:IsAlive() then Fsm:Route() end @@ -303,7 +309,6 @@ end -- @param #string Event The Event string. -- @param #string To The To State string. function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) - self:F2() -- When RTB, don't allow anymore the routing. @@ -312,7 +317,7 @@ function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) end - if AIPatrol:IsAlive() then + if AIPatrol and AIPatrol:IsAlive() then local PatrolRoute = {} @@ -320,43 +325,80 @@ function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) local CurrentCoord = AIPatrol:GetCoordinate() - local ToTargetCoord = self.PatrolZone:GetRandomPointVec2() - ToTargetCoord:SetAlt( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) ) - self:SetTargetDistance( ToTargetCoord ) -- For RTB status check + -- Random altitude. + local altitude=math.random(self.PatrolFloorAltitude, self.PatrolCeilingAltitude) + + -- Random speed in km/h. + local speedkmh = math.random(self.PatrolMinSpeed, self.PatrolMaxSpeed) - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + -- First waypoint is current position. + PatrolRoute[1]=CurrentCoord:WaypointAirTurningPoint(nil, speedkmh, {}, "Current") - --- Create a route point of type air. - local ToPatrolRoutePoint = ToTargetCoord:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, - true - ) + if self.racetrack then + + -- Random heading. + local heading = math.random(self.racetrackheadingmin, self.racetrackheadingmax) + + -- Random leg length. + local leg=math.random(self.racetracklegmin, self.racetracklegmax) + + -- Random duration if any. + local duration = self.racetrackdurationmin + if self.racetrackdurationmax then + duration=math.random(self.racetrackdurationmin, self.racetrackdurationmax) + end + + -- CAP coordinate. + local c0=self.PatrolZone:GetRandomCoordinate() + if self.racetrackcapcoordinates and #self.racetrackcapcoordinates>0 then + c0=self.racetrackcapcoordinates[math.random(#self.racetrackcapcoordinates)] + end + + -- Race track points. + local c1=c0:SetAltitude(altitude) --Core.Point#COORDINATE + local c2=c1:Translate(leg, heading):SetAltitude(altitude) + + self:SetTargetDistance(c0) -- For RTB status check + + -- Debug: + self:T(string.format("Patrol zone race track: v=%.1f knots, h=%.1f ft, heading=%03d, leg=%d m, t=%s sec", UTILS.KmphToKnots(speedkmh), UTILS.MetersToFeet(altitude), heading, leg, tostring(duration))) + --c1:MarkToAll("Race track c1") + --c2:MarkToAll("Race track c2") - PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint - PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint - - local Tasks = {} - Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_A2A_PATROL.PatrolRoute", self ) - PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) - + -- Task to orbit. + local taskOrbit=AIPatrol:TaskOrbit(c1, altitude, UTILS.KmphToMps(speedkmh), c2) + + -- Task function to redo the patrol at other random position. + local taskPatrol=AIPatrol:TaskFunction("AI_A2A_PATROL.PatrolRoute", self) + + -- Controlled task with task condition. + local taskCond=AIPatrol:TaskCondition(nil, nil, nil, nil, duration, nil) + local taskCont=AIPatrol:TaskControlled(taskOrbit, taskCond) + + -- Second waypoint + PatrolRoute[2]=c1:WaypointAirTurningPoint(self.PatrolAltType, speedkmh, {taskCont, taskPatrol}, "CAP Orbit") + + else + + -- Target coordinate. + local ToTargetCoord=self.PatrolZone:GetRandomCoordinate() --Core.Point#COORDINATE + ToTargetCoord:SetAltitude(altitude) + + self:SetTargetDistance( ToTargetCoord ) -- For RTB status check + + local taskReRoute=AIPatrol:TaskFunction( "AI_A2A_PATROL.PatrolRoute", self ) + + PatrolRoute[2]=ToTargetCoord:WaypointAirTurningPoint(self.PatrolAltType, speedkmh, {taskReRoute}, "Patrol Point") + + end + + -- ROE AIPatrol:OptionROEReturnFire() AIPatrol:OptionROTEvadeFire() - - AIPatrol:Route( PatrolRoute, 0.5 ) - end - -end - ---- @param Wrapper.Group#GROUP AIPatrol -function AI_A2A_PATROL.Resume( AIPatrol, Fsm ) - - AIPatrol:I( { "AI_A2A_PATROL.Resume:", AIPatrol:GetName() } ) - if AIPatrol:IsAlive() then - Fsm:__Reset( 1 ) - Fsm:__Route( 5 ) - end + -- Patrol. + AIPatrol:Route( PatrolRoute, 0.5) + end + end + diff --git a/Moose Development/Moose/AI/AI_A2G_BAI.lua b/Moose Development/Moose/AI/AI_A2G_BAI.lua new file mode 100644 index 000000000..f35009033 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -0,0 +1,99 @@ +--- **AI** -- Models the process of air to ground BAI engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_BAI +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_A2G_BAI +-- @extends AI.AI_A2A_Engage#AI_A2A_Engage + + +--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. +-- +-- === +-- +-- @field #AI_A2G_BAI +AI_A2G_BAI = { + ClassName = "AI_A2G_BAI", +} + + + +--- Creates a new AI_A2G_BAI object +-- @param #AI_A2G_BAI self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_BAI +function AI_A2G_BAI:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + local AI_Air = AI_AIR:New( AIGroup ) + local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- #AI_AIR_PATROL + local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + local self = BASE:Inherit( self, AI_Air_Engage ) + + return self +end + + +--- Creates a new AI_A2G_BAI object +-- @param #AI_A2G_BAI self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_BAI +function AI_A2G_BAI:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + return self:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType) +end + +--- Evaluate the attack and create an AttackUnitTask list. +-- @param #AI_A2G_BAI self +-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. +-- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param #number EngageAltitude The altitude to engage the targets. +-- @return #AI_A2G_BAI self +function AI_A2G_BAI:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) + + local AttackUnitTasks = {} + + local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "BAI Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) + end + end + end + + return AttackUnitTasks +end + + diff --git a/Moose Development/Moose/AI/AI_A2G_CAS.lua b/Moose Development/Moose/AI/AI_A2G_CAS.lua new file mode 100644 index 000000000..de24056c9 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -0,0 +1,100 @@ +--- **AI** -- Models the process of air to ground engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_CAS +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_A2G_CAS +-- @extends AI.AI_A2G_Patrol#AI_AIR_PATROL + + +--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. +-- +-- === +-- +-- @field #AI_A2G_CAS +AI_A2G_CAS = { + ClassName = "AI_A2G_CAS", +} + + + +--- Creates a new AI_A2G_CAS object +-- @param #AI_A2G_CAS self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_CAS +function AI_A2G_CAS:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + local AI_Air = AI_AIR:New( AIGroup ) + local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- #AI_AIR_PATROL + local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + local self = BASE:Inherit( self, AI_Air_Engage ) + + return self +end + + +--- Creates a new AI_A2G_CAS object +-- @param #AI_A2G_CAS self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_CAS +function AI_A2G_CAS:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + return self:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType) +end + +--- Evaluate the attack and create an AttackUnitTask list. +-- @param #AI_A2G_CAS self +-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. +-- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param #number EngageAltitude The altitude to engage the targets. +-- @return #AI_A2G_CAS self +function AI_A2G_CAS:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) + + local AttackUnitTasks = {} + + local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "CAS Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) + end + end + end + + return AttackUnitTasks +end + + + diff --git a/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua new file mode 100644 index 000000000..bdf05b150 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -0,0 +1,4711 @@ +--- **AI** - Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. +-- +-- === +-- +-- Features: +-- +-- * Setup quickly an A2G defense system for a coalition. +-- * Setup multiple defense zones to defend specific coordinates in your battlefield. +-- * Setup (SEAD) Suppression of Air Defense squadrons, to gain control in the air of enemy grounds. +-- * Setup (CAS) Controlled Air Support squadrons, to attack closeby enemy ground units near friendly installations. +-- * Setup (BAI) Battleground Air Interdiction squadrons to attack remote enemy ground units and targets. +-- * Define and use a detection network controlled by recce. +-- * Define A2G defense squadrons at airbases, farps and carriers. +-- * Enable airbases for A2G defenses. +-- * Add different planes and helicopter templates to squadrons. +-- * Assign squadrons to execute a specific engagement type depending on threat level of the detected ground enemy unit composition. +-- * Add multiple squadrons to different airbases, farps or carriers. +-- * Define different ranges to engage upon. +-- * Establish an automatic in air refuel process for planes using refuel tankers. +-- * Setup default settings for all squadrons and A2G defenses. +-- * Setup specific settings for specific squadrons. +-- +-- === +-- +-- ## Missions: +-- +-- [AID-A2G - AI A2G Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2G%20-%20AI%20A2G%20Dispatching) +-- +-- === +-- +-- ## YouTube Channel: +-- +-- [DCS WORLD - MOOSE - A2G GCICAP - Build an automatic A2G Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) +-- +-- === +-- +-- # QUICK START GUIDE +-- +-- The following class is available to model an A2G defense system. +-- +-- AI_A2G_DISPATCHER is the main A2G defense class that models the A2G defense system. +-- +-- Before you start using the AI_A2G_DISPATCHER, ask youself the following questions. +-- +-- +-- ## 1. Which coalition am I modeling an A2G defense system for? blue or red? +-- +-- One AI_A2G_DISPATCHER object can create a defense system for **one coalition**, which is blue or red. +-- If you want to create a **mutual defense system**, for both blue and red, then you need to create **two** AI_A2G_DISPATCHER **objects**, +-- each governing their defense system for one coalition. +-- +-- +-- ## 2. Which type of detection will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow). +-- +-- The MOOSE framework leverages the @{Functional.Detection} classes to perform the reconnaissance, detecting enemy units +-- and reporting them to the head quarters. +-- Several types of @{Functional.Detection} classes exist, and the most common characteristics of these classes is that they: +-- +-- * Perform detections from multiple recce as one co-operating entity. +-- * Communicate with a @{Tasking.CommandCenter}, which consolidates each detection. +-- * Groups detections based on a method (per area, per type or per unit). +-- * Communicates detections. +-- +-- +-- ## 3. Which recce units can be used as part of the detection system? Only ground based, or also airborne? +-- +-- Depending on the type of mission you want to achieve, different types of units can be engaged to perform ground enemy targets reconnaissance. +-- Ground recce (FAC) are very useful units to determine the position of enemy ground targets when they spread out over the battlefield at strategic positions. +-- Using their varying detection technology, and especially those ground units which have spotting technology, can be extremely effective at +-- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. +-- Unfortunately, they lack sometimes the visibility to detect targets at greater range, or when scenery is preventing line of sight. +-- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then +-- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! +-- +-- Airborne recce (AFAC) are also very effective. The are capable of patrolling at a functional detection altitude, +-- having an overview of the whole battlefield. However, airborne recce can be vulnerable to air to ground attacks, +-- so you need air superiority to make them effective. +-- Airborne recce will also have varying ground detection technology, which plays a big role in the effectiveness of the reconnaissance. +-- Certain helicopter or plane types have ground searching radars or advanced ground scanning technology, and are very effective +-- compared to air units having only visual detection capabilities. +-- For example, for the red coalition, the Mi-28N and the Su-34; and for the blue side, the reaper, are such effective airborne recce units. +-- +-- Typically, don't want these recce units to engage with the enemy, you want to keep them at position. Therefore, it is a good practice +-- to set the ROE for these recce to hold weapons, and make them invisible from the enemy. +-- +-- It is not possible to perform a recce function as a player (unit). +-- +-- +-- ## 4. How do the defenses decide **when and where to engage** on approaching enemy units? +-- +-- The A2G dispacher needs you to setup (various) defense coordinates, which are strategic positions in the battle field to be defended. +-- Any ground based enemy approaching within the proximity of such a defense point, may trigger for a defensive action by friendly air units. +-- +-- There are 2 important parameters that play a role in the defensive decision making: defensiveness and reactivity. +-- +-- The A2G dispatcher provides various parameters to setup the **defensiveness**, +-- which models the decision **when** a defender will engage with the approaching enemy. +-- Defensiveness is calculated by a probability distribution model when to trigger a defense action, +-- depending on the distance of the enemy unit from the defense coordinates, and a **defensiveness factor**. +-- +-- The other parameter considered for defensive action is **where the enemy is located**, thus the distance from a defense coordinate, +-- which we call the **reactive distance**. By default, the reactive distance is set to 60km, but can be changed by the mission designer +-- using the available method explained further below. +-- The combination of the defensiveness and reactivity results in a model that, the closer the attacker is to the defense point, +-- the higher the probability will be that a defense action will be launched! +-- +-- +-- ## 5. Are defense coordinates and defense reactivity the only parameters? +-- +-- No, depending on the target type, and the threat level of the target, the probability of defense will be higher. +-- In other words, when a SAM-10 radar emitter is detected, its probabilty for defense will be much higher than when a BMP-1 vehicle is +-- detected, even when both enemies are at the same distance from a defense coordinate. +-- This will ensure optimal defenses, SEAD tasks will be launched much more quicker against engaging radar emitters, to ensure air superiority. +-- Approaching main battle tanks will be engaged much faster, than a group of approaching trucks. +-- +-- +-- ## 6. Which Squadrons will I create and which name will I give each Squadron? +-- +-- The A2G defense system works with **Squadrons**. Each Squadron must be given a unique name, that forms the **key** to the squadron. +-- Several options and activities can be set per Squadron. A free format name can be given, but always ensure that the name is meaningfull +-- for your mission, and remember that squadron names are used for communication to the players of your mission. +-- +-- There are mainly 3 types of defenses: **SEAD**, **CAS** and **BAI**. +-- +-- Suppression of Air Defenses (SEAD) are effective agains radar emitters. Close Air Support (CAS) is launched when the enemy is close near friendly units. +-- Battleground Air Interdiction (BAI) tasks are launched when there are no friendlies around. +-- +-- Depending on the defense type, different payloads will be needed. See further points on squadron definition. +-- +-- +-- ## 7. Where will the Squadrons be located? On Airbases? On Carrier Ships? On Farps? +-- +-- Squadrons are placed at the **home base** on an **airfield**, **carrier** or **farp**. +-- Carefully plan where each Squadron will be located as part of the defense system required for mission effective defenses. +-- If the home base of the squadron is too far from assumed enemy positions, then the defenses will be too late. +-- The home bases must be **behind** enemy lines, you want to prevent your home bases to be engaged by enemies! +-- Depending on the units applied for defenses, the home base can be further or closer to the enemies. +-- Any airbase, farp or carrier can act as the launching platform for A2G defenses. +-- Carefully plan which airbases will take part in the coalition. Color each airbase **in the color of the coalition**, using the mission editor, +-- or your air units will not return for landing at the airbase! +-- +-- +-- ## 8. Which helicopter or plane models will I assign for each Squadron? Do I need one plane model or more plane models per squadron? +-- +-- Per Squadron, one or multiple helicopter or plane models can be allocated as **Templates**. +-- These are late activated groups with one airplane or helicopter that start with a specific name, called the **template prefix**. +-- The A2G defense system will select from the given templates a random template to spawn a new plane (group). +-- +-- A squadron will perform specific task types (SEAD, CAS or BAI). So, squadrons will require specific templates for the +-- task types it will perform. A squadron executing SEAD defenses, will require a payload with long range anti-radar seeking missiles. +-- +-- +-- ## 9. Which payloads, skills and skins will these plane models have? +-- +-- Per Squadron, even if you have one plane model, you can still allocate multiple templates of one plane model, +-- each having different payloads, skills and skins. +-- The A2G defense system will select from the given templates a random template to spawn a new plane (group). +-- +-- +-- ## 10. How to squadrons engage in a defensive action? +-- +-- There are two ways how squadrons engage and execute your A2G defenses. +-- Squadrons can start the defense directly from the airbase, farp or carrier. When a squadron launches a defensive group, that group +-- will start directly from the airbase. The other way is to launch early on in the mission a patrolling mechanism. +-- Squadrons will launch air units to patrol in specific zone(s), so that when ground enemy targets are detected, that the airborne +-- A2G defenses can come immediately into action. +-- +-- +-- ## 11. For each Squadron doing a patrol, which zone types will I create? +-- +-- Per zone, evaluate whether you want: +-- +-- * simple trigger zones +-- * polygon zones +-- * moving zones +-- +-- Depending on the type of zone selected, a different @{Zone} object needs to be created from a ZONE_ class. +-- +-- +-- ## 12. Are moving defense coordinates possible? +-- +-- Yes, different COORDINATE types are possible to be used. +-- The COORDINATE_UNIT will help you to specify a defense coodinate that is attached to a moving unit. +-- +-- +-- ## 13. How much defense coordinates do I need to create? +-- +-- It depends, but the idea is to define only the necessary defense points that drive your mission. +-- If you define too much defense points, the performance of your mission may decrease. Per defense point defined, +-- all the possible enemies are evaluated. Note that each defense coordinate has a reach depending on the size of the defense radius. +-- The default defense radius is about 60km, and depending on the defense reactivity, defenses will be launched when the enemy is at +-- close or greater distance from the defense coordinate. +-- +-- +-- ## 14. For each Squadron doing patrols, what are the time intervals and patrol amounts to be performed? +-- +-- For each patrol: +-- +-- * **How many** patrol you want to have airborne at the same time? +-- * **How frequent** you want the defense mechanism to check whether to start a new patrol? +-- +-- other considerations: +-- +-- * **How far** is the patrol area from the engagement "hot zone". You want to ensure that the enemy is reached on time! +-- * **How safe** is the patrol area taking into account air superiority. Is it well defended, are there nearby A2A bases? +-- +-- +-- ## 15. For each Squadron, which takeoff method will I use? +-- +-- For each Squadron, evaluate which takeoff method will be used: +-- +-- * Straight from the air +-- * From the runway +-- * From a parking spot with running engines +-- * From a parking spot with cold engines +-- +-- **The default takeoff method is staight in the air.** +-- This takeoff method is the most useful if you want to avoid airplane clutter at airbases! +-- But it is the least realistic one! +-- +-- +-- ## 16. For each Squadron, which landing method will I use? +-- +-- For each Squadron, evaluate which landing method will be used: +-- +-- * Despawn near the airbase when returning +-- * Despawn after landing on the runway +-- * Despawn after engine shutdown after landing +-- +-- **The default landing method is despawn when near the airbase when returning.** +-- This landing method is the most useful if you want to avoid airplane clutter at airbases! +-- But it is the least realistic one! +-- +-- +-- ## 19. For each Squadron, which **defense overhead** will I use? +-- +-- For each Squadron, depending on the helicopter or airplane type (modern, old) and payload, which overhead is required to provide any defense? +-- +-- In other words, if **X** enemy ground units are detected, how many **Y** defense helicpters or airplanes need to engage (per squadron)? +-- The **Y** is dependent on the type of airplane (era), payload, fuel levels, skills etc. +-- But the most important factor is the payload, which is the amount of A2G weapons the defense can carry to attack the enemy ground units. +-- For example, a Ka-50 can carry 16 vikrs, that means, that it potentially can destroy at least 8 ground units without a reload of ammunication. +-- That means, that one defender can destroy more enemy ground units. +-- Thus, the overhead is a **factor** that will calculate dynamically how many **Y** defenses will be required based on **X** attackers detected. +-- +-- **The default overhead is 1. A smaller value than 1, like 0.25 will decrease the overhead to a 1 / 4 ratio, meaning, +-- one defender for each 4 detected ground enemy units. ** +-- +-- +-- ## 19. For each Squadron, which grouping will I use? +-- +-- When multiple targets are detected, how will defenses be grouped when multiple defense air units are spawned for multiple enemy ground units? +-- Per one, two, three, four? +-- +-- **The default grouping is 1. That means, that each spawned defender will act individually.** +-- But you can specify a number between 1 and 4, so that the defenders will act as a group. +-- +-- === +-- +-- ### Author: **FlightControl** rework of GCICAP + introduction of new concepts (squadrons). +-- +-- @module AI.AI_A2G_Dispatcher +-- @image AI_Air_To_Ground_Dispatching.JPG + + + +do -- AI_A2G_DISPATCHER + + --- AI_A2G_DISPATCHER class. + -- @type AI_A2G_DISPATCHER + -- @extends Tasking.DetectionManager#DETECTION_MANAGER + + --- Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. + -- + -- === + -- + -- When your mission is in the need to take control of the AI to automate and setup a process of air to ground defenses, this is the module you need. + -- The defense system work through the definition of defense coordinates, which are points in your friendly area within the battle field, that your mission need to have defended. + -- Multiple defense coordinates can be setup. Defense coordinates can be strategic or tactical positions or references to strategic units or scenery. + -- The A2G dispatcher will evaluate every x seconds the tactical situation around each defense coordinate. When a defense coordinate + -- is under threat, it will communicate through the command center that defensive actions need to be taken and will launch groups of air units for defense. + -- The level of threat to the defense coordinate varyies upon the strength and types of the enemy units, the distance to the defense point, and the defensiveness parameters. + -- Defensive actions are taken through probability, but the closer and the more threat the enemy poses to the defense coordinate, the faster it will be attacked by friendly A2G units. + -- + -- Please study carefully the underlying explanations how to setup and use this module, as it has many features. + -- It also requires a little study to ensure that you get a good understanding of the defense mechanisms, to ensure a strong + -- defense for your missions. + -- + -- === + -- + -- # USAGE GUIDE + -- + -- ## 1. AI\_A2G\_DISPATCHER constructor: + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_DISPATCHER-ME_1.JPG) + -- + -- + -- The @{#AI_A2G_DISPATCHER.New}() method creates a new AI_A2G_DISPATCHER instance. + -- + -- ### 1.1. Define the **reconnaissance network**: + -- + -- As part of the AI_A2G_DISPATCHER :New() constructor, a reconnaissance network must be given as the first parameter. + -- A reconnaissance network is provided by passing a @{Functional.Detection} object. + -- The most effective reconnaissance for the A2G dispatcher would be to use the @{Functional.Detection#DETECTION_AREAS} object. + -- + -- A reconnaissance network, is used to detect enemy ground targets, + -- potentially group them into areas, and to understand the position, level of threat of the enemy. + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia5.JPG) + -- + -- As explained in the introduction, depending on the type of mission you want to achieve, different types of units can be applied to detect ground enemy targets. + -- Ground based units are very useful to act as a reconnaissance, but they lack sometimes the visibility to detect targets at greater range. + -- Recce are very useful to acquire the position of enemy ground targets when spread out over the battlefield at strategic positions. + -- Ground units also have varying detectors, and especially the ground units which have laser guiding missiles can be extremely effective at + -- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. + -- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then + -- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! + -- + -- Beside ground level units to use for reconnaissance, air units are also very effective. The are capable of patrolling at great speed + -- covering a large terrain. However, airborne recce can be vulnerable to air to ground attacks, and you need air superiority to make then + -- effective. Also the instruments available at the air units play a big role in the effectiveness of the reconnaissance. + -- Air units which have ground detection capabilities will be much more effective than air units with only visual detection capabilities. + -- For the red coalition, the Mi-28N and for the blue side, the reaper are such effective reconnaissance airborne units. + -- + -- Reconnaissance networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection} instance that is given as the first parameter to the A2G dispatcher. + -- By defining in a **smart way the names or name prefixes of the reconnaissance groups**, these groups will be **automatically added or removed** to or from the reconnaissance network, + -- when these groups are spawned in or destroyed during the ongoing battle. + -- By spawning in dynamically additional recce, you can ensure that there is sufficient reconnaissance coverage so the defense mechanism is continuously + -- alerted of new enemy ground targets. + -- + -- The following example defens a new reconnaissance network using a @{Functional.Detection#DETECTION_AREAS} object. + -- + -- -- Define a SET_GROUP object that builds a collection of groups that define the recce network. + -- -- Here we build the network with all the groups that have a name starting with CCCP Recce. + -- DetectionSetGroup = SET_GROUP:New() -- Defene a set of group objects, caled DetectionSetGroup. + -- + -- DetectionSetGroup:FilterPrefixes( { "CCCP Recce" } ) -- The DetectionSetGroup will search for groups that start with the name "CCCP Recce". + -- + -- -- This command will start the dynamic filtering, so when groups spawn in or are destroyed, + -- -- which have a group name starting with "CCCP Recce", then these will be automatically added or removed from the set. + -- DetectionSetGroup:FilterStart() + -- + -- -- This command defines the reconnaissance network. + -- -- It will group any detected ground enemy targets within a radius of 1km. + -- -- It uses the DetectionSetGroup, which defines the set of reconnaissance groups to detect for enemy ground targets. + -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 1000 ) + -- + -- -- Setup the A2A dispatcher, and initialize it. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- + -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **DetectionSetGroup**. + -- **DetectionSetGroup** is then being configured to filter all active groups with a group name starting with `"CCCP Recce"` to be included in the set. + -- **DetectionSetGroup** is then calling `FilterStart()`, which is starting the dynamic filtering or inclusion of these groups. + -- Note that any destroy or new spawn of a group having a name, starting with the above prefix, will be removed or added to the set. + -- + -- Then a new detection object is created from the class `DETECTION_AREAS`. A grouping radius of 1000 meters (1km) is choosen. + -- + -- The `Detection` object is then passed to the @{#AI_A2G_DISPATCHER.New}() method to indicate the reconnaissance network + -- configuration and setup the A2G defense detection mechanism. + -- + -- ### 1.2. Setup the A2G dispatcher for both a red and blue coalition. + -- + -- Following the above described procedure, you'll need to create for each coalition an separate detection network, and a separate A2G dispatcher. + -- Ensure that while doing so, that you name the objects differently both for red and blue coalition. + -- + -- For example like this for the red coalition: + -- + -- DetectionRed = DETECTION_AREAS:New( DetectionSetGroupRed, 1000 ) + -- A2GDispatcherRed = AI_A2G_DISPATCHER:New( DetectionRed ) + -- + -- And for the blue coalition: + -- + -- DetectionBlue = DETECTION_AREAS:New( DetectionSetGroupBlue, 1000 ) + -- A2GDispatcherBlue = AI_A2G_DISPATCHER:New( DetectionBlue ) + -- + -- + -- Note: Also the SET_GROUP objects should be created for each coalition separately, containing each red and blue recce respectively! + -- + -- ### 1.3. Define the enemy ground target **grouping radius**, in case you use DETECTION_AREAS: + -- + -- The target grouping radius is a property of the DETECTION_AREAS class, that was passed to the AI_A2G_DISPATCHER:New() method, + -- but can be changed. The grouping radius should not be too small, but also depends on the types of ground forces and the way you want your mission to evolve. + -- A large radius will mean large groups of enemy ground targets, while making smaller groups will result in a more fragmented defense system. + -- Typically I suggest a grouping radius of 1km. This is the right balance to create efficient defenses. + -- + -- Note that detected targets are constantly re-grouped, that is, when certain detected enemy ground units are moving further than the group radius, + -- then these units will become a separate area being detected. This may result in additional defenses being started by the dispatcher! + -- So don't make this value too small! Again, I advise about 1km or 1000 meters. + -- + -- ## 2. Setup (a) **Defense Coordinate(s)**. + -- + -- As explained above, defense coordinates are the center of your defense operations. + -- The more threat to the defense coordinate, the higher it is likely a defensive action will be launched. + -- + -- Find below an example how to add defense coordinates: + -- + -- -- Add defense coordinates. + -- A2GDispatcher:AddDefenseCoordinate( "HQ", GROUP:FindByName( "HQ" ):GetCoordinate() ) + -- + -- In this example, the coordinate of a group called `"HQ"` is retrieved, using `:GetCoordinate()` + -- This returns a COORDINATE object, pointing to the first unit within the GROUP object. + -- + -- The method @{#AI_A2G_DISPATCHER.AddDefenseCoordinate}() adds a new defense coordinate to the `A2GDispatcher` object. + -- The first parameter is the key of the defense coordinate, the second the coordinate itself. + -- + -- Later, a COORDINATE_UNIT will be added to the framework, which can be used to assign "moving" coordinates to an A2G dispatcher. + -- + -- **REMEMBER!** + -- + -- - **Defense coordinates are the center of the A2G dispatcher defense system!** + -- - **You can define more defense coordinates to defend a larger area.** + -- - **Detected enemy ground targets are not immediately engaged, but are engaged with a reactivity or probability calculation!** + -- + -- But, there is more to it ... + -- + -- + -- ### 2.1. The **Defense Radius**. + -- + -- The defense radius defines the maximum radius that a defense will be initiated around each defense coordinate. + -- So even when there are targets further away than the defense radius, then these targets won't be engaged upon. + -- By default, the defense radius is set to 100km (100.000 meters), but can be changed using the @{#AI_A2G_DISPATCHER.SetDefenseRadius}() method. + -- Note that the defense radius influences the defense reactivity also! The larger the defense radius, the more reactive the defenses will be. + -- + -- For example: + -- + -- A2GDispatcher:SetDefenseRadius( 30000 ) + -- + -- This defines an A2G dispatcher which will engage on enemy ground targets within 30km radius around the defense coordinate. + -- Note that the defense radius **applies to all defense coordinates** defined within the A2G dispatcher. + -- + -- ### 2.2. The **Defense Reactivity**. + -- + -- There are 5 levels that can be configured to tweak the defense reactivity. As explained above, the threat to a defense coordinate is + -- also determined by the distance of the enemy ground target to the defense coordinate. + -- If you want to have a **low** defense reactivity, that is, the probability that an A2G defense will engage to the enemy ground target, then + -- use the @{#AI_A2G_DISPATCHER.SetDefenseReactivityLow}() method. For medium and high reactivity, use the methods + -- @{#AI_A2G_DISPATCHER.SetDefenseReactivityMedium}() and @{#AI_A2G_DISPATCHER.SetDefenseReactivityHigh}() respectively. + -- + -- Note that the reactivity of defenses is always in relation to the Defense Radius! the shorter the distance, + -- the less reactive the defenses will be in terms of distance to enemy ground targets! + -- + -- For example: + -- + -- A2GDispatcher:SetDefenseReactivityHigh() + -- + -- This defines an A2G dispatcher with high defense reactivity. + -- + -- ## 3. **Squadrons**. + -- + -- The A2G dispatcher works with **Squadrons**, that need to be defined using the different methods available. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, farp or carrier, + -- while defining which helicopter or plane **templates** are being used by the squadron and how many **resources** are available. + -- + -- **Multiple squadrons** can be defined within one A2G dispatcher, each having specific defense tasks and defense parameter settings! + -- + -- Squadrons: + -- + -- * Have name (string) that is the identifier or **key** of the squadron. + -- * Have specific helicopter or plane **templates**. + -- * Are located at **one** airbase, farp or carrier. + -- * Optionally have a **limited set of resources**. The default is that squadrons have **unlimited resources**. + -- + -- The name of the squadron given acts as the **squadron key** in all `A2GDispatcher:SetSquadron...()` or `A2GDispatcher:GetSquadron...()` methods. + -- + -- Additionally, squadrons have specific configuration options to: + -- + -- * Control how new helicopters or aircraft are taking off from the airfield, farp or carrier (in the air, cold, hot, at the runway). + -- * Control how returning helicopters or aircraft are landing at the airfield, farp or carrier (in the air near the airbase, after landing, after engine shutdown). + -- * Control the **grouping** of new helicopters or aircraft spawned at the airfield, farp or carrier. If there is more than one helicopter or aircraft to be spawned, these may be grouped. + -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of helicopters, planes, amount of resources and payload (weapon configuration) chosen, + -- the mission designer can choose to increase or reduce the amount of planes spawned. + -- + -- The method @{#AI_A2G_DISPATCHER.SetSquadron}() defines for you a new squadron. + -- The provided parameters are the squadron name, airbase name and a list of template prefixe, and a number that indicates the amount of resources. + -- + -- For example, this defines 3 new squadrons: + -- + -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50" }, 10 ) + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50" }, 10 ) + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50" }, 10 ) + -- + -- The latter 2 will depart from FARPs, which bare the name `"CAS"` and `"BAI"`. + -- + -- + -- ### 3.1. Squadrons **Tasking**. + -- + -- Squadrons can be commanded to execute 3 types of tasks, as explained above: + -- + -- - SEAD: Suppression of Air Defenses, which are ground targets that have medium or long range radar emitters. + -- - CAS : Close Air Support, when there are enemy ground targets close to friendly units. + -- - BAI : Battlefield Air Interdiction, which are targets further away from the frond-line. + -- + -- You need to configure each squadron which task types you want it to perform. Read on ... + -- + -- ### 3.2. Squadrons enemy ground target **engagement types**. + -- + -- There are two ways how targets can be engaged: directly **on call** from the airfield, farp or carrier, or through a **patrol**. + -- + -- Patrols are extremely handy, as these will airborne your helicopters or airplanes in advance. They will patrol in defined zones outlined, + -- and will engage with the targets once commanded. If the patrol zone is close enough to the enemy ground targets, then the time required + -- to engage is heavily minimized! + -- + -- However; patrols come with a side effect: since your resources are airborne, they will be vulnerable to incoming air attacks from the enemy. + -- + -- The mission designer needs to carefully balance the need for patrols or the need for engagement on call from the airfields. + -- + -- ### 3.3. Squadron **on call** engagement. + -- + -- So to make squadrons engage targets from the airfields, use the following methods: + -- + -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSead}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCas}() method. + -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBai}() method. + -- + -- Note that for the tasks, specific helicopter or airplane templates are required to be used, which you can configure using your mission editor. + -- Especially the payload (weapons configuration) is important to get right. + -- + -- For example, the following will define for the squadrons different tasks: + -- + -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) + -- A2GDispatcher:SetSquadronSead( "Maykop SEAD", 120, 250 ) + -- + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) + -- A2GDispatcher:SetSquadronCas( "Maykop CAS", 120, 250 ) + -- + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) + -- A2GDispatcher:SetSquadronBai( "Maykop BAI", 120, 250 ) + -- + -- ### 3.4. Squadron **on patrol engagement**. + -- + -- Squadrons can be setup to patrol in the air near the engagement hot zone. + -- When needed, the A2G defense units will be close to the battle area, and can engage quickly. + -- + -- So to make squadrons engage targets from a patrol zone, use the following methods: + -- + -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrol}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrol}() method. + -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrol}() method. + -- + -- Because a patrol requires more parameters, the following methods must be used to fine-tune the patrols for each squadron. + -- + -- - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrolInterval}() method. + -- - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrolInterval}() method. + -- - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrolInterval}() method. + -- + -- Here an example to setup patrols of various task types: + -- + -- A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Maykop SEAD", PatrolZone, 300, 500, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop SEAD", 2, 30, 60, 1, "SEAD" ) + -- + -- A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) + -- A2GDispatcher:SetSquadronCasPatrol( "Maykop CAS", PatrolZone, 600, 700, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop CAS", 2, 30, 60, 1, "CAS" ) + -- + -- A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) + -- A2GDispatcher:SetSquadronBaiPatrol( "Maykop BAI", PatrolZone, 800, 900, 50, 80, 250, 300 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Maykop BAI", 2, 30, 60, 1, "BAI" ) + -- + -- + -- ### 3.5. Set squadron take-off methods + -- + -- Use the various SetSquadronTakeoff... methods to control how squadrons are taking-off from the home airfield, FARP or ship. + -- + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield. + -- + -- **The default landing method is to spawn new aircraft directly in the air.** + -- + -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency. + -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that: + -- + -- * aircraft will stop waiting for each other or for a landing aircraft before takeoff. + -- * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other. + -- * aircraft may collide at the airbase. + -- * aircraft may be awaiting the landing of a plane currently in the air, but never lands ... + -- + -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs. + -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues! + -- + -- This example sets the default takeoff method to be from the runway. + -- And for a couple of squadrons overrides this default method. + -- + -- -- Setup the Takeoff methods + -- + -- -- The default takeoff + -- A2ADispatcher:SetDefaultTakeOffFromRunway() + -- + -- -- The individual takeoff per squadron + -- A2ADispatcher:SetSquadronTakeoff( "Mineralnye", AI_A2G_DISPATCHER.Takeoff.Air ) + -- A2ADispatcher:SetSquadronTakeoffInAir( "Sochi" ) + -- A2ADispatcher:SetSquadronTakeoffFromRunway( "Mozdok" ) + -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "Maykop" ) + -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "Novo" ) + -- + -- + -- ### 3.5.1. Set Squadron takeoff altitude when spawning new aircraft in the air. + -- + -- In the case of the @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() there is also an other parameter that can be applied. + -- That is modifying or setting the **altitude** from where planes spawn in the air. + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAirAltitude}() to set the altitude for a specific squadron. + -- The default takeoff altitude can be modified or set using the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAirAltitude}(). + -- As part of the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() a parameter can be specified to set the takeoff altitude. + -- If this parameter is not specified, then the default altitude will be used for the squadron. + -- + -- ### 3.5.2. Set Squadron takeoff interval. + -- + -- The different types of available airfields have different amounts of available launching platforms: + -- + -- - Airbases typically have a lot of platforms. + -- - FARPs have 4 platforms. + -- - Ships have 2 to 4 platforms. + -- + -- Depending on the demand of requested takeoffs by the A2G dispatcher, an airfield can become overloaded. Too many aircraft need to be taken + -- off at the same time, which will result in clutter as described above. In order to better control this behaviour, a takeoff scheduler is implemented, + -- which can be used to control how many aircraft are ordered for takeoff between specific time intervals. + -- The takeff intervals can be specified per squadron, which make sense, as each squadron have a "home" airfield. + -- + -- For this purpose, the method @{#AI_A2G_DISPATCHER.SetSquadronTakeOffInterval}() can be used to specify the takeoff intervals of + -- aircraft groups per squadron to avoid cluttering of aircraft at airbases. + -- This is especially useful for FARPs and ships. Each takeoff dispatch is queued by the dispatcher and when the interval time + -- has been reached, a new group will be spawned or activated for takeoff. + -- + -- The interval needs to be estimated, and depends on the time needed for the aircraft group to actually depart from the launch platform, and + -- the way how the aircraft are starting up. Cold starts take the longest duration, hot starts a few seconds, and runway takeoff also a few seconds for FARPs and ships. + -- + -- See the underlying example: + -- + -- -- Imagine a squadron launched from a FARP, with a grouping of 4. + -- -- Aircraft will cold start from the FARP, and thus, a maximum of 4 aircraft can be launched at the same time. + -- -- Additionally, depending on the group composition of the aircraft, defending units will be ordered for takeoff together. + -- -- It takes about 3 to 4 minutes to takeoff helicopters from FARPs in cold start. + -- A2ADispatcher:SetSquadronTakeOffInterval( "Mineralnye", 60 * 4 ) + -- + -- + -- ### 3.6. Set squadron landing methods + -- + -- In analogy with takeoff, the landing methods are to control how squadrons land at the airfield: + -- + -- * @{#AI_A2G_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. + -- * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. + -- + -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. + -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the + -- A2A defense system, as no new CAP or GCI planes can takeoff. + -- Note that the method @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. + -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control. + -- + -- This example defines the default landing method to be at the runway. + -- And for a couple of squadrons overrides this default method. + -- + -- -- Setup the Landing methods + -- + -- -- The default landing method + -- A2ADispatcher:SetDefaultLandingAtRunway() + -- + -- -- The individual landing per squadron + -- A2ADispatcher:SetSquadronLandingAtRunway( "Mineralnye" ) + -- A2ADispatcher:SetSquadronLandingNearAirbase( "Sochi" ) + -- A2ADispatcher:SetSquadronLandingAtEngineShutdown( "Mozdok" ) + -- A2ADispatcher:SetSquadronLandingNearAirbase( "Maykop" ) + -- A2ADispatcher:SetSquadronLanding( "Novo", AI_A2G_DISPATCHER.Landing.AtRunway ) + -- + -- + -- ### 3.7. Set squadron **grouping**. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronGrouping}() to set the grouping of aircraft when spawned in. + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia12.JPG) + -- + -- In the case of **on call** engagement, the @{#AI_A2G_DISPATCHER.SetSquadronGrouping}() method has additional behaviour. + -- When there aren't enough patrol flights airborne, a on call will be initiated for the remaining + -- targets to be engaged. Depending on the grouping parameter, the spawned flights for on call aircraft are grouped into this setting. + -- For example with a group setting of 2, if 3 targets are detected and cannot be engaged by the available patrols or any airborne flight, + -- an additional on call flight needs to be started. + -- + -- The **grouping value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense flights grouping when the tactical situation changes. + -- + -- ### 3.8. Set the squadron **overhead** to balance the effectiveness of the A2G defenses. + -- + -- The effectiveness can be set with the **overhead parameter**. This is a number that is used to calculate the amount of Units that dispatching command will allocate to GCI in surplus of detected amount of units. + -- The **default value** of the overhead parameter is 1.0, which means **equal balance**. + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia11.JPG) + -- + -- However, depending on the (type of) aircraft (strength and payload) in the squadron and the amount of resources available, this parameter can be changed. + -- + -- The @{#AI_A2G_DISPATCHER.SetSquadronOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. + -- + -- For example, a A-10C with full long-distance A2G missiles payload, may still be less effective than a Su-23 with short range A2G missiles... + -- So in this case, one may want to use the @{#AI_A2G_DISPATCHER.SetOverhead}() method to allocate more defending planes as the amount of detected attacking ground units. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that overhead values: + -- + -- * Higher than 1.0, for example 1.5, will increase the defense unit amounts. For 4 attacking ground units detected, 6 aircraft will be spawned. + -- * Lower than 1, for example 0.75, will decrease the defense unit amounts. For 4 attacking ground units detected, only 3 aircraft will be spawned. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking ground units as part of the detected group + -- multiplied by the overhead parameter, and rounded up to the smallest integer. + -- + -- Typically, for A2G defenses, values small than 1 will be used. Here are some good values for a couple of aircraft to support CAS operations: + -- + -- - A-10C: 0.15 + -- - Su-34: 0.15 + -- - A-10A: 0.25 + -- - SU-25T: 0.10 + -- + -- So generically, the amount of missiles that an aircraft can take will determine its attacking effectiveness. The longer the range of the missiles, + -- the less risk that the defender may be destroyed by the enemy, thus, the less aircraft needs to be activated in a defense. + -- + -- The **overhead value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense overhead when the tactical situation changes. + -- + -- ### 3.8. Set the squadron **engage limit**. + -- + -- To limit the amount of aircraft to defend against a large group of intruders, an **engage limit** can be defined per squadron. + -- This limit will avoid an extensive amount of aircraft to engage with the enemy if the attacking ground forces are enormous. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronEngageLimit}() to limit the amount of aircraft that will engage with the enemy, per squadron. + -- + -- ## 4. Set the **fuel treshold**. + -- + -- When aircraft get **out of fuel** to a certain %-tage, which is by default **15% (0.15)**, there are two possible actions that can be taken: + -- - The aircraft will go RTB, and will be replaced with a new aircraft if possible. + -- - The aircraft will refuel at a tanker, if a tanker has been specified for the squadron. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronFuelThreshold}() to set the **squadron fuel treshold** of the aircraft for all squadrons. + -- + -- ## 6. Other configuration options + -- + -- ### 6.1. Set a tactical display panel. + -- + -- Every 30 seconds, a tactical display panel can be shown that illustrates what the status is of the different groups controlled by AI_A2G_DISPATCHER. + -- Use the method @{#AI_A2G_DISPATCHER.SetTacticalDisplay}() to switch on the tactical display panel. The default will not show this panel. + -- Note that there may be some performance impact if this panel is shown. + -- + -- ## 10. Default settings. + -- + -- Default settings configure the standard behaviour of the squadrons. + -- This section a good overview of the different parameters that setup the behaviour of **ALL** the squadrons by default. + -- Note that default behaviour can be tweaked, and thus, this will change the behaviour of all the squadrons. + -- Unless there is a specific behaviour set for a specific squadron, the default configured behaviour will be followed. + -- + -- ## 10.1. Default **takeoff** behaviour. + -- + -- The default takeoff behaviour is set to **in the air**, which means that new spawned aircraft will be spawned directly in the air above the airbase by default. + -- + -- **The default takeoff method can be set for ALL squadrons that don't have an individual takeoff method configured.** + -- + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoff}() is the generic configuration method to control takeoff by default from the air, hot, cold or from the runway. See the method for further details. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffInAir}() will spawn by default new aircraft from the squadron directly in the air. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffFromParkingCold}() will spawn by default new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffFromParkingHot}() will spawn by default new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffFromRunway}() will spawn by default new aircraft at the runway at the airfield. + -- + -- ## 10.2. Default landing behaviour. + -- + -- The default landing behaviour is set to **near the airbase**, which means that returning airplanes will be despawned directly in the air by default. + -- + -- The default landing method can be set for ALL squadrons that don't have an individual landing method configured. + -- + -- * @{#AI_A2G_DISPATCHER.SetDefaultLanding}() is the generic configuration method to control by default landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. + -- * @{#AI_A2G_DISPATCHER.SetDefaultLandingNearAirbase}() will despawn by default the returning aircraft in the air when near the airfield. + -- * @{#AI_A2G_DISPATCHER.SetDefaultLandingAtRunway}() will despawn by default the returning aircraft directly after landing at the runway. + -- * @{#AI_A2G_DISPATCHER.SetDefaultLandingAtEngineShutdown}() will despawn by default the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. + -- + -- ## 10.3. Default **overhead**. + -- + -- The default overhead is set to **0.25**. That essentially means that for each 4 ground enemies there will be 1 aircraft dispatched. + -- + -- The default overhead value can be set for ALL squadrons that don't have an individual overhead value configured. + -- + -- Use the @{#AI_A2G_DISPATCHER.SetDefaultOverhead}() method can be used to set the default overhead or defense strength for ALL squadrons. + -- + -- ## 10.4. Default **grouping**. + -- + -- The default grouping is set to **one airplane**. That essentially means that there won't be any grouping applied by default. + -- + -- The default grouping value can be set for ALL squadrons that don't have an individual grouping value configured. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultGrouping}() to set the **default grouping** of spawned airplanes for all squadrons. + -- + -- ## 10.5. Default RTB fuel treshold. + -- + -- When an airplane gets **out of fuel** to a certain %-tage, which is **15% (0.15)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultFuelThreshold}() to set the **default fuel treshold** of spawned airplanes for all squadrons. + -- + -- ## 10.6. Default RTB damage treshold. + -- + -- When an airplane is **damaged** to a certain %-tage, which is **40% (0.40)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultDamageThreshold}() to set the **default damage treshold** of spawned airplanes for all squadrons. + -- + -- ## 10.7. Default settings for **patrol**. + -- + -- ### 10.7.1. Default **patrol time Interval**. + -- + -- Patrol dispatching is time event driven, and will evaluate in random time intervals if a new patrol needs to be dispatched. + -- + -- The default patrol time interval is between **180** and **600** seconds. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultPatrolTimeInterval}() to set the **default patrol time interval** of dispatched aircraft for ALL squadrons. + -- + -- Note that you can still change the patrol limit and patrol time intervals for each patrol individually using + -- the @{#AI_A2G_DISPATCHER.SetSquadronPatrolTimeInterval}() method. + -- + -- ### 10.7.2. Default **patrol limit**. + -- + -- Multiple patrol can be airborne at the same time for one squadron, which is controlled by the **patrol limit**. + -- The **default patrol limit** is 1 patrol per squadron to be airborne at the same time. + -- Note that the default patrol limit is used when a squadron patrol is defined, and cannot be changed afterwards. + -- So, ensure that you set the default patrol limit **before** you define or setup the squadron patrol. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultPatrolTimeInterval}() to set the **default patrol time interval** of dispatched aircraft patrols for all squadrons. + -- Note that you can still change the patrol limit and patrol time intervals for each patrol individually using + -- the @{#AI_A2G_DISPATCHER.SetSquadronPatrolTimeInterval}() method. + -- + -- ## 10.7.3. Default tanker for refuelling when executing CAP. + -- + -- Instead of sending CAP to RTB when out of fuel, you can let CAP refuel in mid air using a tanker. + -- This greatly increases the efficiency of your CAP operations. + -- + -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected. + -- Then, use the method @{#AI_A2G_DISPATCHER.SetDefaultTanker}() to set the tanker for the dispatcher. + -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultFuelThreshold}() to set the %-tage left in the defender airplane tanks when a refuel action is needed. + -- + -- When the tanker specified is alive and in the air, the tanker will be used for refuelling. + -- + -- For example, the following setup will set the default refuel tanker to "Tanker": + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_DISPATCHER-ME_11.JPG) + -- + -- -- Define the CAP + -- A2ADispatcher:SetSquadron( "Sochi", AIRBASE.Caucasus.Sochi_Adler, { "SQ CCCP SU-34" }, 20 ) + -- A2ADispatcher:SetSquadronCap( "Sochi", ZONE:New( "PatrolZone" ), 4000, 8000, 600, 800, 1000, 1300 ) + -- A2ADispatcher:SetSquadronCapInterval("Sochi", 2, 30, 600, 1 ) + -- A2ADispatcher:SetSquadronGci( "Sochi", 900, 1200 ) + -- + -- -- Set the default tanker for refuelling to "Tanker", when the default fuel treshold has reached 90% fuel left. + -- A2ADispatcher:SetDefaultFuelThreshold( 0.9 ) + -- A2ADispatcher:SetDefaultTanker( "Tanker" ) + -- + -- ## 10.8. Default settings for GCI. + -- + -- ## 10.8.1. Optimal intercept point calculation. + -- + -- When intruders are detected, the intrusion path of the attackers can be monitored by the EWR. + -- Although defender planes might be on standby at the airbase, it can still take some time to get the defenses up in the air if there aren't any defenses airborne. + -- This time can easily take 2 to 3 minutes, and even then the defenders still need to fly towards the target, which takes also time. + -- + -- Therefore, an optimal **intercept point** is calculated which takes a couple of parameters: + -- + -- * The average bearing of the intruders for an amount of seconds. + -- * The average speed of the intruders for an amount of seconds. + -- * An assumed time it takes to get planes operational at the airbase. + -- + -- The **intercept point** will determine: + -- + -- * If there are any friendlies close to engage the target. These can be defenders performing CAP or defenders in RTB. + -- * The optimal airbase from where defenders will takeoff for GCI. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetIntercept}() to modify the assumed intercept delay time to calculate a valid interception. + -- + -- ## 10.8.2. Default Disengage Radius. + -- + -- The radius to **disengage any target** when the **distance** of the defender to the **home base** is larger than the specified meters. + -- The default Disengage Radius is **300km** (300000 meters). Note that the Disengage Radius is applicable to ALL squadrons! + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDisengageRadius}() to modify the default Disengage Radius to another distance setting. + -- + -- ## 11. Airbase capture: + -- + -- Different squadrons can be located at one airbase. + -- If the airbase gets captured, that is, when there is an enemy unit near the airbase, and there aren't anymore friendlies at the airbase, the airbase will change coalition ownership. + -- As a result, the GCI and CAP will stop! + -- However, the squadron will still stay alive. Any airplane that is airborne will continue its operations until all airborne airplanes + -- of the squadron will be destroyed. This to keep consistency of air operations not to confuse the players. + -- + -- + -- + -- + -- @field #AI_A2G_DISPATCHER + AI_A2G_DISPATCHER = { + ClassName = "AI_A2G_DISPATCHER", + Detection = nil, + } + + --- Definition of a Squadron. + -- @type AI_A2G_DISPATCHER.Squadron + -- @field #string Name The Squadron name. + -- @field Wrapper.Airbase#AIRBASE Airbase The home airbase. + -- @field #string AirbaseName The name of the home airbase. + -- @field Core.Spawn#SPAWN Spawn The spawning object. + -- @field #number ResourceCount The number of resources available. + -- @field #list<#string> TemplatePrefixes The list of template prefixes. + -- @field #boolean Captured true if the squadron is captured. + -- @field #number Overhead The overhead for the squadron. + + + --- List of defense coordinates. + -- @type AI_A2G_DISPATCHER.DefenseCoordinates + -- @map <#string,Core.Point#COORDINATE> A list of all defense coordinates mapped per defense coordinate name. + + --- @field #AI_A2G_DISPATCHER.DefenseCoordinates DefenseCoordinates + AI_A2G_DISPATCHER.DefenseCoordinates = {} + + --- Enumerator for spawns at airbases + -- @type AI_A2G_DISPATCHER.Takeoff + -- @extends Wrapper.Group#GROUP.Takeoff + + --- @field #AI_A2G_DISPATCHER.Takeoff Takeoff + AI_A2G_DISPATCHER.Takeoff = GROUP.Takeoff + + --- Defnes Landing location. + -- @field #AI_A2G_DISPATCHER.Landing + AI_A2G_DISPATCHER.Landing = { + NearAirbase = 1, + AtRunway = 2, + AtEngineShutdown = 3, + } + + --- A defense queue item description + -- @type AI_A2G_DISPATCHER.DefenseQueueItem + -- @field Squadron + -- @field #AI_A2G_DISPATCHER.Squadron DefenderSquadron The squadron in the queue. + -- @field DefendersNeeded + -- @field Defense + -- @field DefenseTaskType + -- @field Functional.Detection#DETECTION_BASE AttackerDetection + -- @field DefenderGrouping + -- @field #string SquadronName The name of the squadron. + + --- Queue of planned defenses to be launched. + -- This queue exists because defenses must be launched on FARPS, or in the air, or on an airbase, or on carriers. + -- And some of these platforms have very limited amount of "launching" platforms. + -- Therefore, this queue concept is introduced that queues each defender request. + -- Depending on the location of the launching site, the queued defenders will be launched at varying time intervals. + -- This guarantees that launched defenders are also directly existing ... + -- @type AI_A2G_DISPATCHER.DefenseQueue + -- @list<#AI_A2G_DISPATCHER.DefenseQueueItem> DefenseQueueItem A list of all defenses being queued ... + + --- @field #AI_A2G_DISPATCHER.DefenseQueue DefenseQueue + AI_A2G_DISPATCHER.DefenseQueue = {} + + + --- Defense approach types + -- @type #AI_A2G_DISPATCHER.DefenseApproach + AI_A2G_DISPATCHER.DefenseApproach = { + Random = 1, + Distance = 2, + } + + --- AI_A2G_DISPATCHER constructor. + -- This is defining the A2G DISPATCHER for one coaliton. + -- The Dispatcher works with a @{Functional.Detection#DETECTION_BASE} object that is taking of the detection of targets using the EWR units. + -- The Detection object is polymorphic, depending on the type of detection object choosen, the detection will work differently. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network. + -- @return #AI_A2G_DISPATCHER self + -- @usage + -- + -- -- Setup the Detection, using DETECTION_AREAS. + -- -- First define the SET of GROUPs that are defining the EWR network. + -- -- Here with prefixes DF CCCP AWACS, DF CCCP EWR. + -- DetectionSetGroup = SET_GROUP:New() + -- DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } ) + -- DetectionSetGroup:FilterStart() + -- + -- -- Define the DETECTION_AREAS, using the DetectionSetGroup, with a 30km grouping radius. + -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) -- + -- + function AI_A2G_DISPATCHER:New( Detection ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, DETECTION_MANAGER:New( nil, Detection ) ) -- #AI_A2G_DISPATCHER + + self.Detection = Detection -- Functional.Detection#DETECTION_AREAS + + self.Detection:FilterCategories( Unit.Category.GROUND_UNIT ) + + -- This table models the DefenderSquadron templates. + self.DefenderSquadrons = {} -- The Defender Squadrons. + self.DefenderSpawns = {} + self.DefenderTasks = {} -- The Defenders Tasks. + self.DefenderDefault = {} -- The Defender Default Settings over all Squadrons. + + -- TODO: Check detection through radar. +-- self.Detection:FilterCategories( { Unit.Category.GROUND } ) +-- self.Detection:InitDetectRadar( false ) +-- self.Detection:InitDetectVisual( true ) +-- self.Detection:SetRefreshTimeInterval( 30 ) + + self:SetDefenseRadius() + self:SetDefenseLimit( nil ) + self:SetDefenseApproach( AI_A2G_DISPATCHER.DefenseApproach.Random ) + self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds. + self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km. + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Air ) + self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above the ground. + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.NearAirbase ) + self:SetDefaultOverhead( 1 ) + self:SetDefaultGrouping( 1 ) + self:SetDefaultFuelThreshold( 0.15, 0 ) -- 15% of fuel remaining in the tank will trigger the airplane to return to base or refuel. + self:SetDefaultDamageThreshold( 0.4 ) -- When 40% of damage, go RTB. + self:SetDefaultPatrolTimeInterval( 180, 600 ) -- Between 180 and 600 seconds. + self:SetDefaultPatrolLimit( 1 ) -- Maximum one Patrol per squadron. + + + self:AddTransition( "Started", "Assign", "Started" ) + + --- OnAfter Transition Handler for Event Assign. + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterAssign + -- @param #AI_A2G_DISPATCHER self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Tasking.Task_A2G#AI_A2G Task + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #string PlayerName + + self:AddTransition( "*", "Patrol", "*" ) + + --- Patrol Handler OnBefore for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnBeforePatrol + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Patrol Handler OnAfter for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterPatrol + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Patrol Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] Patrol + -- @param #AI_A2G_DISPATCHER self + + --- Patrol Asynchronous Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] __Patrol + -- @param #AI_A2G_DISPATCHER self + -- @param #number Delay + + self:AddTransition( "*", "Defend", "*" ) + + --- Defend Handler OnBefore for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeDefend + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Defend Handler OnAfter for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterDefend + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Defend Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] Defend + -- @param #AI_A2G_DISPATCHER self + + --- Defend Asynchronous Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] __Defend + -- @param #AI_A2G_DISPATCHER self + -- @param #number Delay + + self:AddTransition( "*", "Engage", "*" ) + + --- Engage Handler OnBefore for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeEngage + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Engage Handler OnAfter for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] OnAfterEngage + -- @param #AI_A2G_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Engage Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] Engage + -- @param #AI_A2G_DISPATCHER self + + --- Engage Asynchronous Trigger for AI_A2G_DISPATCHER + -- @function [parent=#AI_A2G_DISPATCHER] __Engage + -- @param #AI_A2G_DISPATCHER self + -- @param #number Delay + + + -- Subscribe to the CRASH event so that when planes are shot + -- by a Unit from the dispatcher, they will be removed from the detection... + -- This will avoid the detection to still "know" the shot unit until the next detection. + -- Otherwise, a new defense or engage may happen for an already shot plane! + + + self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) + self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) + + + self:HandleEvent( EVENTS.Land ) + self:HandleEvent( EVENTS.EngineShutdown ) + + -- Handle the situation where the airbases are captured. + self:HandleEvent( EVENTS.BaseCaptured ) + + self:SetTacticalDisplay( false ) + + self.DefenderPatrolIndex = 0 + + self:SetDefenseReactivityMedium() + + self.TakeoffScheduleID = self:ScheduleRepeat( 10, 10, 0, nil, self.ResourceTakeoff, self ) + + self:__Start( 1 ) + + return self + end + + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterStart( From, Event, To ) + + self:GetParent( self ).onafterStart( self, From, Event, To ) + + -- Spawn the resources. + for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + DefenderSquadron.Resource = {} + for Resource = 1, DefenderSquadron.ResourceCount or 0 do + self:ResourcePark( DefenderSquadron ) + end + self:I( "Parked resources for squadron " .. DefenderSquadron.Name ) + end + + end + + --- Locks the DefenseItem from being defended. + -- @param #AI_A2G_DISPATCHER self + -- @param #string DetectedItemIndex The index of the detected item. + function AI_A2G_DISPATCHER:Lock( DetectedItemIndex ) + self:F( { DetectedItemIndex = DetectedItemIndex } ) + local DetectedItem = self.Detection:GetDetectedItemByIndex( DetectedItemIndex ) + if DetectedItem then + self:F( { Locked = DetectedItem } ) + self.Detection:LockDetectedItem( DetectedItem ) + end + end + + + --- Unlocks the DefenseItem from being defended. + -- @param #AI_A2G_DISPATCHER self + -- @param #string DetectedItemIndex The index of the detected item. + function AI_A2G_DISPATCHER:Unlock( DetectedItemIndex ) + self:F( { DetectedItemIndex = DetectedItemIndex } ) + self:F( { Index = self.Detection.DetectedItemsByIndex } ) + local DetectedItem = self.Detection:GetDetectedItemByIndex( DetectedItemIndex ) + if DetectedItem then + self:F( { Unlocked = DetectedItem } ) + self.Detection:UnlockDetectedItem( DetectedItem ) + end + end + + + --- Sets maximum zones to be engaged at one time by defenders. + -- @param #AI_A2G_DISPATCHER self + -- @param #number DefenseLimit The maximum amount of detected items to be engaged at the same time. + function AI_A2G_DISPATCHER:SetDefenseLimit( DefenseLimit ) + self:F( { DefenseLimit = DefenseLimit } ) + + self.DefenseLimit = DefenseLimit + end + + + --- Sets the method of the tactical approach of the defenses. + -- @param #AI_A2G_DISPATCHER self + -- @param #number DefenseApproach Use the structure AI_A2G_DISPATCHER.DefenseApproach to set the defense approach. + -- The default defense approach is AI_A2G_DISPATCHER.DefenseApproach.Random. + function AI_A2G_DISPATCHER:SetDefenseApproach( DefenseApproach ) + self:F( { DefenseApproach = DefenseApproach } ) + + self._DefenseApproach = DefenseApproach + end + + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourcePark( DefenderSquadron ) + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) + local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN + Spawn:InitGrouping( 1 ) + local SpawnGroup + if self:IsSquadronVisible( DefenderSquadron.Name ) then + SpawnGroup = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, SPAWN.Takeoff.Cold ) + local GroupName = SpawnGroup:GetName() + DefenderSquadron.Resources = DefenderSquadron.Resources or {} + DefenderSquadron.Resources[TemplateID] = DefenderSquadron.Resources[TemplateID] or {} + DefenderSquadron.Resources[TemplateID][GroupName] = {} + DefenderSquadron.Resources[TemplateID][GroupName] = SpawnGroup + end + end + + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventBaseCaptured( EventData ) + + local AirbaseName = EventData.PlaceName -- The name of the airbase that was captured. + + self:I( "Captured " .. AirbaseName ) + + -- Now search for all squadrons located at the airbase, and sanatize them. + for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do + if Squadron.AirbaseName == AirbaseName then + Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning. + Squadron.Captured = true + self:I( "Squadron " .. SquadronName .. " captured." ) + end + end + end + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventCrashOrDead( EventData ) + self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) + end + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventLand( EventData ) + self:F( "Landed" ) + local DefenderUnit = EventData.IniUnit + local Defender = EventData.IniGroup + local Squadron = self:GetSquadronFromDefender( Defender ) + if Squadron then + self:F( { SquadronName = Squadron.Name } ) + local LandingMethod = self:GetSquadronLanding( Squadron.Name ) + + if LandingMethod == AI_A2G_DISPATCHER.Landing.AtRunway then + local DefenderSize = Defender:GetSize() + if DefenderSize == 1 then + self:RemoveDefenderFromSquadron( Squadron, Defender ) + end + DefenderUnit:Destroy() + self:ResourcePark( Squadron, Defender ) + return + end + if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then + -- Damaged units cannot be repaired anymore. + DefenderUnit:Destroy() + return + end + end + end + + --- @param #AI_A2G_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2G_DISPATCHER:OnEventEngineShutdown( EventData ) + local DefenderUnit = EventData.IniUnit + local Defender = EventData.IniGroup + local Squadron = self:GetSquadronFromDefender( Defender ) + if Squadron then + self:F( { SquadronName = Squadron.Name } ) + local LandingMethod = self:GetSquadronLanding( Squadron.Name ) + if LandingMethod == AI_A2G_DISPATCHER.Landing.AtEngineShutdown and + not DefenderUnit:InAir() then + local DefenderSize = Defender:GetSize() + if DefenderSize == 1 then + self:RemoveDefenderFromSquadron( Squadron, Defender ) + end + DefenderUnit:Destroy() + self:ResourcePark( Squadron, Defender ) + end + end + end + + do -- Manage the defensive behaviour + + --- @param #AI_A2G_DISPATCHER self + -- @param #string DefenseCoordinateName The name of the coordinate to be defended by A2G defenses. + -- @param Core.Point#COORDINATE DefenseCoordinate The coordinate to be defended by A2G defenses. + function AI_A2G_DISPATCHER:AddDefenseCoordinate( DefenseCoordinateName, DefenseCoordinate ) + self.DefenseCoordinates[DefenseCoordinateName] = DefenseCoordinate + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenseReactivityLow() + self.DefenseReactivity = 0.05 + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenseReactivityMedium() + self.DefenseReactivity = 0.15 + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenseReactivityHigh() + self.DefenseReactivity = 0.5 + end + + end + + + --- Define the radius to disengage any target when the distance to the home base is larger than the specified meters. + -- @param #AI_A2G_DISPATCHER self + -- @param #number DisengageRadius (Optional, Default = 300000) The radius to disengage a target when too far from the home base. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Set 50km as the Disengage Radius. + -- A2GDispatcher:SetDisengageRadius( 50000 ) + -- + -- -- Set 100km as the Disengage Radius. + -- A2GDispatcher:SetDisngageRadius() -- 300000 is the default value. + -- + function AI_A2G_DISPATCHER:SetDisengageRadius( DisengageRadius ) + + self.DisengageRadius = DisengageRadius or 300000 + + return self + end + + + --- Define the defense radius to check if a target can be engaged by a squadron group for SEAD, CAS or BAI for defense. + -- When targets are detected that are still really far off, you don't want the AI_A2G_DISPATCHER to launch defenders, as they might need to travel too far. + -- You want it to wait until a certain defend radius is reached, which is calculated as: + -- 1. the **distance of the closest airbase to target**, being smaller than the **Defend Radius**. + -- 2. the **distance to any defense reference point**. + -- + -- The **default** defense radius is defined as **400000** or **40km**. Override the default defense radius when the era of the warfare is early, or, + -- when you don't want to let the AI_A2G_DISPATCHER react immediately when a certain border or area is not being crossed. + -- + -- Use the method @{#AI_A2G_DISPATCHER.SetDefendRadius}() to set a specific defend radius for all squadrons, + -- **the Defense Radius is defined for ALL squadrons which are operational.** + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #number DefenseRadius (Optional, Default = 200000) The defense radius to engage detected targets from the nearest capable and available squadron airbase. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Set 100km as the radius to defend from detected targets from the nearest airbase. + -- A2GDispatcher:SetDefendRadius( 100000 ) + -- + -- -- Set 200km as the radius to defend. + -- A2GDispatcher:SetDefendRadius() -- 200000 is the default value. + -- + function AI_A2G_DISPATCHER:SetDefenseRadius( DefenseRadius ) + + self.DefenseRadius = DefenseRadius or 100000 + + self.Detection:SetAcceptRange( self.DefenseRadius ) + + return self + end + + + + --- Define a border area to simulate a **cold war** scenario. + -- A **cold war** is one where Patrol aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. + -- A **hot war** is one where Patrol aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send Patrol and GCI aircraft to attack it. + -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Core.Zone#ZONE_BASE}. This method needs to be used for this. + -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. Set the noborders parameter to 1 + -- @param #AI_A2G_DISPATCHER self + -- @param Core.Zone#ZONE_BASE BorderZone An object derived from ZONE_BASE, or a list of objects derived from ZONE_BASE. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Set one ZONE_POLYGON object as the border for the A2G dispatcher. + -- local BorderZone = ZONE_POLYGON( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) -- The GROUP object is a late activate helicopter unit. + -- A2GDispatcher:SetBorderZone( BorderZone ) + -- + -- or + -- + -- -- Set two ZONE_POLYGON objects as the border for the A2G dispatcher. + -- local BorderZone1 = ZONE_POLYGON( "CCCP Border1", GROUP:FindByName( "CCCP Border1" ) ) -- The GROUP object is a late activate helicopter unit. + -- local BorderZone2 = ZONE_POLYGON( "CCCP Border2", GROUP:FindByName( "CCCP Border2" ) ) -- The GROUP object is a late activate helicopter unit. + -- A2GDispatcher:SetBorderZone( { BorderZone1, BorderZone2 } ) + -- + -- + function AI_A2G_DISPATCHER:SetBorderZone( BorderZone ) + + self.Detection:SetAcceptZones( BorderZone ) + + return self + end + + --- Display a tactical report every 30 seconds about which aircraft are: + -- * Patrolling + -- * Engaging + -- * Returning + -- * Damaged + -- * Out of Fuel + -- * ... + -- @param #AI_A2G_DISPATCHER self + -- @param #boolean TacticalDisplay Provide a value of **true** to display every 30 seconds a tactical overview. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the Tactical Display for debug mode. + -- A2GDispatcher:SetTacticalDisplay( true ) + -- + function AI_A2G_DISPATCHER:SetTacticalDisplay( TacticalDisplay ) + + self.TacticalDisplay = TacticalDisplay + + return self + end + + + --- Set the default damage treshold when defenders will RTB. + -- The default damage treshold is by default set to 40%, which means that when the airplane is 40% damaged, it will go RTB. + -- @param #AI_A2G_DISPATCHER self + -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the %-tage of the damage treshold before going RTB. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default damage treshold. + -- A2GDispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged. + -- + function AI_A2G_DISPATCHER:SetDefaultDamageThreshold( DamageThreshold ) + + self.DefenderDefault.DamageThreshold = DamageThreshold + + return self + end + + + --- Set the default Patrol time interval for squadrons, which will be used to determine a random Patrol timing. + -- The default Patrol time interval is between 180 and 600 seconds. + -- @param #AI_A2G_DISPATCHER self + -- @param #number PatrolMinSeconds The minimum amount of seconds for the random time interval. + -- @param #number PatrolMaxSeconds The maximum amount of seconds for the random time interval. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol time interval. + -- A2GDispatcher:SetDefaultPatrolTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds. + -- + function AI_A2G_DISPATCHER:SetDefaultPatrolTimeInterval( PatrolMinSeconds, PatrolMaxSeconds ) + + self.DefenderDefault.PatrolMinSeconds = PatrolMinSeconds + self.DefenderDefault.PatrolMaxSeconds = PatrolMaxSeconds + + return self + end + + + --- Set the default Patrol limit for squadrons, which will be used to determine how many Patrol can be airborne at the same time for the squadron. + -- The default Patrol limit is 1 Patrol, which means one Patrol group being spawned. + -- @param #AI_A2G_DISPATCHER self + -- @param #number PatrolLimit The maximum amount of Patrol that can be airborne at the same time for the squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol limit. + -- A2GDispatcher:SetDefaultPatrolLimit( 2 ) -- Maximum 2 Patrol per squadron. + -- + function AI_A2G_DISPATCHER:SetDefaultPatrolLimit( PatrolLimit ) + + self.DefenderDefault.PatrolLimit = PatrolLimit + + return self + end + + + --- Set the default engage limit for squadrons, which will be used to determine how many air units will engage at the same time with the enemy. + -- The default eatrol limit is 1, which means one eatrol group maximum per squadron. + -- @param #AI_A2G_DISPATCHER self + -- @param #number EngageLimit The maximum engages that can be done at the same time per squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol limit. + -- A2GDispatcher:SetDefaultEngageLimit( 2 ) -- Maximum 2 engagements with the enemy per squadron. + -- + function AI_A2G_DISPATCHER:SetDefaultEngageLimit( EngageLimit ) + + self.DefenderDefault.EngageLimit = EngageLimit + + return self + end + + + function AI_A2G_DISPATCHER:SetIntercept( InterceptDelay ) + + self.DefenderDefault.InterceptDelay = InterceptDelay + + local Detection = self.Detection -- Functional.Detection#DETECTION_AREAS + Detection:SetIntercept( true, InterceptDelay ) + + return self + end + + + --- Calculates which defender friendlies are nearby the area, to help protect the area. + -- @param #AI_A2G_DISPATCHER self + -- @param DetectedItem + -- @return #table A list of the defender friendlies nearby, sorted by distance. + function AI_A2G_DISPATCHER:GetDefenderFriendliesNearBy( DetectedItem ) + +-- local DefenderFriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem ) + + local DefenderFriendliesNearBy = {} + + local DetectionCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + local ScanZone = ZONE_RADIUS:New( "ScanZone", DetectionCoordinate:GetVec2(), self.DefenseRadius ) + + ScanZone:Scan( Object.Category.UNIT, { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) + + local DefenderUnits = ScanZone:GetScannedUnits() + + for DefenderUnitID, DefenderUnit in pairs( DefenderUnits ) do + local DefenderUnit = UNIT:FindByName( DefenderUnit:getName() ) + + DefenderFriendliesNearBy[#DefenderFriendliesNearBy+1] = DefenderUnit + end + + + return DefenderFriendliesNearBy + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTasks() + return self.DefenderTasks or {} + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTask( Defender ) + return self.DefenderTasks[Defender] + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTaskFsm( Defender ) + return self:GetDefenderTask( Defender ).Fsm + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTaskTarget( Defender ) + return self:GetDefenderTask( Defender ).Target + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:GetDefenderTaskSquadronName( Defender ) + return self:GetDefenderTask( Defender ).SquadronName + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ClearDefenderTask( Defender ) + if Defender:IsAlive() and self.DefenderTasks[Defender] then + local Target = self.DefenderTasks[Defender].Target + local Message = "Clearing (" .. self.DefenderTasks[Defender].Type .. ") " + Message = Message .. Defender:GetName() + if Target then + Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" + end + self:F( { Target = Message } ) + end + self.DefenderTasks[Defender] = nil + return self + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ClearDefenderTaskTarget( Defender ) + + local DefenderTask = self:GetDefenderTask( Defender ) + + if Defender:IsAlive() and DefenderTask then + local Target = DefenderTask.Target + local Message = "Clearing (" .. DefenderTask.Type .. ") " + Message = Message .. Defender:GetName() + if Target then + Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" + end + self:F( { Target = Message } ) + end + if Defender and DefenderTask and DefenderTask.Target then + DefenderTask.Target = nil + end +-- if Defender and DefenderTask then +-- if DefenderTask.Fsm:Is( "Fuel" ) +-- or DefenderTask.Fsm:Is( "LostControl") +-- or DefenderTask.Fsm:Is( "Damaged" ) then +-- self:ClearDefenderTask( Defender ) +-- end +-- end + return self + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target, Size ) + + self:F( { SquadronName = SquadronName, Defender = Defender:GetName() } ) + + self.DefenderTasks[Defender] = self.DefenderTasks[Defender] or {} + self.DefenderTasks[Defender].Type = Type + self.DefenderTasks[Defender].Fsm = Fsm + self.DefenderTasks[Defender].SquadronName = SquadronName + self.DefenderTasks[Defender].Size = Size + + if Target then + self:SetDefenderTaskTarget( Defender, Target ) + end + return self + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param Wrapper.Group#GROUP AIGroup + function AI_A2G_DISPATCHER:SetDefenderTaskTarget( Defender, AttackerDetection ) + + local Message = "(" .. self.DefenderTasks[Defender].Type .. ") " + Message = Message .. Defender:GetName() + Message = Message .. ( AttackerDetection and ( " target " .. AttackerDetection.Index .. " [" .. AttackerDetection.Set:Count() .. "]" ) ) or "" + self:F( { AttackerDetection = Message } ) + if AttackerDetection then + self.DefenderTasks[Defender].Target = AttackerDetection + end + return self + end + + + --- This is the main method to define Squadrons programmatically. + -- Squadrons: + -- + -- * Have a **name or key** that is the identifier or key of the squadron. + -- * Have **specific plane types** defined by **templates**. + -- * Are **located at one specific airbase**. Multiple squadrons can be located at one airbase through. + -- * Optionally have a limited set of **resources**. The default is that squadrons have unlimited resources. + -- + -- The name of the squadron given acts as the **squadron key** in the AI\_A2G\_DISPATCHER:Squadron...() methods. + -- + -- Additionally, squadrons have specific configuration options to: + -- + -- * Control how new aircraft are **taking off** from the airfield (in the air, cold, hot, at the runway). + -- * Control how returning aircraft are **landing** at the airfield (in the air near the airbase, after landing, after engine shutdown). + -- * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped. + -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned. + -- + -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft. + -- + -- @param #AI_A2G_DISPATCHER self + -- + -- @param #string SquadronName A string (text) that defines the squadron identifier or the key of the Squadron. + -- It can be any name, for example `"104th Squadron"` or `"SQ SQUADRON1"`, whatever. + -- As long as you remember that this name becomes the identifier of your squadron you have defined. + -- You need to use this name in other methods too! + -- + -- @param #string AirbaseName The airbase name where you want to have the squadron located. + -- You need to specify here EXACTLY the name of the airbase as you see it in the mission editor. + -- Examples are `"Batumi"` or `"Tbilisi-Lochini"`. + -- EXACTLY the airbase name, between quotes `""`. + -- To ease the airbase naming when using the LDT editor and IntelliSense, the @{Wrapper.Airbase#AIRBASE} class contains enumerations of the airbases of each map. + -- + -- * Caucasus: @{Wrapper.Airbase#AIRBASE.Caucaus} + -- * Nevada or NTTR: @{Wrapper.Airbase#AIRBASE.Nevada} + -- * Normandy: @{Wrapper.Airbase#AIRBASE.Normandy} + -- + -- @param #string TemplatePrefixes A string or an array of strings specifying the **prefix names of the templates** (not going to explain what is templates here again). + -- Examples are `{ "104th", "105th" }` or `"104th"` or `"Template 1"` or `"BLUE PLANES"`. + -- Just remember that your template (groups late activated) need to start with the prefix you have specified in your code. + -- If you have only one prefix name for a squadron, you don't need to use the `{ }`, otherwise you need to use the brackets. + -- + -- @param #number ResourceCount (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available. + -- + -- @usage + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- @usage + -- -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock... + -- A2GDispatcher:SetSquadron( "Squadron1", "Batumi", "SQ1", 40 ) + -- + -- @usage + -- -- This will create squadron "Sq 1" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" and has 20 planes in stock... + -- -- Note that in this implementation, the A2G dispatcher will select a random plane type when a new plane (group) needs to be spawned for defenses. + -- -- Note the usage of the {} for the airplane templates list. + -- A2GDispatcher:SetSquadron( "Sq 1", "Batumi", { "Mig-29", "Su-27" }, 40 ) + -- + -- @usage + -- -- This will create 2 squadrons "104th" and "23th" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" respectively and each squadron has 10 planes in stock... + -- A2GDispatcher:SetSquadron( "104th", "Batumi", "Mig-29", 10 ) + -- A2GDispatcher:SetSquadron( "23th", "Batumi", "Su-27", 10 ) + -- + -- @usage + -- -- This is an example like the previous, but now with infinite resources. + -- -- The ResourceCount parameter is not given in the SetSquadron method. + -- A2GDispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) + -- A2GDispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) + -- + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + + DefenderSquadron.Name = SquadronName + DefenderSquadron.Airbase = AIRBASE:FindByName( AirbaseName ) + DefenderSquadron.AirbaseName = DefenderSquadron.Airbase:GetName() + if not DefenderSquadron.Airbase then + error( "Cannot find airbase with name:" .. AirbaseName ) + end + + DefenderSquadron.Spawn = {} + if type( TemplatePrefixes ) == "string" then + local SpawnTemplate = TemplatePrefixes + self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) + DefenderSquadron.Spawn[1] = self.DefenderSpawns[SpawnTemplate] + else + for TemplateID, SpawnTemplate in pairs( TemplatePrefixes ) do + self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) + DefenderSquadron.Spawn[#DefenderSquadron.Spawn+1] = self.DefenderSpawns[SpawnTemplate] + end + end + DefenderSquadron.ResourceCount = ResourceCount + DefenderSquadron.TemplatePrefixes = TemplatePrefixes + DefenderSquadron.Captured = false -- Not captured. This flag will be set to true, when the airbase where the squadron is located, is captured. + + self:SetSquadronTakeoffInterval( SquadronName, 0 ) + + self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) + + return self + end + + --- Get an item from the Squadron table. + -- @param #AI_A2G_DISPATCHER self + -- @return #table + function AI_A2G_DISPATCHER:GetSquadron( SquadronName ) + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + + if not DefenderSquadron then + error( "Unknown Squadron:" .. SquadronName ) + end + + return DefenderSquadron + end + + + --- Set the Squadron visible before startup of the dispatcher. + -- All planes will be spawned as uncontrolled on the parking spot. + -- They will lock the parking spot. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Set the Squadron visible before startup of dispatcher. + -- A2GDispatcher:SetSquadronVisible( "Mineralnye" ) + -- + -- TODO: disabling because of bug in queueing. +-- function AI_A2G_DISPATCHER:SetSquadronVisible( SquadronName ) +-- +-- self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} +-- +-- local DefenderSquadron = self:GetSquadron( SquadronName ) +-- +-- DefenderSquadron.Uncontrolled = true +-- self:SetSquadronTakeoffFromParkingCold( SquadronName ) +-- self:SetSquadronLandingAtEngineShutdown( SquadronName ) +-- +-- for SpawnTemplate, DefenderSpawn in pairs( self.DefenderSpawns ) do +-- DefenderSpawn:InitUnControlled() +-- end +-- +-- end + + --- Check if the Squadron is visible before startup of the dispatcher. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #bool true if visible. + -- @usage + -- + -- -- Set the Squadron visible before startup of dispatcher. + -- local IsVisible = A2GDispatcher:IsSquadronVisible( "Mineralnye" ) + -- + function AI_A2G_DISPATCHER:IsSquadronVisible( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron then + return DefenderSquadron.Uncontrolled == true + end + + return nil + + end + + --- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number TakeoffInterval Only Takeoff new units each specified interval in seconds in 10 seconds steps. + -- @usage + -- + -- -- Set the Squadron Takeoff interval every 60 seconds for squadron "SQ50", which is good for a FARP cold start. + -- A2GDispatcher:SetSquadronTakeoffInterval( "SQ50", 60 ) + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffInterval( SquadronName, TakeoffInterval ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron then + DefenderSquadron.TakeoffInterval = TakeoffInterval or 0 + DefenderSquadron.TakeoffTime = 0 + end + + end + + + + --- Set the squadron patrol parameters for a specific task type. + -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. + -- + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for SEAD tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for CAS tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for BAI tasks. + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI". + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronPatrolInterval( "Mineralnye", 2, 30, 60, 1, "SEAD" ) + -- + function AI_A2G_DISPATCHER:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, DefenseTaskType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Patrol = DefenderSquadron[DefenseTaskType] + if Patrol then + Patrol.LowInterval = LowInterval or 180 + Patrol.HighInterval = HighInterval or 600 + Patrol.Probability = Probability or 1 + Patrol.PatrolLimit = PatrolLimit or 1 + Patrol.Scheduler = Patrol.Scheduler or SCHEDULER:New( self ) + local Scheduler = Patrol.Scheduler -- Core.Scheduler#SCHEDULER + local ScheduleID = Patrol.ScheduleID + local Variance = ( Patrol.HighInterval - Patrol.LowInterval ) / 2 + local Repeat = Patrol.LowInterval + Variance + local Randomization = Variance / Repeat + local Start = math.random( 1, Patrol.HighInterval ) + + if ScheduleID then + Scheduler:Stop( ScheduleID ) + end + + Patrol.ScheduleID = Scheduler:Schedule( self, self.SchedulerPatrol, { SquadronName }, Start, Repeat, Randomization ) + else + error( "This squadron does not exist:" .. SquadronName ) + end + + end + + + + --- Set the squadron Patrol parameters for SEAD tasks. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronSeadPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + function AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "SEAD" ) + + end + + + --- Set the squadron Patrol parameters for CAS tasks. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronCasPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + function AI_A2G_DISPATCHER:SetSquadronCasPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "CAS" ) + + end + + + --- Set the squadron Patrol parameters for BAI tasks. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- A2GDispatcher:SetSquadronBaiPatrolInterval( "Mineralnye", 2, 30, 60, 1 ) + -- + function AI_A2G_DISPATCHER:SetSquadronBaiPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability ) + + self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "BAI" ) + + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:GetPatrolDelay( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + self.DefenderSquadrons[SquadronName].Patrol = self.DefenderSquadrons[SquadronName].Patrol or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Patrol = self.DefenderSquadrons[SquadronName].Patrol + if Patrol then + return math.random( Patrol.LowInterval, Patrol.HighInterval ) + else + error( "This squadron does not exist:" .. SquadronName ) + end + end + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #table DefenderSquadron + function AI_A2G_DISPATCHER:CanPatrol( SquadronName, DefenseTaskType ) + self:F({SquadronName = SquadronName}) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron.Captured == false then -- We can only spawn new Patrol if the base has not been captured. + + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + + local Patrol = DefenderSquadron[DefenseTaskType] + if Patrol and Patrol.Patrol == true then + local PatrolCount = self:CountPatrolAirborne( SquadronName, DefenseTaskType ) + self:F( { PatrolCount = PatrolCount, PatrolLimit = Patrol.PatrolLimit, PatrolProbability = Patrol.Probability } ) + if PatrolCount < Patrol.PatrolLimit then + local Probability = math.random() + if Probability <= Patrol.Probability then + return DefenderSquadron, Patrol + end + end + else + self:F( "No patrol for " .. SquadronName ) + end + end + end + return nil + end + + + --- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #table DefenderSquadron + function AI_A2G_DISPATCHER:CanDefend( SquadronName, DefenseTaskType ) + self:F({SquadronName = SquadronName, DefenseTaskType}) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron.Captured == false then -- We can only spawn new defense if the home airbase has not been captured. + + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + if DefenderSquadron[DefenseTaskType] and ( DefenderSquadron[DefenseTaskType].Defend == true ) then + return DefenderSquadron, DefenderSquadron[DefenseTaskType] + end + end + end + return nil + end + + --- Set the squadron engage limit for a specific task type. + -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. + -- + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for SEAD tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for CAS tasks. + -- - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for BAI tasks. + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI". + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronEngageLimit( "Mineralnye", 2, "SEAD" ) -- Engage maximum 2 groups with the enemy for SEAD defense. + -- + function AI_A2G_DISPATCHER:SetSquadronEngageLimit( SquadronName, EngageLimit, DefenseTaskType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Defense = DefenderSquadron[DefenseTaskType] + if Defense then + Defense.EngageLimit = EngageLimit or 1 + else + error( "This squadron does not exist:" .. SquadronName ) + end + + end + + + + + --- Set a squadron to engage for suppression of air defenses, when a defense point is under attack. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the SEAD task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the SEAD task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. + -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @usage + -- + -- -- SEAD Squadron execution. + -- A2GDispatcher:SetSquadronSead( "Mozdok", 900, 1200, 4000, 5000, "BARO" ) + -- A2GDispatcher:SetSquadronSead( "Novo", 900, 2100, 6000, 9000, "BARO" ) + -- A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200, 30, 100, "RADIO" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronSead2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} + + local Sead = DefenderSquadron.SEAD + Sead.Name = SquadronName + Sead.EngageMinSpeed = EngageMinSpeed + Sead.EngageMaxSpeed = EngageMaxSpeed + Sead.EngageFloorAltitude = EngageFloorAltitude or 500 + Sead.EngageCeilingAltitude = EngageCeilingAltitude or 1000 + Sead.EngageAltType = EngageAltType + Sead.Defend = true + + self:I( { SEAD = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + + return self + end + + --- Set a squadron to engage for suppression of air defenses, when a defense point is under attack. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the SEAD task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the SEAD task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. + -- @usage + -- + -- -- SEAD Squadron execution. + -- A2GDispatcher:SetSquadronSead( "Mozdok", 900, 1200, 4000, 5000 ) + -- A2GDispatcher:SetSquadronSead( "Novo", 900, 2100, 6000, 8000 ) + -- A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200, 6000, 10000 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronSead( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + return self:SetSquadronSead2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, "RADIO" ) + end + + --- Set the squadron SEAD engage limit. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for SEAD defense. + -- + function AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit( SquadronName, EngageLimit ) + + self:SetSquadronEngageLimit( SquadronName, EngageLimit, "SEAD" ) + + end + + + + + --- Set a Sead patrol for a Squadron. + -- The Sead patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number PatrolFloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number PatrolCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolAltType The altitude type when patrolling, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. + -- @param #number EngageFloorAltitude (optional, default = 1000m ) The minimum altitude at which the engage can be executed. + -- @param #number EngageCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the engage can be executed. + -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Sead Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol2( "Mineralnye", PatrolZoneEast, 500, 600, 4000, 10000, "BARO", 800, 900, 2000, 3000, "RADIO", ) + -- + function AI_A2G_DISPATCHER:SetSquadronSeadPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} + + local SeadPatrol = DefenderSquadron.SEAD + SeadPatrol.Name = SquadronName + SeadPatrol.Zone = Zone + SeadPatrol.PatrolFloorAltitude = PatrolFloorAltitude + SeadPatrol.PatrolCeilingAltitude = PatrolCeilingAltitude + SeadPatrol.EngageFloorAltitude = EngageFloorAltitude + SeadPatrol.EngageCeilingAltitude = EngageCeilingAltitude + SeadPatrol.PatrolMinSpeed = PatrolMinSpeed + SeadPatrol.PatrolMaxSpeed = PatrolMaxSpeed + SeadPatrol.EngageMinSpeed = EngageMinSpeed + SeadPatrol.EngageMaxSpeed = EngageMaxSpeed + SeadPatrol.PatrolAltType = PatrolAltType + SeadPatrol.EngageAltType = EngageAltType + SeadPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "SEAD" ) + + self:I( { SEAD = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + end + + + --- Set a Sead patrol for a Squadron. + -- The Sead patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Sead Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + function AI_A2G_DISPATCHER:SetSquadronSeadPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + self:SetSquadronSeadPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, FloorAltitude, CeilingAltitude, AltType, EngageMinSpeed, EngageMaxSpeed, FloorAltitude, CeilingAltitude, AltType ) + + end + + + --- Set a squadron to engage for close air support, when a defense point is under attack. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the CAS task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the CAS task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. + -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @usage + -- + -- -- CAS Squadron execution. + -- A2GDispatcher:SetSquadronCas( "Mozdok", 900, 1200, 4000, 5000, "BARO" ) + -- A2GDispatcher:SetSquadronCas( "Novo", 900, 2100, 6000, 9000, "BARO" ) + -- A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200, 30, 100, "RADIO" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronCas2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.CAS = DefenderSquadron.CAS or {} + + local Cas = DefenderSquadron.CAS + Cas.Name = SquadronName + Cas.EngageMinSpeed = EngageMinSpeed + Cas.EngageMaxSpeed = EngageMaxSpeed + Cas.EngageFloorAltitude = EngageFloorAltitude or 500 + Cas.EngageCeilingAltitude = EngageCeilingAltitude or 1000 + Cas.EngageAltType = EngageAltType + Cas.Defend = true + + self:I( { CAS = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + + return self + end + + --- Set a squadron to engage for close air support, when a defense point is under attack. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the CAS task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the CAS task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. + -- @usage + -- + -- -- CAS Squadron execution. + -- A2GDispatcher:SetSquadronCas( "Mozdok", 900, 1200, 4000, 5000 ) + -- A2GDispatcher:SetSquadronCas( "Novo", 900, 2100, 6000, 8000 ) + -- A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200, 6000, 10000 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronCas( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + return self:SetSquadronCas2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, "RADIO" ) + end + + + --- Set the squadron CAS engage limit. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for CAS defense. + -- + function AI_A2G_DISPATCHER:SetSquadronCasEngageLimit( SquadronName, EngageLimit ) + + self:SetSquadronEngageLimit( SquadronName, EngageLimit, "CAS" ) + + end + + + --- Set a Cas patrol for a Squadron. + -- The Cas patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number PatrolFloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number PatrolCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolAltType The altitude type when patrolling, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. + -- @param #number EngageFloorAltitude (optional, default = 1000m ) The minimum altitude at which the engage can be executed. + -- @param #number EngageCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the engage can be executed. + -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Cas Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasPatrol2( "Mineralnye", PatrolZoneEast, 500, 600, 4000, 10000, "BARO", 800, 900, 2000, 3000, "RADIO", ) + -- + function AI_A2G_DISPATCHER:SetSquadronCasPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.CAS = DefenderSquadron.CAS or {} + + local CasPatrol = DefenderSquadron.CAS + CasPatrol.Name = SquadronName + CasPatrol.Zone = Zone + CasPatrol.PatrolFloorAltitude = PatrolFloorAltitude + CasPatrol.PatrolCeilingAltitude = PatrolCeilingAltitude + CasPatrol.EngageFloorAltitude = EngageFloorAltitude + CasPatrol.EngageCeilingAltitude = EngageCeilingAltitude + CasPatrol.PatrolMinSpeed = PatrolMinSpeed + CasPatrol.PatrolMaxSpeed = PatrolMaxSpeed + CasPatrol.EngageMinSpeed = EngageMinSpeed + CasPatrol.EngageMaxSpeed = EngageMaxSpeed + CasPatrol.PatrolAltType = PatrolAltType + CasPatrol.EngageAltType = EngageAltType + CasPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "CAS" ) + + self:I( { CAS = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + end + + + --- Set a Cas patrol for a Squadron. + -- The Cas patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Cas Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronCasPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + function AI_A2G_DISPATCHER:SetSquadronCasPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + self:SetSquadronCasPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, FloorAltitude, CeilingAltitude, AltType, EngageMinSpeed, EngageMaxSpeed, FloorAltitude, CeilingAltitude, AltType ) + + end + + --- Set a squadron to engage for a battlefield area interdiction, when a defense point is under attack. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the BAI task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the BAI task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. + -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @usage + -- + -- -- BAI Squadron execution. + -- A2GDispatcher:SetSquadronBai( "Mozdok", 900, 1200, 4000, 5000, "BARO" ) + -- A2GDispatcher:SetSquadronBai( "Novo", 900, 2100, 6000, 9000, "BARO" ) + -- A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200, 30, 100, "RADIO" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronBai2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.BAI = DefenderSquadron.BAI or {} + + local Bai = DefenderSquadron.BAI + Bai.Name = SquadronName + Bai.EngageMinSpeed = EngageMinSpeed + Bai.EngageMaxSpeed = EngageMaxSpeed + Bai.EngageFloorAltitude = EngageFloorAltitude or 500 + Bai.EngageCeilingAltitude = EngageCeilingAltitude or 1000 + Bai.EngageAltType = EngageAltType + Bai.Defend = true + + self:I( { BAI = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + + return self + end + + --- Set a squadron to engage for a battlefield area interdiction, when a defense point is under attack. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the BAI task can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the BAI task can be executed. + -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. + -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. + -- @usage + -- + -- -- BAI Squadron execution. + -- A2GDispatcher:SetSquadronBai( "Mozdok", 900, 1200, 4000, 5000 ) + -- A2GDispatcher:SetSquadronBai( "Novo", 900, 2100, 6000, 8000 ) + -- A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200, 6000, 10000 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronBai( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + return self:SetSquadronBai2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, "RADIO" ) + end + + + --- Set the squadron BAI engage limit. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for BAI defense. + -- + function AI_A2G_DISPATCHER:SetSquadronBaiEngageLimit( SquadronName, EngageLimit ) + + self:SetSquadronEngageLimit( SquadronName, EngageLimit, "BAI" ) + + end + + + --- Set a Bai patrol for a Squadron. + -- The Bai patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number PatrolFloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number PatrolCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolAltType The altitude type when patrolling, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. + -- @param #number EngageFloorAltitude (optional, default = 1000m ) The minimum altitude at which the engage can be executed. + -- @param #number EngageCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the engage can be executed. + -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Bai Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiPatrol2( "Mineralnye", PatrolZoneEast, 500, 600, 4000, 10000, "BARO", 800, 900, 2000, 3000, "RADIO", ) + -- + function AI_A2G_DISPATCHER:SetSquadronBaiPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.BAI = DefenderSquadron.BAI or {} + + local BaiPatrol = DefenderSquadron.BAI + BaiPatrol.Name = SquadronName + BaiPatrol.Zone = Zone + BaiPatrol.PatrolFloorAltitude = PatrolFloorAltitude + BaiPatrol.PatrolCeilingAltitude = PatrolCeilingAltitude + BaiPatrol.EngageFloorAltitude = EngageFloorAltitude + BaiPatrol.EngageCeilingAltitude = EngageCeilingAltitude + BaiPatrol.PatrolMinSpeed = PatrolMinSpeed + BaiPatrol.PatrolMaxSpeed = PatrolMaxSpeed + BaiPatrol.EngageMinSpeed = EngageMinSpeed + BaiPatrol.EngageMaxSpeed = EngageMaxSpeed + BaiPatrol.PatrolAltType = PatrolAltType + BaiPatrol.EngageAltType = EngageAltType + BaiPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "BAI" ) + + self:I( { BAI = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } ) + end + + --- Set a Bai patrol for a Squadron. + -- The Bai patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed. + -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed. + -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed. + -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed. + -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed. + -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed. + -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed. + -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Bai Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- A2GDispatcher:SetSquadronBaiPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- + function AI_A2G_DISPATCHER:SetSquadronBaiPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType ) + + self:SetSquadronBaiPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, FloorAltitude, CeilingAltitude, AltType, EngageMinSpeed, EngageMaxSpeed, FloorAltitude, CeilingAltitude, AltType ) + + end + + + --- Defines the default amount of extra planes that will take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- A2GDispatcher:SetDefaultOverhead( 1.5 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultOverhead( Overhead ) + + self.DefenderDefault.Overhead = Overhead + + return self + end + + + --- Defines the amount of extra planes that will take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- A2GDispatcher:SetSquadronOverhead( "SquadronName", 1.5 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronOverhead( SquadronName, Overhead ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Overhead = Overhead + + return self + end + + + --- Gets the overhead of planes as part of the defense system, in comparison with the attackers. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- local SquadronOverhead = A2GDispatcher:GetSquadronOverhead( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:GetSquadronOverhead( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron.Overhead or self.DefenderDefault.Overhead + end + + + --- Sets the default grouping of new airplanes spawned. + -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set a grouping by default per 2 airplanes. + -- A2GDispatcher:SetDefaultGrouping( 2 ) + -- + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultGrouping( Grouping ) + + self.DefenderDefault.Grouping = Grouping + + return self + end + + + --- Sets the grouping of new airplanes spawned. + -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set a grouping per 2 airplanes. + -- A2GDispatcher:SetSquadronGrouping( "SquadronName", 2 ) + -- + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronGrouping( SquadronName, Grouping ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Grouping = Grouping + + return self + end + + + --- Sets the engage probability if the squadron will engage on a detected target. + -- This can be configured per squadron, to ensure that each squadron as a specific defensive probability setting. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number EngageProbability The probability when the squadron will consider to engage the detected target. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set an defense probability for squadron SquadronName of 50%. + -- -- This will result that this squadron has 50% chance to engage on a detected target. + -- A2GDispatcher:SetSquadronEngageProbability( "SquadronName", 0.5 ) + -- + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronEngageProbability( SquadronName, EngageProbability ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.EngageProbability = EngageProbability + + return self + end + + + --- Defines the default method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Air ) + -- + -- -- Let new flights by default take-off from the runway. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Runway ) + -- + -- -- Let new flights by default take-off from the airbase hot. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Hot ) + -- + -- -- Let new flights by default take-off from the airbase cold. + -- A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Cold ) + -- + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoff( Takeoff ) + + self.DefenderDefault.Takeoff = Takeoff + + return self + end + + --- Defines the method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Air ) + -- + -- -- Let new flights take-off from the runway. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Runway ) + -- + -- -- Let new flights take-off from the airbase hot. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Hot ) + -- + -- -- Let new flights take-off from the airbase cold. + -- A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Cold ) + -- + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoff( SquadronName, Takeoff ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Takeoff = Takeoff + + return self + end + + + --- Gets the default method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- local TakeoffMethod = A2GDispatcher:GetDefaultTakeoff() + -- if TakeOffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetDefaultTakeoff( ) + + return self.DefenderDefault.Takeoff + end + + --- Gets the method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- local TakeoffMethod = A2GDispatcher:GetSquadronTakeoff( "SquadronName" ) + -- if TakeOffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetSquadronTakeoff( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron.Takeoff or self.DefenderDefault.Takeoff + end + + + --- Sets flights to default take-off in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- A2GDispatcher:SetDefaultTakeoffInAir() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffInAir() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Air ) + + return self + end + + + --- Sets flights to take-off in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number TakeoffAltitude (optional) The altitude in meters above the ground. If not given, the default takeoff altitude will be used. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2GDispatcher:SetSquadronTakeoffInAir( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffInAir( SquadronName, TakeoffAltitude ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Air ) + + if TakeoffAltitude then + self:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) + end + + return self + end + + + --- Sets flights by default to take-off from the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off from the runway. + -- A2GDispatcher:SetDefaultTakeoffFromRunway() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffFromRunway() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Runway ) + + return self + end + + + --- Sets flights to take-off from the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from the runway. + -- A2GDispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffFromRunway( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Runway ) + + return self + end + + + --- Sets flights by default to take-off from the airbase at a hot location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off at a hot parking spot. + -- A2GDispatcher:SetDefaultTakeoffFromParkingHot() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffFromParkingHot() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Hot ) + + return self + end + + --- Sets flights to take-off from the airbase at a hot location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- A2GDispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffFromParkingHot( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Hot ) + + return self + end + + + --- Sets flights to by default take-off from the airbase at a cold location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from a cold parking spot. + -- A2GDispatcher:SetDefaultTakeoffFromParkingCold() + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffFromParkingCold() + + self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Cold ) + + return self + end + + + --- Sets flights to take-off from the airbase at a cold location, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from a cold parking spot. + -- A2GDispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffFromParkingCold( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Cold ) + + return self + end + + + --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. + -- @param #AI_A2G_DISPATCHER self + -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set the default takeoff altitude when taking off in the air. + -- A2GDispatcher:SetDefaultTakeoffInAirAltitude( 2000 ) -- This makes planes start at 2000 meters above the ground. + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetDefaultTakeoffInAirAltitude( TakeoffAltitude ) + + self.DefenderDefault.TakeoffAltitude = TakeoffAltitude + + return self + end + + --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Set the default takeoff altitude when taking off in the air. + -- A2GDispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes planes start at 2000 meters above the ground. + -- + -- @return #AI_A2G_DISPATCHER + -- + function AI_A2G_DISPATCHER:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.TakeoffAltitude = TakeoffAltitude + + return self + end + + + --- Defines the default method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default despawn near the airbase when returning. + -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.NearAirbase ) + -- + -- -- Let new flights by default despawn after landing land at the runway. + -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.AtRunway ) + -- + -- -- Let new flights by default despawn after landing and parking, and after engine shutdown. + -- A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.AtEngineShutdown ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLanding( Landing ) + + self.DefenderDefault.Landing = Landing + + return self + end + + + --- Defines the method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights despawn near the airbase when returning. + -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.NearAirbase ) + -- + -- -- Let new flights despawn after landing land at the runway. + -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.AtRunway ) + -- + -- -- Let new flights despawn after landing and parking, and after engine shutdown. + -- A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.AtEngineShutdown ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLanding( SquadronName, Landing ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.Landing = Landing + + return self + end + + + --- Gets the default method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default despawn near the airbase when returning. + -- local LandingMethod = A2GDispatcher:GetDefaultLanding( AI_A2G_Dispatcher.Landing.NearAirbase ) + -- if LandingMethod == AI_A2G_Dispatcher.Landing.NearAirbase then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetDefaultLanding() + + return self.DefenderDefault.Landing + end + + + --- Gets the method at which flights will land and despawn as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let new flights despawn near the airbase when returning. + -- local LandingMethod = A2GDispatcher:GetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.NearAirbase ) + -- if LandingMethod == AI_A2G_Dispatcher.Landing.NearAirbase then + -- ... + -- end + -- + function AI_A2G_DISPATCHER:GetSquadronLanding( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron.Landing or self.DefenderDefault.Landing + end + + + --- Sets flights by default to land and despawn near the airbase in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights by default to land near the airbase and despawn. + -- A2GDispatcher:SetDefaultLandingNearAirbase() + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLandingNearAirbase() + + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.NearAirbase ) + + return self + end + + + --- Sets flights to land and despawn near the airbase in the air, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights to land near the airbase and despawn. + -- A2GDispatcher:SetSquadronLandingNearAirbase( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.NearAirbase ) + + return self + end + + + --- Sets flights by default to land and despawn at the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights by default land at the runway and despawn. + -- A2GDispatcher:SetDefaultLandingAtRunway() + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLandingAtRunway() + + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.AtRunway ) + + return self + end + + + --- Sets flights to land and despawn at the runway, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights land at the runway and despawn. + -- A2GDispatcher:SetSquadronLandingAtRunway( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLandingAtRunway( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.AtRunway ) + + return self + end + + + --- Sets flights by default to land and despawn at engine shutdown, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights by default land and despawn at engine shutdown. + -- A2GDispatcher:SetDefaultLandingAtEngineShutdown() + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetDefaultLandingAtEngineShutdown() + + self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.AtEngineShutdown ) + + return self + end + + + --- Sets flights to land and despawn at engine shutdown, as part of the defense system. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local A2GDispatcher = AI_A2G_DISPATCHER:New( ... ) + -- + -- -- Let flights land and despawn at engine shutdown. + -- A2GDispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.AtEngineShutdown ) + + return self + end + + --- Set the default fuel treshold when defenders will RTB or Refuel in the air. + -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + -- @param #AI_A2G_DISPATCHER self + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + function AI_A2G_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold ) + + self.DefenderDefault.FuelThreshold = FuelThreshold + + return self + end + + + --- Set the fuel treshold for the squadron when defenders will RTB or Refuel in the air. + -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- A2GDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + function AI_A2G_DISPATCHER:SetSquadronFuelThreshold( SquadronName, FuelThreshold ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.FuelThreshold = FuelThreshold + + return self + end + + --- Set the default tanker where defenders will Refuel in the air. + -- @param #AI_A2G_DISPATCHER self + -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + -- -- Now Setup the default tanker. + -- A2GDispatcher:SetDefaultTanker( "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. + function AI_A2G_DISPATCHER:SetDefaultTanker( TankerName ) + + self.DefenderDefault.TankerName = TankerName + + return self + end + + + --- Set the squadron tanker where defenders will Refuel in the air. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Now Setup the A2G dispatcher, and initialize it using the Detection object. + -- A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the squadron fuel treshold. + -- A2GDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + -- -- Now Setup the squadron tanker. + -- A2GDispatcher:SetSquadronTanker( "SquadronName", "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. + function AI_A2G_DISPATCHER:SetSquadronTanker( SquadronName, TankerName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.TankerName = TankerName + + return self + end + + + --- Set the frequency of communication and the mode of communication for voice overs. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number RadioFrequency The frequency of communication. + -- @param #number RadioModulation The modulation of communication. + -- @param #number RadioPower The power in Watts of communication. + function AI_A2G_DISPATCHER:SetSquadronRadioFrequency( SquadronName, RadioFrequency, RadioModulation, RadioPower ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.RadioFrequency = RadioFrequency + DefenderSquadron.RadioModulation = RadioModulation or radio.modulation.AM + DefenderSquadron.RadioPower = RadioPower or 100 + + if DefenderSquadron.RadioQueue then + DefenderSquadron.RadioQueue:Stop() + end + + DefenderSquadron.RadioQueue = nil + + DefenderSquadron.RadioQueue = RADIOSPEECH:New( DefenderSquadron.RadioFrequency, DefenderSquadron.RadioModulation ) + DefenderSquadron.RadioQueue.power = DefenderSquadron.RadioPower + DefenderSquadron.RadioQueue:Start( 0.5 ) + + DefenderSquadron.RadioQueue:SetLanguage( DefenderSquadron.Language ) + end + + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:AddDefenderToSquadron( Squadron, Defender, Size ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + self.Defenders[ DefenderName ] = Squadron + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount - Size + end + self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount + Defender:GetSize() + end + self.Defenders[ DefenderName ] = nil + self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) + end + + function AI_A2G_DISPATCHER:GetSquadronFromDefender( Defender ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + self:F( { DefenderName = DefenderName } ) + return self.Defenders[ DefenderName ] + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:CountPatrolAirborne( SquadronName, DefenseTaskType ) + + local PatrolCount = 0 + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + if DefenderSquadron then + for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do + if DefenderTask.SquadronName == SquadronName then + if DefenderTask.Type == DefenseTaskType then + if AIGroup:IsAlive() then + -- Check if the Patrol is patrolling or engaging. If not, this is not a valid Patrol, even if it is alive! + -- The Patrol could be damaged, lost control, or out of fuel! + if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" ) + or DefenderTask.Fsm:Is( "Started" ) then + PatrolCount = PatrolCount + 1 + end + end + end + end + end + end + + return PatrolCount + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:CountDefendersEngaged( AttackerDetection, AttackerCount ) + + -- First, count the active AIGroups Units, targetting the DetectedSet + local DefendersEngaged = 0 + local DefendersTotal = 0 + + local AttackerSet = AttackerDetection.Set + local DefendersMissing = AttackerCount + --DetectedSet:Flush() + + local DefenderTasks = self:GetDefenderTasks() + for DefenderGroup, DefenderTask in pairs( DefenderTasks ) do + local Defender = DefenderGroup -- Wrapper.Group#GROUP + local DefenderTaskTarget = DefenderTask.Target + local DefenderSquadronName = DefenderTask.SquadronName + local DefenderSize = DefenderTask.Size + + -- Count the total of defenders on the battlefield. + --local DefenderSize = Defender:GetInitialSize() + if DefenderTask.Target then + --if DefenderTask.Fsm:Is( "Engaging" ) then + self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) + DefendersTotal = DefendersTotal + DefenderSize + if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then + + local SquadronOverhead = self:GetSquadronOverhead( DefenderSquadronName ) + self:F( { SquadronOverhead = SquadronOverhead } ) + if DefenderSize then + DefendersEngaged = DefendersEngaged + DefenderSize + DefendersMissing = DefendersMissing - DefenderSize / SquadronOverhead + self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) + else + DefendersEngaged = 0 + end + end + --end + end + + + end + + for QueueID, QueueItem in pairs( self.DefenseQueue ) do + local QueueItem = QueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem + if QueueItem.AttackerDetection and QueueItem.AttackerDetection.ItemID == AttackerDetection.ItemID then + DefendersMissing = DefendersMissing - QueueItem.DefendersNeeded / QueueItem.DefenderSquadron.Overhead + --DefendersEngaged = DefendersEngaged + QueueItem.DefenderGrouping + self:F( { QueueItemName = QueueItem.Defense, QueueItem_ItemID = QueueItem.AttackerDetection.ItemID, DetectedItem = AttackerDetection.ItemID, DefendersMissing = DefendersMissing } ) + end + end + + self:F( { DefenderCount = DefendersEngaged } ) + + return DefendersTotal, DefendersEngaged, DefendersMissing + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:CountDefenders( AttackerDetection, DefenderCount, DefenderTaskType ) + + local Friendlies = nil + + local AttackerSet = AttackerDetection.Set + local AttackerCount = AttackerSet:Count() + + local DefenderFriendlies = self:GetDefenderFriendliesNearBy( AttackerDetection ) + + for FriendlyDistance, DefenderFriendlyUnit in UTILS.spairs( DefenderFriendlies or {} ) do + -- We only allow to engage targets as long as the units on both sides are balanced. + if AttackerCount > DefenderCount then + local FriendlyGroup = DefenderFriendlyUnit:GetGroup() -- Wrapper.Group#GROUP + if FriendlyGroup and FriendlyGroup:IsAlive() then + -- Ok, so we have a friendly near the potential target. + -- Now we need to check if the AIGroup has a Task. + local DefenderTask = self:GetDefenderTask( FriendlyGroup ) + if DefenderTask then + -- The Task should be of the same type. + if DefenderTaskType == DefenderTask.Type then + -- If there is no target, then add the AIGroup to the ResultAIGroups for Engagement to the AttackerSet + if DefenderTask.Target == nil then + if DefenderTask.Fsm:Is( "Returning" ) + or DefenderTask.Fsm:Is( "Patrolling" ) then + Friendlies = Friendlies or {} + Friendlies[FriendlyGroup] = FriendlyGroup + DefenderCount = DefenderCount + FriendlyGroup:GetSize() + self:F( { Friendly = FriendlyGroup:GetName(), FriendlyDistance = FriendlyDistance } ) + end + end + end + end + end + else + break + end + end + + return Friendlies + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + local SquadronName = DefenderSquadron.Name + DefendersNeeded = DefendersNeeded or 4 + local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping + DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded + + if self:IsSquadronVisible( SquadronName ) then + + -- Here we Patrol the new planes. + -- The Resources table is filled in advance. + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) -- Choose the template. + + -- We determine the grouping based on the parameters set. + self:F( { DefenderGrouping = DefenderGrouping } ) + + -- New we will form the group to spawn in. + -- We search for the first free resource matching the template. + local DefenderUnitIndex = 1 + local DefenderPatrolTemplate = nil + local DefenderName = nil + for GroupName, DefenderGroup in pairs( DefenderSquadron.Resources[TemplateID] or {} ) do + self:F( { GroupName = GroupName } ) + local DefenderTemplate = _DATABASE:GetGroupTemplate( GroupName ) + if DefenderUnitIndex == 1 then + DefenderPatrolTemplate = UTILS.DeepCopy( DefenderTemplate ) + self.DefenderPatrolIndex = self.DefenderPatrolIndex + 1 + --DefenderPatrolTemplate.name = SquadronName .. "#" .. self.DefenderPatrolIndex .. "#" .. GroupName + DefenderPatrolTemplate.name = GroupName + DefenderName = DefenderPatrolTemplate.name + else + -- Add the unit in the template to the DefenderPatrolTemplate. + local DefenderUnitTemplate = DefenderTemplate.units[1] + DefenderPatrolTemplate.units[DefenderUnitIndex] = DefenderUnitTemplate + end + DefenderPatrolTemplate.units[DefenderUnitIndex].name = string.format( DefenderPatrolTemplate.name .. '-%02d', DefenderUnitIndex ) + DefenderPatrolTemplate.units[DefenderUnitIndex].unitId = nil + DefenderUnitIndex = DefenderUnitIndex + 1 + DefenderSquadron.Resources[TemplateID][GroupName] = nil + if DefenderUnitIndex > DefenderGrouping then + break + end + + end + + if DefenderPatrolTemplate then + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local SpawnGroup = GROUP:Register( DefenderName ) + DefenderPatrolTemplate.lateActivation = nil + DefenderPatrolTemplate.uncontrolled = nil + local Takeoff = self:GetSquadronTakeoff( SquadronName ) + DefenderPatrolTemplate.route.points[1].type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + DefenderPatrolTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + local Defender = _DATABASE:Spawn( DefenderPatrolTemplate ) + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + Defender:Activate() + return Defender, DefenderGrouping + end + else + local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN + if DefenderGrouping then + Spawn:InitGrouping( DefenderGrouping ) + else + Spawn:InitGrouping() + end + + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local Defender = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + return Defender, DefenderGrouping + end + + return nil, nil + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterPatrol( From, Event, To, SquadronName, DefenseTaskType ) + + local DefenderSquadron, Patrol = self:CanPatrol( SquadronName, DefenseTaskType ) + + -- Determine if there are sufficient resources to form a complete group for patrol. + if DefenderSquadron then + local DefendersNeeded + local DefendersGrouping = ( DefenderSquadron.Grouping or self.DefenderDefault.Grouping ) + if DefenderSquadron.ResourceCount == nil then + DefendersNeeded = DefendersGrouping + else + if DefenderSquadron.ResourceCount >= DefendersGrouping then + DefendersNeeded = DefendersGrouping + else + DefendersNeeded = DefenderSquadron.ResourceCount + end + end + + if Patrol then + self:ResourceQueue( true, DefenderSquadron, DefendersNeeded, Patrol, DefenseTaskType, nil, SquadronName ) + end + end + + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceQueue( Patrol, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName ) + + self:F( { DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName } ) + + local DefenseQueueItem = {} -- #AI_A2G_DISPATCHER.DefenderQueueItem + + + DefenseQueueItem.Patrol = Patrol + DefenseQueueItem.DefenderSquadron = DefenderSquadron + DefenseQueueItem.DefendersNeeded = DefendersNeeded + DefenseQueueItem.Defense = Defense + DefenseQueueItem.DefenseTaskType = DefenseTaskType + DefenseQueueItem.AttackerDetection = AttackerDetection + DefenseQueueItem.SquadronName = SquadronName + + table.insert( self.DefenseQueue, DefenseQueueItem ) + self:F( { QueueItems = #self.DefenseQueue } ) + + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceTakeoff() + + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + self:F( { DefenseQueueID } ) + end + + for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do + + if #self.DefenseQueue > 0 then + + self:F( { SquadronName, Squadron.Name, Squadron.TakeoffTime, Squadron.TakeoffInterval, timer.getTime() } ) + + local DefenseQueueItem = self.DefenseQueue[1] + self:F( {DefenderSquadron=DefenseQueueItem.DefenderSquadron} ) + + if DefenseQueueItem.SquadronName == SquadronName then + + if Squadron.TakeoffTime + Squadron.TakeoffInterval < timer.getTime() then + Squadron.TakeoffTime = timer.getTime() + + if DefenseQueueItem.Patrol == true then + self:ResourcePatrol( DefenseQueueItem.DefenderSquadron, DefenseQueueItem.DefendersNeeded, DefenseQueueItem.Defense, DefenseQueueItem.DefenseTaskType, DefenseQueueItem.AttackerDetection, DefenseQueueItem.SquadronName ) + else + self:ResourceEngage( DefenseQueueItem.DefenderSquadron, DefenseQueueItem.DefendersNeeded, DefenseQueueItem.Defense, DefenseQueueItem.DefenseTaskType, DefenseQueueItem.AttackerDetection, DefenseQueueItem.SquadronName ) + end + table.remove( self.DefenseQueue, 1 ) + end + end + end + + end + + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourcePatrol( DefenderSquadron, DefendersNeeded, Patrol, DefenseTaskType, AttackerDetection, SquadronName ) + + + self:F({DefenderSquadron=DefenderSquadron}) + self:F({DefendersNeeded=DefendersNeeded}) + self:F({Patrol=Patrol}) + self:F({DefenseTaskType=DefenseTaskType}) + self:F({AttackerDetection=AttackerDetection}) + self:F({SquadronName=SquadronName}) + + local DefenderGroup, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + if DefenderGroup then + + local AI_A2G_PATROL = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } + + local AI_A2G_Fsm = AI_A2G_PATROL[DefenseTaskType]:New2( DefenderGroup, Patrol.EngageMinSpeed, Patrol.EngageMaxSpeed, Patrol.EngageFloorAltitude, Patrol.EngageCeilingAltitude, Patrol.EngageAltType, Patrol.Zone, Patrol.PatrolFloorAltitude, Patrol.PatrolCeilingAltitude, Patrol.PatrolMinSpeed, Patrol.PatrolMaxSpeed, Patrol.PatrolAltType ) + AI_A2G_Fsm:SetDispatcher( self ) + AI_A2G_Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + AI_A2G_Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + AI_A2G_Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + AI_A2G_Fsm:SetDisengageRadius( self.DisengageRadius ) + AI_A2G_Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName ) + AI_A2G_Fsm:Start() + + self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, AI_A2G_Fsm, nil, DefenderGrouping ) + + function AI_A2G_Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) + self:F({"Takeoff", DefenderGroup:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() -- #string + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", wheels up.", DefenderGroup ) + AI_A2G_Fsm:Patrol() -- Engage on the TargetSetUnit + end + end + + function AI_A2G_Fsm:onafterPatrolRoute( DefenderGroup, From, Event, To ) + self:F({"PatrolRoute", DefenderGroup:GetName()}) + self:GetParent(self).onafterPatrolRoute( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + if Squadron then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", patrolling.", DefenderGroup ) + end + + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) + end + + function AI_A2G_Fsm:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", DefenderGroup:GetName()}) + + self:GetParent(self).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron and AttackSetUnit:Count() > 0 then + local FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE + + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", moving on to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + end + end + + function AI_A2G_Fsm:OnAfterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", DefenderGroup:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + local FirstUnit = AttackSetUnit:GetFirst() + if FirstUnit then + local Coordinate = FirstUnit:GetCoordinate() + + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + end + end + + function AI_A2G_Fsm:onafterRTB( DefenderGroup, From, Event, To ) + self:F({"RTB", DefenderGroup:GetName()}) + self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", returning to base.", DefenderGroup ) + + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_Fsm:onafterLostControl( DefenderGroup, From, Event, To ) + self:F({"LostControl", DefenderGroup:GetName()}) + self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", lost control." ) + if DefenderGroup:IsAboveRunway() then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + end + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_Fsm:onafterHome( DefenderGroup, From, Event, To, Action ) + self:F({"Home", DefenderGroup:GetName()}) + self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", landing at base.", DefenderGroup ) + + if Action and Action == "Destroy" then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + end + + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + Dispatcher:ResourcePark( Squadron, DefenderGroup ) + end + end + end + + end + + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ResourceEngage( DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName ) + + self:F({DefenderSquadron=DefenderSquadron}) + self:F({DefendersNeeded=DefendersNeeded}) + self:F({Defense=Defense}) + self:F({DefenseTaskType=DefenseTaskType}) + self:F({AttackerDetection=AttackerDetection}) + self:F({SquadronName=SquadronName}) + + local DefenderGroup, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + if DefenderGroup then + + local AI_A2G_ENGAGE = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } + + local AI_A2G_Fsm = AI_A2G_ENGAGE[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude, Defense.EngageAltType ) -- AI.AI_AIR_ENGAGE + AI_A2G_Fsm:SetDispatcher( self ) + AI_A2G_Fsm:SetHomeAirbase( DefenderSquadron.Airbase ) + AI_A2G_Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 ) + AI_A2G_Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold ) + AI_A2G_Fsm:SetDisengageRadius( self.DisengageRadius ) + AI_A2G_Fsm:Start() + + self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, AI_A2G_Fsm, AttackerDetection, DefenderGrouping ) + + function AI_A2G_Fsm:onafterTakeoff( DefenderGroup, From, Event, To ) + self:F({"Defender Birth", DefenderGroup:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + local DefenderTarget = Dispatcher:GetDefenderTaskTarget( DefenderGroup ) + + self:F( { DefenderTarget = DefenderTarget } ) + + if DefenderTarget then + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", wheels up.", DefenderGroup ) + AI_A2G_Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit + end + end + + function AI_A2G_Fsm:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", DefenderGroup:GetName()}) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + + if Squadron then + local FirstUnit = AttackSetUnit:GetFirst() + local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE + + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", on route to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + end + self:GetParent(self).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit ) + end + + function AI_A2G_Fsm:OnAfterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F({"Engage Route", DefenderGroup:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + local FirstUnit = AttackSetUnit:GetFirst() + if FirstUnit then + local Coordinate = FirstUnit:GetCoordinate() + + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup ) + end + end + + function AI_A2G_Fsm:onafterRTB( DefenderGroup, From, Event, To ) + self:F({"Defender RTB", DefenderGroup:GetName()}) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", returning to base.", DefenderGroup ) + + self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To ) + + Dispatcher:ClearDefenderTaskTarget( DefenderGroup ) + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_Fsm:onafterLostControl( DefenderGroup, From, Event, To ) + self:F({"Defender LostControl", DefenderGroup:GetName()}) + self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + --Dispatcher:MessageToPlayers( Squadron, "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." ) + + if DefenderGroup:IsAboveRunway() then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + end + end + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_Fsm:onafterHome( DefenderGroup, From, Event, To, Action ) + self:F({"Defender Home", DefenderGroup:GetName()}) + self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To ) + + local DefenderName = DefenderGroup:GetCallsign() + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup ) + Dispatcher:MessageToPlayers( Squadron, DefenderName .. ", landing at base.", DefenderGroup ) + + if Action and Action == "Destroy" then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + end + + if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup ) + DefenderGroup:Destroy() + Dispatcher:ResourcePark( Squadron, DefenderGroup ) + end + end + end + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterEngage( From, Event, To, AttackerDetection, Defenders ) + + if Defenders then + + for DefenderID, Defender in pairs( Defenders or {} ) do + + local Fsm = self:GetDefenderTaskFsm( Defender ) + Fsm:Engage( AttackerDetection.Set ) -- Engage on the TargetSetUnit + + self:SetDefenderTaskTarget( Defender, AttackerDetection ) + + end + end + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:HasDefenseLine( DefenseCoordinate, DetectedItem ) + + local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) + + -- Now check if this coordinate is not in a danger zone, meaning, that the attack line is not crossing other coordinates. + -- (y1 – y2)x + (x2 – x1)y + (x1y2 – x2y1) = 0 + + local c1 = DefenseCoordinate + local c2 = AttackCoordinate + + local a = c1.z - c2.z -- Calculate a + local b = c2.x - c1.x -- Calculate b + local c = c1.x * c2.z - c2.x * c1.z -- calculate c + + local ok = true + + -- Now we check if each coordinate radius of about 30km of each attack is crossing a defense line. If yes, then this is not a good attack! + for AttackItemID, CheckAttackItem in pairs( self.Detection:GetDetectedItems() ) do + + -- Only compare other detected coordinates. + if AttackItemID ~= DetectedItem.ID then + + local CheckAttackCoordinate = self.Detection:GetDetectedItemCoordinate( CheckAttackItem ) + + local x = CheckAttackCoordinate.x + local y = CheckAttackCoordinate.z + local r = 5000 + + -- now we check if the coordinate is intersecting with the defense line. + + local IntersectDistance = ( math.abs( a * x + b * y + c ) ) / math.sqrt( a * a + b * b ) + self:F( { IntersectDistance = IntersectDistance, x = x, y = y } ) + + local IntersectAttackDistance = CheckAttackCoordinate:Get2DDistance( DefenseCoordinate ) + + self:F( { IntersectAttackDistance=IntersectAttackDistance, EvaluateDistance=EvaluateDistance } ) + + -- If the distance of the attack coordinate is larger than the test radius; then the line intersects, and this is not a good coordinate. + if IntersectDistance < r and IntersectAttackDistance < EvaluateDistance then + ok = false + break + end + end + end + + return ok + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterDefend( From, Event, To, DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, DefenderFriendlies, DefenseTaskType ) + + self:F( { From, Event, To, DetectedItem.Index, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing, DefenderFriendlies = DefenderFriendlies } ) + + DetectedItem.Type = DefenseTaskType -- This is set to report the task type in the status panel. + + local AttackerSet = DetectedItem.Set + local AttackerUnit = AttackerSet:GetFirst() + + if AttackerUnit and AttackerUnit:IsAlive() then + local AttackerCount = AttackerSet:Count() + local DefenderCount = 0 + + for DefenderID, DefenderGroup in pairs( DefenderFriendlies or {} ) do + + -- Here we check if the defenders have a defense line to the attackers. + -- If the attackers are behind enemy lines or too close to an other defense line; then don´t engage. + local DefenseCoordinate = DefenderGroup:GetCoordinate() + local HasDefenseLine = self:HasDefenseLine( DefenseCoordinate, DetectedItem ) + + if HasDefenseLine == true then + local SquadronName = self:GetDefenderTask( DefenderGroup ).SquadronName + local SquadronOverhead = self:GetSquadronOverhead( SquadronName ) + + local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) + Fsm:EngageRoute( AttackerSet ) -- Engage on the TargetSetUnit + + self:SetDefenderTaskTarget( DefenderGroup, DetectedItem ) + + local DefenderGroupSize = DefenderGroup:GetSize() + DefendersMissing = DefendersMissing - DefenderGroupSize / SquadronOverhead + DefendersTotal = DefendersTotal + DefenderGroupSize / SquadronOverhead + end + + if DefendersMissing <= 0 then + break + end + end + + self:F( { DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } ) + DefenderCount = DefendersMissing + + local ClosestDistance = 0 + local EngageSquadronName = nil + + local BreakLoop = false + + while( DefenderCount > 0 and not BreakLoop ) do + + self:F( { DefenderSquadrons = self.DefenderSquadrons } ) + + for SquadronName, DefenderSquadron in UTILS.rpairs( self.DefenderSquadrons or {} ) do + + if DefenderSquadron[DefenseTaskType] then + + local AirbaseCoordinate = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE + local AttackerCoord = AttackerUnit:GetCoordinate() + local InterceptCoord = DetectedItem.InterceptCoord + self:F( { InterceptCoord = InterceptCoord } ) + if InterceptCoord then + local InterceptDistance = AirbaseCoordinate:Get2DDistance( InterceptCoord ) + local AirbaseDistance = AirbaseCoordinate:Get2DDistance( AttackerCoord ) + self:F( { InterceptDistance = InterceptDistance, AirbaseDistance = AirbaseDistance, InterceptCoord = InterceptCoord } ) + + -- Only intercept if the distance to target is smaller or equal to the GciRadius limit. + if AirbaseDistance <= self.DefenseRadius then + + -- Check if there is a defense line... + local HasDefenseLine = self:HasDefenseLine( AirbaseCoordinate, DetectedItem ) + if HasDefenseLine == true then + local EngageProbability = ( DefenderSquadron.EngageProbability or 1 ) + local Probability = math.random() + if Probability < EngageProbability then + EngageSquadronName = SquadronName + break + end + end + end + end + end + end + + if EngageSquadronName then + + local DefenderSquadron, Defense = self:CanDefend( EngageSquadronName, DefenseTaskType ) + + if Defense then + + local DefenderOverhead = DefenderSquadron.Overhead or self.DefenderDefault.Overhead + local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping + local DefendersNeeded = math.ceil( DefenderCount * DefenderOverhead ) + + self:F( { Overhead = DefenderOverhead, SquadronOverhead = DefenderSquadron.Overhead , DefaultOverhead = self.DefenderDefault.Overhead } ) + self:F( { Grouping = DefenderGrouping, SquadronGrouping = DefenderSquadron.Grouping, DefaultGrouping = self.DefenderDefault.Grouping } ) + self:F( { DefendersCount = DefenderCount, DefendersNeeded = DefendersNeeded } ) + + -- Validate that the maximum limit of Defenders has been reached. + -- If yes, then cancel the engaging of more defenders. + local DefendersLimit = DefenderSquadron.EngageLimit or self.DefenderDefault.EngageLimit + if DefendersLimit then + if DefendersTotal >= DefendersLimit then + DefendersNeeded = 0 + BreakLoop = true + else + -- If the total of amount of defenders + the defenders needed, is larger than the limit of defenders, + -- then the defenders needed is the difference between defenders total - defenders limit. + if DefendersTotal + DefendersNeeded > DefendersLimit then + DefendersNeeded = DefendersLimit - DefendersTotal + end + end + end + + -- DefenderSquadron.ResourceCount can have the value nil, which expresses unlimited resources. + -- DefendersNeeded cannot exceed DefenderSquadron.ResourceCount! + if DefenderSquadron.ResourceCount and DefendersNeeded > DefenderSquadron.ResourceCount then + DefendersNeeded = DefenderSquadron.ResourceCount + BreakLoop = true + end + + while ( DefendersNeeded > 0 ) do + self:ResourceQueue( false, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, DetectedItem, EngageSquadronName ) + DefendersNeeded = DefendersNeeded - DefenderGrouping + DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead + end -- while ( DefendersNeeded > 0 ) do + else + -- No more resources, try something else. + -- Subject for a later enhancement to try to depart from another squadron and disable this one. + BreakLoop = true + break + end + else + -- There isn't any closest airbase anymore, break the loop. + break + end + end -- if DefenderSquadron then + end -- if AttackerUnit + end + + + + --- Creates an SEAD task when the targets have radars. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged. + -- @return #nil If there are no targets to be set. + function AI_A2G_DISPATCHER:Evaluate_SEAD( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT + local AttackerCount = AttackerSet:HasSEAD() -- Is the AttackerSet a SEAD group, then the amount of radar emitters will be returned; that need to be attacked. + + if ( AttackerCount > 0 ) then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount ) + + self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + + local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "SEAD" ) + + + + if DetectedItem.IsDetected == true then + + return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups + end + end + + return 0, 0, 0 + end + + + --- Creates an CAS task. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged. + -- @return #nil If there are no targets to be set. + function AI_A2G_DISPATCHER:Evaluate_CAS( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT + local AttackerCount = AttackerSet:Count() + local AttackerRadarCount = AttackerSet:HasSEAD() + local IsFriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + local IsCas = ( AttackerRadarCount == 0 ) and ( IsFriendliesNearBy == true ) -- Is the AttackerSet a CAS group? + + if IsCas == true then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount ) + + self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + + local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "CAS" ) + + if DetectedItem.IsDetected == true then + + return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups + end + end + + return 0, 0, 0 + end + + + --- Evaluates an BAI task. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged. + -- @return #nil If there are no targets to be set. + function AI_A2G_DISPATCHER:Evaluate_BAI( DetectedItem ) + self:F( { DetectedItem.ItemID } ) + + local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT + local AttackerCount = AttackerSet:Count() + local AttackerRadarCount = AttackerSet:HasSEAD() + local IsFriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + local IsBai = ( AttackerRadarCount == 0 ) and ( IsFriendliesNearBy == false ) -- Is the AttackerSet a BAI group? + + if IsBai == true then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount ) + + self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + + local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "BAI" ) + + if DetectedItem.IsDetected == true then + + return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups + end + end + + return 0, 0, 0 + end + + + --- Determine the distance as the keys of reference of the detected items. + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:Keys( DetectedItem ) + + self:F( { DetectedItem = DetectedItem } ) + + local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + local ShortestDistance = 999999999 + + for DefenseCoordinateName, DefenseCoordinate in pairs( self.DefenseCoordinates ) do + local DefenseCoordinate = DefenseCoordinate -- Core.Point#COORDINATE + + local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) + + if EvaluateDistance <= ShortestDistance then + ShortestDistance = EvaluateDistance + end + end + + return ShortestDistance + end + + --- Assigns A2G AI Tasks in relation to the detected items. + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:Order( DetectedItem ) + local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + local ShortestDistance = 999999999 + + for DefenseCoordinateName, DefenseCoordinate in pairs( self.DefenseCoordinates ) do + local DefenseCoordinate = DefenseCoordinate -- Core.Point#COORDINATE + + local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate ) + + if EvaluateDistance <= ShortestDistance then + ShortestDistance = EvaluateDistance + end + end + + return ShortestDistance + end + + --- Shows the tactical display. + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:ShowTacticalDisplay( Detection ) + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local TaskReport = REPORT:New() + + local DefenseTotal = 0 + + local Report = REPORT:New( "\nTactical Overview" ) + + local DefenderGroupCount = 0 + local DefendersTotal = 0 + + -- Now that all obsolete tasks are removed, loop through the detected targets. + --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do + + if not self.Detection:IsDetectedItemLocked( DetectedItem ) == true then + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT + local DetectedCount = DetectedSet:Count() + local DetectedZone = DetectedItem.Zone + + self:F( { "Target ID", DetectedItem.ItemID } ) + + self:F( { DefenseLimit = self.DefenseLimit, DefenseTotal = DefenseTotal } ) + DetectedSet:Flush( self ) + + local DetectedID = DetectedItem.ID + local DetectionIndex = DetectedItem.Index + local DetectedItemChanged = DetectedItem.Changed + + -- Show tactical situation + local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G() + Report:Add( string.format( " - %1s%s ( %04s ): ( #%02d - %-4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "â– ", ThreatLevel ) ) ) + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + local Defender = Defender -- Wrapper.Group#GROUP + if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then + if Defender:IsAlive() then + DefenderGroupCount = DefenderGroupCount + 1 + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Defender:GetName(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + end + end + + Report:Add( "\n - No Targets:") + local TaskCount = 0 + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + TaskCount = TaskCount + 1 + local Defender = Defender -- Wrapper.Group#GROUP + if not DefenderTask.Target then + if Defender:IsAlive() then + local DefenderHasTask = Defender:HasTask() + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + DefenderGroupCount = DefenderGroupCount + 1 + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Defender:GetName(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + Report:Add( string.format( "\n - %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) + + Report:Add( string.format( "\n - %d Queued Aircraft Launches", #self.DefenseQueue ) ) + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + local DefenseQueueItem = DefenseQueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem + Report:Add( string.format( " - %s - %s", DefenseQueueItem.SquadronName, DefenseQueueItem.DefenderSquadron.TakeoffTime, DefenseQueueItem.DefenderSquadron.TakeoffInterval) ) + + end + + Report:Add( string.format( "\n - Squadron Resources: ", #self.DefenseQueue ) ) + for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + Report:Add( string.format( " - %s - %s", DefenderSquadronName, DefenderSquadron.ResourceCount and tostring(DefenderSquadron.ResourceCount) or "n/a" ) ) + end + + self:F( Report:Text( "\n" ) ) + trigger.action.outText( Report:Text( "\n" ), 25 ) + + end + + --- Assigns A2G AI Tasks in relation to the detected items. + -- @param #AI_A2G_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. + -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. + function AI_A2G_DISPATCHER:ProcessDetected( Detection ) + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local TaskReport = REPORT:New() + + local DefenseTotal = 0 + + for DefenderGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do + local DefenderGroup = DefenderGroup -- Wrapper.Group#GROUP + local DefenderTaskFsm = self:GetDefenderTaskFsm( DefenderGroup ) + --if DefenderTaskFsm:Is( "LostControl" ) then + -- self:ClearDefenderTask( DefenderGroup ) + --end + if not DefenderGroup:IsAlive() then + self:F( { Defender = DefenderGroup:GetName(), DefenderState = DefenderTaskFsm:GetState() } ) + if not DefenderTaskFsm:Is( "Started" ) then + self:ClearDefenderTask( DefenderGroup ) + end + else + -- TODO: prio 1, what is this index stuff again, simplify it. + if DefenderTask.Target then + self:F( { TargetIndex = DefenderTask.Target.Index } ) + local AttackerItem = Detection:GetDetectedItemByIndex( DefenderTask.Target.Index ) + if not AttackerItem then + self:F( { "Removing obsolete Target:", DefenderTask.Target.Index } ) + self:ClearDefenderTaskTarget( DefenderGroup ) + else + if DefenderTask.Target.Set then + local TargetCount = DefenderTask.Target.Set:Count() + if TargetCount == 0 then + self:F( { "All Targets destroyed in Target, removing:", DefenderTask.Target.Index } ) + self:ClearDefenderTask( DefenderGroup ) + end + end + end + end + end + end + +-- for DefenderGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do +-- DefenseTotal = DefenseTotal + 1 +-- end + + local Report = REPORT:New( "\nTactical Overview" ) + + local DefenderGroupCount = 0 + + local DefendersTotal = 0 + + -- Now that all obsolete tasks are removed, loop through the detected targets. + --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedDistance, DetectedItem in UTILS.kpairs( Detection:GetDetectedItems(), function( t ) return self:Keys( t ) end, function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do + + if not self.Detection:IsDetectedItemLocked( DetectedItem ) == true then + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT + local DetectedCount = DetectedSet:Count() + local DetectedZone = DetectedItem.Zone + + self:F( { "Target ID", DetectedItem.ItemID } ) + + self:F( { DefenseLimit = self.DefenseLimit, DefenseTotal = DefenseTotal } ) + DetectedSet:Flush( self ) + + local DetectedID = DetectedItem.ID + local DetectionIndex = DetectedItem.Index + local DetectedItemChanged = DetectedItem.Changed + + local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + -- Calculate if for this DetectedItem if a defense needs to be initiated. + -- This calculation is based on the distance between the defense point and the attackers, and the defensiveness parameter. + -- The attackers closest to the defense coordinates will be handled first, or course! + + local EngageDefenses = nil + + self:F( { DetectedDistance = DetectedDistance, DefenseRadius = self.DefenseRadius } ) + if DetectedDistance <= self.DefenseRadius then + + self:F( { DetectedApproach = self._DefenseApproach } ) + if self._DefenseApproach == AI_A2G_DISPATCHER.DefenseApproach.Distance then + EngageDefenses = true + self:F( { EngageDefenses = EngageDefenses } ) + end + + if self._DefenseApproach == AI_A2G_DISPATCHER.DefenseApproach.Random then + local DistanceProbability = ( self.DefenseRadius / DetectedDistance * self.DefenseReactivity ) + local DefenseProbability = math.random() + + self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } ) + + if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then + EngageDefenses = true + end + end + + + end + + self:F( { EngageDefenses = EngageDefenses, DefenseLimit = self.DefenseLimit, DefenseTotal = DefenseTotal } ) + + -- There needs to be an EngageCoordinate. + -- If self.DefenseLimit is set (thus limit the amount of defenses to one zone), then only start a new defense if the maximum has not been reached. + -- If self.DefenseLimit has not been set, there is an unlimited amount of zones to be defended. + if ( EngageDefenses and ( self.DefenseLimit and DefenseTotal < self.DefenseLimit ) or not self.DefenseLimit ) then + do + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_SEAD( DetectedItem ) -- Returns a SET_UNIT with the SEAD targets to be engaged... + if DefendersMissing > 0 then + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "SEAD" ) + end + end + + do + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_CAS( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... + if DefendersMissing > 0 then + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "CAS" ) + end + end + + do + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_BAI( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged... + if DefendersMissing > 0 then + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "BAI" ) + end + end + end + + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + local Defender = Defender -- Wrapper.Group#GROUP + if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then + DefenseTotal = DefenseTotal + 1 + end + end + + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + local DefenseQueueItem = DefenseQueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem + if DefenseQueueItem.AttackerDetection and DefenseQueueItem.AttackerDetection.Index and DefenseQueueItem.AttackerDetection.Index == DetectedItem.Index then + DefenseTotal = DefenseTotal + 1 + end + end + + if self.TacticalDisplay then + -- Show tactical situation + local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G() + Report:Add( string.format( " - %1s%s ( %4s ): ( #%d - %4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "â– ", ThreatLevel ) ) ) + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + local Defender = Defender -- Wrapper.Group#GROUP + if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then + if Defender:IsAlive() then + DefenderGroupCount = DefenderGroupCount + 1 + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Defender:GetName(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + end + end + end + + if self.TacticalDisplay then + Report:Add( "\n - No Targets:") + local TaskCount = 0 + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + TaskCount = TaskCount + 1 + local Defender = Defender -- Wrapper.Group#GROUP + if not DefenderTask.Target then + if Defender:IsAlive() then + local DefenderHasTask = Defender:HasTask() + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + DefenderGroupCount = DefenderGroupCount + 1 + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Defender:GetName(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + Report:Add( string.format( "\n - %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) + + Report:Add( string.format( "\n - %d Queued Aircraft Launches", #self.DefenseQueue ) ) + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + local DefenseQueueItem = DefenseQueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem + Report:Add( string.format( " - %s - %s", DefenseQueueItem.SquadronName, DefenseQueueItem.DefenderSquadron.TakeoffTime, DefenseQueueItem.DefenderSquadron.TakeoffInterval) ) + + end + + Report:Add( string.format( "\n - Squadron Resources: ", #self.DefenseQueue ) ) + for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + Report:Add( string.format( " - %s - %d", DefenderSquadronName, DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount or "n/a" ) ) + end + + self:F( Report:Text( "\n" ) ) + trigger.action.outText( Report:Text( "\n" ), 25 ) + end + + return true + end + +end + +do + + --- Calculates which HUMAN friendlies are nearby the area. + -- @param #AI_A2G_DISPATCHER self + -- @param DetectedItem The detected item. + -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. + function AI_A2G_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem ) + + local DetectedSet = DetectedItem.Set + local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem ) + + local PlayerTypes = {} + local PlayersCount = 0 + + if PlayersNearBy then + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do + local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT + local PlayerName = PlayerUnit:GetPlayerName() + --self:F( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } ) + if PlayerUnit:IsAirPlane() and PlayerName ~= nil then + local FriendlyUnitThreatLevel = PlayerUnit:GetThreatLevel() + PlayersCount = PlayersCount + 1 + local PlayerType = PlayerUnit:GetTypeName() + PlayerTypes[PlayerName] = PlayerType + if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + end + end + end + + end + + --self:F( { PlayersCount = PlayersCount } ) + + local PlayerTypesReport = REPORT:New() + + if PlayersCount > 0 then + for PlayerName, PlayerType in pairs( PlayerTypes ) do + PlayerTypesReport:Add( string.format('"%s" in %s', PlayerName, PlayerType ) ) + end + else + PlayerTypesReport:Add( "-" ) + end + + + return PlayersCount, PlayerTypesReport + end + + --- Calculates which friendlies are nearby the area. + -- @param #AI_A2G_DISPATCHER self + -- @param DetectedItem The detected item. + -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. + function AI_A2G_DISPATCHER:GetFriendliesNearBy( DetectedItem ) + + local DetectedSet = DetectedItem.Set + local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem ) + + local FriendlyTypes = {} + local FriendliesCount = 0 + + if FriendlyUnitsNearBy then + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G() + for FriendlyUnitName, FriendlyUnitData in pairs( FriendlyUnitsNearBy ) do + local FriendlyUnit = FriendlyUnitData -- Wrapper.Unit#UNIT + if FriendlyUnit:IsAirPlane() then + local FriendlyUnitThreatLevel = FriendlyUnit:GetThreatLevel() + FriendliesCount = FriendliesCount + 1 + local FriendlyType = FriendlyUnit:GetTypeName() + FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and ( FriendlyTypes[FriendlyType] + 1 ) or 1 + if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + end + end + end + + end + + --self:F( { FriendliesCount = FriendliesCount } ) + + local FriendlyTypesReport = REPORT:New() + + if FriendliesCount > 0 then + for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do + FriendlyTypesReport:Add( string.format("%d of %s", FriendlyTypeCount, FriendlyType ) ) + end + else + FriendlyTypesReport:Add( "-" ) + end + + + return FriendliesCount, FriendlyTypesReport + end + + --- Schedules a new Patrol for the given SquadronName. + -- @param #AI_A2G_DISPATCHER self + -- @param #string SquadronName The squadron name. + function AI_A2G_DISPATCHER:SchedulerPatrol( SquadronName ) + local PatrolTaskTypes = { "SEAD", "CAS", "BAI" } + local PatrolTaskType = PatrolTaskTypes[math.random(1,3)] + self:Patrol( SquadronName, PatrolTaskType ) + end + +end + diff --git a/Moose Development/Moose/AI/AI_A2G_SEAD.lua b/Moose Development/Moose/AI/AI_A2G_SEAD.lua new file mode 100644 index 000000000..6a0a32e3c --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -0,0 +1,152 @@ +--- **AI** -- Models the process of air to ground SEAD engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_SEAD +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_A2G_SEAD +-- @extends AI.AI_A2G_Patrol#AI_AIR_PATROL + + +--- Implements the core functions to SEAD intruders. Use the Engage trigger to intercept intruders. +-- +-- ![Process](..\Presentations\AI_GCI\Dia3.JPG) +-- +-- The AI_A2G_SEAD is assigned a @{Wrapper.Group} and this must be done before the AI_A2G_SEAD process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_GCI\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_GCI\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_GCI\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_GCI\Dia13.JPG) +-- +-- ## 1. AI_A2G_SEAD constructor +-- +-- * @{#AI_A2G_SEAD.New}(): Creates a new AI_A2G_SEAD object. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_GCI\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_GCI#AI_A2G_SEAD.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_A2G_SEAD.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_A2G_SEAD +AI_A2G_SEAD = { + ClassName = "AI_A2G_SEAD", +} + + + +--- Creates a new AI_A2G_SEAD object +-- @param #AI_A2G_SEAD self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_SEAD +function AI_A2G_SEAD:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + local AI_Air = AI_AIR:New( AIGroup ) + local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- #AI_AIR_PATROL + local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + local self = BASE:Inherit( self, AI_Air_Engage ) + + return self +end + + +--- Creates a new AI_A2G_SEAD object +-- @param #AI_A2G_SEAD self +-- @param Wrapper.Group#GROUP AIGroup +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @return #AI_A2G_SEAD +function AI_A2G_SEAD:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + return self:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) +end + + +--- Evaluate the attack and create an AttackUnitTask list. +-- @param #AI_A2G_SEAD self +-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack. +-- @param Wrappper.Group#GROUP DefenderGroup The group of defenders. +-- @param #number EngageAltitude The altitude to engage the targets. +-- @return #AI_A2G_SEAD self +function AI_A2G_SEAD:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) + + local AttackUnitTasks = {} + + local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 ) + for AttackUnitID, AttackUnit in ipairs( AttackSetUnitPerThreatLevel ) do + if AttackUnit then + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + local HasRadar = AttackUnit:HasSEAD() + if HasRadar then + self:F( { "SEAD Unit:", AttackUnit:GetName() } ) + AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude ) + end + end + end + end + + return AttackUnitTasks +end + diff --git a/Moose Development/Moose/AI/AI_A2A.lua b/Moose Development/Moose/AI/AI_Air.lua similarity index 58% rename from Moose Development/Moose/AI/AI_A2A.lua rename to Moose Development/Moose/AI/AI_Air.lua index c96fa4e43..213e58757 100644 --- a/Moose Development/Moose/AI/AI_A2A.lua +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -1,4 +1,4 @@ ---- **AI** -- (R2.2) - Models the process of air operations for airplanes. +--- **AI** - Models the process of AI air operations. -- -- === -- @@ -6,102 +6,99 @@ -- -- === -- --- @module AI.AI_A2A --- @image AI_Air_To_Air_Dispatching.JPG +-- @module AI.AI_Air +-- @image MOOSE.JPG ---BASE:TraceClass("AI_A2A") - - ---- @type AI_A2A +--- @type AI_AIR -- @extends Core.Fsm#FSM_CONTROLLABLE ---- The AI_A2A class implements the core functions to operate an AI @{Wrapper.Group} A2A tasking. +--- The AI_AIR class implements the core functions to operate an AI @{Wrapper.Group}. -- -- --- ## AI_A2A constructor +-- # 1) AI_AIR constructor -- --- * @{#AI_A2A.New}(): Creates a new AI_A2A object. +-- * @{#AI_AIR.New}(): Creates a new AI_AIR object. -- --- ## 2. AI_A2A is a FSM +-- # 2) AI_AIR is a Finite State Machine. -- --- ![Process](..\Presentations\AI_PATROL\Dia2.JPG) +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. -- --- ### 2.1. AI_A2A States +-- So, each of the rows have the following structure. -- --- * **None** ( Group ): The process is not started yet. --- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. --- * **Returning** ( Group ): The AI is returning to Base. --- * **Stopped** ( Group ): The process is stopped. --- * **Crashed** ( Group ): The AI has crashed or is dead. +-- * **From** => **Event** => **To** -- --- ### 2.2. AI_A2A Events +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. -- --- * **Start** ( Group ): Start the process. --- * **Stop** ( Group ): Stop the process. --- * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone. --- * **RTB** ( Group ): Route the AI to the home base. --- * **Detect** ( Group ): The AI is detecting targets. --- * **Detected** ( Group ): The AI has detected new targets. --- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. --- --- ## 3. Set or Get the AI controllable +-- These are the different possible state transitions of this state machine implementation: -- --- * @{#AI_A2A.SetControllable}(): Set the AIControllable. --- * @{#AI_A2A.GetControllable}(): Get the AIControllable. +-- * Idle => Start => Monitoring -- --- @field #AI_A2A -AI_A2A = { - ClassName = "AI_A2A", +-- ## 2.1) AI_AIR States. +-- +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_AIR Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- @field #AI_AIR +AI_AIR = { + ClassName = "AI_AIR", } ---- Creates a new AI_A2A object --- @param #AI_A2A self --- @param Wrapper.Group#GROUP AIGroup The GROUP object to receive the A2A Process. --- @return #AI_A2A -function AI_A2A:New( AIGroup ) +AI_AIR.TaskDelay = 0.5 -- The delay of each task given to the AI. + +--- Creates a new AI_AIR process. +-- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup The group object to receive the A2G Process. +-- @return #AI_AIR +function AI_AIR:New( AIGroup ) -- Inherits from BASE - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_A2A + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_AIR self:SetControllable( AIGroup ) - self:SetFuelThreshold( .2, 60 ) - self:SetDamageThreshold( 0.4 ) - self:SetDisengageRadius( 70000 ) - self:SetStartState( "Stopped" ) - + + self:AddTransition( "*", "Queue", "Queued" ) + self:AddTransition( "*", "Start", "Started" ) - --- Start Handler OnBefore for AI_A2A - -- @function [parent=#AI_A2A] OnBeforeStart - -- @param #AI_A2A self + --- Start Handler OnBefore for AI_AIR + -- @function [parent=#AI_AIR] OnBeforeStart + -- @param #AI_AIR self -- @param #string From -- @param #string Event -- @param #string To -- @return #boolean - --- Start Handler OnAfter for AI_A2A - -- @function [parent=#AI_A2A] OnAfterStart - -- @param #AI_A2A self + --- Start Handler OnAfter for AI_AIR + -- @function [parent=#AI_AIR] OnAfterStart + -- @param #AI_AIR self -- @param #string From -- @param #string Event -- @param #string To - --- Start Trigger for AI_A2A - -- @function [parent=#AI_A2A] Start - -- @param #AI_A2A self + --- Start Trigger for AI_AIR + -- @function [parent=#AI_AIR] Start + -- @param #AI_AIR self - --- Start Asynchronous Trigger for AI_A2A - -- @function [parent=#AI_A2A] __Start - -- @param #AI_A2A self + --- Start Asynchronous Trigger for AI_AIR + -- @function [parent=#AI_AIR] __Start + -- @param #AI_AIR self -- @param #number Delay self:AddTransition( "*", "Stop", "Stopped" ) --- OnLeave Transition Handler for State Stopped. --- @function [parent=#AI_A2A] OnLeaveStopped --- @param #AI_A2A self +-- @function [parent=#AI_AIR] OnLeaveStopped +-- @param #AI_AIR self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. @@ -109,16 +106,16 @@ function AI_A2A:New( AIGroup ) -- @return #boolean Return false to cancel Transition. --- OnEnter Transition Handler for State Stopped. --- @function [parent=#AI_A2A] OnEnterStopped --- @param #AI_A2A self +-- @function [parent=#AI_AIR] OnEnterStopped +-- @param #AI_AIR self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. --- OnBefore Transition Handler for Event Stop. --- @function [parent=#AI_A2A] OnBeforeStop --- @param #AI_A2A self +-- @function [parent=#AI_AIR] OnBeforeStop +-- @param #AI_AIR self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. @@ -126,27 +123,27 @@ function AI_A2A:New( AIGroup ) -- @return #boolean Return false to cancel Transition. --- OnAfter Transition Handler for Event Stop. --- @function [parent=#AI_A2A] OnAfterStop --- @param #AI_A2A self +-- @function [parent=#AI_AIR] OnAfterStop +-- @param #AI_AIR self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. --- Synchronous Event Trigger for Event Stop. --- @function [parent=#AI_A2A] Stop --- @param #AI_A2A self +-- @function [parent=#AI_AIR] Stop +-- @param #AI_AIR self --- Asynchronous Event Trigger for Event Stop. --- @function [parent=#AI_A2A] __Stop --- @param #AI_A2A self +-- @function [parent=#AI_AIR] __Stop +-- @param #AI_AIR self -- @param #number Delay The delay in seconds. - self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A. + self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR. --- OnBefore Transition Handler for Event Status. --- @function [parent=#AI_A2A] OnBeforeStatus --- @param #AI_A2A self +-- @function [parent=#AI_AIR] OnBeforeStatus +-- @param #AI_AIR self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. @@ -154,27 +151,27 @@ function AI_A2A:New( AIGroup ) -- @return #boolean Return false to cancel Transition. --- OnAfter Transition Handler for Event Status. --- @function [parent=#AI_A2A] OnAfterStatus --- @param #AI_A2A self +-- @function [parent=#AI_AIR] OnAfterStatus +-- @param #AI_AIR self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. --- Synchronous Event Trigger for Event Status. --- @function [parent=#AI_A2A] Status --- @param #AI_A2A self +-- @function [parent=#AI_AIR] Status +-- @param #AI_AIR self --- Asynchronous Event Trigger for Event Status. --- @function [parent=#AI_A2A] __Status --- @param #AI_A2A self +-- @function [parent=#AI_AIR] __Status +-- @param #AI_AIR self -- @param #number Delay The delay in seconds. - self:AddTransition( "*", "RTB", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A. + self:AddTransition( "*", "RTB", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR. --- OnBefore Transition Handler for Event RTB. --- @function [parent=#AI_A2A] OnBeforeRTB --- @param #AI_A2A self +-- @function [parent=#AI_AIR] OnBeforeRTB +-- @param #AI_AIR self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. @@ -182,25 +179,25 @@ function AI_A2A:New( AIGroup ) -- @return #boolean Return false to cancel Transition. --- OnAfter Transition Handler for Event RTB. --- @function [parent=#AI_A2A] OnAfterRTB --- @param #AI_A2A self +-- @function [parent=#AI_AIR] OnAfterRTB +-- @param #AI_AIR self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. --- Synchronous Event Trigger for Event RTB. --- @function [parent=#AI_A2A] RTB --- @param #AI_A2A self +-- @function [parent=#AI_AIR] RTB +-- @param #AI_AIR self --- Asynchronous Event Trigger for Event RTB. --- @function [parent=#AI_A2A] __RTB --- @param #AI_A2A self +-- @function [parent=#AI_AIR] __RTB +-- @param #AI_AIR self -- @param #number Delay The delay in seconds. --- OnLeave Transition Handler for State Returning. --- @function [parent=#AI_A2A] OnLeaveReturning --- @param #AI_A2A self +-- @function [parent=#AI_AIR] OnLeaveReturning +-- @param #AI_AIR self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. @@ -208,8 +205,8 @@ function AI_A2A:New( AIGroup ) -- @return #boolean Return false to cancel Transition. --- OnEnter Transition Handler for State Returning. --- @function [parent=#AI_A2A] OnEnterReturning --- @param #AI_A2A self +-- @function [parent=#AI_AIR] OnEnterReturning +-- @param #AI_AIR self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. @@ -217,30 +214,30 @@ function AI_A2A:New( AIGroup ) self:AddTransition( "Patrolling", "Refuel", "Refuelling" ) - --- Refuel Handler OnBefore for AI_A2A - -- @function [parent=#AI_A2A] OnBeforeRefuel - -- @param #AI_A2A self + --- Refuel Handler OnBefore for AI_AIR + -- @function [parent=#AI_AIR] OnBeforeRefuel + -- @param #AI_AIR self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From -- @param #string Event -- @param #string To -- @return #boolean - --- Refuel Handler OnAfter for AI_A2A - -- @function [parent=#AI_A2A] OnAfterRefuel - -- @param #AI_A2A self + --- Refuel Handler OnAfter for AI_AIR + -- @function [parent=#AI_AIR] OnAfterRefuel + -- @param #AI_AIR self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From -- @param #string Event -- @param #string To - --- Refuel Trigger for AI_A2A - -- @function [parent=#AI_A2A] Refuel - -- @param #AI_A2A self + --- Refuel Trigger for AI_AIR + -- @function [parent=#AI_AIR] Refuel + -- @param #AI_AIR self - --- Refuel Asynchronous Trigger for AI_A2A - -- @function [parent=#AI_A2A] __Refuel - -- @param #AI_A2A self + --- Refuel Asynchronous Trigger for AI_AIR + -- @function [parent=#AI_AIR] __Refuel + -- @param #AI_AIR self -- @param #number Delay self:AddTransition( "*", "Takeoff", "Airborne" ) @@ -266,15 +263,17 @@ function GROUP:OnEventTakeoff( EventData, Fsm ) self:UnHandleEvent( EVENTS.Takeoff ) end -function AI_A2A:SetDispatcher( Dispatcher ) + + +function AI_AIR:SetDispatcher( Dispatcher ) self.Dispatcher = Dispatcher end -function AI_A2A:GetDispatcher() +function AI_AIR:GetDispatcher() return self.Dispatcher end -function AI_A2A:SetTargetDistance( Coordinate ) +function AI_AIR:SetTargetDistance( Coordinate ) local CurrentCoord = self.Controllable:GetCoordinate() self.TargetDistance = CurrentCoord:Get2DDistance( Coordinate ) @@ -283,7 +282,7 @@ function AI_A2A:SetTargetDistance( Coordinate ) end -function AI_A2A:ClearTargetDistance() +function AI_AIR:ClearTargetDistance() self.TargetDistance = nil self.ClosestTargetDistance = nil @@ -291,11 +290,11 @@ end --- Sets (modifies) the minimum and maximum speed of the patrol. --- @param #AI_A2A self +-- @param #AI_AIR self -- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. -- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. --- @return #AI_A2A self -function AI_A2A:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) +-- @return #AI_AIR self +function AI_AIR:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) self.PatrolMinSpeed = PatrolMinSpeed @@ -303,12 +302,25 @@ function AI_A2A:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) end +--- Sets (modifies) the minimum and maximum RTB speed of the patrol. +-- @param #AI_AIR self +-- @param DCS#Speed RTBMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed RTBMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @return #AI_AIR self +function AI_AIR:SetRTBSpeed( RTBMinSpeed, RTBMaxSpeed ) + self:F( { RTBMinSpeed, RTBMaxSpeed } ) + + self.RTBMinSpeed = RTBMinSpeed + self.RTBMaxSpeed = RTBMaxSpeed +end + + --- Sets the floor and ceiling altitude of the patrol. --- @param #AI_A2A self +-- @param #AI_AIR self -- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. -- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @return #AI_A2A self -function AI_A2A:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) +-- @return #AI_AIR self +function AI_AIR:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) self.PatrolFloorAltitude = PatrolFloorAltitude @@ -317,20 +329,20 @@ end --- Sets the home airbase. --- @param #AI_A2A self +-- @param #AI_AIR self -- @param Wrapper.Airbase#AIRBASE HomeAirbase --- @return #AI_A2A self -function AI_A2A:SetHomeAirbase( HomeAirbase ) +-- @return #AI_AIR self +function AI_AIR:SetHomeAirbase( HomeAirbase ) self:F2( { HomeAirbase } ) self.HomeAirbase = HomeAirbase end --- Sets to refuel at the given tanker. --- @param #AI_A2A self +-- @param #AI_AIR self -- @param Wrapper.Group#GROUP TankerName The group name of the tanker as defined within the Mission Editor or spawned. --- @return #AI_A2A self -function AI_A2A:SetTanker( TankerName ) +-- @return #AI_AIR self +function AI_AIR:SetTanker( TankerName ) self:F2( { TankerName } ) self.TankerName = TankerName @@ -338,19 +350,19 @@ end --- Sets the disengage range, that when engaging a target beyond the specified range, the engagement will be cancelled and the plane will RTB. --- @param #AI_A2A self +-- @param #AI_AIR self -- @param #number DisengageRadius The disengage range. --- @return #AI_A2A self -function AI_A2A:SetDisengageRadius( DisengageRadius ) +-- @return #AI_AIR self +function AI_AIR:SetDisengageRadius( DisengageRadius ) self:F2( { DisengageRadius } ) self.DisengageRadius = DisengageRadius end --- Set the status checking off. --- @param #AI_A2A self --- @return #AI_A2A self -function AI_A2A:SetStatusOff() +-- @param #AI_AIR self +-- @return #AI_AIR self +function AI_AIR:SetStatusOff() self:F2() self.CheckStatus = false @@ -359,16 +371,16 @@ end --- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. -- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. --- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_A2A. +-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_AIR. -- Once the time is finished, the old AI will return to the base. --- @param #AI_A2A self --- @param #number PatrolFuelThresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. --- @param #number PatrolOutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. --- @return #AI_A2A self -function AI_A2A:SetFuelThreshold( PatrolFuelThresholdPercentage, PatrolOutOfFuelOrbitTime ) +-- @param #AI_AIR self +-- @param #number FuelThresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. +-- @param #number OutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. +-- @return #AI_AIR self +function AI_AIR:SetFuelThreshold( FuelThresholdPercentage, OutOfFuelOrbitTime ) - self.PatrolFuelThresholdPercentage = PatrolFuelThresholdPercentage - self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime + self.FuelThresholdPercentage = FuelThresholdPercentage + self.OutOfFuelOrbitTime = OutOfFuelOrbitTime self.Controllable:OptionRTBBingoFuel( false ) @@ -381,10 +393,10 @@ end -- the AI will return immediately to the home base (RTB). -- Note that for groups, the average damage of the complete group will be calculated. -- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. --- @param #AI_A2A self +-- @param #AI_AIR self -- @param #number PatrolDamageThreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. --- @return #AI_A2A self -function AI_A2A:SetDamageThreshold( PatrolDamageThreshold ) +-- @return #AI_AIR self +function AI_AIR:SetDamageThreshold( PatrolDamageThreshold ) self.PatrolManageDamage = true self.PatrolDamageThreshold = PatrolDamageThreshold @@ -392,14 +404,16 @@ function AI_A2A:SetDamageThreshold( PatrolDamageThreshold ) return self end + + --- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. --- @param #AI_A2A self --- @return #AI_A2A self +-- @param #AI_AIR self +-- @return #AI_AIR self -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. -function AI_A2A:onafterStart( Controllable, From, Event, To ) +function AI_AIR:onafterStart( Controllable, From, Event, To ) self:__Status( 10 ) -- Check status status every 30 seconds. @@ -411,16 +425,27 @@ function AI_A2A:onafterStart( Controllable, From, Event, To ) Controllable:OptionROTVertical() end +--- Coordinates the approriate returning action. +-- @param #AI_AIR self +-- @return #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR:onafterReturn( Controllable, From, Event, To ) + self:__RTB( self.TaskDelay ) + +end ---- @param #AI_A2A self -function AI_A2A:onbeforeStatus() +--- @param #AI_AIR self +function AI_AIR:onbeforeStatus() return self.CheckStatus end ---- @param #AI_A2A self -function AI_A2A:onafterStatus() +--- @param #AI_AIR self +function AI_AIR:onafterStatus() if self.Controllable and self.Controllable:IsAlive() then @@ -430,10 +455,9 @@ function AI_A2A:onafterStatus() if not self:Is( "Holding" ) and not self:Is( "Returning" ) then local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) - self:F({DistanceFromHomeBase=DistanceFromHomeBase}) if DistanceFromHomeBase > self.DisengageRadius then - self:E( self.Controllable:GetName() .. " is too far from home base, RTB!" ) + self:I( self.Controllable:GetName() .. " is too far from home base, RTB!" ) self:Hold( 300 ) RTB = false end @@ -448,19 +472,23 @@ function AI_A2A:onafterStatus() -- end - if not self:Is( "Fuel" ) and not self:Is( "Home" ) then + if not self:Is( "Fuel" ) and not self:Is( "Home" ) and not self:is( "Refuelling" )then + local Fuel = self.Controllable:GetFuelMin() - self:F({Fuel=Fuel, PatrolFuelThresholdPercentage=self.PatrolFuelThresholdPercentage}) - if Fuel < self.PatrolFuelThresholdPercentage then + + -- If the fuel in the controllable is below the treshold percentage, + -- then send for refuel in case of a tanker, otherwise RTB. + if Fuel < self.FuelThresholdPercentage then + if self.TankerName then - self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) + self:I( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) self:Refuel() else - self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... RTB!" ) + self:I( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... RTB!" ) local OldAIControllable = self.Controllable local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) - local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.PatrolOutOfFuelOrbitTime,nil ) ) + local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.OutOfFuelOrbitTime,nil ) ) OldAIControllable:SetTask( TimedOrbitTask, 10 ) self:Fuel() @@ -469,18 +497,25 @@ function AI_A2A:onafterStatus() else end end + + if self:Is( "Fuel" ) and not self:Is( "Home" ) and not self:is( "Refuelling" ) then + RTB = true + end -- TODO: Check GROUP damage function. local Damage = self.Controllable:GetLife() local InitialLife = self.Controllable:GetLife0() - self:F( { Damage = Damage, InitialLife = InitialLife, DamageThreshold = self.PatrolDamageThreshold } ) + + -- If the group is damaged, then RTB. + -- Note that a group can consist of more units, so if one unit is damaged of a group, the mission may continue. + -- The damaged unit will RTB due to DCS logic, and the others will continue to engage. if ( Damage / InitialLife ) < self.PatrolDamageThreshold then - self:E( self.Controllable:GetName() .. " is damaged: " .. Damage .. " ... RTB!" ) + self:I( self.Controllable:GetName() .. " is damaged: " .. Damage .. " ... RTB!" ) self:Damaged() RTB = true self:SetStatusOff() end - + -- Check if planes went RTB and are out of control. -- We only check if planes are out of control, when they are in duty. if self.Controllable:HasTask() == false then @@ -489,11 +524,12 @@ function AI_A2A:onafterStatus() not self:Is( "Fuel" ) and not self:Is( "Damaged" ) and not self:Is( "Home" ) then - if self.IdleCount >= 2 then + if self.IdleCount >= 10 then if Damage ~= InitialLife then self:Damaged() else - self:E( self.Controllable:GetName() .. " control lost! " ) + self:I( self.Controllable:GetName() .. " control lost! " ) + self:LostControl() end else @@ -505,7 +541,7 @@ function AI_A2A:onafterStatus() end if RTB == true then - self:__RTB( 0.5 ) + self:__RTB( self.TaskDelay ) end if not self:Is("Home") then @@ -517,22 +553,22 @@ end --- @param Wrapper.Group#GROUP AIGroup -function AI_A2A.RTBRoute( AIGroup, Fsm ) +function AI_AIR.RTBRoute( AIGroup, Fsm ) - AIGroup:F( { "AI_A2A.RTBRoute:", AIGroup:GetName() } ) + AIGroup:F( { "AI_AIR.RTBRoute:", AIGroup:GetName() } ) if AIGroup:IsAlive() then - Fsm:__RTB( 0.5 ) + Fsm:RTB() end end --- @param Wrapper.Group#GROUP AIGroup -function AI_A2A.RTBHold( AIGroup, Fsm ) +function AI_AIR.RTBHold( AIGroup, Fsm ) - AIGroup:F( { "AI_A2A.RTBHold:", AIGroup:GetName() } ) + AIGroup:F( { "AI_AIR.RTBHold:", AIGroup:GetName() } ) if AIGroup:IsAlive() then - Fsm:__RTB( 0.5 ) + Fsm:__RTB( Fsm.TaskDelay ) Fsm:Return() local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) AIGroup:SetTask( Task ) @@ -541,74 +577,92 @@ function AI_A2A.RTBHold( AIGroup, Fsm ) end ---- @param #AI_A2A self +--- @param #AI_AIR self -- @param Wrapper.Group#GROUP AIGroup -function AI_A2A:onafterRTB( AIGroup, From, Event, To ) +function AI_AIR:onafterRTB( AIGroup, From, Event, To ) self:F( { AIGroup, From, Event, To } ) if AIGroup and AIGroup:IsAlive() then - self:E( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) + self:I( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) self:ClearTargetDistance() - AIGroup:ClearTasks() + --AIGroup:ClearTasks() local EngageRoute = {} --- Calculate the target route point. - local CurrentCoord = AIGroup:GetCoordinate() + local FromCoord = AIGroup:GetCoordinate() local ToTargetCoord = self.HomeAirbase:GetCoordinate() - local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) - local ToAirbaseAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) - local Distance = CurrentCoord:Get2DDistance( ToTargetCoord ) + if not self.RTBMinSpeed and not self.RTBMaxSpeed then + local RTBSpeedMax = AIGroup:GetSpeedMax() + self:SetRTBSpeed( RTBSpeedMax * 0.25, RTBSpeedMax * 0.25 ) + end - local ToAirbaseCoord = CurrentCoord:Translate( 5000, ToAirbaseAngle ) + local RTBSpeed = math.random( self.RTBMinSpeed, self.RTBMaxSpeed ) + local ToAirbaseAngle = FromCoord:GetAngleDegrees( FromCoord:GetDirectionVec3( ToTargetCoord ) ) + + local Distance = FromCoord:Get2DDistance( ToTargetCoord ) + + local ToAirbaseCoord = FromCoord:Translate( 5000, ToAirbaseAngle ) if Distance < 5000 then - self:E( "RTB and near the airbase!" ) + self:I( "RTB and near the airbase!" ) self:Home() return end + + if not AIGroup:InAir() == true then + self:I( "Not anymore in the air, considered Home." ) + self:Home() + return + end + + + --- Create a route point of type air. + local FromRTBRoutePoint = FromCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + RTBSpeed, + true + ) + --- Create a route point of type air. local ToRTBRoutePoint = ToAirbaseCoord:WaypointAir( self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, - ToTargetSpeed, + RTBSpeed, true ) - self:F( { Angle = ToAirbaseAngle, ToTargetSpeed = ToTargetSpeed } ) - self:T2( { self.MinSpeed, self.MaxSpeed, ToTargetSpeed } ) - - EngageRoute[#EngageRoute+1] = ToRTBRoutePoint + EngageRoute[#EngageRoute+1] = FromRTBRoutePoint EngageRoute[#EngageRoute+1] = ToRTBRoutePoint + local Tasks = {} + Tasks[#Tasks+1] = AIGroup:TaskFunction( "AI_AIR.RTBRoute", self ) + + EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks ) + AIGroup:OptionROEHoldFire() AIGroup:OptionROTEvadeFire() - --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... - AIGroup:WayPointInitialize( EngageRoute ) - - local Tasks = {} - Tasks[#Tasks+1] = AIGroup:TaskFunction( "AI_A2A.RTBRoute", self ) - EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks ) - --- NOW ROUTE THE GROUP! - AIGroup:Route( EngageRoute, 0.5 ) + AIGroup:Route( EngageRoute, self.TaskDelay ) end end ---- @param #AI_A2A self +--- @param #AI_AIR self -- @param Wrapper.Group#GROUP AIGroup -function AI_A2A:onafterHome( AIGroup, From, Event, To ) +function AI_AIR:onafterHome( AIGroup, From, Event, To ) self:F( { AIGroup, From, Event, To } ) - self:E( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) + self:I( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) if AIGroup and AIGroup:IsAlive() then end @@ -617,22 +671,22 @@ end ---- @param #AI_A2A self +--- @param #AI_AIR self -- @param Wrapper.Group#GROUP AIGroup -function AI_A2A:onafterHold( AIGroup, From, Event, To, HoldTime ) +function AI_AIR:onafterHold( AIGroup, From, Event, To, HoldTime ) self:F( { AIGroup, From, Event, To } ) - self:E( "Group " .. self.Controllable:GetName() .. " ... Holding! ( " .. self:GetState() .. " )" ) + self:I( "Group " .. self.Controllable:GetName() .. " ... Holding! ( " .. self:GetState() .. " )" ) if AIGroup and AIGroup:IsAlive() then local OrbitTask = AIGroup:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) local TimedOrbitTask = AIGroup:TaskControlled( OrbitTask, AIGroup:TaskCondition( nil, nil, nil, nil, HoldTime , nil ) ) - local RTBTask = AIGroup:TaskFunction( "AI_A2A.RTBHold", self ) + local RTBTask = AIGroup:TaskFunction( "AI_AIR.RTBHold", self ) local OrbitHoldTask = AIGroup:TaskOrbitCircle( 4000, self.PatrolMinSpeed ) - --AIGroup:SetState( AIGroup, "AI_A2A", self ) + --AIGroup:SetState( AIGroup, "AI_AIR", self ) AIGroup:SetTask( AIGroup:TaskCombo( { TimedOrbitTask, RTBTask, OrbitHoldTask } ), 1 ) end @@ -640,98 +694,112 @@ function AI_A2A:onafterHold( AIGroup, From, Event, To, HoldTime ) end --- @param Wrapper.Group#GROUP AIGroup -function AI_A2A.Resume( AIGroup, Fsm ) +function AI_AIR.Resume( AIGroup, Fsm ) - AIGroup:I( { "AI_A2A.Resume:", AIGroup:GetName() } ) + AIGroup:I( { "AI_AIR.Resume:", AIGroup:GetName() } ) if AIGroup:IsAlive() then - Fsm:__RTB( 0.5 ) + Fsm:__RTB( Fsm.TaskDelay ) end end ---- @param #AI_A2A self +--- @param #AI_AIR self -- @param Wrapper.Group#GROUP AIGroup -function AI_A2A:onafterRefuel( AIGroup, From, Event, To ) +function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) self:F( { AIGroup, From, Event, To } ) - self:E( "Group " .. self.Controllable:GetName() .. " ... Refuelling! ( " .. self:GetState() .. " )" ) - if AIGroup and AIGroup:IsAlive() then + + -- Get tanker group. local Tanker = GROUP:FindByName( self.TankerName ) - if Tanker:IsAlive() and Tanker:IsAirPlane() then + + if Tanker and Tanker:IsAlive() and Tanker:IsAirPlane() then + + self:I( "Group " .. self.Controllable:GetName() .. " ... Refuelling! State=" .. self:GetState() .. ", Refuelling tanker " .. self.TankerName ) local RefuelRoute = {} --- Calculate the target route point. - local CurrentCoord = AIGroup:GetCoordinate() + local FromRefuelCoord = AIGroup:GetCoordinate() local ToRefuelCoord = Tanker:GetCoordinate() local ToRefuelSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) --- Create a route point of type air. - local ToRefuelRoutePoint = ToRefuelCoord:WaypointAir( - self.PatrolAltType, - POINT_VEC3.RoutePointType.TurningPoint, - POINT_VEC3.RoutePointAction.TurningPoint, - ToRefuelSpeed, - true - ) + local FromRefuelRoutePoint = FromRefuelCoord:WaypointAir(self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToRefuelSpeed, true) + + --- Create a route point of type air. NOT used! + local ToRefuelRoutePoint = Tanker:GetCoordinate():WaypointAir(self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToRefuelSpeed, true) self:F( { ToRefuelSpeed = ToRefuelSpeed } ) - RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint + RefuelRoute[#RefuelRoute+1] = FromRefuelRoutePoint RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint AIGroup:OptionROEHoldFire() AIGroup:OptionROTEvadeFire() + + -- Get Class name for .Resume function + local classname=self:GetClassName() + + -- AI_A2A_CAP can call this function but does not have a .Resume function. Try to fix. + if classname=="AI_A2A_CAP" then + classname="AI_AIR_PATROL" + end + + env.info("FF refueling classname="..classname) local Tasks = {} Tasks[#Tasks+1] = AIGroup:TaskRefueling() - Tasks[#Tasks+1] = AIGroup:TaskFunction( self:GetClassName() .. ".Resume", self ) + Tasks[#Tasks+1] = AIGroup:TaskFunction( classname .. ".Resume", self ) RefuelRoute[#RefuelRoute].task = AIGroup:TaskCombo( Tasks ) - AIGroup:Route( RefuelRoute, 0.5 ) + AIGroup:Route( RefuelRoute, self.TaskDelay ) + else + + -- No tanker defined ==> RTB! self:RTB() + end + end end ---- @param #AI_A2A self -function AI_A2A:onafterDead() +--- @param #AI_AIR self +function AI_AIR:onafterDead() self:SetStatusOff() end ---- @param #AI_A2A self +--- @param #AI_AIR self -- @param Core.Event#EVENTDATA EventData -function AI_A2A:OnCrash( EventData ) +function AI_AIR:OnCrash( EventData ) if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:E( self.Controllable:GetUnits() ) if #self.Controllable:GetUnits() == 1 then - self:__Crash( 1, EventData ) + self:__Crash( self.TaskDelay, EventData ) end end end ---- @param #AI_A2A self +--- @param #AI_AIR self -- @param Core.Event#EVENTDATA EventData -function AI_A2A:OnEjection( EventData ) +function AI_AIR:OnEjection( EventData ) if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__Eject( 1, EventData ) + self:__Eject( self.TaskDelay, EventData ) end end ---- @param #AI_A2A self +--- @param #AI_AIR self -- @param Core.Event#EVENTDATA EventData -function AI_A2A:OnPilotDead( EventData ) +function AI_AIR:OnPilotDead( EventData ) if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:__PilotDead( 1, EventData ) + self:__PilotDead( self.TaskDelay, EventData ) end end diff --git a/Moose Development/Moose/AI/AI_Air_Dispatcher.lua b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua new file mode 100644 index 000000000..7565f7a64 --- /dev/null +++ b/Moose Development/Moose/AI/AI_Air_Dispatcher.lua @@ -0,0 +1,3248 @@ +--- **AI** - Create an automated AIR defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. +-- +-- === +-- +-- Features: +-- +-- * Setup quickly an AIR defense system for a coalition. +-- * Setup multiple defense zones to defend specific coordinates in your battlefield. +-- * Setup (SEAD) Suppression of Air Defense squadrons, to gain control in the air of enemy grounds. +-- * Setup (CAS) Controlled Air Support squadrons, to attack closeby enemy ground units near friendly installations. +-- * Setup (BAI) Battleground Air Interdiction squadrons to attack remote enemy ground units and targets. +-- * Define and use a detection network controlled by recce. +-- * Define AIR defense squadrons at airbases, farps and carriers. +-- * Enable airbases for AIR defenses. +-- * Add different planes and helicopter templates to squadrons. +-- * Assign squadrons to execute a specific engagement type depending on threat level of the detected ground enemy unit composition. +-- * Add multiple squadrons to different airbases, farps or carriers. +-- * Define different ranges to engage upon. +-- * Establish an automatic in air refuel process for planes using refuel tankers. +-- * Setup default settings for all squadrons and AIR defenses. +-- * Setup specific settings for specific squadrons. +-- +-- === +-- +-- ## Missions: +-- +-- [AID-AIR - AI AIR Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-AIR%20-%20AI%20AIR%20Dispatching) +-- +-- === +-- +-- ## YouTube Channel: +-- +-- [DCS WORLD - MOOSE - AIR GCICAP - Build an automatic AIR Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) +-- +-- === +-- +-- # QUICK START GUIDE +-- +-- The following class is available to model an AIR defense system. +-- +-- AI_AIR_DISPATCHER is the main AIR defense class that models the AIR defense system. +-- +-- Before you start using the AI_AIR_DISPATCHER, ask youself the following questions. +-- +-- +-- ## 1. Which coalition am I modeling an AIR defense system for? blue or red? +-- +-- One AI_AIR_DISPATCHER object can create a defense system for **one coalition**, which is blue or red. +-- If you want to create a **mutual defense system**, for both blue and red, then you need to create **two** AI_AIR_DISPATCHER **objects**, +-- each governing their defense system for one coalition. +-- +-- +-- ## 2. Which type of detection will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow). +-- +-- The MOOSE framework leverages the @{Functional.Detection} classes to perform the reconnaissance, detecting enemy units +-- and reporting them to the head quarters. +-- Several types of @{Functional.Detection} classes exist, and the most common characteristics of these classes is that they: +-- +-- * Perform detections from multiple recce as one co-operating entity. +-- * Communicate with a @{Tasking.CommandCenter}, which consolidates each detection. +-- * Groups detections based on a method (per area, per type or per unit). +-- * Communicates detections. +-- +-- +-- ## 3. Which recce units can be used as part of the detection system? Only ground based, or also airborne? +-- +-- Depending on the type of mission you want to achieve, different types of units can be engaged to perform ground enemy targets reconnaissance. +-- Ground recce (FAC) are very useful units to determine the position of enemy ground targets when they spread out over the battlefield at strategic positions. +-- Using their varying detection technology, and especially those ground units which have spotting technology, can be extremely effective at +-- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. +-- Unfortunately, they lack sometimes the visibility to detect targets at greater range, or when scenery is preventing line of sight. +-- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then +-- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! +-- +-- Airborne recce (AFAC) are also very effective. The are capable of patrolling at a functional detection altitude, +-- having an overview of the whole battlefield. However, airborne recce can be vulnerable to air to ground attacks, +-- so you need air superiority to make them effective. +-- Airborne recce will also have varying ground detection technology, which plays a big role in the effectiveness of the reconnaissance. +-- Certain helicopter or plane types have ground searching radars or advanced ground scanning technology, and are very effective +-- compared to air units having only visual detection capabilities. +-- For example, for the red coalition, the Mi-28N and the Su-34; and for the blue side, the reaper, are such effective airborne recce units. +-- +-- Typically, don't want these recce units to engage with the enemy, you want to keep them at position. Therefore, it is a good practice +-- to set the ROE for these recce to hold weapons, and make them invisible from the enemy. +-- +-- It is not possible to perform a recce function as a player (unit). +-- +-- +-- ## 4. How do the defenses decide **when and where to engage** on approaching enemy units? +-- +-- The AIR dispacher needs you to setup (various) defense coordinates, which are strategic positions in the battle field to be defended. +-- Any ground based enemy approaching within the proximity of such a defense point, may trigger for a defensive action by friendly air units. +-- +-- There are 2 important parameters that play a role in the defensive decision making: defensiveness and reactivity. +-- +-- The AIR dispatcher provides various parameters to setup the **defensiveness**, +-- which models the decision **when** a defender will engage with the approaching enemy. +-- Defensiveness is calculated by a probability distribution model when to trigger a defense action, +-- depending on the distance of the enemy unit from the defense coordinates, and a **defensiveness factor**. +-- +-- The other parameter considered for defensive action is **where the enemy is located**, thus the distance from a defense coordinate, +-- which we call the **reactive distance**. By default, the reactive distance is set to 60km, but can be changed by the mission designer +-- using the available method explained further below. +-- The combination of the defensiveness and reactivity results in a model that, the closer the attacker is to the defense point, +-- the higher the probability will be that a defense action will be launched! +-- +-- +-- ## 5. Are defense coordinates and defense reactivity the only parameters? +-- +-- No, depending on the target type, and the threat level of the target, the probability of defense will be higher. +-- In other words, when a SAM-10 radar emitter is detected, its probabilty for defense will be much higher than when a BMP-1 vehicle is +-- detected, even when both enemies are at the same distance from a defense coordinate. +-- This will ensure optimal defenses, SEAD tasks will be launched much more quicker against engaging radar emitters, to ensure air superiority. +-- Approaching main battle tanks will be engaged much faster, than a group of approaching trucks. +-- +-- +-- ## 6. Which Squadrons will I create and which name will I give each Squadron? +-- +-- The AIR defense system works with **Squadrons**. Each Squadron must be given a unique name, that forms the **key** to the squadron. +-- Several options and activities can be set per Squadron. A free format name can be given, but always ensure that the name is meaningfull +-- for your mission, and remember that squadron names are used for communication to the players of your mission. +-- +-- There are mainly 3 types of defenses: **SEAD**, **CAS** and **BAI**. +-- +-- Suppression of Air Defenses (SEAD) are effective agains radar emitters. Close Air Support (CAS) is launched when the enemy is close near friendly units. +-- Battleground Air Interdiction (BAI) tasks are launched when there are no friendlies around. +-- +-- Depending on the defense type, different payloads will be needed. See further points on squadron definition. +-- +-- +-- ## 7. Where will the Squadrons be located? On Airbases? On Carrier Ships? On Farps? +-- +-- Squadrons are placed at the **home base** on an **airfield**, **carrier** or **farp**. +-- Carefully plan where each Squadron will be located as part of the defense system required for mission effective defenses. +-- If the home base of the squadron is too far from assumed enemy positions, then the defenses will be too late. +-- The home bases must be **behind** enemy lines, you want to prevent your home bases to be engaged by enemies! +-- Depending on the units applied for defenses, the home base can be further or closer to the enemies. +-- Any airbase, farp or carrier can act as the launching platform for AIR defenses. +-- Carefully plan which airbases will take part in the coalition. Color each airbase **in the color of the coalition**, using the mission editor, +-- or your air units will not return for landing at the airbase! +-- +-- +-- ## 8. Which helicopter or plane models will I assign for each Squadron? Do I need one plane model or more plane models per squadron? +-- +-- Per Squadron, one or multiple helicopter or plane models can be allocated as **Templates**. +-- These are late activated groups with one airplane or helicopter that start with a specific name, called the **template prefix**. +-- The AIR defense system will select from the given templates a random template to spawn a new plane (group). +-- +-- A squadron will perform specific task types (SEAD, CAS or BAI). So, squadrons will require specific templates for the +-- task types it will perform. A squadron executing SEAD defenses, will require a payload with long range anti-radar seeking missiles. +-- +-- +-- ## 9. Which payloads, skills and skins will these plane models have? +-- +-- Per Squadron, even if you have one plane model, you can still allocate multiple templates of one plane model, +-- each having different payloads, skills and skins. +-- The AIR defense system will select from the given templates a random template to spawn a new plane (group). +-- +-- +-- ## 10. How to squadrons engage in a defensive action? +-- +-- There are two ways how squadrons engage and execute your AIR defenses. +-- Squadrons can start the defense directly from the airbase, farp or carrier. When a squadron launches a defensive group, that group +-- will start directly from the airbase. The other way is to launch early on in the mission a patrolling mechanism. +-- Squadrons will launch air units to patrol in specific zone(s), so that when ground enemy targets are detected, that the airborne +-- AIR defenses can come immediately into action. +-- +-- +-- ## 11. For each Squadron doing a patrol, which zone types will I create? +-- +-- Per zone, evaluate whether you want: +-- +-- * simple trigger zones +-- * polygon zones +-- * moving zones +-- +-- Depending on the type of zone selected, a different @{Zone} object needs to be created from a ZONE_ class. +-- +-- +-- ## 12. Are moving defense coordinates possible? +-- +-- Yes, different COORDINATE types are possible to be used. +-- The COORDINATE_UNIT will help you to specify a defense coodinate that is attached to a moving unit. +-- +-- +-- ## 13. How much defense coordinates do I need to create? +-- +-- It depends, but the idea is to define only the necessary defense points that drive your mission. +-- If you define too much defense points, the performance of your mission may decrease. Per defense point defined, +-- all the possible enemies are evaluated. Note that each defense coordinate has a reach depending on the size of the defense radius. +-- The default defense radius is about 60km, and depending on the defense reactivity, defenses will be launched when the enemy is at +-- close or greater distance from the defense coordinate. +-- +-- +-- ## 14. For each Squadron doing patrols, what are the time intervals and patrol amounts to be performed? +-- +-- For each patrol: +-- +-- * **How many** patrol you want to have airborne at the same time? +-- * **How frequent** you want the defense mechanism to check whether to start a new patrol? +-- +-- other considerations: +-- +-- * **How far** is the patrol area from the engagement "hot zone". You want to ensure that the enemy is reached on time! +-- * **How safe** is the patrol area taking into account air superiority. Is it well defended, are there nearby A2A bases? +-- +-- +-- ## 15. For each Squadron, which takeoff method will I use? +-- +-- For each Squadron, evaluate which takeoff method will be used: +-- +-- * Straight from the air +-- * From the runway +-- * From a parking spot with running engines +-- * From a parking spot with cold engines +-- +-- **The default takeoff method is staight in the air.** +-- This takeoff method is the most useful if you want to avoid airplane clutter at airbases! +-- But it is the least realistic one! +-- +-- +-- ## 16. For each Squadron, which landing method will I use? +-- +-- For each Squadron, evaluate which landing method will be used: +-- +-- * Despawn near the airbase when returning +-- * Despawn after landing on the runway +-- * Despawn after engine shutdown after landing +-- +-- **The default landing method is despawn when near the airbase when returning.** +-- This landing method is the most useful if you want to avoid airplane clutter at airbases! +-- But it is the least realistic one! +-- +-- +-- ## 19. For each Squadron, which **defense overhead** will I use? +-- +-- For each Squadron, depending on the helicopter or airplane type (modern, old) and payload, which overhead is required to provide any defense? +-- +-- In other words, if **X** enemy ground units are detected, how many **Y** defense helicpters or airplanes need to engage (per squadron)? +-- The **Y** is dependent on the type of airplane (era), payload, fuel levels, skills etc. +-- But the most important factor is the payload, which is the amount of AIR weapons the defense can carry to attack the enemy ground units. +-- For example, a Ka-50 can carry 16 vikrs, that means, that it potentially can destroy at least 8 ground units without a reload of ammunication. +-- That means, that one defender can destroy more enemy ground units. +-- Thus, the overhead is a **factor** that will calculate dynamically how many **Y** defenses will be required based on **X** attackers detected. +-- +-- **The default overhead is 1. A smaller value than 1, like 0.25 will decrease the overhead to a 1 / 4 ratio, meaning, +-- one defender for each 4 detected ground enemy units. ** +-- +-- +-- ## 19. For each Squadron, which grouping will I use? +-- +-- When multiple targets are detected, how will defenses be grouped when multiple defense air units are spawned for multiple enemy ground units? +-- Per one, two, three, four? +-- +-- **The default grouping is 1. That means, that each spawned defender will act individually.** +-- But you can specify a number between 1 and 4, so that the defenders will act as a group. +-- +-- === +-- +-- ### Author: **FlightControl** rework of GCICAP + introduction of new concepts (squadrons). +-- +-- @module AI.AI_AIR_Dispatcher +-- @image AI_Air_To_Ground_Dispatching.JPG + + + +do -- AI_AIR_DISPATCHER + + --- AI_AIR_DISPATCHER class. + -- @type AI_AIR_DISPATCHER + -- @extends Tasking.DetectionManager#DETECTION_MANAGER + + --- Create an automated AIR defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAP operations. + -- + -- === + -- + -- When your mission is in the need to take control of the AI to automate and setup a process of air to ground defenses, this is the module you need. + -- The defense system work through the definition of defense coordinates, which are points in your friendly area within the battle field, that your mission need to have defended. + -- Multiple defense coordinates can be setup. Defense coordinates can be strategic or tactical positions or references to strategic units or scenery. + -- The AIR dispatcher will evaluate every x seconds the tactical situation around each defense coordinate. When a defense coordinate + -- is under threat, it will communicate through the command center that defensive actions need to be taken and will launch groups of air units for defense. + -- The level of threat to the defense coordinate varyies upon the strength and types of the enemy units, the distance to the defense point, and the defensiveness parameters. + -- Defensive actions are taken through probability, but the closer and the more threat the enemy poses to the defense coordinate, the faster it will be attacked by friendly AIR units. + -- + -- Please study carefully the underlying explanations how to setup and use this module, as it has many features. + -- It also requires a little study to ensure that you get a good understanding of the defense mechanisms, to ensure a strong + -- defense for your missions. + -- + -- === + -- + -- # USAGE GUIDE + -- + -- ## 1. AI\_AIR\_DISPATCHER constructor: + -- + -- ![Banner Image](..\Presentations\AI_AIR_DISPATCHER\AI_AIR_DISPATCHER-ME_1.JPG) + -- + -- + -- The @{#AI_AIR_DISPATCHER.New}() method creates a new AI_AIR_DISPATCHER instance. + -- + -- ### 1.1. Define the **reconnaissance network**: + -- + -- As part of the AI_AIR_DISPATCHER :New() constructor, a reconnaissance network must be given as the first parameter. + -- A reconnaissance network is provided by passing a @{Functional.Detection} object. + -- The most effective reconnaissance for the AIR dispatcher would be to use the @{Functional.Detection#DETECTION_AREAS} object. + -- + -- A reconnaissance network, is used to detect enemy ground targets, + -- potentially group them into areas, and to understand the position, level of threat of the enemy. + -- + -- ![Banner Image](..\Presentations\AI_AIR_DISPATCHER\Dia5.JPG) + -- + -- As explained in the introduction, depending on the type of mission you want to achieve, different types of units can be applied to detect ground enemy targets. + -- Ground based units are very useful to act as a reconnaissance, but they lack sometimes the visibility to detect targets at greater range. + -- Recce are very useful to acquire the position of enemy ground targets when spread out over the battlefield at strategic positions. + -- Ground units also have varying detectors, and especially the ground units which have laser guiding missiles can be extremely effective at + -- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective. + -- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then + -- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully! + -- + -- Beside ground level units to use for reconnaissance, air units are also very effective. The are capable of patrolling at great speed + -- covering a large terrain. However, airborne recce can be vulnerable to air to ground attacks, and you need air superiority to make then + -- effective. Also the instruments available at the air units play a big role in the effectiveness of the reconnaissance. + -- Air units which have ground detection capabilities will be much more effective than air units with only visual detection capabilities. + -- For the red coalition, the Mi-28N and for the blue side, the reaper are such effective reconnaissance airborne units. + -- + -- Reconnaissance networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection} instance that is given as the first parameter to the AIR dispatcher. + -- By defining in a **smart way the names or name prefixes of the reconnaissance groups**, these groups will be **automatically added or removed** to or from the reconnaissance network, + -- when these groups are spawned in or destroyed during the ongoing battle. + -- By spawning in dynamically additional recce, you can ensure that there is sufficient reconnaissance coverage so the defense mechanism is continuously + -- alerted of new enemy ground targets. + -- + -- The following example defens a new reconnaissance network using a @{Functional.Detection#DETECTION_AREAS} object. + -- + -- -- Define a SET_GROUP object that builds a collection of groups that define the recce network. + -- -- Here we build the network with all the groups that have a name starting with CCCP Recce. + -- DetectionSetGroup = SET_GROUP:New() -- Defene a set of group objects, caled DetectionSetGroup. + -- + -- DetectionSetGroup:FilterPrefixes( { "CCCP Recce" } ) -- The DetectionSetGroup will search for groups that start with the name "CCCP Recce". + -- + -- -- This command will start the dynamic filtering, so when groups spawn in or are destroyed, + -- -- which have a group name starting with "CCCP Recce", then these will be automatically added or removed from the set. + -- DetectionSetGroup:FilterStart() + -- + -- -- This command defines the reconnaissance network. + -- -- It will group any detected ground enemy targets within a radius of 1km. + -- -- It uses the DetectionSetGroup, which defines the set of reconnaissance groups to detect for enemy ground targets. + -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 1000 ) + -- + -- -- Setup the A2A dispatcher, and initialize it. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- + -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **DetectionSetGroup**. + -- **DetectionSetGroup** is then being configured to filter all active groups with a group name starting with `"CCCP Recce"` to be included in the set. + -- **DetectionSetGroup** is then calling `FilterStart()`, which is starting the dynamic filtering or inclusion of these groups. + -- Note that any destroy or new spawn of a group having a name, starting with the above prefix, will be removed or added to the set. + -- + -- Then a new detection object is created from the class `DETECTION_AREAS`. A grouping radius of 1000 meters (1km) is choosen. + -- + -- The `Detection` object is then passed to the @{#AI_AIR_DISPATCHER.New}() method to indicate the reconnaissance network + -- configuration and setup the AIR defense detection mechanism. + -- + -- ### 1.2. Setup the AIR dispatcher for both a red and blue coalition. + -- + -- Following the above described procedure, you'll need to create for each coalition an separate detection network, and a separate AIR dispatcher. + -- Ensure that while doing so, that you name the objects differently both for red and blue coalition. + -- + -- For example like this for the red coalition: + -- + -- DetectionRed = DETECTION_AREAS:New( DetectionSetGroupRed, 1000 ) + -- AIRDispatcherRed = AI_AIR_DISPATCHER:New( DetectionRed ) + -- + -- And for the blue coalition: + -- + -- DetectionBlue = DETECTION_AREAS:New( DetectionSetGroupBlue, 1000 ) + -- AIRDispatcherBlue = AI_AIR_DISPATCHER:New( DetectionBlue ) + -- + -- + -- Note: Also the SET_GROUP objects should be created for each coalition separately, containing each red and blue recce respectively! + -- + -- ### 1.3. Define the enemy ground target **grouping radius**, in case you use DETECTION_AREAS: + -- + -- The target grouping radius is a property of the DETECTION_AREAS class, that was passed to the AI_AIR_DISPATCHER:New() method, + -- but can be changed. The grouping radius should not be too small, but also depends on the types of ground forces and the way you want your mission to evolve. + -- A large radius will mean large groups of enemy ground targets, while making smaller groups will result in a more fragmented defense system. + -- Typically I suggest a grouping radius of 1km. This is the right balance to create efficient defenses. + -- + -- Note that detected targets are constantly re-grouped, that is, when certain detected enemy ground units are moving further than the group radius, + -- then these units will become a separate area being detected. This may result in additional defenses being started by the dispatcher! + -- So don't make this value too small! Again, I advise about 1km or 1000 meters. + -- + -- ## 2. Setup (a) **Defense Coordinate(s)**. + -- + -- As explained above, defense coordinates are the center of your defense operations. + -- The more threat to the defense coordinate, the higher it is likely a defensive action will be launched. + -- + -- Find below an example how to add defense coordinates: + -- + -- -- Add defense coordinates. + -- AIRDispatcher:AddDefenseCoordinate( "HQ", GROUP:FindByName( "HQ" ):GetCoordinate() ) + -- + -- In this example, the coordinate of a group called `"HQ"` is retrieved, using `:GetCoordinate()` + -- This returns a COORDINATE object, pointing to the first unit within the GROUP object. + -- + -- The method @{#AI_AIR_DISPATCHER.AddDefenseCoordinate}() adds a new defense coordinate to the `AIRDispatcher` object. + -- The first parameter is the key of the defense coordinate, the second the coordinate itself. + -- + -- Later, a COORDINATE_UNIT will be added to the framework, which can be used to assign "moving" coordinates to an AIR dispatcher. + -- + -- **REMEMBER!** + -- + -- - **Defense coordinates are the center of the AIR dispatcher defense system!** + -- - **You can define more defense coordinates to defend a larger area.** + -- - **Detected enemy ground targets are not immediately engaged, but are engaged with a reactivity or probability calculation!** + -- + -- But, there is more to it ... + -- + -- + -- ### 2.1. The **Defense Radius**. + -- + -- The defense radius defines the maximum radius that a defense will be initiated around each defense coordinate. + -- So even when there are targets further away than the defense radius, then these targets won't be engaged upon. + -- By default, the defense radius is set to 100km (100.000 meters), but can be changed using the @{#AI_AIR_DISPATCHER.SetDefenseRadius}() method. + -- Note that the defense radius influences the defense reactivity also! The larger the defense radius, the more reactive the defenses will be. + -- + -- For example: + -- + -- AIRDispatcher:SetDefenseRadius( 30000 ) + -- + -- This defines an AIR dispatcher which will engage on enemy ground targets within 30km radius around the defense coordinate. + -- Note that the defense radius **applies to all defense coordinates** defined within the AIR dispatcher. + -- + -- ### 2.2. The **Defense Reactivity**. + -- + -- There are 5 levels that can be configured to tweak the defense reactivity. As explained above, the threat to a defense coordinate is + -- also determined by the distance of the enemy ground target to the defense coordinate. + -- If you want to have a **low** defense reactivity, that is, the probability that an AIR defense will engage to the enemy ground target, then + -- use the @{#AI_AIR_DISPATCHER.SetDefenseReactivityLow}() method. For medium and high reactivity, use the methods + -- @{#AI_AIR_DISPATCHER.SetDefenseReactivityMedium}() and @{#AI_AIR_DISPATCHER.SetDefenseReactivityHigh}() respectively. + -- + -- Note that the reactivity of defenses is always in relation to the Defense Radius! the shorter the distance, + -- the less reactive the defenses will be in terms of distance to enemy ground targets! + -- + -- For example: + -- + -- AIRDispatcher:SetDefenseReactivityHigh() + -- + -- This defines an AIR dispatcher with high defense reactivity. + -- + -- ## 3. **Squadrons**. + -- + -- The AIR dispatcher works with **Squadrons**, that need to be defined using the different methods available. + -- + -- Use the method @{#AI_AIR_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, farp or carrier, + -- while defining which helicopter or plane **templates** are being used by the squadron and how many **resources** are available. + -- + -- **Multiple squadrons** can be defined within one AIR dispatcher, each having specific defense tasks and defense parameter settings! + -- + -- Squadrons: + -- + -- * Have name (string) that is the identifier or **key** of the squadron. + -- * Have specific helicopter or plane **templates**. + -- * Are located at **one** airbase, farp or carrier. + -- * Optionally have a **limited set of resources**. The default is that squadrons have **unlimited resources**. + -- + -- The name of the squadron given acts as the **squadron key** in all `AIRDispatcher:SetSquadron...()` or `AIRDispatcher:GetSquadron...()` methods. + -- + -- Additionally, squadrons have specific configuration options to: + -- + -- * Control how new helicopters or aircraft are taking off from the airfield, farp or carrier (in the air, cold, hot, at the runway). + -- * Control how returning helicopters or aircraft are landing at the airfield, farp or carrier (in the air near the airbase, after landing, after engine shutdown). + -- * Control the **grouping** of new helicopters or aircraft spawned at the airfield, farp or carrier. If there is more than one helicopter or aircraft to be spawned, these may be grouped. + -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of helicopters, planes, amount of resources and payload (weapon configuration) chosen, + -- the mission designer can choose to increase or reduce the amount of planes spawned. + -- + -- The method @{#AI_AIR_DISPATCHER.SetSquadron}() defines for you a new squadron. + -- The provided parameters are the squadron name, airbase name and a list of template prefixe, and a number that indicates the amount of resources. + -- + -- For example, this defines 3 new squadrons: + -- + -- AIRDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50" }, 10 ) + -- AIRDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50" }, 10 ) + -- AIRDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50" }, 10 ) + -- + -- The latter 2 will depart from FARPs, which bare the name `"CAS"` and `"BAI"`. + -- + -- + -- ### 3.1. Squadrons **Tasking**. + -- + -- Squadrons can be commanded to execute 3 types of tasks, as explained above: + -- + -- - SEAD: Suppression of Air Defenses, which are ground targets that have medium or long range radar emitters. + -- - CAS : Close Air Support, when there are enemy ground targets close to friendly units. + -- - BAI : Battlefield Air Interdiction, which are targets further away from the frond-line. + -- + -- You need to configure each squadron which task types you want it to perform. Read on ... + -- + -- ### 3.2. Squadrons enemy ground target **engagement types**. + -- + -- There are two ways how targets can be engaged: directly **on call** from the airfield, farp or carrier, or through a **patrol**. + -- + -- Patrols are extremely handy, as these will airborne your helicopters or airplanes in advance. They will patrol in defined zones outlined, + -- and will engage with the targets once commanded. If the patrol zone is close enough to the enemy ground targets, then the time required + -- to engage is heavily minimized! + -- + -- However; patrols come with a side effect: since your resources are airborne, they will be vulnerable to incoming air attacks from the enemy. + -- + -- The mission designer needs to carefully balance the need for patrols or the need for engagement on call from the airfields. + -- + -- ### 3.3. Squadron **on call** engagement. + -- + -- So to make squadrons engage targets from the airfields, use the following methods: + -- + -- - For SEAD, use the @{#AI_AIR_DISPATCHER.SetSquadronSead}() method. + -- - For CAS, use the @{#AI_AIR_DISPATCHER.SetSquadronCas}() method. + -- - For BAI, use the @{#AI_AIR_DISPATCHER.SetSquadronBai}() method. + -- + -- Note that for the tasks, specific helicopter or airplane templates are required to be used, which you can configure using your mission editor. + -- Especially the payload (weapons configuration) is important to get right. + -- + -- For example, the following will define for the squadrons different tasks: + -- + -- AIRDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) + -- AIRDispatcher:SetSquadronSead( "Maykop SEAD", 120, 250 ) + -- + -- AIRDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) + -- AIRDispatcher:SetSquadronCas( "Maykop CAS", 120, 250 ) + -- + -- AIRDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) + -- AIRDispatcher:SetSquadronBai( "Maykop BAI", 120, 250 ) + -- + -- ### 3.4. Squadron **on patrol engagement**. + -- + -- Squadrons can be setup to patrol in the air near the engagement hot zone. + -- When needed, the AIR defense units will be close to the battle area, and can engage quickly. + -- + -- So to make squadrons engage targets from a patrol zone, use the following methods: + -- + -- - For SEAD, use the @{#AI_AIR_DISPATCHER.SetSquadronSeadPatrol}() method. + -- - For CAS, use the @{#AI_AIR_DISPATCHER.SetSquadronCasPatrol}() method. + -- - For BAI, use the @{#AI_AIR_DISPATCHER.SetSquadronBaiPatrol}() method. + -- + -- Because a patrol requires more parameters, the following methods must be used to fine-tune the patrols for each squadron. + -- + -- - For SEAD, use the @{#AI_AIR_DISPATCHER.SetSquadronSeadPatrolInterval}() method. + -- - For CAS, use the @{#AI_AIR_DISPATCHER.SetSquadronCasPatrolInterval}() method. + -- - For BAI, use the @{#AI_AIR_DISPATCHER.SetSquadronBaiPatrolInterval}() method. + -- + -- Here an example to setup patrols of various task types: + -- + -- AIRDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 ) + -- AIRDispatcher:SetSquadronSeadPatrol( "Maykop SEAD", PatrolZone, 300, 500, 50, 80, 250, 300 ) + -- AIRDispatcher:SetSquadronPatrolInterval( "Maykop SEAD", 2, 30, 60, 1, "SEAD" ) + -- + -- AIRDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 ) + -- AIRDispatcher:SetSquadronCasPatrol( "Maykop CAS", PatrolZone, 600, 700, 50, 80, 250, 300 ) + -- AIRDispatcher:SetSquadronPatrolInterval( "Maykop CAS", 2, 30, 60, 1, "CAS" ) + -- + -- AIRDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 ) + -- AIRDispatcher:SetSquadronBaiPatrol( "Maykop BAI", PatrolZone, 800, 900, 50, 80, 250, 300 ) + -- AIRDispatcher:SetSquadronPatrolInterval( "Maykop BAI", 2, 30, 60, 1, "BAI" ) + -- + -- + -- ### 3.5. Set squadron take-off methods + -- + -- Use the various SetSquadronTakeoff... methods to control how squadrons are taking-off from the home airfield, FARP or ship. + -- + -- * @{#AI_AIR_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details. + -- * @{#AI_AIR_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air. + -- * @{#AI_AIR_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_AIR_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_AIR_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield. + -- + -- **The default landing method is to spawn new aircraft directly in the air.** + -- + -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency. + -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that: + -- + -- * aircraft will stop waiting for each other or for a landing aircraft before takeoff. + -- * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other. + -- * aircraft may collide at the airbase. + -- * aircraft may be awaiting the landing of a plane currently in the air, but never lands ... + -- + -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs. + -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues! + -- + -- This example sets the default takeoff method to be from the runway. + -- And for a couple of squadrons overrides this default method. + -- + -- -- Setup the Takeoff methods + -- + -- -- The default takeoff + -- A2ADispatcher:SetDefaultTakeOffFromRunway() + -- + -- -- The individual takeoff per squadron + -- A2ADispatcher:SetSquadronTakeoff( "Mineralnye", AI_AIR_DISPATCHER.Takeoff.Air ) + -- A2ADispatcher:SetSquadronTakeoffInAir( "Sochi" ) + -- A2ADispatcher:SetSquadronTakeoffFromRunway( "Mozdok" ) + -- A2ADispatcher:SetSquadronTakeoffFromParkingCold( "Maykop" ) + -- A2ADispatcher:SetSquadronTakeoffFromParkingHot( "Novo" ) + -- + -- + -- ### 3.5.1. Set Squadron takeoff altitude when spawning new aircraft in the air. + -- + -- In the case of the @{#AI_AIR_DISPATCHER.SetSquadronTakeoffInAir}() there is also an other parameter that can be applied. + -- That is modifying or setting the **altitude** from where planes spawn in the air. + -- Use the method @{#AI_AIR_DISPATCHER.SetSquadronTakeoffInAirAltitude}() to set the altitude for a specific squadron. + -- The default takeoff altitude can be modified or set using the method @{#AI_AIR_DISPATCHER.SetSquadronTakeoffInAirAltitude}(). + -- As part of the method @{#AI_AIR_DISPATCHER.SetSquadronTakeoffInAir}() a parameter can be specified to set the takeoff altitude. + -- If this parameter is not specified, then the default altitude will be used for the squadron. + -- + -- ### 3.5.2. Set Squadron takeoff interval. + -- + -- The different types of available airfields have different amounts of available launching platforms: + -- + -- - Airbases typically have a lot of platforms. + -- - FARPs have 4 platforms. + -- - Ships have 2 to 4 platforms. + -- + -- Depending on the demand of requested takeoffs by the AIR dispatcher, an airfield can become overloaded. Too many aircraft need to be taken + -- off at the same time, which will result in clutter as described above. In order to better control this behaviour, a takeoff scheduler is implemented, + -- which can be used to control how many aircraft are ordered for takeoff between specific time intervals. + -- The takeff intervals can be specified per squadron, which make sense, as each squadron have a "home" airfield. + -- + -- For this purpose, the method @{#AI_AIR_DISPATCHER.SetSquadronTakeOffInterval}() can be used to specify the takeoff intervals of + -- aircraft groups per squadron to avoid cluttering of aircraft at airbases. + -- This is especially useful for FARPs and ships. Each takeoff dispatch is queued by the dispatcher and when the interval time + -- has been reached, a new group will be spawned or activated for takeoff. + -- + -- The interval needs to be estimated, and depends on the time needed for the aircraft group to actually depart from the launch platform, and + -- the way how the aircraft are starting up. Cold starts take the longest duration, hot starts a few seconds, and runway takeoff also a few seconds for FARPs and ships. + -- + -- See the underlying example: + -- + -- -- Imagine a squadron launched from a FARP, with a grouping of 4. + -- -- Aircraft will cold start from the FARP, and thus, a maximum of 4 aircraft can be launched at the same time. + -- -- Additionally, depending on the group composition of the aircraft, defending units will be ordered for takeoff together. + -- -- It takes about 3 to 4 minutes to takeoff helicopters from FARPs in cold start. + -- A2ADispatcher:SetSquadronTakeOffInterval( "Mineralnye", 60 * 4 ) + -- + -- + -- ### 3.6. Set squadron landing methods + -- + -- In analogy with takeoff, the landing methods are to control how squadrons land at the airfield: + -- + -- * @{#AI_AIR_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. + -- * @{#AI_AIR_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield. + -- * @{#AI_AIR_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway. + -- * @{#AI_AIR_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. + -- + -- You can use these methods to minimize the airbase coodination overhead and to increase the airbase efficiency. + -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the + -- A2A defense system, as no new CAP or GCI planes can takeoff. + -- Note that the method @{#AI_AIR_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft. + -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control. + -- + -- This example defines the default landing method to be at the runway. + -- And for a couple of squadrons overrides this default method. + -- + -- -- Setup the Landing methods + -- + -- -- The default landing method + -- A2ADispatcher:SetDefaultLandingAtRunway() + -- + -- -- The individual landing per squadron + -- A2ADispatcher:SetSquadronLandingAtRunway( "Mineralnye" ) + -- A2ADispatcher:SetSquadronLandingNearAirbase( "Sochi" ) + -- A2ADispatcher:SetSquadronLandingAtEngineShutdown( "Mozdok" ) + -- A2ADispatcher:SetSquadronLandingNearAirbase( "Maykop" ) + -- A2ADispatcher:SetSquadronLanding( "Novo", AI_AIR_DISPATCHER.Landing.AtRunway ) + -- + -- + -- ### 3.7. Set squadron **grouping**. + -- + -- Use the method @{#AI_AIR_DISPATCHER.SetSquadronGrouping}() to set the grouping of aircraft when spawned in. + -- + -- ![Banner Image](..\Presentations\AI_AIR_DISPATCHER\Dia12.JPG) + -- + -- In the case of **on call** engagement, the @{#AI_AIR_DISPATCHER.SetSquadronGrouping}() method has additional behaviour. + -- When there aren't enough patrol flights airborne, a on call will be initiated for the remaining + -- targets to be engaged. Depending on the grouping parameter, the spawned flights for on call aircraft are grouped into this setting. + -- For example with a group setting of 2, if 3 targets are detected and cannot be engaged by the available patrols or any airborne flight, + -- an additional on call flight needs to be started. + -- + -- The **grouping value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense flights grouping when the tactical situation changes. + -- + -- ### 3.8. Set the squadron **overhead** to balance the effectiveness of the AIR defenses. + -- + -- The effectiveness can be set with the **overhead parameter**. This is a number that is used to calculate the amount of Units that dispatching command will allocate to GCI in surplus of detected amount of units. + -- The **default value** of the overhead parameter is 1.0, which means **equal balance**. + -- + -- ![Banner Image](..\Presentations\AI_AIR_DISPATCHER\Dia11.JPG) + -- + -- However, depending on the (type of) aircraft (strength and payload) in the squadron and the amount of resources available, this parameter can be changed. + -- + -- The @{#AI_AIR_DISPATCHER.SetSquadronOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. + -- + -- For example, a A-10C with full long-distance AIR missiles payload, may still be less effective than a Su-23 with short range AIR missiles... + -- So in this case, one may want to use the @{#AI_AIR_DISPATCHER.SetOverhead}() method to allocate more defending planes as the amount of detected attacking ground units. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that overhead values: + -- + -- * Higher than 1.0, for example 1.5, will increase the defense unit amounts. For 4 attacking ground units detected, 6 aircraft will be spawned. + -- * Lower than 1, for example 0.75, will decrease the defense unit amounts. For 4 attacking ground units detected, only 3 aircraft will be spawned. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking ground units as part of the detected group + -- multiplied by the overhead parameter, and rounded up to the smallest integer. + -- + -- Typically, for AIR defenses, values small than 1 will be used. Here are some good values for a couple of aircraft to support CAS operations: + -- + -- - A-10C: 0.15 + -- - Su-34: 0.15 + -- - A-10A: 0.25 + -- - SU-25T: 0.10 + -- + -- So generically, the amount of missiles that an aircraft can take will determine its attacking effectiveness. The longer the range of the missiles, + -- the less risk that the defender may be destroyed by the enemy, thus, the less aircraft needs to be activated in a defense. + -- + -- The **overhead value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense overhead when the tactical situation changes. + -- + -- ### 3.8. Set the squadron **engage limit**. + -- + -- To limit the amount of aircraft to defend against a large group of intruders, an **engage limit** can be defined per squadron. + -- This limit will avoid an extensive amount of aircraft to engage with the enemy if the attacking ground forces are enormous. + -- + -- Use the method @{#AI_AIR_DISPATCHER.SetSquadronEngageLimit}() to limit the amount of aircraft that will engage with the enemy, per squadron. + -- + -- ## 4. Set the **fuel treshold**. + -- + -- When aircraft get **out of fuel** to a certain %-tage, which is by default **15% (0.15)**, there are two possible actions that can be taken: + -- - The aircraft will go RTB, and will be replaced with a new aircraft if possible. + -- - The aircraft will refuel at a tanker, if a tanker has been specified for the squadron. + -- + -- Use the method @{#AI_AIR_DISPATCHER.SetSquadronFuelThreshold}() to set the **squadron fuel treshold** of the aircraft for all squadrons. + -- + -- ## 6. Other configuration options + -- + -- ### 6.1. Set a tactical display panel. + -- + -- Every 30 seconds, a tactical display panel can be shown that illustrates what the status is of the different groups controlled by AI_AIR_DISPATCHER. + -- Use the method @{#AI_AIR_DISPATCHER.SetTacticalDisplay}() to switch on the tactical display panel. The default will not show this panel. + -- Note that there may be some performance impact if this panel is shown. + -- + -- ## 10. Default settings. + -- + -- Default settings configure the standard behaviour of the squadrons. + -- This section a good overview of the different parameters that setup the behaviour of **ALL** the squadrons by default. + -- Note that default behaviour can be tweaked, and thus, this will change the behaviour of all the squadrons. + -- Unless there is a specific behaviour set for a specific squadron, the default configured behaviour will be followed. + -- + -- ## 10.1. Default **takeoff** behaviour. + -- + -- The default takeoff behaviour is set to **in the air**, which means that new spawned aircraft will be spawned directly in the air above the airbase by default. + -- + -- **The default takeoff method can be set for ALL squadrons that don't have an individual takeoff method configured.** + -- + -- * @{#AI_AIR_DISPATCHER.SetDefaultTakeoff}() is the generic configuration method to control takeoff by default from the air, hot, cold or from the runway. See the method for further details. + -- * @{#AI_AIR_DISPATCHER.SetDefaultTakeoffInAir}() will spawn by default new aircraft from the squadron directly in the air. + -- * @{#AI_AIR_DISPATCHER.SetDefaultTakeoffFromParkingCold}() will spawn by default new aircraft in without running engines at a parking spot at the airfield. + -- * @{#AI_AIR_DISPATCHER.SetDefaultTakeoffFromParkingHot}() will spawn by default new aircraft in with running engines at a parking spot at the airfield. + -- * @{#AI_AIR_DISPATCHER.SetDefaultTakeoffFromRunway}() will spawn by default new aircraft at the runway at the airfield. + -- + -- ## 10.2. Default landing behaviour. + -- + -- The default landing behaviour is set to **near the airbase**, which means that returning airplanes will be despawned directly in the air by default. + -- + -- The default landing method can be set for ALL squadrons that don't have an individual landing method configured. + -- + -- * @{#AI_AIR_DISPATCHER.SetDefaultLanding}() is the generic configuration method to control by default landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown. + -- * @{#AI_AIR_DISPATCHER.SetDefaultLandingNearAirbase}() will despawn by default the returning aircraft in the air when near the airfield. + -- * @{#AI_AIR_DISPATCHER.SetDefaultLandingAtRunway}() will despawn by default the returning aircraft directly after landing at the runway. + -- * @{#AI_AIR_DISPATCHER.SetDefaultLandingAtEngineShutdown}() will despawn by default the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines. + -- + -- ## 10.3. Default **overhead**. + -- + -- The default overhead is set to **0.25**. That essentially means that for each 4 ground enemies there will be 1 aircraft dispatched. + -- + -- The default overhead value can be set for ALL squadrons that don't have an individual overhead value configured. + -- + -- Use the @{#AI_AIR_DISPATCHER.SetDefaultOverhead}() method can be used to set the default overhead or defense strength for ALL squadrons. + -- + -- ## 10.4. Default **grouping**. + -- + -- The default grouping is set to **one airplane**. That essentially means that there won't be any grouping applied by default. + -- + -- The default grouping value can be set for ALL squadrons that don't have an individual grouping value configured. + -- + -- Use the method @{#AI_AIR_DISPATCHER.SetDefaultGrouping}() to set the **default grouping** of spawned airplanes for all squadrons. + -- + -- ## 10.5. Default RTB fuel treshold. + -- + -- When an airplane gets **out of fuel** to a certain %-tage, which is **15% (0.15)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- + -- Use the method @{#AI_AIR_DISPATCHER.SetDefaultFuelThreshold}() to set the **default fuel treshold** of spawned airplanes for all squadrons. + -- + -- ## 10.6. Default RTB damage treshold. + -- + -- When an airplane is **damaged** to a certain %-tage, which is **40% (0.40)**, it will go RTB, and will be replaced with a new airplane when applicable. + -- + -- Use the method @{#AI_AIR_DISPATCHER.SetDefaultDamageThreshold}() to set the **default damage treshold** of spawned airplanes for all squadrons. + -- + -- ## 10.7. Default settings for **patrol**. + -- + -- ### 10.7.1. Default **patrol time Interval**. + -- + -- Patrol dispatching is time event driven, and will evaluate in random time intervals if a new patrol needs to be dispatched. + -- + -- The default patrol time interval is between **180** and **600** seconds. + -- + -- Use the method @{#AI_AIR_DISPATCHER.SetDefaultPatrolTimeInterval}() to set the **default patrol time interval** of dispatched aircraft for ALL squadrons. + -- + -- Note that you can still change the patrol limit and patrol time intervals for each patrol individually using + -- the @{#AI_AIR_DISPATCHER.SetSquadronPatrolTimeInterval}() method. + -- + -- ### 10.7.2. Default **patrol limit**. + -- + -- Multiple patrol can be airborne at the same time for one squadron, which is controlled by the **patrol limit**. + -- The **default patrol limit** is 1 patrol per squadron to be airborne at the same time. + -- Note that the default patrol limit is used when a squadron patrol is defined, and cannot be changed afterwards. + -- So, ensure that you set the default patrol limit **before** you define or setup the squadron patrol. + -- + -- Use the method @{#AI_AIR_DISPATCHER.SetDefaultPatrolTimeInterval}() to set the **default patrol time interval** of dispatched aircraft patrols for all squadrons. + -- Note that you can still change the patrol limit and patrol time intervals for each patrol individually using + -- the @{#AI_AIR_DISPATCHER.SetSquadronPatrolTimeInterval}() method. + -- + -- ## 10.7.3. Default tanker for refuelling when executing CAP. + -- + -- Instead of sending CAP to RTB when out of fuel, you can let CAP refuel in mid air using a tanker. + -- This greatly increases the efficiency of your CAP operations. + -- + -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected. + -- Then, use the method @{#AI_AIR_DISPATCHER.SetDefaultTanker}() to set the tanker for the dispatcher. + -- Use the method @{#AI_AIR_DISPATCHER.SetDefaultFuelThreshold}() to set the %-tage left in the defender airplane tanks when a refuel action is needed. + -- + -- When the tanker specified is alive and in the air, the tanker will be used for refuelling. + -- + -- For example, the following setup will set the default refuel tanker to "Tanker": + -- + -- ![Banner Image](..\Presentations\AI_AIR_DISPATCHER\AI_AIR_DISPATCHER-ME_11.JPG) + -- + -- -- Define the CAP + -- A2ADispatcher:SetSquadron( "Sochi", AIRBASE.Caucasus.Sochi_Adler, { "SQ CCCP SU-34" }, 20 ) + -- A2ADispatcher:SetSquadronCap( "Sochi", ZONE:New( "PatrolZone" ), 4000, 8000, 600, 800, 1000, 1300 ) + -- A2ADispatcher:SetSquadronCapInterval("Sochi", 2, 30, 600, 1 ) + -- A2ADispatcher:SetSquadronGci( "Sochi", 900, 1200 ) + -- + -- -- Set the default tanker for refuelling to "Tanker", when the default fuel treshold has reached 90% fuel left. + -- A2ADispatcher:SetDefaultFuelThreshold( 0.9 ) + -- A2ADispatcher:SetDefaultTanker( "Tanker" ) + -- + -- ## 10.8. Default settings for GCI. + -- + -- ## 10.8.1. Optimal intercept point calculation. + -- + -- When intruders are detected, the intrusion path of the attackers can be monitored by the EWR. + -- Although defender planes might be on standby at the airbase, it can still take some time to get the defenses up in the air if there aren't any defenses airborne. + -- This time can easily take 2 to 3 minutes, and even then the defenders still need to fly towards the target, which takes also time. + -- + -- Therefore, an optimal **intercept point** is calculated which takes a couple of parameters: + -- + -- * The average bearing of the intruders for an amount of seconds. + -- * The average speed of the intruders for an amount of seconds. + -- * An assumed time it takes to get planes operational at the airbase. + -- + -- The **intercept point** will determine: + -- + -- * If there are any friendlies close to engage the target. These can be defenders performing CAP or defenders in RTB. + -- * The optimal airbase from where defenders will takeoff for GCI. + -- + -- Use the method @{#AI_AIR_DISPATCHER.SetIntercept}() to modify the assumed intercept delay time to calculate a valid interception. + -- + -- ## 10.8.2. Default Disengage Radius. + -- + -- The radius to **disengage any target** when the **distance** of the defender to the **home base** is larger than the specified meters. + -- The default Disengage Radius is **300km** (300000 meters). Note that the Disengage Radius is applicable to ALL squadrons! + -- + -- Use the method @{#AI_AIR_DISPATCHER.SetDisengageRadius}() to modify the default Disengage Radius to another distance setting. + -- + -- ## 11. Airbase capture: + -- + -- Different squadrons can be located at one airbase. + -- If the airbase gets captured, that is, when there is an enemy unit near the airbase, and there aren't anymore friendlies at the airbase, the airbase will change coalition ownership. + -- As a result, the GCI and CAP will stop! + -- However, the squadron will still stay alive. Any airplane that is airborne will continue its operations until all airborne airplanes + -- of the squadron will be destroyed. This to keep consistency of air operations not to confuse the players. + -- + -- + -- + -- + -- @field #AI_AIR_DISPATCHER + AI_AIR_DISPATCHER = { + ClassName = "AI_AIR_DISPATCHER", + Detection = nil, + } + + --- Definition of a Squadron. + -- @type AI_AIR_DISPATCHER.Squadron + -- @field #string Name The Squadron name. + -- @field Wrapper.Airbase#AIRBASE Airbase The home airbase. + -- @field #string AirbaseName The name of the home airbase. + -- @field Core.Spawn#SPAWN Spawn The spawning object. + -- @field #number ResourceCount The number of resources available. + -- @field #list<#string> TemplatePrefixes The list of template prefixes. + -- @field #boolean Captured true if the squadron is captured. + -- @field #number Overhead The overhead for the squadron. + + + --- List of defense coordinates. + -- @type AI_AIR_DISPATCHER.DefenseCoordinates + -- @map <#string,Core.Point#COORDINATE> A list of all defense coordinates mapped per defense coordinate name. + + --- @field #AI_AIR_DISPATCHER.DefenseCoordinates DefenseCoordinates + AI_AIR_DISPATCHER.DefenseCoordinates = {} + + --- Enumerator for spawns at airbases + -- @type AI_AIR_DISPATCHER.Takeoff + -- @extends Wrapper.Group#GROUP.Takeoff + + --- @field #AI_AIR_DISPATCHER.Takeoff Takeoff + AI_AIR_DISPATCHER.Takeoff = GROUP.Takeoff + + --- Defnes Landing location. + -- @field #AI_AIR_DISPATCHER.Landing + AI_AIR_DISPATCHER.Landing = { + NearAirbase = 1, + AtRunway = 2, + AtEngineShutdown = 3, + } + + --- A defense queue item description + -- @type AI_AIR_DISPATCHER.DefenseQueueItem + -- @field Squadron + -- @field #AI_AIR_DISPATCHER.Squadron DefenderSquadron The squadron in the queue. + -- @field DefendersNeeded + -- @field Defense + -- @field DefenseTaskType + -- @field Functional.Detection#DETECTION_BASE AttackerDetection + -- @field DefenderGrouping + -- @field #string SquadronName The name of the squadron. + + --- Queue of planned defenses to be launched. + -- This queue exists because defenses must be launched on FARPS, or in the air, or on an airbase, or on carriers. + -- And some of these platforms have very limited amount of "launching" platforms. + -- Therefore, this queue concept is introduced that queues each defender request. + -- Depending on the location of the launching site, the queued defenders will be launched at varying time intervals. + -- This guarantees that launched defenders are also directly existing ... + -- @type AI_AIR_DISPATCHER.DefenseQueue + -- @list<#AI_AIR_DISPATCHER.DefenseQueueItem> DefenseQueueItem A list of all defenses being queued ... + + --- @field #AI_AIR_DISPATCHER.DefenseQueue DefenseQueue + AI_AIR_DISPATCHER.DefenseQueue = {} + + + --- Defense approach types + -- @type #AI_AIR_DISPATCHER.DefenseApproach + AI_AIR_DISPATCHER.DefenseApproach = { + Random = 1, + Distance = 2, + } + + --- AI_AIR_DISPATCHER constructor. + -- This is defining the AIR DISPATCHER for one coaliton. + -- The Dispatcher works with a @{Functional.Detection#DETECTION_BASE} object that is taking of the detection of targets using the EWR units. + -- The Detection object is polymorphic, depending on the type of detection object choosen, the detection will work differently. + -- @param #AI_AIR_DISPATCHER self + -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network. + -- @return #AI_AIR_DISPATCHER self + -- @usage + -- + -- -- Setup the Detection, using DETECTION_AREAS. + -- -- First define the SET of GROUPs that are defining the EWR network. + -- -- Here with prefixes DF CCCP AWACS, DF CCCP EWR. + -- DetectionSetGroup = SET_GROUP:New() + -- DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } ) + -- DetectionSetGroup:FilterStart() + -- + -- -- Define the DETECTION_AREAS, using the DetectionSetGroup, with a 30km grouping radius. + -- Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 ) + -- + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) -- + -- + function AI_AIR_DISPATCHER:New( Detection ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, DETECTION_MANAGER:New( nil, Detection ) ) -- #AI_AIR_DISPATCHER + + self.Detection = Detection -- Functional.Detection#DETECTION_AREAS + + self.Detection:FilterCategories( Unit.Category.GROUND_UNIT ) + + -- This table models the DefenderSquadron templates. + self.DefenderSquadrons = {} -- The Defender Squadrons. + self.DefenderSpawns = {} + self.DefenderTasks = {} -- The Defenders Tasks. + self.DefenderDefault = {} -- The Defender Default Settings over all Squadrons. + + -- TODO: Check detection through radar. +-- self.Detection:FilterCategories( { Unit.Category.GROUND } ) +-- self.Detection:InitDetectRadar( false ) +-- self.Detection:InitDetectVisual( true ) +-- self.Detection:SetRefreshTimeInterval( 30 ) + + self:SetDefenseRadius() + self:SetDefenseLimit( nil ) + self:SetDefenseApproach( AI_AIR_DISPATCHER.DefenseApproach.Random ) + self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds. + self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km. + + self:SetDefaultTakeoff( AI_AIR_DISPATCHER.Takeoff.Air ) + self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above the ground. + self:SetDefaultLanding( AI_AIR_DISPATCHER.Landing.NearAirbase ) + self:SetDefaultOverhead( 1 ) + self:SetDefaultGrouping( 1 ) + self:SetDefaultFuelThreshold( 0.15, 0 ) -- 15% of fuel remaining in the tank will trigger the airplane to return to base or refuel. + self:SetDefaultDamageThreshold( 0.4 ) -- When 40% of damage, go RTB. + self:SetDefaultPatrolTimeInterval( 180, 600 ) -- Between 180 and 600 seconds. + self:SetDefaultPatrolLimit( 1 ) -- Maximum one Patrol per squadron. + + + self:AddTransition( "Started", "Assign", "Started" ) + + --- OnAfter Transition Handler for Event Assign. + -- @function [parent=#AI_AIR_DISPATCHER] OnAfterAssign + -- @param #AI_AIR_DISPATCHER self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Tasking.Task_AIR#AI_AIR Task + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #string PlayerName + + self:AddTransition( "*", "Patrol", "*" ) + + --- Patrol Handler OnBefore for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] OnBeforePatrol + -- @param #AI_AIR_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Patrol Handler OnAfter for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] OnAfterPatrol + -- @param #AI_AIR_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Patrol Trigger for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] Patrol + -- @param #AI_AIR_DISPATCHER self + + --- Patrol Asynchronous Trigger for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] __Patrol + -- @param #AI_AIR_DISPATCHER self + -- @param #number Delay + + self:AddTransition( "*", "Defend", "*" ) + + --- Defend Handler OnBefore for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] OnBeforeDefend + -- @param #AI_AIR_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Defend Handler OnAfter for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] OnAfterDefend + -- @param #AI_AIR_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Defend Trigger for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] Defend + -- @param #AI_AIR_DISPATCHER self + + --- Defend Asynchronous Trigger for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] __Defend + -- @param #AI_AIR_DISPATCHER self + -- @param #number Delay + + self:AddTransition( "*", "Engage", "*" ) + + --- Engage Handler OnBefore for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] OnBeforeEngage + -- @param #AI_AIR_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Engage Handler OnAfter for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] OnAfterEngage + -- @param #AI_AIR_DISPATCHER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Engage Trigger for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] Engage + -- @param #AI_AIR_DISPATCHER self + + --- Engage Asynchronous Trigger for AI_AIR_DISPATCHER + -- @function [parent=#AI_AIR_DISPATCHER] __Engage + -- @param #AI_AIR_DISPATCHER self + -- @param #number Delay + + + -- Subscribe to the CRASH event so that when planes are shot + -- by a Unit from the dispatcher, they will be removed from the detection... + -- This will avoid the detection to still "know" the shot unit until the next detection. + -- Otherwise, a new defense or engage may happen for an already shot plane! + + + self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) + self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) + + + self:HandleEvent( EVENTS.Land ) + self:HandleEvent( EVENTS.EngineShutdown ) + + -- Handle the situation where the airbases are captured. + self:HandleEvent( EVENTS.BaseCaptured ) + + self:SetTacticalDisplay( false ) + + self.DefenderPatrolIndex = 0 + + self:SetDefenseReactivityMedium() + + self.TakeoffScheduleID = self:ScheduleRepeat( 10, 10, 0, nil, self.ResourceTakeoff, self ) + + self:__Start( 1 ) + + return self + end + + + --- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:onafterStart( From, Event, To ) + + self:GetParent( self ).onafterStart( self, From, Event, To ) + + -- Spawn the resources. + for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + DefenderSquadron.Resource = {} + for Resource = 1, DefenderSquadron.ResourceCount or 0 do + self:ResourcePark( DefenderSquadron ) + end + self:I( "Parked resources for squadron " .. DefenderSquadron.Name ) + end + + end + + --- Locks the DefenseItem from being defended. + -- @param #AI_AIR_DISPATCHER self + -- @param #string DetectedItemIndex The index of the detected item. + function AI_AIR_DISPATCHER:Lock( DetectedItemIndex ) + self:F( { DetectedItemIndex = DetectedItemIndex } ) + local DetectedItem = self.Detection:GetDetectedItemByIndex( DetectedItemIndex ) + if DetectedItem then + self:F( { Locked = DetectedItem } ) + self.Detection:LockDetectedItem( DetectedItem ) + end + end + + + --- Unlocks the DefenseItem from being defended. + -- @param #AI_AIR_DISPATCHER self + -- @param #string DetectedItemIndex The index of the detected item. + function AI_AIR_DISPATCHER:Unlock( DetectedItemIndex ) + self:F( { DetectedItemIndex = DetectedItemIndex } ) + self:F( { Index = self.Detection.DetectedItemsByIndex } ) + local DetectedItem = self.Detection:GetDetectedItemByIndex( DetectedItemIndex ) + if DetectedItem then + self:F( { Unlocked = DetectedItem } ) + self.Detection:UnlockDetectedItem( DetectedItem ) + end + end + + + --- Sets maximum zones to be engaged at one time by defenders. + -- @param #AI_AIR_DISPATCHER self + -- @param #number DefenseLimit The maximum amount of detected items to be engaged at the same time. + function AI_AIR_DISPATCHER:SetDefenseLimit( DefenseLimit ) + self:F( { DefenseLimit = DefenseLimit } ) + + self.DefenseLimit = DefenseLimit + end + + + --- Sets the method of the tactical approach of the defenses. + -- @param #AI_AIR_DISPATCHER self + -- @param #number DefenseApproach Use the structure AI_AIR_DISPATCHER.DefenseApproach to set the defense approach. + -- The default defense approach is AI_AIR_DISPATCHER.DefenseApproach.Random. + function AI_AIR_DISPATCHER:SetDefenseApproach( DefenseApproach ) + self:F( { DefenseApproach = DefenseApproach } ) + + self._DefenseApproach = DefenseApproach + end + + + --- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:ResourcePark( DefenderSquadron ) + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) + local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN + Spawn:InitGrouping( 1 ) + local SpawnGroup + if self:IsSquadronVisible( DefenderSquadron.Name ) then + SpawnGroup = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, SPAWN.Takeoff.Cold ) + local GroupName = SpawnGroup:GetName() + DefenderSquadron.Resources = DefenderSquadron.Resources or {} + DefenderSquadron.Resources[TemplateID] = DefenderSquadron.Resources[TemplateID] or {} + DefenderSquadron.Resources[TemplateID][GroupName] = {} + DefenderSquadron.Resources[TemplateID][GroupName] = SpawnGroup + end + end + + + --- @param #AI_AIR_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_AIR_DISPATCHER:OnEventBaseCaptured( EventData ) + + local AirbaseName = EventData.PlaceName -- The name of the airbase that was captured. + + self:I( "Captured " .. AirbaseName ) + + -- Now search for all squadrons located at the airbase, and sanatize them. + for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do + if Squadron.AirbaseName == AirbaseName then + Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning. + Squadron.Captured = true + self:I( "Squadron " .. SquadronName .. " captured." ) + end + end + end + + --- @param #AI_AIR_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_AIR_DISPATCHER:OnEventCrashOrDead( EventData ) + self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) + end + + --- @param #AI_AIR_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_AIR_DISPATCHER:OnEventLand( EventData ) + self:F( "Landed" ) + local DefenderUnit = EventData.IniUnit + local Defender = EventData.IniGroup + local Squadron = self:GetSquadronFromDefender( Defender ) + if Squadron then + self:F( { SquadronName = Squadron.Name } ) + local LandingMethod = self:GetSquadronLanding( Squadron.Name ) + + if LandingMethod == AI_AIR_DISPATCHER.Landing.AtRunway then + local DefenderSize = Defender:GetSize() + if DefenderSize == 1 then + self:RemoveDefenderFromSquadron( Squadron, Defender ) + end + DefenderUnit:Destroy() + self:ResourcePark( Squadron, Defender ) + return + end + if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then + -- Damaged units cannot be repaired anymore. + DefenderUnit:Destroy() + return + end + end + end + + --- @param #AI_AIR_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_AIR_DISPATCHER:OnEventEngineShutdown( EventData ) + local DefenderUnit = EventData.IniUnit + local Defender = EventData.IniGroup + local Squadron = self:GetSquadronFromDefender( Defender ) + if Squadron then + self:F( { SquadronName = Squadron.Name } ) + local LandingMethod = self:GetSquadronLanding( Squadron.Name ) + if LandingMethod == AI_AIR_DISPATCHER.Landing.AtEngineShutdown and + not DefenderUnit:InAir() then + local DefenderSize = Defender:GetSize() + if DefenderSize == 1 then + self:RemoveDefenderFromSquadron( Squadron, Defender ) + end + DefenderUnit:Destroy() + self:ResourcePark( Squadron, Defender ) + end + end + end + + do -- Manage the defensive behaviour + + --- @param #AI_AIR_DISPATCHER self + -- @param #string DefenseCoordinateName The name of the coordinate to be defended by AIR defenses. + -- @param Core.Point#COORDINATE DefenseCoordinate The coordinate to be defended by AIR defenses. + function AI_AIR_DISPATCHER:AddDefenseCoordinate( DefenseCoordinateName, DefenseCoordinate ) + self.DefenseCoordinates[DefenseCoordinateName] = DefenseCoordinate + end + + --- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:SetDefenseReactivityLow() + self.DefenseReactivity = 0.05 + end + + --- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:SetDefenseReactivityMedium() + self.DefenseReactivity = 0.15 + end + + --- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:SetDefenseReactivityHigh() + self.DefenseReactivity = 0.5 + end + + end + + + --- Define the radius to disengage any target when the distance to the home base is larger than the specified meters. + -- @param #AI_AIR_DISPATCHER self + -- @param #number DisengageRadius (Optional, Default = 300000) The radius to disengage a target when too far from the home base. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Set 50km as the Disengage Radius. + -- AIRDispatcher:SetDisengageRadius( 50000 ) + -- + -- -- Set 100km as the Disengage Radius. + -- AIRDispatcher:SetDisngageRadius() -- 300000 is the default value. + -- + function AI_AIR_DISPATCHER:SetDisengageRadius( DisengageRadius ) + + self.DisengageRadius = DisengageRadius or 300000 + + return self + end + + + --- Define the defense radius to check if a target can be engaged by a squadron group for SEAD, CAS or BAI for defense. + -- When targets are detected that are still really far off, you don't want the AI_AIR_DISPATCHER to launch defenders, as they might need to travel too far. + -- You want it to wait until a certain defend radius is reached, which is calculated as: + -- 1. the **distance of the closest airbase to target**, being smaller than the **Defend Radius**. + -- 2. the **distance to any defense reference point**. + -- + -- The **default** defense radius is defined as **400000** or **40km**. Override the default defense radius when the era of the warfare is early, or, + -- when you don't want to let the AI_AIR_DISPATCHER react immediately when a certain border or area is not being crossed. + -- + -- Use the method @{#AI_AIR_DISPATCHER.SetDefendRadius}() to set a specific defend radius for all squadrons, + -- **the Defense Radius is defined for ALL squadrons which are operational.** + -- + -- @param #AI_AIR_DISPATCHER self + -- @param #number DefenseRadius (Optional, Default = 200000) The defense radius to engage detected targets from the nearest capable and available squadron airbase. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Set 100km as the radius to defend from detected targets from the nearest airbase. + -- AIRDispatcher:SetDefendRadius( 100000 ) + -- + -- -- Set 200km as the radius to defend. + -- AIRDispatcher:SetDefendRadius() -- 200000 is the default value. + -- + function AI_AIR_DISPATCHER:SetDefenseRadius( DefenseRadius ) + + self.DefenseRadius = DefenseRadius or 100000 + + self.Detection:SetAcceptRange( self.DefenseRadius ) + + return self + end + + + + --- Define a border area to simulate a **cold war** scenario. + -- A **cold war** is one where Patrol aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. + -- A **hot war** is one where Patrol aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send Patrol and GCI aircraft to attack it. + -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Core.Zone#ZONE_BASE}. This method needs to be used for this. + -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. Set the noborders parameter to 1 + -- @param #AI_AIR_DISPATCHER self + -- @param Core.Zone#ZONE_BASE BorderZone An object derived from ZONE_BASE, or a list of objects derived from ZONE_BASE. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Set one ZONE_POLYGON object as the border for the AIR dispatcher. + -- local BorderZone = ZONE_POLYGON( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) -- The GROUP object is a late activate helicopter unit. + -- AIRDispatcher:SetBorderZone( BorderZone ) + -- + -- or + -- + -- -- Set two ZONE_POLYGON objects as the border for the AIR dispatcher. + -- local BorderZone1 = ZONE_POLYGON( "CCCP Border1", GROUP:FindByName( "CCCP Border1" ) ) -- The GROUP object is a late activate helicopter unit. + -- local BorderZone2 = ZONE_POLYGON( "CCCP Border2", GROUP:FindByName( "CCCP Border2" ) ) -- The GROUP object is a late activate helicopter unit. + -- AIRDispatcher:SetBorderZone( { BorderZone1, BorderZone2 } ) + -- + -- + function AI_AIR_DISPATCHER:SetBorderZone( BorderZone ) + + self.Detection:SetAcceptZones( BorderZone ) + + return self + end + + --- Display a tactical report every 30 seconds about which aircraft are: + -- * Patrolling + -- * Engaging + -- * Returning + -- * Damaged + -- * Out of Fuel + -- * ... + -- @param #AI_AIR_DISPATCHER self + -- @param #boolean TacticalDisplay Provide a value of **true** to display every 30 seconds a tactical overview. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the Tactical Display for debug mode. + -- AIRDispatcher:SetTacticalDisplay( true ) + -- + function AI_AIR_DISPATCHER:SetTacticalDisplay( TacticalDisplay ) + + self.TacticalDisplay = TacticalDisplay + + return self + end + + + --- Set the default damage treshold when defenders will RTB. + -- The default damage treshold is by default set to 40%, which means that when the airplane is 40% damaged, it will go RTB. + -- @param #AI_AIR_DISPATCHER self + -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the %-tage of the damage treshold before going RTB. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default damage treshold. + -- AIRDispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged. + -- + function AI_AIR_DISPATCHER:SetDefaultDamageThreshold( DamageThreshold ) + + self.DefenderDefault.DamageThreshold = DamageThreshold + + return self + end + + + --- Set the default Patrol time interval for squadrons, which will be used to determine a random Patrol timing. + -- The default Patrol time interval is between 180 and 600 seconds. + -- @param #AI_AIR_DISPATCHER self + -- @param #number PatrolMinSeconds The minimum amount of seconds for the random time interval. + -- @param #number PatrolMaxSeconds The maximum amount of seconds for the random time interval. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol time interval. + -- AIRDispatcher:SetDefaultPatrolTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds. + -- + function AI_AIR_DISPATCHER:SetDefaultPatrolTimeInterval( PatrolMinSeconds, PatrolMaxSeconds ) + + self.DefenderDefault.PatrolMinSeconds = PatrolMinSeconds + self.DefenderDefault.PatrolMaxSeconds = PatrolMaxSeconds + + return self + end + + + --- Set the default Patrol limit for squadrons, which will be used to determine how many Patrol can be airborne at the same time for the squadron. + -- The default Patrol limit is 1 Patrol, which means one Patrol group being spawned. + -- @param #AI_AIR_DISPATCHER self + -- @param #number PatrolLimit The maximum amount of Patrol that can be airborne at the same time for the squadron. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol limit. + -- AIRDispatcher:SetDefaultPatrolLimit( 2 ) -- Maximum 2 Patrol per squadron. + -- + function AI_AIR_DISPATCHER:SetDefaultPatrolLimit( PatrolLimit ) + + self.DefenderDefault.PatrolLimit = PatrolLimit + + return self + end + + + --- Set the default engage limit for squadrons, which will be used to determine how many air units will engage at the same time with the enemy. + -- The default eatrol limit is 1, which means one eatrol group maximum per squadron. + -- @param #AI_AIR_DISPATCHER self + -- @param #number EngageLimit The maximum engages that can be done at the same time per squadron. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default Patrol limit. + -- AIRDispatcher:SetDefaultEngageLimit( 2 ) -- Maximum 2 engagements with the enemy per squadron. + -- + function AI_AIR_DISPATCHER:SetDefaultEngageLimit( EngageLimit ) + + self.DefenderDefault.EngageLimit = EngageLimit + + return self + end + + + function AI_AIR_DISPATCHER:SetIntercept( InterceptDelay ) + + self.DefenderDefault.InterceptDelay = InterceptDelay + + local Detection = self.Detection -- Functional.Detection#DETECTION_AREAS + Detection:SetIntercept( true, InterceptDelay ) + + return self + end + + + --- Calculates which defender friendlies are nearby the area, to help protect the area. + -- @param #AI_AIR_DISPATCHER self + -- @param DetectedItem + -- @return #table A list of the defender friendlies nearby, sorted by distance. + function AI_AIR_DISPATCHER:GetDefenderFriendliesNearBy( DetectedItem ) + +-- local DefenderFriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem ) + + local DefenderFriendliesNearBy = {} + + local DetectionCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + + local ScanZone = ZONE_RADIUS:New( "ScanZone", DetectionCoordinate:GetVec2(), self.DefenseRadius ) + + ScanZone:Scan( Object.Category.UNIT, { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) + + local DefenderUnits = ScanZone:GetScannedUnits() + + for DefenderUnitID, DefenderUnit in pairs( DefenderUnits ) do + local DefenderUnit = UNIT:FindByName( DefenderUnit:getName() ) + + DefenderFriendliesNearBy[#DefenderFriendliesNearBy+1] = DefenderUnit + end + + + return DefenderFriendliesNearBy + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:GetDefenderTasks() + return self.DefenderTasks or {} + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:GetDefenderTask( Defender ) + return self.DefenderTasks[Defender] + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:GetDefenderTaskFsm( Defender ) + return self:GetDefenderTask( Defender ).Fsm + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:GetDefenderTaskTarget( Defender ) + return self:GetDefenderTask( Defender ).Target + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:GetDefenderTaskSquadronName( Defender ) + return self:GetDefenderTask( Defender ).SquadronName + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:ClearDefenderTask( Defender ) + if Defender:IsAlive() and self.DefenderTasks[Defender] then + local Target = self.DefenderTasks[Defender].Target + local Message = "Clearing (" .. self.DefenderTasks[Defender].Type .. ") " + Message = Message .. Defender:GetName() + if Target then + Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" + end + self:F( { Target = Message } ) + end + self.DefenderTasks[Defender] = nil + return self + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:ClearDefenderTaskTarget( Defender ) + + local DefenderTask = self:GetDefenderTask( Defender ) + + if Defender:IsAlive() and DefenderTask then + local Target = DefenderTask.Target + local Message = "Clearing (" .. DefenderTask.Type .. ") " + Message = Message .. Defender:GetName() + if Target then + Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or "" + end + self:F( { Target = Message } ) + end + if Defender and DefenderTask and DefenderTask.Target then + DefenderTask.Target = nil + end +-- if Defender and DefenderTask then +-- if DefenderTask.Fsm:Is( "Fuel" ) +-- or DefenderTask.Fsm:Is( "LostControl") +-- or DefenderTask.Fsm:Is( "Damaged" ) then +-- self:ClearDefenderTask( Defender ) +-- end +-- end + return self + end + + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target, Size ) + + self:F( { SquadronName = SquadronName, Defender = Defender:GetName() } ) + + self.DefenderTasks[Defender] = self.DefenderTasks[Defender] or {} + self.DefenderTasks[Defender].Type = Type + self.DefenderTasks[Defender].Fsm = Fsm + self.DefenderTasks[Defender].SquadronName = SquadronName + self.DefenderTasks[Defender].Size = Size + + if Target then + self:SetDefenderTaskTarget( Defender, Target ) + end + return self + end + + + --- + -- @param #AI_AIR_DISPATCHER self + -- @param Wrapper.Group#GROUP AIGroup + function AI_AIR_DISPATCHER:SetDefenderTaskTarget( Defender, AttackerDetection ) + + local Message = "(" .. self.DefenderTasks[Defender].Type .. ") " + Message = Message .. Defender:GetName() + Message = Message .. ( AttackerDetection and ( " target " .. AttackerDetection.Index .. " [" .. AttackerDetection.Set:Count() .. "]" ) ) or "" + self:F( { AttackerDetection = Message } ) + if AttackerDetection then + self.DefenderTasks[Defender].Target = AttackerDetection + end + return self + end + + + --- This is the main method to define Squadrons programmatically. + -- Squadrons: + -- + -- * Have a **name or key** that is the identifier or key of the squadron. + -- * Have **specific plane types** defined by **templates**. + -- * Are **located at one specific airbase**. Multiple squadrons can be located at one airbase through. + -- * Optionally have a limited set of **resources**. The default is that squadrons have unlimited resources. + -- + -- The name of the squadron given acts as the **squadron key** in the AI\_AIR\_DISPATCHER:Squadron...() methods. + -- + -- Additionally, squadrons have specific configuration options to: + -- + -- * Control how new aircraft are **taking off** from the airfield (in the air, cold, hot, at the runway). + -- * Control how returning aircraft are **landing** at the airfield (in the air near the airbase, after landing, after engine shutdown). + -- * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped. + -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned. + -- + -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft. + -- + -- @param #AI_AIR_DISPATCHER self + -- + -- @param #string SquadronName A string (text) that defines the squadron identifier or the key of the Squadron. + -- It can be any name, for example `"104th Squadron"` or `"SQ SQUADRON1"`, whatever. + -- As long as you remember that this name becomes the identifier of your squadron you have defined. + -- You need to use this name in other methods too! + -- + -- @param #string AirbaseName The airbase name where you want to have the squadron located. + -- You need to specify here EXACTLY the name of the airbase as you see it in the mission editor. + -- Examples are `"Batumi"` or `"Tbilisi-Lochini"`. + -- EXACTLY the airbase name, between quotes `""`. + -- To ease the airbase naming when using the LDT editor and IntelliSense, the @{Wrapper.Airbase#AIRBASE} class contains enumerations of the airbases of each map. + -- + -- * Caucasus: @{Wrapper.Airbase#AIRBASE.Caucaus} + -- * Nevada or NTTR: @{Wrapper.Airbase#AIRBASE.Nevada} + -- * Normandy: @{Wrapper.Airbase#AIRBASE.Normandy} + -- + -- @param #string TemplatePrefixes A string or an array of strings specifying the **prefix names of the templates** (not going to explain what is templates here again). + -- Examples are `{ "104th", "105th" }` or `"104th"` or `"Template 1"` or `"BLUE PLANES"`. + -- Just remember that your template (groups late activated) need to start with the prefix you have specified in your code. + -- If you have only one prefix name for a squadron, you don't need to use the `{ }`, otherwise you need to use the brackets. + -- + -- @param #number ResourceCount (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available. + -- + -- @usage + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- @usage + -- -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock... + -- AIRDispatcher:SetSquadron( "Squadron1", "Batumi", "SQ1", 40 ) + -- + -- @usage + -- -- This will create squadron "Sq 1" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" and has 20 planes in stock... + -- -- Note that in this implementation, the AIR dispatcher will select a random plane type when a new plane (group) needs to be spawned for defenses. + -- -- Note the usage of the {} for the airplane templates list. + -- AIRDispatcher:SetSquadron( "Sq 1", "Batumi", { "Mig-29", "Su-27" }, 40 ) + -- + -- @usage + -- -- This will create 2 squadrons "104th" and "23th" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" respectively and each squadron has 10 planes in stock... + -- AIRDispatcher:SetSquadron( "104th", "Batumi", "Mig-29", 10 ) + -- AIRDispatcher:SetSquadron( "23th", "Batumi", "Su-27", 10 ) + -- + -- @usage + -- -- This is an example like the previous, but now with infinite resources. + -- -- The ResourceCount parameter is not given in the SetSquadron method. + -- AIRDispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) + -- AIRDispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) + -- + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) + + local Squadron = AI_AIR_SQUADRON:New( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) + + return self:SetSquadron2( Squadron ) + end + + --- This is the new method to define Squadrons programmatically. + -- + -- Define a squadron using the AI_AIR_SQUADRON class. + -- + -- Squadrons: + -- + -- * Have a **name or key** that is the identifier or key of the squadron. + -- * Have **specific plane types** defined by **templates**. + -- * Are **located at one specific airbase**. Multiple squadrons can be located at one airbase through. + -- * Optionally have a limited set of **resources**. The default is that squadrons have unlimited resources. + -- + -- The name of the squadron given acts as the **squadron key** in the AI\_AIR\_DISPATCHER:Squadron...() methods. + -- + -- Additionally, squadrons have specific configuration options to: + -- + -- * Control how new aircraft are **taking off** from the airfield (in the air, cold, hot, at the runway). + -- * Control how returning aircraft are **landing** at the airfield (in the air near the airbase, after landing, after engine shutdown). + -- * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped. + -- * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned. + -- + -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft. + -- + -- @param #AI_AIR_DISPATCHER self + -- @param AI.AI_Air_Squadron#AI_AIR_SQUADRON Squadron The Air Squadron to be set active for the Air Dispatcher. + -- + -- @usage + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- @usage + -- -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock... + -- AIRDispatcher:SetSquadron( "Squadron1", "Batumi", "SQ1", 40 ) + -- + -- @usage + -- -- This will create squadron "Sq 1" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" and has 20 planes in stock... + -- -- Note that in this implementation, the AIR dispatcher will select a random plane type when a new plane (group) needs to be spawned for defenses. + -- -- Note the usage of the {} for the airplane templates list. + -- AIRDispatcher:SetSquadron( "Sq 1", "Batumi", { "Mig-29", "Su-27" }, 40 ) + -- + -- @usage + -- -- This will create 2 squadrons "104th" and "23th" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" respectively and each squadron has 10 planes in stock... + -- AIRDispatcher:SetSquadron( "104th", "Batumi", "Mig-29", 10 ) + -- AIRDispatcher:SetSquadron( "23th", "Batumi", "Su-27", 10 ) + -- + -- @usage + -- -- This is an example like the previous, but now with infinite resources. + -- -- The ResourceCount parameter is not given in the SetSquadron method. + -- AIRDispatcher:SetSquadron( "104th", "Batumi", "Mig-29" ) + -- AIRDispatcher:SetSquadron( "23th", "Batumi", "Su-27" ) + -- + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadron2( Squadron ) + + local SquadronName = Squadron:GetName() -- Retrieves the Squadron Name. + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + + return self + end + + --- Get the @{AI.AI_Air_Squadron} object from the Squadron Name given. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The Squadron Name to search the Squadron object. + -- @return AI.AI_Air_Squadron#AI_AIR_SQUADRON The Squadron object. + function AI_AIR_DISPATCHER:GetSquadron( SquadronName ) + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + + if not DefenderSquadron then + error( "Unknown Squadron for Dispatcher:" .. SquadronName ) + end + + return DefenderSquadron + end + + + --- Set the Squadron visible before startup of the dispatcher. + -- All planes will be spawned as uncontrolled on the parking spot. + -- They will lock the parking spot. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Set the Squadron visible before startup of dispatcher. + -- AIRDispatcher:SetSquadronVisible( "Mineralnye" ) + -- + -- TODO: disabling because of bug in queueing. +-- function AI_AIR_DISPATCHER:SetSquadronVisible( SquadronName ) +-- +-- self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} +-- +-- local DefenderSquadron = self:GetSquadron( SquadronName ) +-- +-- DefenderSquadron.Uncontrolled = true +-- self:SetSquadronTakeoffFromParkingCold( SquadronName ) +-- self:SetSquadronLandingAtEngineShutdown( SquadronName ) +-- +-- for SpawnTemplate, DefenderSpawn in pairs( self.DefenderSpawns ) do +-- DefenderSpawn:InitUnControlled() +-- end +-- +-- end + + --- Check if the Squadron is visible before startup of the dispatcher. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #bool true if visible. + -- @usage + -- + -- -- Set the Squadron visible before startup of dispatcher. + -- local IsVisible = AIRDispatcher:IsSquadronVisible( "Mineralnye" ) + -- + function AI_AIR_DISPATCHER:IsSquadronVisible( SquadronName ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron then + return DefenderSquadron.Uncontrolled == true + end + + return nil + + end + + --- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number TakeoffInterval Only Takeoff new units each specified interval in seconds in 10 seconds steps. + -- @usage + -- + -- -- Set the Squadron Takeoff interval every 60 seconds for squadron "SQ50", which is good for a FARP cold start. + -- AIRDispatcher:SetSquadronTakeoffInterval( "SQ50", 60 ) + -- + function AI_AIR_DISPATCHER:SetSquadronTakeoffInterval( SquadronName, TakeoffInterval ) + + self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + if DefenderSquadron then + DefenderSquadron.TakeoffInterval = TakeoffInterval or 0 + DefenderSquadron.TakeoffTime = 0 + end + + end + + + + --- Set the squadron patrol parameters for a specific task type. + -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. + -- + -- - @{#AI_AIR_DISPATCHER:SetSquadronSeadPatrolInterval} for SEAD tasks. + -- - @{#AI_AIR_DISPATCHER:SetSquadronSeadPatrolInterval} for CAS tasks. + -- - @{#AI_AIR_DISPATCHER:SetSquadronSeadPatrolInterval} for BAI tasks. + -- + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that a Patrol is a group, so can consist out of 1 to 4 airplanes. The default is 1 Patrol group. + -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds. + -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds. + -- @param #number Probability Is not in use, you can skip this parameter. + -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI". + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- AIRDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 ) + -- AIRDispatcher:SetSquadronPatrolInterval( "Mineralnye", 2, 30, 60, 1, "SEAD" ) + -- + function AI_AIR_DISPATCHER:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, DefenseTaskType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Patrol = DefenderSquadron[DefenseTaskType] + if Patrol then + Patrol.LowInterval = LowInterval or 180 + Patrol.HighInterval = HighInterval or 600 + Patrol.Probability = Probability or 1 + Patrol.PatrolLimit = PatrolLimit or 1 + Patrol.Scheduler = Patrol.Scheduler or SCHEDULER:New( self ) + local Scheduler = Patrol.Scheduler -- Core.Scheduler#SCHEDULER + local ScheduleID = Patrol.ScheduleID + local Variance = ( Patrol.HighInterval - Patrol.LowInterval ) / 2 + local Repeat = Patrol.LowInterval + Variance + local Randomization = Variance / Repeat + local Start = math.random( 1, Patrol.HighInterval ) + + if ScheduleID then + Scheduler:Stop( ScheduleID ) + end + + Patrol.ScheduleID = Scheduler:Schedule( self, self.SchedulerPatrol, { SquadronName }, Start, Repeat, Randomization ) + else + error( "This squadron does not exist:" .. SquadronName ) + end + + end + + + + --- Set the squadron engage limit for a specific task type. + -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods. + -- + -- - @{#AI_AIR_DISPATCHER:SetSquadronSeadEngageLimit} for SEAD tasks. + -- - @{#AI_AIR_DISPATCHER:SetSquadronSeadEngageLimit} for CAS tasks. + -- - @{#AI_AIR_DISPATCHER:SetSquadronSeadEngageLimit} for BAI tasks. + -- + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron. + -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI". + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Patrol Squadron execution. + -- PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) ) + -- AIRDispatcher:SetSquadronEngageLimit( "Mineralnye", 2, "SEAD" ) -- Engage maximum 2 groups with the enemy for SEAD defense. + -- + function AI_AIR_DISPATCHER:SetSquadronEngageLimit( SquadronName, EngageLimit, DefenseTaskType ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + local Defense = DefenderSquadron[DefenseTaskType] + if Defense then + Defense.EngageLimit = EngageLimit or 1 + else + error( "This squadron does not exist:" .. SquadronName ) + end + + end + + + + + + --- Defines the default amount of extra planes that will take-off as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_AIR_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance AIR missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- AIRDispatcher:SetDefaultOverhead( 1.5 ) + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetDefaultOverhead( Overhead ) + + self.DefenderDefault.Overhead = Overhead + + return self + end + + + --- Defines the amount of extra planes that will take-off as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Overhead The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_AIR_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance AIR missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- AIRDispatcher:SetSquadronOverhead( "SquadronName", 1.5 ) + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadronOverhead( SquadronName, Overhead ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron:SetOverhead( Overhead ) + + return self + end + + + --- Gets the overhead of planes as part of the defense system, in comparison with the attackers. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number The %-tage of Units that dispatching command will allocate to intercept in surplus of detected amount of units. + -- The default overhead is 1, so equal balance. The @{#AI_AIR_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength, + -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance AIR missiles payload, may still be less effective than a F-15C with short missiles... + -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes. + -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: + -- + -- * Higher than 1, will increase the defense unit amounts. + -- * Lower than 1, will decrease the defense unit amounts. + -- + -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group + -- multiplied by the Overhead and rounded up to the smallest integer. + -- + -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution. + -- + -- See example below. + -- + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2. + -- -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 => rounded up gives 3. + -- -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes. + -- -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6 => rounded up gives 6 planes. + -- + -- local SquadronOverhead = AIRDispatcher:GetSquadronOverhead( "SquadronName" ) + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:GetSquadronOverhead( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron:GetOverhead() or self.DefenderDefault.Overhead + end + + + --- Sets the default grouping of new airplanes spawned. + -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. + -- @param #AI_AIR_DISPATCHER self + -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Set a grouping by default per 2 airplanes. + -- AIRDispatcher:SetDefaultGrouping( 2 ) + -- + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetDefaultGrouping( Grouping ) + + self.DefenderDefault.Grouping = Grouping + + return self + end + + + --- Sets the grouping of new airplanes spawned. + -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Grouping The level of grouping that will be applied of the Patrol or GCI defenders. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Set a grouping per 2 airplanes. + -- AIRDispatcher:SetSquadronGrouping( "SquadronName", 2 ) + -- + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadronGrouping( SquadronName, Grouping ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron:SetGrouping( Grouping ) + + return self + end + + + --- Sets the engage probability if the squadron will engage on a detected target. + -- This can be configured per squadron, to ensure that each squadron as a specific defensive probability setting. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number EngageProbability The probability when the squadron will consider to engage the detected target. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Set an defense probability for squadron SquadronName of 50%. + -- -- This will result that this squadron has 50% chance to engage on a detected target. + -- AIRDispatcher:SetSquadronEngageProbability( "SquadronName", 0.5 ) + -- + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadronEngageProbability( SquadronName, EngageProbability ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron:SetEngageProbability( EngageProbability ) + + return self + end + + + --- Defines the default method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- AIRDispatcher:SetDefaultTakeoff( AI_AIR_Dispatcher.Takeoff.Air ) + -- + -- -- Let new flights by default take-off from the runway. + -- AIRDispatcher:SetDefaultTakeoff( AI_AIR_Dispatcher.Takeoff.Runway ) + -- + -- -- Let new flights by default take-off from the airbase hot. + -- AIRDispatcher:SetDefaultTakeoff( AI_AIR_Dispatcher.Takeoff.Hot ) + -- + -- -- Let new flights by default take-off from the airbase cold. + -- AIRDispatcher:SetDefaultTakeoff( AI_AIR_Dispatcher.Takeoff.Cold ) + -- + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetDefaultTakeoff( Takeoff ) + + self.DefenderDefault.Takeoff = Takeoff + + return self + end + + --- Defines the method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- AIRDispatcher:SetSquadronTakeoff( "SquadronName", AI_AIR_Dispatcher.Takeoff.Air ) + -- + -- -- Let new flights take-off from the runway. + -- AIRDispatcher:SetSquadronTakeoff( "SquadronName", AI_AIR_Dispatcher.Takeoff.Runway ) + -- + -- -- Let new flights take-off from the airbase hot. + -- AIRDispatcher:SetSquadronTakeoff( "SquadronName", AI_AIR_Dispatcher.Takeoff.Hot ) + -- + -- -- Let new flights take-off from the airbase cold. + -- AIRDispatcher:SetSquadronTakeoff( "SquadronName", AI_AIR_Dispatcher.Takeoff.Cold ) + -- + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetSquadronTakeoff( SquadronName, Takeoff ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron:SetTakeoff( Takeoff ) + + return self + end + + + --- Gets the default method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- local TakeoffMethod = AIRDispatcher:GetDefaultTakeoff() + -- if TakeOffMethod == , AI_AIR_Dispatcher.Takeoff.InAir then + -- ... + -- end + -- + function AI_AIR_DISPATCHER:GetDefaultTakeoff( ) + + return self.DefenderDefault.Takeoff + end + + --- Gets the method at which new flights will spawn and take-off as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- local TakeoffMethod = AIRDispatcher:GetSquadronTakeoff( "SquadronName" ) + -- if TakeOffMethod == , AI_AIR_Dispatcher.Takeoff.InAir then + -- ... + -- end + -- + function AI_AIR_DISPATCHER:GetSquadronTakeoff( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron:GetTakeoff() or self.DefenderDefault.Takeoff + end + + + --- Sets flights to default take-off in the air, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off in the air. + -- AIRDispatcher:SetDefaultTakeoffInAir() + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetDefaultTakeoffInAir() + + self:SetDefaultTakeoff( AI_AIR_DISPATCHER.Takeoff.Air ) + + return self + end + + + --- Sets flights to take-off in the air, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number TakeoffAltitude (optional) The altitude in meters above the ground. If not given, the default takeoff altitude will be used. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- AIRDispatcher:SetSquadronTakeoffInAir( "SquadronName" ) + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetSquadronTakeoffInAir( SquadronName, TakeoffAltitude ) + + self:SetSquadronTakeoff( SquadronName, AI_AIR_DISPATCHER.Takeoff.Air ) + + if TakeoffAltitude then + self:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) + end + + return self + end + + + --- Sets flights by default to take-off from the runway, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off from the runway. + -- AIRDispatcher:SetDefaultTakeoffFromRunway() + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetDefaultTakeoffFromRunway() + + self:SetDefaultTakeoff( AI_AIR_DISPATCHER.Takeoff.Runway ) + + return self + end + + + --- Sets flights to take-off from the runway, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from the runway. + -- AIRDispatcher:SetSquadronTakeoffFromRunway( "SquadronName" ) + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetSquadronTakeoffFromRunway( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_AIR_DISPATCHER.Takeoff.Runway ) + + return self + end + + + --- Sets flights by default to take-off from the airbase at a hot location, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default take-off at a hot parking spot. + -- AIRDispatcher:SetDefaultTakeoffFromParkingHot() + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetDefaultTakeoffFromParkingHot() + + self:SetDefaultTakeoff( AI_AIR_DISPATCHER.Takeoff.Hot ) + + return self + end + + --- Sets flights to take-off from the airbase at a hot location, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off in the air. + -- AIRDispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" ) + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetSquadronTakeoffFromParkingHot( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_AIR_DISPATCHER.Takeoff.Hot ) + + return self + end + + + --- Sets flights to by default take-off from the airbase at a cold location, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from a cold parking spot. + -- AIRDispatcher:SetDefaultTakeoffFromParkingCold() + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetDefaultTakeoffFromParkingCold() + + self:SetDefaultTakeoff( AI_AIR_DISPATCHER.Takeoff.Cold ) + + return self + end + + + --- Sets flights to take-off from the airbase at a cold location, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights take-off from a cold parking spot. + -- AIRDispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" ) + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetSquadronTakeoffFromParkingCold( SquadronName ) + + self:SetSquadronTakeoff( SquadronName, AI_AIR_DISPATCHER.Takeoff.Cold ) + + return self + end + + + --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. + -- @param #AI_AIR_DISPATCHER self + -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Set the default takeoff altitude when taking off in the air. + -- AIRDispatcher:SetDefaultTakeoffInAirAltitude( 2000 ) -- This makes planes start at 2000 meters above the ground. + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetDefaultTakeoffInAirAltitude( TakeoffAltitude ) + + self.DefenderDefault.TakeoffAltitude = TakeoffAltitude + + return self + end + + --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number TakeoffAltitude The altitude in meters above the ground. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Set the default takeoff altitude when taking off in the air. + -- AIRDispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes planes start at 2000 meters above the ground. + -- + -- @return #AI_AIR_DISPATCHER + -- + function AI_AIR_DISPATCHER:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron.TakeoffAltitude = TakeoffAltitude + + return self + end + + + --- Defines the default method at which flights will land and despawn as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default despawn near the airbase when returning. + -- AIRDispatcher:SetDefaultLanding( AI_AIR_Dispatcher.Landing.NearAirbase ) + -- + -- -- Let new flights by default despawn after landing land at the runway. + -- AIRDispatcher:SetDefaultLanding( AI_AIR_Dispatcher.Landing.AtRunway ) + -- + -- -- Let new flights by default despawn after landing and parking, and after engine shutdown. + -- AIRDispatcher:SetDefaultLanding( AI_AIR_Dispatcher.Landing.AtEngineShutdown ) + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetDefaultLanding( Landing ) + + self.DefenderDefault.Landing = Landing + + return self + end + + + --- Defines the method at which flights will land and despawn as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights despawn near the airbase when returning. + -- AIRDispatcher:SetSquadronLanding( "SquadronName", AI_AIR_Dispatcher.Landing.NearAirbase ) + -- + -- -- Let new flights despawn after landing land at the runway. + -- AIRDispatcher:SetSquadronLanding( "SquadronName", AI_AIR_Dispatcher.Landing.AtRunway ) + -- + -- -- Let new flights despawn after landing and parking, and after engine shutdown. + -- AIRDispatcher:SetSquadronLanding( "SquadronName", AI_AIR_Dispatcher.Landing.AtEngineShutdown ) + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadronLanding( SquadronName, Landing ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron:SetLanding( Landing ) + + return self + end + + + --- Gets the default method at which flights will land and despawn as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights by default despawn near the airbase when returning. + -- local LandingMethod = AIRDispatcher:GetDefaultLanding( AI_AIR_Dispatcher.Landing.NearAirbase ) + -- if LandingMethod == AI_AIR_Dispatcher.Landing.NearAirbase then + -- ... + -- end + -- + function AI_AIR_DISPATCHER:GetDefaultLanding() + + return self.DefenderDefault.Landing + end + + + --- Gets the method at which flights will land and despawn as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let new flights despawn near the airbase when returning. + -- local LandingMethod = AIRDispatcher:GetSquadronLanding( "SquadronName", AI_AIR_Dispatcher.Landing.NearAirbase ) + -- if LandingMethod == AI_AIR_Dispatcher.Landing.NearAirbase then + -- ... + -- end + -- + function AI_AIR_DISPATCHER:GetSquadronLanding( SquadronName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + return DefenderSquadron:GetLanding() or self.DefenderDefault.Landing + end + + + --- Sets flights by default to land and despawn near the airbase in the air, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let flights by default to land near the airbase and despawn. + -- AIRDispatcher:SetDefaultLandingNearAirbase() + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetDefaultLandingNearAirbase() + + self:SetDefaultLanding( AI_AIR_DISPATCHER.Landing.NearAirbase ) + + return self + end + + + --- Sets flights to land and despawn near the airbase in the air, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let flights to land near the airbase and despawn. + -- AIRDispatcher:SetSquadronLandingNearAirbase( "SquadronName" ) + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_AIR_DISPATCHER.Landing.NearAirbase ) + + return self + end + + + --- Sets flights by default to land and despawn at the runway, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let flights by default land at the runway and despawn. + -- AIRDispatcher:SetDefaultLandingAtRunway() + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetDefaultLandingAtRunway() + + self:SetDefaultLanding( AI_AIR_DISPATCHER.Landing.AtRunway ) + + return self + end + + + --- Sets flights to land and despawn at the runway, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let flights land at the runway and despawn. + -- AIRDispatcher:SetSquadronLandingAtRunway( "SquadronName" ) + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadronLandingAtRunway( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_AIR_DISPATCHER.Landing.AtRunway ) + + return self + end + + + --- Sets flights by default to land and despawn at engine shutdown, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let flights by default land and despawn at engine shutdown. + -- AIRDispatcher:SetDefaultLandingAtEngineShutdown() + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetDefaultLandingAtEngineShutdown() + + self:SetDefaultLanding( AI_AIR_DISPATCHER.Landing.AtEngineShutdown ) + + return self + end + + + --- Sets flights to land and despawn at engine shutdown, as part of the defense system. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @usage: + -- + -- local AIRDispatcher = AI_AIR_DISPATCHER:New( ... ) + -- + -- -- Let flights land and despawn at engine shutdown. + -- AIRDispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" ) + -- + -- @return #AI_AIR_DISPATCHER + function AI_AIR_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName ) + + self:SetSquadronLanding( SquadronName, AI_AIR_DISPATCHER.Landing.AtEngineShutdown ) + + return self + end + + --- Set the default fuel treshold when defenders will RTB or Refuel in the air. + -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + -- @param #AI_AIR_DISPATCHER self + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- AIRDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + function AI_AIR_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold ) + + self.DefenderDefault.FuelThreshold = FuelThreshold + + return self + end + + + --- Set the fuel treshold for the squadron when defenders will RTB or Refuel in the air. + -- The fuel treshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- AIRDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + function AI_AIR_DISPATCHER:SetSquadronFuelThreshold( SquadronName, FuelThreshold ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron:SetFuelThreshold( FuelThreshold ) + + return self + end + + --- Set the default tanker where defenders will Refuel in the air. + -- @param #AI_AIR_DISPATCHER self + -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the default fuel treshold. + -- AIRDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + -- -- Now Setup the default tanker. + -- AIRDispatcher:SetDefaultTanker( "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. + function AI_AIR_DISPATCHER:SetDefaultTanker( TankerName ) + + self.DefenderDefault.TankerName = TankerName + + return self + end + + + --- Set the squadron tanker where defenders will Refuel in the air. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. + -- @return #AI_AIR_DISPATCHER + -- @usage + -- + -- -- Now Setup the AIR dispatcher, and initialize it using the Detection object. + -- AIRDispatcher = AI_AIR_DISPATCHER:New( Detection ) + -- + -- -- Now Setup the squadron fuel treshold. + -- AIRDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- + -- -- Now Setup the squadron tanker. + -- AIRDispatcher:SetSquadronTanker( "SquadronName", "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor. + function AI_AIR_DISPATCHER:SetSquadronTanker( SquadronName, TankerName ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron:SetTankerName( TankerName ) + + return self + end + + + --- Set the frequency of communication and the mode of communication for voice overs. + -- @param #AI_AIR_DISPATCHER self + -- @param #string SquadronName The name of the squadron. + -- @param #number RadioFrequency The frequency of communication. + -- @param #number RadioModulation The modulation of communication. + -- @param #number RadioPower The power in Watts of communication. + function AI_AIR_DISPATCHER:SetSquadronRadioFrequency( SquadronName, RadioFrequency, RadioModulation, RadioPower, Language ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + DefenderSquadron:SetRadio( RadioFrequency, RadioModulation, RadioPower, DefenderSquadron.Language ) + end + + + -- TODO: Need to model the resources in a squadron. + + --- @param #AI_AIR_DISPATCHER self + -- @param AI.AI_Air_Squadron#AI_AIR_SQUADRON Squadron + function AI_AIR_DISPATCHER:AddDefenderToSquadron( Squadron, Defender, Size ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + self.Defenders[ DefenderName ] = Squadron + local SquadronResourceCount = Squadron:GetResourceCount() + if SquadronResourceCount then + Squadron:RemoveResources( Size ) + end + self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) + end + + --- @param #AI_AIR_DISPATCHER self + -- @param AI.AI_Air_Squadron#AI_AIR_SQUADRON Squadron + function AI_AIR_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + local SquadronResourceCount = Squadron:GetResourceCount() + if SquadronResourceCount then + Squadron:AddResources( Defender:GetSize() ) + end + self.Defenders[ DefenderName ] = nil + self:F( { DefenderName = DefenderName, SquadronResourceCount = SquadronResourceCount } ) + end + + --- @param #AI_AIR_DISPATCHER self + -- @param Wrapper.Group#GROUP Defender + -- @return AI.AI_Air_Squadron#AI_AIR_SQUADRON The Squadron. + function AI_AIR_DISPATCHER:GetSquadronFromDefender( Defender ) + self.Defenders = self.Defenders or {} + local DefenderName = Defender:GetName() + self:F( { DefenderName = DefenderName } ) + return self.Defenders[ DefenderName ] + end + + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:CountPatrolAirborne( SquadronName, DefenseTaskType ) + + local PatrolCount = 0 + + local DefenderSquadron = self.DefenderSquadrons[SquadronName] + if DefenderSquadron then + for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do + if DefenderTask.SquadronName == SquadronName then + if DefenderTask.Type == DefenseTaskType then + if AIGroup:IsAlive() then + -- Check if the Patrol is patrolling or engaging. If not, this is not a valid Patrol, even if it is alive! + -- The Patrol could be damaged, lost control, or out of fuel! + if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" ) + or DefenderTask.Fsm:Is( "Started" ) then + PatrolCount = PatrolCount + 1 + end + end + end + end + end + end + + return PatrolCount + end + + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:CountDefendersEngaged( AttackerDetection, AttackerCount ) + + -- First, count the active AIGroups Units, targetting the DetectedSet + local DefendersEngaged = 0 + local DefendersTotal = 0 + + local AttackerSet = AttackerDetection.Set + local DefendersMissing = AttackerCount + --DetectedSet:Flush() + + local DefenderTasks = self:GetDefenderTasks() + for DefenderGroup, DefenderTask in pairs( DefenderTasks ) do + local Defender = DefenderGroup -- Wrapper.Group#GROUP + local DefenderTaskTarget = DefenderTask.Target + local DefenderSquadronName = DefenderTask.SquadronName + local DefenderSize = DefenderTask.Size + + -- Count the total of defenders on the battlefield. + --local DefenderSize = Defender:GetInitialSize() + if DefenderTask.Target then + --if DefenderTask.Fsm:Is( "Engaging" ) then + self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) + DefendersTotal = DefendersTotal + DefenderSize + if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then + + local SquadronOverhead = self:GetSquadronOverhead( DefenderSquadronName ) + self:F( { SquadronOverhead = SquadronOverhead } ) + if DefenderSize then + DefendersEngaged = DefendersEngaged + DefenderSize + DefendersMissing = DefendersMissing - DefenderSize / SquadronOverhead + self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) + else + DefendersEngaged = 0 + end + end + --end + end + + + end + + for QueueID, QueueItem in pairs( self.DefenseQueue ) do + local QueueItem = QueueItem -- #AI_AIR_DISPATCHER.DefenseQueueItem + if QueueItem.AttackerDetection and QueueItem.AttackerDetection.ItemID == AttackerDetection.ItemID then + DefendersMissing = DefendersMissing - QueueItem.DefendersNeeded / QueueItem.DefenderSquadron.Overhead + --DefendersEngaged = DefendersEngaged + QueueItem.DefenderGrouping + self:F( { QueueItemName = QueueItem.Defense, QueueItem_ItemID = QueueItem.AttackerDetection.ItemID, DetectedItem = AttackerDetection.ItemID, DefendersMissing = DefendersMissing } ) + end + end + + self:F( { DefenderCount = DefendersEngaged } ) + + return DefendersTotal, DefendersEngaged, DefendersMissing + end + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:CountDefenders( AttackerDetection, DefenderCount, DefenderTaskType ) + + local Friendlies = nil + + local AttackerSet = AttackerDetection.Set + local AttackerCount = AttackerSet:Count() + + local DefenderFriendlies = self:GetDefenderFriendliesNearBy( AttackerDetection ) + + for FriendlyDistance, DefenderFriendlyUnit in UTILS.spairs( DefenderFriendlies or {} ) do + -- We only allow to engage targets as long as the units on both sides are balanced. + if AttackerCount > DefenderCount then + local FriendlyGroup = DefenderFriendlyUnit:GetGroup() -- Wrapper.Group#GROUP + if FriendlyGroup and FriendlyGroup:IsAlive() then + -- Ok, so we have a friendly near the potential target. + -- Now we need to check if the AIGroup has a Task. + local DefenderTask = self:GetDefenderTask( FriendlyGroup ) + if DefenderTask then + -- The Task should be of the same type. + if DefenderTaskType == DefenderTask.Type then + -- If there is no target, then add the AIGroup to the ResultAIGroups for Engagement to the AttackerSet + if DefenderTask.Target == nil then + if DefenderTask.Fsm:Is( "Returning" ) + or DefenderTask.Fsm:Is( "Patrolling" ) then + Friendlies = Friendlies or {} + Friendlies[FriendlyGroup] = FriendlyGroup + DefenderCount = DefenderCount + FriendlyGroup:GetSize() + self:F( { Friendly = FriendlyGroup:GetName(), FriendlyDistance = FriendlyDistance } ) + end + end + end + end + end + else + break + end + end + + return Friendlies + end + + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + local SquadronName = DefenderSquadron.Name + DefendersNeeded = DefendersNeeded or 4 + local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping + DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded + + if self:IsSquadronVisible( SquadronName ) then + + -- Here we Patrol the new planes. + -- The Resources table is filled in advance. + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) -- Choose the template. + + -- We determine the grouping based on the parameters set. + self:F( { DefenderGrouping = DefenderGrouping } ) + + -- New we will form the group to spawn in. + -- We search for the first free resource matching the template. + local DefenderUnitIndex = 1 + local DefenderPatrolTemplate = nil + local DefenderName = nil + for GroupName, DefenderGroup in pairs( DefenderSquadron.Resources[TemplateID] or {} ) do + self:F( { GroupName = GroupName } ) + local DefenderTemplate = _DATABASE:GetGroupTemplate( GroupName ) + if DefenderUnitIndex == 1 then + DefenderPatrolTemplate = UTILS.DeepCopy( DefenderTemplate ) + self.DefenderPatrolIndex = self.DefenderPatrolIndex + 1 + --DefenderPatrolTemplate.name = SquadronName .. "#" .. self.DefenderPatrolIndex .. "#" .. GroupName + DefenderPatrolTemplate.name = GroupName + DefenderName = DefenderPatrolTemplate.name + else + -- Add the unit in the template to the DefenderPatrolTemplate. + local DefenderUnitTemplate = DefenderTemplate.units[1] + DefenderPatrolTemplate.units[DefenderUnitIndex] = DefenderUnitTemplate + end + DefenderPatrolTemplate.units[DefenderUnitIndex].name = string.format( DefenderPatrolTemplate.name .. '-%02d', DefenderUnitIndex ) + DefenderPatrolTemplate.units[DefenderUnitIndex].unitId = nil + DefenderUnitIndex = DefenderUnitIndex + 1 + DefenderSquadron.Resources[TemplateID][GroupName] = nil + if DefenderUnitIndex > DefenderGrouping then + break + end + + end + + if DefenderPatrolTemplate then + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local SpawnGroup = GROUP:Register( DefenderName ) + DefenderPatrolTemplate.lateActivation = nil + DefenderPatrolTemplate.uncontrolled = nil + local Takeoff = self:GetSquadronTakeoff( SquadronName ) + DefenderPatrolTemplate.route.points[1].type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + DefenderPatrolTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + local Defender = _DATABASE:Spawn( DefenderPatrolTemplate ) + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + Defender:Activate() + return Defender, DefenderGrouping + end + else + local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN + if DefenderGrouping then + Spawn:InitGrouping( DefenderGrouping ) + else + Spawn:InitGrouping() + end + + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local Defender = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + return Defender, DefenderGrouping + end + + return nil, nil + end + + + --- + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:ResourceQueue( Patrol, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName ) + + self:F( { DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName } ) + + local DefenseQueueItem = {} -- #AI_AIR_DISPATCHER.DefenderQueueItem + + + DefenseQueueItem.Patrol = Patrol + DefenseQueueItem.DefenderSquadron = DefenderSquadron + DefenseQueueItem.DefendersNeeded = DefendersNeeded + DefenseQueueItem.Defense = Defense + DefenseQueueItem.DefenseTaskType = DefenseTaskType + DefenseQueueItem.AttackerDetection = AttackerDetection + DefenseQueueItem.SquadronName = SquadronName + + table.insert( self.DefenseQueue, DefenseQueueItem ) + self:F( { QueueItems = #self.DefenseQueue } ) + + end + + + + + --- Shows the tactical display. + -- @param #AI_AIR_DISPATCHER self + function AI_AIR_DISPATCHER:ShowTacticalDisplay( Detection ) + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local TaskReport = REPORT:New() + + local DefenseTotal = 0 + + local Report = REPORT:New( "\nTactical Overview" ) + + local DefenderGroupCount = 0 + local DefendersTotal = 0 + + -- Now that all obsolete tasks are removed, loop through the detected targets. + --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) < self:Order(t[b]) end ) do + + if not self.Detection:IsDetectedItemLocked( DetectedItem ) == true then + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT + local DetectedCount = DetectedSet:Count() + local DetectedZone = DetectedItem.Zone + + self:F( { "Target ID", DetectedItem.ItemID } ) + + self:F( { DefenseLimit = self.DefenseLimit, DefenseTotal = DefenseTotal } ) + DetectedSet:Flush( self ) + + local DetectedID = DetectedItem.ID + local DetectionIndex = DetectedItem.Index + local DetectedItemChanged = DetectedItem.Changed + + -- Show tactical situation + local ThreatLevel = DetectedItem.Set:CalculateThreatLevelAIR() + Report:Add( string.format( " - %1s%s ( %04s ): ( #%02d - %-4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep( "â– ", ThreatLevel ) ) ) + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + local Defender = Defender -- Wrapper.Group#GROUP + if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then + if Defender:IsAlive() then + DefenderGroupCount = DefenderGroupCount + 1 + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Defender:GetName(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + end + end + + Report:Add( "\n - No Targets:") + local TaskCount = 0 + for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do + TaskCount = TaskCount + 1 + local Defender = Defender -- Wrapper.Group#GROUP + if not DefenderTask.Target then + if Defender:IsAlive() then + local DefenderHasTask = Defender:HasTask() + local Fuel = Defender:GetFuelMin() * 100 + local Damage = Defender:GetLife() / Defender:GetLife0() * 100 + DefenderGroupCount = DefenderGroupCount + 1 + Report:Add( string.format( " - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", + Defender:GetName(), + DefenderTask.Type, + DefenderTask.Fsm:GetState(), + Defender:GetSize(), + Fuel, + Damage, + Defender:HasTask() == true and "Executing" or "Idle" ) ) + end + end + end + Report:Add( string.format( "\n - %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) + + Report:Add( string.format( "\n - %d Queued Aircraft Launches", #self.DefenseQueue ) ) + for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do + local DefenseQueueItem = DefenseQueueItem -- #AI_AIR_DISPATCHER.DefenseQueueItem + Report:Add( string.format( " - %s - %s", DefenseQueueItem.SquadronName, DefenseQueueItem.DefenderSquadron.TakeoffTime, DefenseQueueItem.DefenderSquadron.TakeoffInterval) ) + + end + + Report:Add( string.format( "\n - Squadron Resources: ", #self.DefenseQueue ) ) + for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do + Report:Add( string.format( " - %s - %d", DefenderSquadronName, DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount or "n/a" ) ) + end + + self:F( Report:Text( "\n" ) ) + trigger.action.outText( Report:Text( "\n" ), 25 ) + + end + +end + + +do + + --- Calculates which HUMAN friendlies are nearby the area. + -- @param #AI_AIR_DISPATCHER self + -- @param DetectedItem The detected item. + -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. + function AI_AIR_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem ) + + local DetectedSet = DetectedItem.Set + local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem ) + + local PlayerTypes = {} + local PlayersCount = 0 + + if PlayersNearBy then + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelAIR() + for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do + local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT + local PlayerName = PlayerUnit:GetPlayerName() + --self:F( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } ) + if PlayerUnit:IsAirPlane() and PlayerName ~= nil then + local FriendlyUnitThreatLevel = PlayerUnit:GetThreatLevel() + PlayersCount = PlayersCount + 1 + local PlayerType = PlayerUnit:GetTypeName() + PlayerTypes[PlayerName] = PlayerType + if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + end + end + end + + end + + --self:F( { PlayersCount = PlayersCount } ) + + local PlayerTypesReport = REPORT:New() + + if PlayersCount > 0 then + for PlayerName, PlayerType in pairs( PlayerTypes ) do + PlayerTypesReport:Add( string.format('"%s" in %s', PlayerName, PlayerType ) ) + end + else + PlayerTypesReport:Add( "-" ) + end + + + return PlayersCount, PlayerTypesReport + end + + --- Calculates which friendlies are nearby the area. + -- @param #AI_AIR_DISPATCHER self + -- @param DetectedItem The detected item. + -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type. + function AI_AIR_DISPATCHER:GetFriendliesNearBy( DetectedItem ) + + local DetectedSet = DetectedItem.Set + local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem ) + + local FriendlyTypes = {} + local FriendliesCount = 0 + + if FriendlyUnitsNearBy then + local DetectedTreatLevel = DetectedSet:CalculateThreatLevelAIR() + for FriendlyUnitName, FriendlyUnitData in pairs( FriendlyUnitsNearBy ) do + local FriendlyUnit = FriendlyUnitData -- Wrapper.Unit#UNIT + if FriendlyUnit:IsAirPlane() then + local FriendlyUnitThreatLevel = FriendlyUnit:GetThreatLevel() + FriendliesCount = FriendliesCount + 1 + local FriendlyType = FriendlyUnit:GetTypeName() + FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and ( FriendlyTypes[FriendlyType] + 1 ) or 1 + if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then + end + end + end + + end + + --self:F( { FriendliesCount = FriendliesCount } ) + + local FriendlyTypesReport = REPORT:New() + + if FriendliesCount > 0 then + for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do + FriendlyTypesReport:Add( string.format("%d of %s", FriendlyTypeCount, FriendlyType ) ) + end + else + FriendlyTypesReport:Add( "-" ) + end + + + return FriendliesCount, FriendlyTypesReport + end + + +end + diff --git a/Moose Development/Moose/AI/AI_Air_Engage.lua b/Moose Development/Moose/AI/AI_Air_Engage.lua new file mode 100644 index 000000000..2679d472d --- /dev/null +++ b/Moose Development/Moose/AI/AI_Air_Engage.lua @@ -0,0 +1,597 @@ +--- **AI** -- Models the process of air to ground engagement for airplanes and helicopters. +-- +-- This is a class used in the @{AI_A2G_Dispatcher}. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Air_Engage +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_AIR_ENGAGE +-- @extends AI.AI_AIR#AI_AIR + + +--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. +-- +-- ![Process](..\Presentations\AI_GCI\Dia3.JPG) +-- +-- The AI_AIR_ENGAGE is assigned a @{Wrapper.Group} and this must be done before the AI_AIR_ENGAGE process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_GCI\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_GCI\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_GCI\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_GCI\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_GCI\Dia13.JPG) +-- +-- ## 1. AI_AIR_ENGAGE constructor +-- +-- * @{#AI_AIR_ENGAGE.New}(): Creates a new AI_AIR_ENGAGE object. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_GCI\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_GCI#AI_AIR_ENGAGE.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_GCI\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_AIR_ENGAGE.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_AIR_ENGAGE +AI_AIR_ENGAGE = { + ClassName = "AI_AIR_ENGAGE", +} + + + +--- Creates a new AI_AIR_ENGAGE object +-- @param #AI_AIR_ENGAGE self +-- @param AI.AI_Air#AI_AIR AI_Air The AI_AIR FSM. +-- @param Wrapper.Group#GROUP AIGroup The AI group. +-- @param DCS#Speed EngageMinSpeed (optional, default = 50% of max speed) The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement. +-- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement. +-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO". +-- @return #AI_AIR_ENGAGE +function AI_AIR_ENGAGE:New( AI_Air, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_Air ) -- #AI_AIR_ENGAGE + + self.Accomplished = false + self.Engaging = false + + local SpeedMax = AIGroup:GetSpeedMax() + + self.EngageMinSpeed = EngageMinSpeed or SpeedMax * 0.5 + self.EngageMaxSpeed = EngageMaxSpeed or SpeedMax * 0.75 + self.EngageFloorAltitude = EngageFloorAltitude or 1000 + self.EngageCeilingAltitude = EngageCeilingAltitude or 1500 + self.EngageAltType = EngageAltType or "RADIO" + + self:AddTransition( { "Started", "Engaging", "Returning", "Airborne", "Patrolling" }, "EngageRoute", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. + + --- OnBefore Transition Handler for Event EngageRoute. + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeEngageRoute + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event EngageRoute. + -- @function [parent=#AI_AIR_ENGAGE] OnAfterEngageRoute + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event EngageRoute. + -- @function [parent=#AI_AIR_ENGAGE] EngageRoute + -- @param #AI_AIR_ENGAGE self + + --- Asynchronous Event Trigger for Event EngageRoute. + -- @function [parent=#AI_AIR_ENGAGE] __EngageRoute + -- @param #AI_AIR_ENGAGE self + -- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_AIR_ENGAGE] OnLeaveEngaging +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_AIR_ENGAGE] OnEnterEngaging +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( { "Started", "Engaging", "Returning", "Airborne", "Patrolling" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. + + --- OnBefore Transition Handler for Event Engage. + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeEngage + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Engage. + -- @function [parent=#AI_AIR_ENGAGE] OnAfterEngage + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Engage. + -- @function [parent=#AI_AIR_ENGAGE] Engage + -- @param #AI_AIR_ENGAGE self + + --- Asynchronous Event Trigger for Event Engage. + -- @function [parent=#AI_AIR_ENGAGE] __Engage + -- @param #AI_AIR_ENGAGE self + -- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_AIR_ENGAGE] OnLeaveEngaging +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Engaging. +-- @function [parent=#AI_AIR_ENGAGE] OnEnterEngaging +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. + + --- OnBefore Transition Handler for Event Fired. + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeFired + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Fired. + -- @function [parent=#AI_AIR_ENGAGE] OnAfterFired + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Fired. + -- @function [parent=#AI_AIR_ENGAGE] Fired + -- @param #AI_AIR_ENGAGE self + + --- Asynchronous Event Trigger for Event Fired. + -- @function [parent=#AI_AIR_ENGAGE] __Fired + -- @param #AI_AIR_ENGAGE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. + + --- OnBefore Transition Handler for Event Destroy. + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeDestroy + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Destroy. + -- @function [parent=#AI_AIR_ENGAGE] OnAfterDestroy + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_AIR_ENGAGE] Destroy + -- @param #AI_AIR_ENGAGE self + + --- Asynchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_AIR_ENGAGE] __Destroy + -- @param #AI_AIR_ENGAGE self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. + + --- OnBefore Transition Handler for Event Abort. + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeAbort + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Abort. + -- @function [parent=#AI_AIR_ENGAGE] OnAfterAbort + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Abort. + -- @function [parent=#AI_AIR_ENGAGE] Abort + -- @param #AI_AIR_ENGAGE self + + --- Asynchronous Event Trigger for Event Abort. + -- @function [parent=#AI_AIR_ENGAGE] __Abort + -- @param #AI_AIR_ENGAGE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE. + + --- OnBefore Transition Handler for Event Accomplish. + -- @function [parent=#AI_AIR_ENGAGE] OnBeforeAccomplish + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Accomplish. + -- @function [parent=#AI_AIR_ENGAGE] OnAfterAccomplish + -- @param #AI_AIR_ENGAGE self + -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_AIR_ENGAGE] Accomplish + -- @param #AI_AIR_ENGAGE self + + --- Asynchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_AIR_ENGAGE] __Accomplish + -- @param #AI_AIR_ENGAGE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( { "Patrolling", "Engaging" }, "Refuel", "Refuelling" ) + + return self +end + +--- onafter event handler for Start event. +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The AI group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_ENGAGE:onafterStart( AIGroup, From, Event, To ) + + self:GetParent( self, AI_AIR_ENGAGE ).onafterStart( self, AIGroup, From, Event, To ) + + AIGroup:HandleEvent( EVENTS.Takeoff, nil, self ) + +end + + + +--- onafter event handler for Engage event. +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_ENGAGE:onafterEngage( AIGroup, From, Event, To ) + -- TODO: This function is overwritten below! + self:HandleEvent( EVENTS.Dead ) +end + +-- todo: need to fix this global function + + +--- onbefore event handler for Engage event. +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_ENGAGE:onbeforeEngage( AIGroup, From, Event, To ) + if self.Accomplished == true then + return false + end + return true +end + +--- onafter event handler for Abort event. +-- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The AI Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_ENGAGE:onafterAbort( AIGroup, From, Event, To ) + AIGroup:ClearTasks() + self:Return() +end + + +--- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_ENGAGE:onafterAccomplish( AIGroup, From, Event, To ) + self.Accomplished = true + --self:SetDetectionOff() +end + +--- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @param Core.Event#EVENTDATA EventData +function AI_AIR_ENGAGE:onafterDestroy( AIGroup, From, Event, To, EventData ) + + if EventData.IniUnit then + self.AttackUnits[EventData.IniUnit] = nil + end +end + +--- @param #AI_AIR_ENGAGE self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR_ENGAGE:OnEventDead( EventData ) + self:F( { "EventDead", EventData } ) + + if EventData.IniDCSUnit then + if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then + self:__Destroy( self.TaskDelay, EventData ) + end + end +end + + +--- @param Wrapper.Group#GROUP AIControllable +function AI_AIR_ENGAGE.___EngageRoute( AIGroup, Fsm, AttackSetUnit ) + Fsm:I(string.format("AI_AIR_ENGAGE.___EngageRoute: %s", tostring(AIGroup:GetName()))) + + if AIGroup and AIGroup:IsAlive() then + Fsm:__EngageRoute( Fsm.TaskDelay or 0.1, AttackSetUnit ) + end +end + + +--- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit ) + self:I( { DefenderGroup, From, Event, To, AttackSetUnit } ) + + local DefenderGroupName = DefenderGroup:GetName() + + self.AttackSetUnit = AttackSetUnit -- Kept in memory in case of resume from refuel in air! + + local AttackCount = AttackSetUnit:Count() + + if AttackCount > 0 then + + if DefenderGroup:IsAlive() then + + local EngageAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) + local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + + -- Determine the distance to the target. + -- If it is less than 10km, then attack without a route. + -- Otherwise perform a route attack. + + local DefenderCoord = DefenderGroup:GetPointVec3() + DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetCoord = AttackSetUnit:GetFirst():GetPointVec3() + TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) + + -- TODO: A factor of * 3 is way too close. This causes the AI not to engange until merged sometimes! + if TargetDistance <= EngageDistance * 9 then + + self:I(string.format("AI_AIR_ENGAGE onafterEngageRoute ==> __Engage - target distance = %.1f km", TargetDistance/1000)) + self:__Engage( 0.1, AttackSetUnit ) + + else + + self:I(string.format("FF AI_AIR_ENGAGE onafterEngageRoute ==> Routing - target distance = %.1f km", TargetDistance/1000)) + + local EngageRoute = {} + local AttackTasks = {} + + --- Calculate the target route point. + + local FromWP = DefenderCoord:WaypointAir(self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, EngageSpeed, true) + + EngageRoute[#EngageRoute+1] = FromWP + + self:SetTargetDistance( TargetCoord ) -- For RTB status check + + local FromEngageAngle = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) ) + local ToCoord=DefenderCoord:Translate( EngageDistance, FromEngageAngle, true ) + + local ToWP = ToCoord:WaypointAir(self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, EngageSpeed, true) + + EngageRoute[#EngageRoute+1] = ToWP + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_AIR_ENGAGE.___EngageRoute", self, AttackSetUnit ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + + DefenderGroup:OptionROEReturnFire() + DefenderGroup:OptionROTEvadeFire() + + DefenderGroup:Route( EngageRoute, self.TaskDelay or 0.1 ) + end + + end + else + -- TODO: This will make an A2A Dispatcher CAP flight to return rather than going back to patrolling! + self:I( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + end +end + + +--- @param Wrapper.Group#GROUP AIControllable +function AI_AIR_ENGAGE.___Engage( AIGroup, Fsm, AttackSetUnit ) + + Fsm:I(string.format("AI_AIR_ENGAGE.___Engage: %s", tostring(AIGroup:GetName()))) + + if AIGroup and AIGroup:IsAlive() then + local delay=Fsm.TaskDelay or 0.1 + Fsm:__Engage(delay, AttackSetUnit) + end +end + + +--- @param #AI_AIR_ENGAGE self +-- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + + local DefenderGroupName = DefenderGroup:GetName() + + self.AttackSetUnit = AttackSetUnit -- Kept in memory in case of resume from refuel in air! + + local AttackCount = AttackSetUnit:Count() + self:I({AttackCount = AttackCount}) + + if AttackCount > 0 then + + if DefenderGroup and DefenderGroup:IsAlive() then + + local EngageAltitude = math.random( self.EngageFloorAltitude or 500, self.EngageCeilingAltitude or 1000 ) + local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + + local DefenderCoord = DefenderGroup:GetPointVec3() + DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetCoord = AttackSetUnit:GetFirst():GetPointVec3() + TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude. + + local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) + + local EngageRoute = {} + local AttackTasks = {} + + local FromWP = DefenderCoord:WaypointAir(self.EngageAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, EngageSpeed, true) + EngageRoute[#EngageRoute+1] = FromWP + + self:SetTargetDistance( TargetCoord ) -- For RTB status check + + local FromEngageAngle = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) ) + local ToCoord=DefenderCoord:Translate( EngageDistance, FromEngageAngle, true ) + + local ToWP = ToCoord:WaypointAir(self.EngageAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, EngageSpeed, true) + EngageRoute[#EngageRoute+1] = ToWP + + -- TODO: A factor of * 3 this way too low. This causes the AI NOT to engage until very close or even merged sometimes. Some A2A missiles have a much longer range! Needs more frequent updates of the task! + if TargetDistance <= EngageDistance * 9 then + + local AttackUnitTasks = self:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) -- Polymorphic + + if #AttackUnitTasks == 0 then + self:I( DefenderGroupName .. ": No valid targets found -> Going RTB") + self:Return() + return + else + local text=string.format("%s: Engaging targets at distance %.2f NM", DefenderGroupName, UTILS.MetersToNM(TargetDistance)) + self:I(text) + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTEvadeFire() + DefenderGroup:OptionKeepWeaponsOnThreat() + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskCombo( AttackUnitTasks ) + end + end + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_AIR_ENGAGE.___Engage", self, AttackSetUnit ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + + DefenderGroup:Route( EngageRoute, self.TaskDelay or 0.1 ) + + end + else + -- TODO: This will make an A2A Dispatcher CAP flight to return rather than going back to patrolling! + self:I( DefenderGroupName .. ": No targets found -> returning.") + self:Return() + return + end +end + +--- @param Wrapper.Group#GROUP AIEngage +function AI_AIR_ENGAGE.Resume( AIEngage, Fsm ) + + AIEngage:F( { "Resume:", AIEngage:GetName() } ) + if AIEngage and AIEngage:IsAlive() then + Fsm:__Reset( Fsm.TaskDelay or 0.1 ) + Fsm:__EngageRoute( Fsm.TaskDelay or 0.2, Fsm.AttackSetUnit ) + end + +end diff --git a/Moose Development/Moose/AI/AI_Air_Patrol.lua b/Moose Development/Moose/AI/AI_Air_Patrol.lua new file mode 100644 index 000000000..32bd99cea --- /dev/null +++ b/Moose Development/Moose/AI/AI_Air_Patrol.lua @@ -0,0 +1,398 @@ +--- **AI** -- Models the process of A2G patrolling and engaging ground targets for airplanes and helicopters. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Air_Patrol +-- @image AI_Air_To_Ground_Patrol.JPG + +--- @type AI_AIR_PATROL +-- @extends AI.AI_Air#AI_AIR + + +--- The AI_AIR_PATROL class implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group} +-- and automatically engage any airborne enemies that are within a certain range or within a certain zone. +-- +-- ![Process](..\Presentations\AI_CAP\Dia3.JPG) +-- +-- The AI_AIR_PATROL is assigned a @{Wrapper.Group} and this must be done before the AI_AIR_PATROL process can be started using the **Start** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia4.JPG) +-- +-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits. +-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits. +-- +-- ![Process](..\Presentations\AI_CAP\Dia5.JPG) +-- +-- This cycle will continue. +-- +-- ![Process](..\Presentations\AI_CAP\Dia6.JPG) +-- +-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event. +-- +-- ![Process](..\Presentations\AI_CAP\Dia9.JPG) +-- +-- When enemies are detected, the AI will automatically engage the enemy. +-- +-- ![Process](..\Presentations\AI_CAP\Dia10.JPG) +-- +-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB. +-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land. +-- +-- ![Process](..\Presentations\AI_CAP\Dia13.JPG) +-- +-- ## 1. AI_AIR_PATROL constructor +-- +-- * @{#AI_AIR_PATROL.New}(): Creates a new AI_AIR_PATROL object. +-- +-- ## 2. AI_AIR_PATROL is a FSM +-- +-- ![Process](..\Presentations\AI_CAP\Dia2.JPG) +-- +-- ### 2.1 AI_AIR_PATROL States +-- +-- * **None** ( Group ): The process is not started yet. +-- * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone. +-- * **Engaging** ( Group ): The AI is engaging the bogeys. +-- * **Returning** ( Group ): The AI is returning to Base.. +-- +-- ### 2.2 AI_AIR_PATROL Events +-- +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.PatrolRoute}**: Route the AI to a new random 3D point within the Patrol Zone. +-- * **@{#AI_AIR_PATROL.Engage}**: Let the AI engage the bogeys. +-- * **@{#AI_AIR_PATROL.Abort}**: Aborts the engagement and return patrolling in the patrol zone. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. +-- * **@{#AI_AIR_PATROL.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. +-- * **@{#AI_AIR_PATROL.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. +-- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. +-- +-- ## 3. Set the Range of Engagement +-- +-- ![Range](..\Presentations\AI_CAP\Dia11.JPG) +-- +-- An optional range can be set in meters, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- The range can be beyond or smaller than the range of the Patrol Zone. +-- The range is applied at the position of the AI. +-- Use the method @{AI.AI_CAP#AI_AIR_PATROL.SetEngageRange}() to define that range. +-- +-- ## 4. Set the Zone of Engagement +-- +-- ![Zone](..\Presentations\AI_CAP\Dia12.JPG) +-- +-- An optional @{Zone} can be set, +-- that will define when the AI will engage with the detected airborne enemy targets. +-- Use the method @{AI.AI_Cap#AI_AIR_PATROL.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_AIR_PATROL +AI_AIR_PATROL = { + ClassName = "AI_AIR_PATROL", +} + +--- Creates a new AI_AIR_PATROL object +-- @param #AI_AIR_PATROL self +-- @param AI.AI_Air#AI_AIR AI_Air The AI_AIR FSM. +-- @param Wrapper.Group#GROUP AIGroup The AI group. +-- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. +-- @param DCS#Altitude PatrolFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO. +-- @return #AI_AIR_PATROL +function AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_Air ) -- #AI_AIR_PATROL + + local SpeedMax = AIGroup:GetSpeedMax() + + self.PatrolZone = PatrolZone + + self.PatrolFloorAltitude = PatrolFloorAltitude or 1000 + self.PatrolCeilingAltitude = PatrolCeilingAltitude or 1500 + self.PatrolMinSpeed = PatrolMinSpeed or SpeedMax * 0.5 + self.PatrolMaxSpeed = PatrolMaxSpeed or SpeedMax * 0.75 + + -- defafult PatrolAltType to "RADIO" if not specified + self.PatrolAltType = PatrolAltType or "RADIO" + + self:AddTransition( { "Started", "Airborne", "Refuelling" }, "Patrol", "Patrolling" ) + + --- OnBefore Transition Handler for Event Patrol. + -- @function [parent=#AI_AIR_PATROL] OnBeforePatrol + -- @param #AI_AIR_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Patrol. + -- @function [parent=#AI_AIR_PATROL] OnAfterPatrol + -- @param #AI_AIR_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Patrol. + -- @function [parent=#AI_AIR_PATROL] Patrol + -- @param #AI_AIR_PATROL self + + --- Asynchronous Event Trigger for Event Patrol. + -- @function [parent=#AI_AIR_PATROL] __Patrol + -- @param #AI_AIR_PATROL self + -- @param #number Delay The delay in seconds. + + --- OnLeave Transition Handler for State Patrolling. + -- @function [parent=#AI_AIR_PATROL] OnLeavePatrolling + -- @param #AI_AIR_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnEnter Transition Handler for State Patrolling. + -- @function [parent=#AI_AIR_PATROL] OnEnterPatrolling + -- @param #AI_AIR_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + self:AddTransition( "Patrolling", "PatrolRoute", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_PATROL. + + --- OnBefore Transition Handler for Event PatrolRoute. + -- @function [parent=#AI_AIR_PATROL] OnBeforePatrolRoute + -- @param #AI_AIR_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event PatrolRoute. + -- @function [parent=#AI_AIR_PATROL] OnAfterPatrolRoute + -- @param #AI_AIR_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event PatrolRoute. + -- @function [parent=#AI_AIR_PATROL] PatrolRoute + -- @param #AI_AIR_PATROL self + + --- Asynchronous Event Trigger for Event PatrolRoute. + -- @function [parent=#AI_AIR_PATROL] __PatrolRoute + -- @param #AI_AIR_PATROL self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_PATROL. + + return self +end + + +--- Set the Engage Range when the AI will engage with airborne enemies. +-- @param #AI_AIR_PATROL self +-- @param #number EngageRange The Engage Range. +-- @return #AI_AIR_PATROL self +function AI_AIR_PATROL:SetEngageRange( EngageRange ) + self:F2() + + if EngageRange then + self.EngageRange = EngageRange + else + self.EngageRange = nil + end +end + +--- Set race track parameters. CAP flights will perform race track patterns rather than randomly patrolling the zone. +-- @param #AI_AIR_PATROL self +-- @param #number LegMin Min Length of the race track leg in meters. Default 10,000 m. +-- @param #number LegMax Max length of the race track leg in meters. Default 15,000 m. +-- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. from South to North. +-- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. from South to North. +-- @param #number DurationMin (Optional) Min duration before switching the orbit position. Default is keep same orbit until RTB or engage. +-- @param #number DurationMax (Optional) Max duration before switching the orbit position. Default is keep same orbit until RTB or engage. +-- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. +-- @return #AI_AIR_PATROL self +function AI_AIR_PATROL:SetRaceTrackPattern(LegMin, LegMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates) + + self.racetrack=true + self.racetracklegmin=LegMin or 10000 + self.racetracklegmax=LegMax or 15000 + self.racetrackheadingmin=HeadingMin or 0 + self.racetrackheadingmax=HeadingMax or 180 + self.racetrackdurationmin=DurationMin + self.racetrackdurationmax=DurationMax + + if self.racetrackdurationmax and not self.racetrackdurationmin then + self.racetrackdurationmin=self.racetrackdurationmax + end + + self.racetrackcapcoordinates=CapCoordinates + +end + + + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_AIR_PATROL self +-- @return #AI_AIR_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_PATROL:onafterPatrol( AIPatrol, From, Event, To ) + self:F2() + + self:ClearTargetDistance() + + self:__PatrolRoute( self.TaskDelay ) + + AIPatrol:OnReSpawn( + function( PatrolGroup ) + self:__Reset( self.TaskDelay ) + self:__PatrolRoute( self.TaskDelay ) + end + ) +end + +--- This statis method is called from the route path within the last task at the last waaypoint of the AIPatrol. +-- Note that this method is required, as triggers the next route when patrolling for the AIPatrol. +-- @param Wrapper.Group#GROUP AIPatrol The AI group. +-- @param #AI_AIR_PATROL Fsm The FSM. +function AI_AIR_PATROL.___PatrolRoute( AIPatrol, Fsm ) + + AIPatrol:F( { "AI_AIR_PATROL.___PatrolRoute:", AIPatrol:GetName() } ) + + if AIPatrol and AIPatrol:IsAlive() then + Fsm:PatrolRoute() + end + +end + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_AIR_PATROL self +-- @param Wrapper.Group#GROUP AIPatrol The Group managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To ) + + self:F2() + + -- When RTB, don't allow anymore the routing. + if From == "RTB" then + return + end + + + if AIPatrol and AIPatrol:IsAlive() then + + local PatrolRoute = {} + + --- Calculate the target route point. + + local CurrentCoord = AIPatrol:GetCoordinate() + + local altitude= math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) + + local ToTargetCoord = self.PatrolZone:GetRandomPointVec2() + ToTargetCoord:SetAlt( altitude ) + self:SetTargetDistance( ToTargetCoord ) -- For RTB status check + + local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + local speedkmh=ToTargetSpeed + + local FromWP = CurrentCoord:WaypointAir(self.PatrolAltType or "RADIO", POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, true) + PatrolRoute[#PatrolRoute+1] = FromWP + + if self.racetrack then + + -- Random heading. + local heading = math.random(self.racetrackheadingmin, self.racetrackheadingmax) + + -- Random leg length. + local leg=math.random(self.racetracklegmin, self.racetracklegmax) + + -- Random duration if any. + local duration = self.racetrackdurationmin + if self.racetrackdurationmax then + duration=math.random(self.racetrackdurationmin, self.racetrackdurationmax) + end + + -- CAP coordinate. + local c0=self.PatrolZone:GetRandomCoordinate() + if self.racetrackcapcoordinates and #self.racetrackcapcoordinates>0 then + c0=self.racetrackcapcoordinates[math.random(#self.racetrackcapcoordinates)] + end + + -- Race track points. + local c1=c0:SetAltitude(altitude) --Core.Point#COORDINATE + local c2=c1:Translate(leg, heading):SetAltitude(altitude) + + self:SetTargetDistance(c0) -- For RTB status check + + -- Debug: + self:T(string.format("Patrol zone race track: v=%.1f knots, h=%.1f ft, heading=%03d, leg=%d m, t=%s sec", UTILS.KmphToKnots(speedkmh), UTILS.MetersToFeet(altitude), heading, leg, tostring(duration))) + --c1:MarkToAll("Race track c1") + --c2:MarkToAll("Race track c2") + + -- Task to orbit. + local taskOrbit=AIPatrol:TaskOrbit(c1, altitude, UTILS.KmphToMps(speedkmh), c2) + + -- Task function to redo the patrol at other random position. + local taskPatrol=AIPatrol:TaskFunction("AI_AIR_PATROL.___PatrolRoute", self) + + -- Controlled task with task condition. + local taskCond=AIPatrol:TaskCondition(nil, nil, nil, nil, duration, nil) + local taskCont=AIPatrol:TaskControlled(taskOrbit, taskCond) + + -- Second waypoint + PatrolRoute[2]=c1:WaypointAirTurningPoint(self.PatrolAltType, speedkmh, {taskCont, taskPatrol}, "CAP Orbit") + + else + + --- Create a route point of type air. + local ToWP = ToTargetCoord:WaypointAir(self.PatrolAltType, POINT_VEC3.RoutePointType.TurningPoint, POINT_VEC3.RoutePointAction.TurningPoint, ToTargetSpeed, true) + PatrolRoute[#PatrolRoute+1] = ToWP + + local Tasks = {} + Tasks[#Tasks+1] = AIPatrol:TaskFunction("AI_AIR_PATROL.___PatrolRoute", self) + PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) + + end + + AIPatrol:OptionROEReturnFire() + AIPatrol:OptionROTEvadeFire() + + AIPatrol:Route( PatrolRoute, self.TaskDelay ) + + end + +end + +--- @param Wrapper.Group#GROUP AIPatrol +function AI_AIR_PATROL.Resume( AIPatrol, Fsm ) + + AIPatrol:F( { "AI_AIR_PATROL.Resume:", AIPatrol:GetName() } ) + if AIPatrol and AIPatrol:IsAlive() then + Fsm:__Reset( Fsm.TaskDelay ) + Fsm:__PatrolRoute( Fsm.TaskDelay ) + end + +end diff --git a/Moose Development/Moose/AI/AI_Air_Squadron.lua b/Moose Development/Moose/AI/AI_Air_Squadron.lua new file mode 100644 index 000000000..69e660877 --- /dev/null +++ b/Moose Development/Moose/AI/AI_Air_Squadron.lua @@ -0,0 +1,289 @@ +--- **AI** - Models squadrons for airplanes and helicopters. +-- +-- This is a class used in the @{AI_Air_Dispatcher} and derived dispatcher classes. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Air_Squadron +-- @image MOOSE.JPG + + + +--- @type AI_AIR_SQUADRON +-- @extends Core.Base#BASE + + +--- Implements the core functions modeling squadrons for airplanes and helicopters. +-- +-- === +-- +-- @field #AI_AIR_SQUADRON +AI_AIR_SQUADRON = { + ClassName = "AI_AIR_SQUADRON", +} + + + +--- Creates a new AI_AIR_SQUADRON object +-- @param #AI_AIR_SQUADRON self +-- @return #AI_AIR_SQUADRON +function AI_AIR_SQUADRON:New( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) + + self:I( { Air_Squadron = { SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) + + local AI_Air_Squadron = BASE:New() -- #AI_AIR_SQUADRON + + AI_Air_Squadron.Name = SquadronName + AI_Air_Squadron.Airbase = AIRBASE:FindByName( AirbaseName ) + AI_Air_Squadron.AirbaseName = AI_Air_Squadron.Airbase:GetName() + if not AI_Air_Squadron.Airbase then + error( "Cannot find airbase with name:" .. AirbaseName ) + end + + AI_Air_Squadron.Spawn = {} + if type( TemplatePrefixes ) == "string" then + local SpawnTemplate = TemplatePrefixes + self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) + AI_Air_Squadron.Spawn[1] = self.DefenderSpawns[SpawnTemplate] + else + for TemplateID, SpawnTemplate in pairs( TemplatePrefixes ) do + self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 ) + AI_Air_Squadron.Spawn[#AI_Air_Squadron.Spawn+1] = self.DefenderSpawns[SpawnTemplate] + end + end + AI_Air_Squadron.ResourceCount = ResourceCount + AI_Air_Squadron.TemplatePrefixes = TemplatePrefixes + AI_Air_Squadron.Captured = false -- Not captured. This flag will be set to true, when the airbase where the squadron is located, is captured. + + self:SetSquadronLanguage( SquadronName, "EN" ) -- Squadrons speak English by default. + + return AI_Air_Squadron +end + +--- Set the Name of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #string Name The Squadron Name. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetName( Name ) + + self.Name = Name + + return self +end + +--- Get the Name of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #string The Squadron Name. +function AI_AIR_SQUADRON:GetName() + + return self.Name +end + +--- Set the ResourceCount of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number ResourceCount The Squadron ResourceCount. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetResourceCount( ResourceCount ) + + self.ResourceCount = ResourceCount + + return self +end + +--- Get the ResourceCount of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #number The Squadron ResourceCount. +function AI_AIR_SQUADRON:GetResourceCount() + + return self.ResourceCount +end + +--- Add Resources to the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number Resources The Resources to be added. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:AddResources( Resources ) + + self.ResourceCount = self.ResourceCount + Resources + + return self +end + +--- Remove Resources to the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number Resources The Resources to be removed. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:RemoveResources( Resources ) + + self.ResourceCount = self.ResourceCount - Resources + + return self +end + +--- Set the Overhead of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number Overhead The Squadron Overhead. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetOverhead( Overhead ) + + self.Overhead = Overhead + + return self +end + +--- Get the Overhead of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #number The Squadron Overhead. +function AI_AIR_SQUADRON:GetOverhead() + + return self.Overhead +end + +--- Set the Grouping of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number Grouping The Squadron Grouping. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetGrouping( Grouping ) + + self.Grouping = Grouping + + return self +end + +--- Get the Grouping of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #number The Squadron Grouping. +function AI_AIR_SQUADRON:GetGrouping() + + return self.Grouping +end + +--- Set the FuelThreshold of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number FuelThreshold The Squadron FuelThreshold. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetFuelThreshold( FuelThreshold ) + + self.FuelThreshold = FuelThreshold + + return self +end + +--- Get the FuelThreshold of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #number The Squadron FuelThreshold. +function AI_AIR_SQUADRON:GetFuelThreshold() + + return self.FuelThreshold +end + +--- Set the EngageProbability of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number EngageProbability The Squadron EngageProbability. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetEngageProbability( EngageProbability ) + + self.EngageProbability = EngageProbability + + return self +end + +--- Get the EngageProbability of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #number The Squadron EngageProbability. +function AI_AIR_SQUADRON:GetEngageProbability() + + return self.EngageProbability +end + +--- Set the Takeoff of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number Takeoff The Squadron Takeoff. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetTakeoff( Takeoff ) + + self.Takeoff = Takeoff + + return self +end + +--- Get the Takeoff of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #number The Squadron Takeoff. +function AI_AIR_SQUADRON:GetTakeoff() + + return self.Takeoff +end + +--- Set the Landing of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number Landing The Squadron Landing. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetLanding( Landing ) + + self.Landing = Landing + + return self +end + +--- Get the Landing of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #number The Squadron Landing. +function AI_AIR_SQUADRON:GetLanding() + + return self.Landing +end + +--- Set the TankerName of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #string TankerName The Squadron Tanker Name. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetTankerName( TankerName ) + + self.TankerName = TankerName + + return self +end + +--- Get the Tanker Name of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @return #string The Squadron Tanker Name. +function AI_AIR_SQUADRON:GetTankerName() + + return self.TankerName +end + + +--- Set the Radio of the Squadron. +-- @param #AI_AIR_SQUADRON self +-- @param #number RadioFrequency The frequency of communication. +-- @param #number RadioModulation The modulation of communication. +-- @param #number RadioPower The power in Watts of communication. +-- @param #string Language The language of the radio speech. +-- @return #AI_AIR_SQUADRON The Squadron. +function AI_AIR_SQUADRON:SetRadio( RadioFrequency, RadioModulation, RadioPower, Language ) + + self.RadioFrequency = RadioFrequency + self.RadioModulation = RadioModulation or radio.modulation.AM + self.RadioPower = RadioPower or 100 + + if self.RadioSpeech then + self.RadioSpeech:Stop() + end + + self.RadioSpeech = nil + + self.RadioSpeech = RADIOSPEECH:New( RadioFrequency, RadioModulation ) + self.RadioSpeech.power = RadioPower + self.RadioSpeech:Start( 0.5 ) + + self.RadioSpeech:SetLanguage( Language ) + + return self +end + + diff --git a/Moose Development/Moose/AI/AI_Balancer.lua b/Moose Development/Moose/AI/AI_Balancer.lua index 1b18bf79d..9c3be56e7 100644 --- a/Moose Development/Moose/AI/AI_Balancer.lua +++ b/Moose Development/Moose/AI/AI_Balancer.lua @@ -196,8 +196,12 @@ function AI_BALANCER:onenterDestroying( SetGroup, From, Event, To, ClientName, A SetGroup:Flush( self ) end ---- @param #AI_BALANCER self +--- RTB +-- @param #AI_BALANCER self -- @param Core.Set#SET_GROUP SetGroup +-- @param #string From +-- @param #string Event +-- @param #string To -- @param Wrapper.Group#GROUP AIGroup function AI_BALANCER:onenterReturning( SetGroup, From, Event, To, AIGroup ) @@ -213,10 +217,13 @@ function AI_BALANCER:onenterReturning( SetGroup, From, Event, To, AIGroup ) local PointVec2 = POINT_VEC2:New( AIGroup:GetVec2().x, AIGroup:GetVec2().y ) local ClosestAirbase = self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2( PointVec2 ) self:T( ClosestAirbase.AirbaseName ) + --[[ AIGroup:MessageToRed( "Returning to " .. ClosestAirbase:GetName().. " ...", 30 ) local RTBRoute = AIGroup:RouteReturnToAirbase( ClosestAirbase ) AIGroupTemplate.route = RTBRoute AIGroup:Respawn( AIGroupTemplate ) + ]] + AIGroup:RouteRTB(ClosestAirbase) end end diff --git a/Moose Development/Moose/AI/AI_CAP.lua b/Moose Development/Moose/AI/AI_CAP.lua index 1ffdbc23b..5366db4b4 100644 --- a/Moose Development/Moose/AI/AI_CAP.lua +++ b/Moose Development/Moose/AI/AI_CAP.lua @@ -421,7 +421,7 @@ end -- @param #string To The To State string. function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To ) - if Controllable:IsAlive() then + if Controllable and Controllable:IsAlive() then local EngageRoute = {} diff --git a/Moose Development/Moose/AI/AI_Cargo.lua b/Moose Development/Moose/AI/AI_Cargo.lua index ed76eca7b..586c0ab51 100644 --- a/Moose Development/Moose/AI/AI_Cargo.lua +++ b/Moose Development/Moose/AI/AI_Cargo.lua @@ -141,6 +141,17 @@ function AI_CARGO:New( Carrier, CargoSet ) -- @param #string From -- @param #string Event -- @param #string To + + --- On after Deployed event. + -- @function [parent=#AI_CARGO] OnAfterDeployed + -- @param #AI_CARGO self + -- @param Wrapper.Group#GROUP Carrier + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + -- @param #boolean Defend Defend for APCs. + for _, CarrierUnit in pairs( Carrier:GetUnits() ) do local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT @@ -256,9 +267,10 @@ function AI_CARGO:onbeforeLoad( Carrier, From, Event, To, PickupZone ) self:F( { "In radius", CarrierUnit:GetName() } ) local CargoWeight = Cargo:GetWeight() + local CarrierSpace=Carrier_Weight[CarrierUnit] -- Only when there is space within the bay to load the next cargo item! - if Carrier_Weight[CarrierUnit] > CargoWeight then --and CargoBayFreeVolume > CargoVolume then + if CarrierSpace > CargoWeight then Carrier:RouteStop() --Cargo:Ungroup() Cargo:__Board( -LoadDelay, CarrierUnit ) @@ -275,6 +287,8 @@ function AI_CARGO:onbeforeLoad( Carrier, From, Event, To, PickupZone ) -- Ok, we loaded a cargo, now we can stop the loop. break + else + self:T(string.format("WARNING: Cargo too heavy for carrier %s. Cargo=%.1f > %.1f free space", tostring(CarrierUnit:GetName()), CargoWeight, CarrierSpace)) end end end @@ -554,6 +568,7 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- @param #boolean Defend Defend for APCs. function AI_CARGO:onafterDeployed( Carrier, From, Event, To, DeployZone, Defend ) self:F( { Carrier, From, Event, To, DeployZone = DeployZone, Defend = Defend } ) diff --git a/Moose Development/Moose/AI/AI_Cargo_Airplane.lua b/Moose Development/Moose/AI/AI_Cargo_Airplane.lua index 947d7ec5b..2b3277bde 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Airplane.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Airplane.lua @@ -76,25 +76,31 @@ function AI_CARGO_AIRPLANE:New( Airplane, CargoSet ) --- Pickup Handler OnAfter for AI_CARGO_AIRPLANE -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterPickup -- @param #AI_CARGO_AIRPLANE self - -- @param Wrapper.Group#GROUP Airplane Cargo plane. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where troops are picked up. - -- @param #number Speed in km/h for travelling to pickup base. + -- @param Wrapper.Group#GROUP Airplane Cargo transport plane. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff. + -- @param #number Speed Speed in km/h for travelling to pickup base. + -- @param #number Height Height in meters to move to the pickup coordinate. + -- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up. --- Pickup Trigger for AI_CARGO_AIRPLANE -- @function [parent=#AI_CARGO_AIRPLANE] Pickup -- @param #AI_CARGO_AIRPLANE self - -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where troops are picked up. - -- @param #number Speed in km/h for travelling to pickup base. + -- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff. + -- @param #number Speed Speed in km/h for travelling to pickup base. + -- @param #number Height Height in meters to move to the pickup coordinate. + -- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up. --- Pickup Asynchronous Trigger for AI_CARGO_AIRPLANE -- @function [parent=#AI_CARGO_AIRPLANE] __Pickup -- @param #AI_CARGO_AIRPLANE self -- @param #number Delay Delay in seconds. - -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where troops are picked up. - -- @param #number Speed in km/h for travelling to pickup base. + -- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff. + -- @param #number Speed Speed in km/h for travelling to pickup base. + -- @param #number Height Height in meters to move to the pickup coordinate. + -- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up. --- Deploy Handler OnBefore for AI_CARGO_AIRPLANE -- @function [parent=#AI_CARGO_AIRPLANE] OnBeforeDeploy @@ -111,24 +117,30 @@ function AI_CARGO_AIRPLANE:New( Airplane, CargoSet ) -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterDeploy -- @param #AI_CARGO_AIRPLANE self -- @param Wrapper.Group#GROUP Airplane Cargo plane. - -- @param #string From - -- @param #string Event - -- @param #string To - -- @param Wrapper.Airbase#AIRBASE Airbase Destination airbase where troops are deployed. - -- @param #number Speed Speed in km/h for travelling to deploy base. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff. + -- @param #number Speed Speed in km/h for travelling to the deploy base. + -- @param #number Height Height in meters to move to the home coordinate. + -- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. --- Deploy Trigger for AI_CARGO_AIRPLANE -- @function [parent=#AI_CARGO_AIRPLANE] Deploy -- @param #AI_CARGO_AIRPLANE self - -- @param Wrapper.Airbase#AIRBASE Airbase Destination airbase where troops are deployed. - -- @param #number Speed Speed in km/h for travelling to deploy base. + -- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff. + -- @param #number Speed Speed in km/h for travelling to the deploy base. + -- @param #number Height Height in meters to move to the home coordinate. + -- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. --- Deploy Asynchronous Trigger for AI_CARGO_AIRPLANE -- @function [parent=#AI_CARGO_AIRPLANE] __Deploy -- @param #AI_CARGO_AIRPLANE self -- @param #number Delay Delay in seconds. - -- @param Wrapper.Airbase#AIRBASE Airbase Destination airbase where troops are deployed. - -- @param #number Speed Speed in km/h for travelling to deploy base. + -- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff. + -- @param #number Speed Speed in km/h for travelling to the deploy base. + -- @param #number Height Height in meters to move to the home coordinate. + -- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. --- On after Loaded event, i.e. triggered when the cargo is inside the carrier. -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterLoaded @@ -137,6 +149,16 @@ function AI_CARGO_AIRPLANE:New( Airplane, CargoSet ) -- @param From -- @param Event -- @param To + + + --- On after Deployed event. + -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterDeployed + -- @param #AI_CARGO_AIRPLANE self + -- @param Wrapper.Group#GROUP Airplane Cargo plane. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. -- Set carrier. self:SetCarrier( Airplane ) @@ -259,15 +281,17 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Core.Point#COORDINATE Coordinate --- @param #number Speed in km/h for travelling to pickup base. +-- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff. +-- @param #number Speed Speed in km/h for travelling to pickup base. -- @param #number Height Height in meters to move to the pickup coordinate. --- @param Core.Zone#ZONE_AIRBASE (optional) PickupZone The zone where the cargo will be picked up. +-- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up. function AI_CARGO_AIRPLANE:onafterPickup( Airplane, From, Event, To, Coordinate, Speed, Height, PickupZone ) if Airplane and Airplane:IsAlive() then - self.PickupZone = PickupZone + local airbasepickup=Coordinate:GetClosestAirbase() + + self.PickupZone = PickupZone or ZONE_AIRBASE:New(airbasepickup:GetName()) -- Get closest airbase of current position. local ClosestAirbase, DistToAirbase=Airplane:GetCoordinate():GetClosestAirbase() @@ -280,11 +304,10 @@ function AI_CARGO_AIRPLANE:onafterPickup( Airplane, From, Event, To, Coordinate, end -- Set pickup airbase. - local Airbase = PickupZone:GetAirbase() + local Airbase = self.PickupZone:GetAirbase() -- Distance from closest to pickup airbase ==> we need to know if we are already at the pickup airbase. local Dist = Airbase:GetCoordinate():Get2DDistance(ClosestAirbase:GetCoordinate()) - --env.info("Distance closest to pickup airbase = "..Dist) if Airplane:InAir() or Dist>500 then @@ -305,7 +328,7 @@ function AI_CARGO_AIRPLANE:onafterPickup( Airplane, From, Event, To, Coordinate, end - self:GetParent( self, AI_CARGO_AIRPLANE ).onafterPickup( self, Airplane, From, Event, To, Coordinate, Speed, Height, PickupZone ) + self:GetParent( self, AI_CARGO_AIRPLANE ).onafterPickup( self, Airplane, From, Event, To, Coordinate, Speed, Height, self.PickupZone ) end @@ -318,15 +341,19 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Core.Point#COORDINATE Coordinate --- @param #number Speed in km/h for travelling to pickup base. +-- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff. +-- @param #number Speed Speed in km/h for travelling to the deploy base. -- @param #number Height Height in meters to move to the home coordinate. --- @param Core.Zone#ZONE_AIRBASE DeployZone The zone where the cargo will be deployed. +-- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. function AI_CARGO_AIRPLANE:onafterDeploy( Airplane, From, Event, To, Coordinate, Speed, Height, DeployZone ) if Airplane and Airplane:IsAlive()~=nil then - local Airbase = DeployZone:GetAirbase() + local Airbase = Coordinate:GetClosestAirbase() + + if DeployZone then + Airbase=DeployZone:GetAirbase() + end -- Activate uncontrolled airplane. if Airplane:IsAlive()==false then @@ -354,6 +381,7 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. +-- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. function AI_CARGO_AIRPLANE:onafterUnload( Airplane, From, Event, To, DeployZone ) local UnboardInterval = 10 diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua index 311a07b59..424a0f814 100644 --- a/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua @@ -1151,6 +1151,10 @@ function AI_CARGO_DISPATCHER:onafterMonitor() self.PickupCargo[Carrier] = CargoCoordinate PickupCargo = Cargo break + else + local text=string.format("WARNING: Cargo %s is too heavy to be loaded into transport. Cargo weight %.1f > %.1f load capacity of carrier %s.", + tostring(Cargo:GetName()), Cargo:GetWeight(), LargestLoadCapacity, tostring(Carrier:GetName())) + self:I(text) end end end diff --git a/Moose Development/Moose/AI/AI_Escort.lua b/Moose Development/Moose/AI/AI_Escort.lua new file mode 100644 index 000000000..af3a4c304 --- /dev/null +++ b/Moose Development/Moose/AI/AI_Escort.lua @@ -0,0 +1,2182 @@ +--- **Functional** -- Taking the lead of AI escorting your flight or of other AI. +-- +-- === +-- +-- ## Features: +-- +-- * Escort navigation commands. +-- * Escort hold at position commands. +-- * Escorts reporting detected targets. +-- * Escorts scanning targets in advance. +-- * Escorts attacking specific targets. +-- * Request assistance from other groups for attack. +-- * Manage rule of engagement of escorts. +-- * Manage the allowed evasion techniques of escorts. +-- * Make escort to execute a defined mission or path. +-- * Escort tactical situation reporting. +-- +-- === +-- +-- ## Missions: +-- +-- [ESC - Escorting](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/ESC%20-%20Escorting) +-- +-- === +-- +-- Allows you to interact with escorting AI on your flight and take the lead. +-- +-- Each escorting group can be commanded with a complete set of radio commands (radio menu in your flight, and then F10). +-- +-- The radio commands will vary according the category of the group. The richest set of commands are with helicopters and airPlanes. +-- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. +-- +-- Escorts detect targets using a built-in detection mechanism. The detected targets are reported at a specified time interval. +-- Once targets are reported, each escort has these targets as menu options to command the attack of these targets. +-- Targets are by default grouped per area of 5000 meters, but the kind of detection and the grouping range can be altered. +-- +-- Different formations can be selected in the Flight menu: Trail, Stack, Left Line, Right Line, Left Wing, Right Wing, Central Wing and Boxed formations are available. +-- The Flight menu also allows for a mass attack, where all of the escorts are commanded to attack a target. +-- +-- Escorts can emit flares to reports their location. They can be commanded to hold at a location, which can be their current or the leader location. +-- In this way, you can spread out the escorts over the battle field before a coordinated attack. +-- +-- But basically, the escort class provides 4 modes of operation, and depending on the mode, you are either leading the flight, or following the flight. +-- +-- ## Leading the flight +-- +-- When leading the flight, you are expected to guide the escorts towards the target areas, +-- and carefully coordinate the attack based on the threat levels reported, and the available weapons +-- carried by the escorts. Ground ships or ground troops can execute A-assisted attacks, when they have long-range ground precision weapons for attack. +-- +-- ## Following the flight +-- +-- Escorts can be commanded to execute a specific mission path. In this mode, the escorts are in the lead. +-- You as a player, are following the escorts, and are commanding them to progress the mission while +-- ensuring that the escorts survive. You are joining the escorts in the battlefield. They will detect and report targets +-- and you will ensure that the attacks are well coordinated, assigning the correct escort type for the detected target +-- type. Once the attack is finished, the escort will resume the mission it was assigned. +-- In other words, you can use the escorts for reconnaissance, and for guiding the attack. +-- Imagine you as a mi-8 pilot, assigned to pickup cargo. Two ka-50s are guiding the way, and you are +-- following. You are in control. The ka-50s detect targets, report them, and you command how the attack +-- will commence and from where. You can control where the escorts are holding position and which targets +-- are attacked first. You are in control how the ka-50s will follow their mission path. +-- +-- Escorts can act as part of a AI A2G dispatcher offensive. In this way, You was a player are in control. +-- The mission is defined by the A2G dispatcher, and you are responsible to join the flight and ensure that the +-- attack is well coordinated. +-- +-- It is with great proud that I present you this class, and I hope you will enjoy the functionality and the dynamism +-- it brings in your DCS world simulations. +-- +-- # RADIO MENUs that can be created: +-- +-- Find a summary below of the current available commands: +-- +-- ## Navigation ...: +-- +-- Escort group navigation functions: +-- +-- * **"Join-Up":** The escort group fill follow you in the assigned formation. +-- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. +-- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. +-- +-- ## Hold position ...: +-- +-- Escort group navigation functions: +-- +-- * **"At current location":** The escort group will hover above the ground at the position they were. The altitude can be specified as a parameter. +-- * **"At my location":** The escort group will hover or orbit at the position where you are. The escort will fly to your location and hold position. The altitude can be specified as a parameter. +-- +-- ## Report targets ...: +-- +-- Report targets will make the escort group to report any target that it identifies within detection range. Any detected target can be attacked using the "Attack Targets" menu function. (see below). +-- +-- * **"Report now":** Will report the current detected targets. +-- * **"Report targets on":** Will make the escorts to report the detected targets and will fill the "Attack Targets" menu list. +-- * **"Report targets off":** Will stop detecting targets. +-- +-- ## Attack targets ...: +-- +-- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. +-- This menu will be available in Flight menu or in each Escort menu. +-- +-- ## Scan targets ...: +-- +-- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or rejoin formation. +-- +-- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. +-- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. +-- +-- ## Request assistance from ...: +-- +-- This menu item will list all detected targets within a 15km range, similar as with the menu item **Attack Targets**. +-- This menu item allows to request attack support from other ground based escorts supporting the current escort. +-- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. +-- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. +-- +-- ## ROE ...: +-- +-- Sets the Rules of Engagement (ROE) of the escort group when in flight. +-- +-- * **"Hold Fire":** The escort group will hold fire. +-- * **"Return Fire":** The escort group will return fire. +-- * **"Open Fire":** The escort group will open fire on designated targets. +-- * **"Weapon Free":** The escort group will engage with any target. +-- +-- ## Evasion ...: +-- +-- Will define the evasion techniques that the escort group will perform during flight or combat. +-- +-- * **"Fight until death":** The escort group will have no reaction to threats. +-- * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed. +-- * **"Evade enemy fire":** The rescort group will evade enemy fire before firing. +-- * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres. +-- +-- ## Resume Mission ...: +-- +-- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. +-- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission. +-- +-- === +-- +-- ### Authors: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Escort +-- @image Escorting.JPG + + + +--- @type AI_ESCORT +-- @extends AI.AI_Formation#AI_FORMATION + + +-- TODO: Add the menus when the class Start method is activated. +-- TODO: Remove the menus when the class Stop method is called. + +--- AI_ESCORT class +-- +-- # AI_ESCORT construction methods. +-- +-- Create a new AI_ESCORT object with the @{#AI_ESCORT.New} method: +-- +-- * @{#AI_ESCORT.New}: Creates a new AI_ESCORT object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT}, with an optional briefing text. +-- +-- @usage +-- -- Declare a new EscortPlanes object as follows: +-- +-- -- First find the GROUP object and the CLIENT object. +-- local EscortUnit = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. +-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. +-- +-- -- Now use these 2 objects to construct the new EscortPlanes object. +-- EscortPlanes = AI_ESCORT:New( EscortUnit, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) +-- +-- @field #AI_ESCORT +AI_ESCORT = { + ClassName = "AI_ESCORT", + EscortName = nil, -- The Escort Name + EscortUnit = nil, + EscortGroup = nil, + EscortMode = 1, + Targets = {}, -- The identified targets + FollowScheduler = nil, + ReportTargets = true, + OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE, + OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, + SmokeDirectionVector = false, + TaskPoints = {} +} + +--- @field Functional.Detection#DETECTION_AREAS +AI_ESCORT.Detection = nil + +--- MENUPARAM type +-- @type MENUPARAM +-- @field #AI_ESCORT ParamSelf +-- @field #Distance ParamDistance +-- @field #function ParamFunction +-- @field #string ParamMessage + +--- AI_ESCORT class constructor for an AI group +-- @param #AI_ESCORT self +-- @param Wrapper.Client#CLIENT EscortUnit The client escorted by the EscortGroup. +-- @param Core.Set#SET_GROUP EscortGroupSet The set of group AI escorting the EscortUnit. +-- @param #string EscortName Name of the escort. +-- @param #string EscortBriefing A text showing the AI_ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @return #AI_ESCORT self +-- @usage +-- -- Declare a new EscortPlanes object as follows: +-- +-- -- First find the GROUP object and the CLIENT object. +-- local EscortUnit = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. +-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. +-- +-- -- Now use these 2 objects to construct the new EscortPlanes object. +-- EscortPlanes = AI_ESCORT:New( EscortUnit, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) +function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) + + local self = BASE:Inherit( self, AI_FORMATION:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) ) -- #AI_ESCORT + self:F( { EscortUnit, EscortGroupSet } ) + + self.PlayerUnit = self.FollowUnit -- Wrapper.Unit#UNIT + self.PlayerGroup = self.FollowUnit:GetGroup() -- Wrapper.Group#GROUP + + self.EscortName = EscortName + self.EscortGroupSet = EscortGroupSet + + self.EscortGroupSet:SetSomeIteratorLimit( 8 ) + + self.EscortBriefing = EscortBriefing + + self.Menu = {} + +-- if not EscortBriefing then +-- EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " .. +-- "We're escorting your flight. " .. +-- "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n", +-- 60, EscortUnit +-- ) +-- else +-- EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") " .. EscortBriefing, +-- 60, EscortUnit +-- ) +-- end + + self.FollowDistance = 100 + self.CT1 = 0 + self.GT1 = 0 + + + EscortGroupSet:ForEachGroup( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + -- Set EscortGroup known at EscortUnit. + if not self.PlayerUnit._EscortGroups then + self.PlayerUnit._EscortGroups = {} + end + + if not self.PlayerUnit._EscortGroups[EscortGroup:GetName()] then + self.PlayerUnit._EscortGroups[EscortGroup:GetName()] = {} + self.PlayerUnit._EscortGroups[EscortGroup:GetName()].EscortGroup = EscortGroup + self.PlayerUnit._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName + self.PlayerUnit._EscortGroups[EscortGroup:GetName()].Detection = self.Detection + end + end + ) + + self:SetFlightReportType( self.__Enum.ReportType.All ) + + return self +end + + +function AI_ESCORT:_InitFlightMenus() + + self:SetFlightMenuJoinUp() + self:SetFlightMenuFormation( "Trail" ) + self:SetFlightMenuFormation( "Stack" ) + self:SetFlightMenuFormation( "LeftLine" ) + self:SetFlightMenuFormation( "RightLine" ) + self:SetFlightMenuFormation( "LeftWing" ) + self:SetFlightMenuFormation( "RightWing" ) + self:SetFlightMenuFormation( "Vic" ) + self:SetFlightMenuFormation( "Box" ) + + self:SetFlightMenuHoldAtEscortPosition() + self:SetFlightMenuHoldAtLeaderPosition() + + self:SetFlightMenuFlare() + self:SetFlightMenuSmoke() + + self:SetFlightMenuROE() + self:SetFlightMenuROT() + + self:SetFlightMenuTargets() + self:SetFlightMenuReportType() + +end + +function AI_ESCORT:_InitEscortMenus( EscortGroup ) + + EscortGroup.EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroup:GetCallsign(), self.MainMenu ) + + self:SetEscortMenuJoinUp( EscortGroup ) + self:SetEscortMenuResumeMission( EscortGroup ) + + self:SetEscortMenuHoldAtEscortPosition( EscortGroup ) + self:SetEscortMenuHoldAtLeaderPosition( EscortGroup ) + + self:SetEscortMenuFlare( EscortGroup ) + self:SetEscortMenuSmoke( EscortGroup ) + + self:SetEscortMenuROE( EscortGroup ) + self:SetEscortMenuROT( EscortGroup ) + + self:SetEscortMenuTargets( EscortGroup ) + +end + +function AI_ESCORT:_InitEscortRoute( EscortGroup ) + + EscortGroup.MissionRoute = EscortGroup:GetTaskRoute() + +end + + +--- @param #AI_ESCORT self +-- @param Core.Set#SET_GROUP EscortGroupSet +function AI_ESCORT:onafterStart( EscortGroupSet ) + + self:F() + + EscortGroupSet:ForEachGroup( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + EscortGroup:WayPointInitialize() + + EscortGroup:OptionROTVertical() + EscortGroup:OptionROEOpenFire() + end + ) + + -- TODO:Revise this... + local LeaderEscort = EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP + if LeaderEscort then + local Report = REPORT:New( "Escort reporting:" ) + Report:Add( "Joining Up " .. EscortGroupSet:GetUnitTypeNames():Text( ", " ) .. " from " .. LeaderEscort:GetCoordinate():ToString( self.PlayerUnit ) ) + LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.PlayerUnit ) + end + + self.Detection = DETECTION_AREAS:New( EscortGroupSet, 5000 ) + + -- This only makes the escort report detections made by the escort, not through DLINK. + -- These must be enquired using other facilities. + -- In this way, the escort will report the target areas that are relevant for the mission. + self.Detection:InitDetectVisual( true ) + self.Detection:InitDetectIRST( true ) + self.Detection:InitDetectOptical( true ) + self.Detection:InitDetectRadar( true ) + self.Detection:InitDetectRWR( true ) + + self.Detection:SetAcceptRange( 100000 ) + + self.Detection:__Start( 30 ) + + self.MainMenu = MENU_GROUP:New( self.PlayerGroup, self.EscortName ) + self.FlightMenu = MENU_GROUP:New( self.PlayerGroup, "Flight", self.MainMenu ) + + self:_InitFlightMenus() + + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + + self:_InitEscortMenus( EscortGroup ) + self:_InitEscortRoute( EscortGroup ) + + self:SetFlightModeFormation( EscortGroup ) + + --- @param #AI_ESCORT self + -- @param Core.Event#EVENTDATA EventData + function EscortGroup:OnEventDeadOrCrash( EventData ) + self:F( { "EventDead", EventData } ) + self.EscortMenu:Remove() + end + + EscortGroup:HandleEvent( EVENTS.Dead, EscortGroup.OnEventDeadOrCrash ) + EscortGroup:HandleEvent( EVENTS.Crash, EscortGroup.OnEventDeadOrCrash ) + + end + ) + + +end + +--- @param #AI_ESCORT self +-- @param Core.Set#SET_GROUP EscortGroupSet +function AI_ESCORT:onafterStop( EscortGroupSet ) + + self:F() + + EscortGroupSet:ForEachGroup( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + EscortGroup:WayPointInitialize() + + EscortGroup:OptionROTVertical() + EscortGroup:OptionROEOpenFire() + end + ) + + self.Detection:Stop() + + self.MainMenu:Remove() + +end + +--- Set a Detection method for the EscortUnit to be reported upon. +-- Detection methods are based on the derived classes from DETECTION_BASE. +-- @param #AI_ESCORT self +-- @param Functional.Detection#DETECTION_AREAS Detection +function AI_ESCORT:SetDetection( Detection ) + + self.Detection = Detection + self.EscortGroup.Detection = self.Detection + self.PlayerUnit._EscortGroups[self.EscortGroup:GetName()].Detection = self.EscortGroup.Detection + + Detection:__Start( 1 ) + +end + +--- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to. +-- This allows to visualize where the escort is flying to. +-- @param #AI_ESCORT self +-- @param #boolean SmokeDirection If true, then the direction vector will be smoked. +function AI_ESCORT:TestSmokeDirectionVector( SmokeDirection ) + self.SmokeDirectionVector = ( SmokeDirection == true ) and true or false +end + + +--- Defines the default menus for helicopters. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @param #number ZLevels The amount of levels on the Z-axis. +-- @return #AI_ESCORT +function AI_ESCORT:MenusHelicopters( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) + self:F() + +-- self:MenuScanForTargets( 100, 60 ) + + self.XStart = XStart or 50 + self.XSpace = XSpace or 50 + self.YStart = YStart or 50 + self.YSpace = YSpace or 50 + self.ZStart = ZStart or 50 + self.ZSpace = ZSpace or 50 + self.ZLevels = ZLevels or 10 + + self:MenuJoinUp() + self:MenuFormationTrail(self.XStart,self.XSpace,self.YStart) + self:MenuFormationStack(self.XStart,self.XSpace,self.YStart,self.YSpace) + self:MenuFormationLeftLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationRightLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationLeftWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationRightWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationVic(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace) + self:MenuFormationBox(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace,self.ZLevels) + + self:MenuHoldAtEscortPosition( 30 ) + self:MenuHoldAtEscortPosition( 100 ) + self:MenuHoldAtEscortPosition( 500 ) + self:MenuHoldAtLeaderPosition( 30, 500 ) + + self:MenuFlare() + self:MenuSmoke() + + self:MenuTargets( 60 ) + self:MenuAssistedAttack() + self:MenuROE() + self:MenuROT() + + return self +end + + +--- Defines the default menus for airplanes. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @param #number ZLevels The amount of levels on the Z-axis. +-- @return #AI_ESCORT +function AI_ESCORT:MenusAirplanes( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) + self:F() + +-- self:MenuScanForTargets( 100, 60 ) + + self.XStart = XStart or 50 + self.XSpace = XSpace or 50 + self.YStart = YStart or 50 + self.YSpace = YSpace or 50 + self.ZStart = ZStart or 50 + self.ZSpace = ZSpace or 50 + self.ZLevels = ZLevels or 10 + + self:MenuJoinUp() + self:MenuFormationTrail(self.XStart,self.XSpace,self.YStart) + self:MenuFormationStack(self.XStart,self.XSpace,self.YStart,self.YSpace) + self:MenuFormationLeftLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationRightLine(self.XStart,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationLeftWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationRightWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace) + self:MenuFormationVic(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace) + self:MenuFormationBox(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace,self.ZLevels) + + self:MenuHoldAtEscortPosition( 1000, 500 ) + self:MenuHoldAtLeaderPosition( 1000, 500 ) + + self:MenuFlare() + self:MenuSmoke() + + self:MenuTargets( 60 ) + self:MenuAssistedAttack() + self:MenuROE() + self:MenuROT() + + return self +end + + +function AI_ESCORT:SetFlightMenuFormation( Formation ) + + local FormationID = "Formation" .. Formation + + local MenuFormation = self.Menu[FormationID] + + if MenuFormation then + local Arguments = MenuFormation.Arguments + --self:I({Arguments=unpack(Arguments)}) + local FlightMenuFormation = MENU_GROUP:New( self.PlayerGroup, "Formation", self.MainMenu ) + local MenuFlightFormationID = MENU_GROUP_COMMAND:New( self.PlayerGroup, Formation, FlightMenuFormation, + function ( self, Formation, ... ) + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup, self, Formation, Arguments ) + if EscortGroup:IsAir() then + self:E({FormationID=FormationID}) + self[FormationID]( self, unpack(Arguments) ) + end + end, self, Formation, Arguments + ) + end, self, Formation, Arguments + ) + end + + return self +end + + +function AI_ESCORT:MenuFormation( Formation, ... ) + + local FormationID = "Formation"..Formation + self.Menu[FormationID] = self.Menu[FormationID] or {} + self.Menu[FormationID].Arguments = arg + +end + + +--- Defines a menu slot to let the escort to join in a trail formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationTrail( XStart, XSpace, YStart ) + + self:MenuFormation( "Trail", XStart, XSpace, YStart ) + + return self +end + +--- Defines a menu slot to let the escort to join in a stacked formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationStack( XStart, XSpace, YStart, YSpace ) + + self:MenuFormation( "Stack", XStart, XSpace, YStart, YSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a leFt wing formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationLeftLine( XStart, YStart, ZStart, ZSpace ) + + self:MenuFormation( "LeftLine", XStart, YStart, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a right line formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationRightLine( XStart, YStart, ZStart, ZSpace ) + + self:MenuFormation( "RightLine", XStart, YStart, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a left wing formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationLeftWing( XStart, XSpace, YStart, ZStart, ZSpace ) + + self:MenuFormation( "LeftWing", XStart, XSpace, YStart, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a right wing formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationRightWing( XStart, XSpace, YStart, ZStart, ZSpace ) + + self:MenuFormation( "RightWing", XStart, XSpace, YStart, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a center wing formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationCenterWing( XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) + + self:MenuFormation( "CenterWing", XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a vic formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationVic( XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) + + self:MenuFormation( "Vic", XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) + + return self +end + + +--- Defines a menu slot to let the escort to join in a box formation. +-- This menu will appear under **Formation**. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. +-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. +-- @param #number ZLevels The amount of levels on the Z-axis. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFormationBox( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) + + self:MenuFormation( "Box", XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) + + return self +end + +function AI_ESCORT:SetFlightMenuJoinUp() + + if self.Menu.JoinUp == true then + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + local FlightMenuJoinUp = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up", FlightMenuReportNavigation, AI_ESCORT._FlightJoinUp, self ) + end + +end + + +--- Sets a menu slot to join formation for an escort. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:SetEscortMenuJoinUp( EscortGroup ) + + if self.Menu.JoinUp == true then + if EscortGroup:IsAir() then + local EscortGroupName = EscortGroup:GetName() + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) + local EscortMenuJoinUp = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up", EscortMenuReportNavigation, AI_ESCORT._JoinUp, self, EscortGroup ) + end + end +end + + + +--- Defines --- Defines a menu slot to let the escort to join formation. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:MenuJoinUp() + + self.Menu.JoinUp = true + + return self +end + + +function AI_ESCORT:SetFlightMenuHoldAtEscortPosition() + + for _, MenuHoldAtEscortPosition in pairs( self.Menu.HoldAtEscortPosition ) do + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + + local FlightMenuHoldPosition = MENU_GROUP_COMMAND + :New( + self.PlayerGroup, + MenuHoldAtEscortPosition.MenuText, + FlightMenuReportNavigation, + AI_ESCORT._FlightHoldPosition, + self, + nil, + MenuHoldAtEscortPosition.Height, + MenuHoldAtEscortPosition.Speed + ) + + end + return self +end + +function AI_ESCORT:SetEscortMenuHoldAtEscortPosition( EscortGroup ) + + for _, HoldAtEscortPosition in pairs( self.Menu.HoldAtEscortPosition ) do + if EscortGroup:IsAir() then + local EscortGroupName = EscortGroup:GetName() + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) + local EscortMenuHoldPosition = MENU_GROUP_COMMAND + :New( + self.PlayerGroup, + HoldAtEscortPosition.MenuText, + EscortMenuReportNavigation, + AI_ESCORT._HoldPosition, + self, + EscortGroup, + EscortGroup, + HoldAtEscortPosition.Height, + HoldAtEscortPosition.Speed + ) + end + end + + return self +end + + +--- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. +-- This menu will appear under **Hold position**. +-- @param #AI_ESCORT self +-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCS#Time Speed Optional parameter that lets the escort orbit with a specified speed. The default value is a speed that is average for the type of airplane or helicopter. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #AI_ESCORT +function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat ) + self:F( { Height, Speed, MenuTextFormat } ) + + if not Height then + Height = 30 + end + + if not Speed then + Speed = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Speed == 0 then + MenuText = string.format( "Hold at %d meter", Height ) + else + MenuText = string.format( "Hold at %d meter at %d", Height, Speed ) + end + else + if Speed == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Speed ) + end + end + + self.Menu.HoldAtEscortPosition = self.Menu.HoldAtEscortPosition or {} + self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition+1] = {} + self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].Height = Height + self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].Speed = Speed + self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].MenuText = MenuText + + return self +end + + +function AI_ESCORT:SetFlightMenuHoldAtLeaderPosition() + + for _, MenuHoldAtLeaderPosition in pairs( self.Menu.HoldAtLeaderPosition ) do + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + + local FlightMenuHoldAtLeaderPosition = MENU_GROUP_COMMAND + :New( + self.PlayerGroup, + MenuHoldAtLeaderPosition.MenuText, + FlightMenuReportNavigation, + AI_ESCORT._FlightHoldPosition, + self, + self.PlayerGroup, + MenuHoldAtLeaderPosition.Height, + MenuHoldAtLeaderPosition.Speed + ) + end + + return self +end + +function AI_ESCORT:SetEscortMenuHoldAtLeaderPosition( EscortGroup ) + + for _, HoldAtLeaderPosition in pairs( self.Menu.HoldAtLeaderPosition ) do + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) + + local EscortMenuHoldAtLeaderPosition = MENU_GROUP_COMMAND + :New( + self.PlayerGroup, + HoldAtLeaderPosition.MenuText, + EscortMenuReportNavigation, + AI_ESCORT._HoldPosition, + self, + self.PlayerGroup, + EscortGroup, + HoldAtLeaderPosition.Height, + HoldAtLeaderPosition.Speed + ) + end + end + + return self +end + +--- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds. +-- This menu will appear under **Navigation**. +-- @param #AI_ESCORT self +-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCS#Time Speed Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #AI_ESCORT +function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat ) + self:F( { Height, Speed, MenuTextFormat } ) + + if not Height then + Height = 30 + end + + if not Speed then + Speed = 0 + end + + local MenuText = "" + if not MenuTextFormat then + if Speed == 0 then + MenuText = string.format( "Rejoin and hold at %d meter", Height ) + else + MenuText = string.format( "Rejoin and hold at %d meter at %d", Height, Speed ) + end + else + if Speed == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Speed ) + end + end + + self.Menu.HoldAtLeaderPosition = self.Menu.HoldAtLeaderPosition or {} + self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition+1] = {} + self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].Height = Height + self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].Speed = Speed + self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].MenuText = MenuText + + return self +end + +--- Defines a menu slot to let the escort scan for targets at a certain height for a certain time in seconds. +-- This menu will appear under **Scan targets**. +-- @param #AI_ESCORT self +-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCS#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. +-- @return #AI_ESCORT +function AI_ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) + self:F( { Height, Seconds, MenuTextFormat } ) + + if self.EscortGroup:IsAir() then + if not self.EscortMenuScan then + self.EscortMenuScan = MENU_GROUP:New( self.PlayerGroup, "Scan for targets", self.EscortMenu ) + end + + if not Height then + Height = 100 + end + + if not Seconds then + Seconds = 30 + end + + local MenuText = "" + if not MenuTextFormat then + if Seconds == 0 then + MenuText = string.format( "At %d meter", Height ) + else + MenuText = string.format( "At %d meter for %d seconds", Height, Seconds ) + end + else + if Seconds == 0 then + MenuText = string.format( MenuTextFormat, Height ) + else + MenuText = string.format( MenuTextFormat, Height, Seconds ) + end + end + + if not self.EscortMenuScanForTargets then + self.EscortMenuScanForTargets = {} + end + + self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1] = MENU_GROUP_COMMAND + :New( + self.PlayerGroup, + MenuText, + self.EscortMenuScan, + AI_ESCORT._ScanTargets, + self, + 30 + ) + end + + return self +end + + +function AI_ESCORT:SetFlightMenuFlare() + + for _, MenuFlare in pairs( self.Menu.Flare) do + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + local FlightMenuFlare = MENU_GROUP:New( self.PlayerGroup, MenuFlare.MenuText, FlightMenuReportNavigation ) + + local FlightMenuFlareGreenFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Green, "Released a green flare!" ) + local FlightMenuFlareRedFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Red, "Released a red flare!" ) + local FlightMenuFlareWhiteFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.White, "Released a white flare!" ) + local FlightMenuFlareYellowFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release yellow flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Yellow, "Released a yellow flare!" ) + end + + return self +end + +function AI_ESCORT:SetEscortMenuFlare( EscortGroup ) + + for _, MenuFlare in pairs( self.Menu.Flare) do + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) + local EscortMenuFlare = MENU_GROUP:New( self.PlayerGroup, MenuFlare.MenuText, EscortMenuReportNavigation ) + + local EscortMenuFlareGreen = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Green, "Released a green flare!" ) + local EscortMenuFlareRed = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Red, "Released a red flare!" ) + local EscortMenuFlareWhite = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.White, "Released a white flare!" ) + local EscortMenuFlareYellow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release yellow flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Yellow, "Released a yellow flare!" ) + end + end + + return self +end + + + +--- Defines a menu slot to let the escort disperse a flare in a certain color. +-- This menu will appear under **Navigation**. +-- The flare will be fired from the first unit in the group. +-- @param #AI_ESCORT self +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. +-- @return #AI_ESCORT +function AI_ESCORT:MenuFlare( MenuTextFormat ) + self:F() + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Flare" + else + MenuText = MenuTextFormat + end + + self.Menu.Flare = self.Menu.Flare or {} + self.Menu.Flare[#self.Menu.Flare+1] = {} + self.Menu.Flare[#self.Menu.Flare].MenuText = MenuText + + return self +end + + +function AI_ESCORT:SetFlightMenuSmoke() + + for _, MenuSmoke in pairs( self.Menu.Smoke) do + local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu ) + local FlightMenuSmoke = MENU_GROUP:New( self.PlayerGroup, MenuSmoke.MenuText, FlightMenuReportNavigation ) + + local FlightMenuSmokeGreenFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Green, "Releasing green smoke!" ) + local FlightMenuSmokeRedFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Red, "Releasing red smoke!" ) + local FlightMenuSmokeWhiteFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.White, "Releasing white smoke!" ) + local FlightMenuSmokeOrangeFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release orange smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Orange, "Releasing orange smoke!" ) + local FlightMenuSmokeBlueFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release blue smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Blue, "Releasing blue smoke!" ) + end + + return self +end + + +function AI_ESCORT:SetEscortMenuSmoke( EscortGroup ) + + for _, MenuSmoke in pairs( self.Menu.Smoke) do + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu ) + local EscortMenuSmoke = MENU_GROUP:New( self.PlayerGroup, MenuSmoke.MenuText, EscortMenuReportNavigation ) + + local EscortMenuSmokeGreen = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Green, "Releasing green smoke!" ) + local EscortMenuSmokeRed = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Red, "Releasing red smoke!" ) + local EscortMenuSmokeWhite = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.White, "Releasing white smoke!" ) + local EscortMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release orange smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Orange, "Releasing orange smoke!" ) + local EscortMenuSmokeBlue = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release blue smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Blue, "Releasing blue smoke!" ) + end + end + + return self +end + + +--- Defines a menu slot to let the escort disperse a smoke in a certain color. +-- This menu will appear under **Navigation**. +-- Note that smoke menu options will only be displayed for ships and ground units. Not for air units. +-- The smoke will be fired from the first unit in the group. +-- @param #AI_ESCORT self +-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed. +-- @return #AI_ESCORT +function AI_ESCORT:MenuSmoke( MenuTextFormat ) + self:F() + + local MenuText = "" + if not MenuTextFormat then + MenuText = "Smoke" + else + MenuText = MenuTextFormat + end + + self.Menu.Smoke = self.Menu.Smoke or {} + self.Menu.Smoke[#self.Menu.Smoke+1] = {} + self.Menu.Smoke[#self.Menu.Smoke].MenuText = MenuText + + return self +end + +function AI_ESCORT:SetFlightMenuReportType() + + local FlightMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", self.FlightMenu ) + local MenuStamp = FlightMenuReportTargets:GetStamp() + + local FlightReportType = self:GetFlightReportType() + + if FlightReportType ~= self.__Enum.ReportType.All then + local FlightMenuReportTargetsAll = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report all targets", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeAll, self ) + :SetTag( "ReportType" ) + :SetStamp( MenuStamp ) + end + + if FlightReportType == self.__Enum.ReportType.All or FlightReportType ~= self.__Enum.ReportType.Airborne then + local FlightMenuReportTargetsAirborne = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report airborne targets", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeAirborne, self ) + :SetTag( "ReportType" ) + :SetStamp( MenuStamp ) + end + + if FlightReportType == self.__Enum.ReportType.All or FlightReportType ~= self.__Enum.ReportType.GroundRadar then + local FlightMenuReportTargetsGroundRadar = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report gound radar targets", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeGroundRadar, self ) + :SetTag( "ReportType" ) + :SetStamp( MenuStamp ) + end + if FlightReportType == self.__Enum.ReportType.All or FlightReportType ~= self.__Enum.ReportType.Ground then + local FlightMenuReportTargetsGround = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report ground targets", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeGround, self ) + :SetTag( "ReportType" ) + :SetStamp( MenuStamp ) + end + + FlightMenuReportTargets:RemoveSubMenus( MenuStamp, "ReportType" ) + +end + + +function AI_ESCORT:SetFlightMenuTargets() + + local FlightMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", self.FlightMenu ) + + -- Report Targets + local FlightMenuReportTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets now!", FlightMenuReportTargets, AI_ESCORT._FlightReportNearbyTargetsNow, self ) + local FlightMenuReportTargetsOn = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, true ) + local FlightMenuReportTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, false ) + + -- Attack Targets + self.FlightMenuAttack = MENU_GROUP:New( self.PlayerGroup, "Attack targets", self.FlightMenu ) + local FlightMenuAttackNearby = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Attack nearest targets", self.FlightMenuAttack, AI_ESCORT._FlightAttackNearestTarget, self ):SetTag( "Attack" ) + local FlightMenuAttackNearbyAir = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Attack nearest airborne targets", self.FlightMenuAttack, AI_ESCORT._FlightAttackNearestTarget, self, self.__Enum.ReportType.Air ):SetTag( "Attack" ) + local FlightMenuAttackNearbyGround = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Attack nearest ground targets", self.FlightMenuAttack, AI_ESCORT._FlightAttackNearestTarget, self, self.__Enum.ReportType.Ground ):SetTag( "Attack" ) + + for _, MenuTargets in pairs( self.Menu.Targets) do + MenuTargets.FlightReportTargetsScheduler = SCHEDULER:New( self, self._FlightReportTargetsScheduler, {}, MenuTargets.Interval, MenuTargets.Interval ) + end + + return self +end + + +function AI_ESCORT:SetEscortMenuTargets( EscortGroup ) + + for _, MenuTargets in pairs( self.Menu.Targets) do + if EscortGroup:IsAir() then + local EscortGroupName = EscortGroup:GetName() + --local EscortMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", EscortGroup.EscortMenu ) + + -- Report Targets + EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets", EscortGroup.EscortMenu, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup, true ) + --EscortGroup.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, true ) + --EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false ) + + -- Attack Targets + --local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu ) + + EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, 1, MenuTargets.Interval ) + EscortGroup.ResumeScheduler = SCHEDULER:New( self, self._ResumeScheduler, { EscortGroup }, 1, 60 ) + end + end + + return self +end + + + +--- Defines a menu slot to let the escort report their current detected targets with a specified time interval in seconds. +-- This menu will appear under **Report targets**. +-- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed. +-- @param #AI_ESCORT self +-- @param DCS#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. +-- @return #AI_ESCORT +function AI_ESCORT:MenuTargets( Seconds ) + self:F( { Seconds } ) + + if not Seconds then + Seconds = 30 + end + + self.Menu.Targets = self.Menu.Targets or {} + self.Menu.Targets[#self.Menu.Targets+1] = {} + self.Menu.Targets[#self.Menu.Targets].Interval = Seconds + + return self +end + +--- Defines a menu slot to let the escort attack its detected targets using assisted attack from another escort joined also with the client. +-- This menu will appear under **Request assistance from**. +-- Note that this method needs to be preceded with the method MenuTargets. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:MenuAssistedAttack() + self:F() + + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if not EscortGroup:IsAir() then + -- Request assistance from other escorts. + -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane... + self.EscortMenuTargetAssistance = MENU_GROUP:New( self.PlayerGroup, "Request assistance from", EscortGroup.EscortMenu ) + end + end + ) + + return self +end + +function AI_ESCORT:SetFlightMenuROE() + + for _, MenuROE in pairs( self.Menu.ROE) do + local FlightMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", self.FlightMenu ) + + local FlightMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire", FlightMenuROE, AI_ESCORT._FlightROEHoldFire, self, "Holding weapons!" ) + local FlightMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire", FlightMenuROE, AI_ESCORT._FlightROEReturnFire, self, "Returning fire!" ) + local FlightMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire", FlightMenuROE, AI_ESCORT._FlightROEOpenFire, self, "Open fire at designated targets!" ) + local FlightMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", FlightMenuROE, AI_ESCORT._FlightROEWeaponFree, self, "Engaging all targets!" ) + end + + return self +end + + +function AI_ESCORT:SetEscortMenuROE( EscortGroup ) + + for _, MenuROE in pairs( self.Menu.ROE) do + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", EscortGroup.EscortMenu ) + + if EscortGroup:OptionROEHoldFirePossible() then + local EscortMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEHoldFire, "Holding weapons!" ) + end + if EscortGroup:OptionROEReturnFirePossible() then + local EscortMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEReturnFire, "Returning fire!" ) + end + if EscortGroup:OptionROEOpenFirePossible() then + EscortGroup.EscortMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEOpenFire, "Opening fire on designated targets!!" ) + end + if EscortGroup:OptionROEWeaponFreePossible() then + EscortGroup.EscortMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEWeaponFree, "Opening fire on targets of opportunity!" ) + end + end + end + + return self +end + + +--- Defines a menu to let the escort set its rules of engagement. +-- All rules of engagement will appear under the menu **ROE**. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:MenuROE() + self:F() + + self.Menu.ROE = self.Menu.ROE or {} + self.Menu.ROE[#self.Menu.ROE+1] = {} + + return self +end + + +function AI_ESCORT:SetFlightMenuROT() + + for _, MenuROT in pairs( self.Menu.ROT) do + local FlightMenuROT = MENU_GROUP:New( self.PlayerGroup, "Reaction On Threat", self.FlightMenu ) + + local FlightMenuROTNoReaction = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death", FlightMenuROT, AI_ESCORT._FlightROTNoReaction, self, "Fighting until death!" ) + local FlightMenuROTPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", FlightMenuROT, AI_ESCORT._FlightROTPassiveDefense, self, "Defending using jammers, chaff and flares!" ) + local FlightMenuROTEvadeFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire", FlightMenuROT, AI_ESCORT._FlightROTEvadeFire, self, "Evading on enemy fire!" ) + local FlightMenuROTVertical = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire", FlightMenuROT, AI_ESCORT._FlightROTVertical, self, "Evading on enemy fire with vertical manoeuvres!" ) + end + + return self +end + + +function AI_ESCORT:SetEscortMenuROT( EscortGroup ) + + for _, MenuROT in pairs( self.Menu.ROT) do + if EscortGroup:IsAir() then + + local EscortGroupName = EscortGroup:GetName() + local EscortMenuROT = MENU_GROUP:New( self.PlayerGroup, "Reaction On Threat", EscortGroup.EscortMenu ) + + if not EscortGroup.EscortMenuEvasion then + -- Reaction to Threats + if EscortGroup:OptionROTNoReactionPossible() then + local EscortMenuEvasionNoReaction = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTNoReaction, "Fighting until death!" ) + end + if EscortGroup:OptionROTPassiveDefensePossible() then + local EscortMenuEvasionPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTPassiveDefense, "Defending using jammers, chaff and flares!" ) + end + if EscortGroup:OptionROTEvadeFirePossible() then + local EscortMenuEvasionEvadeFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTEvadeFire, "Evading on enemy fire!" ) + end + if EscortGroup:OptionROTVerticalPossible() then + local EscortMenuOptionEvasionVertical = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTVertical, "Evading on enemy fire with vertical manoeuvres!" ) + end + end + end + end + + return self +end + + + +--- Defines a menu to let the escort set its evasion when under threat. +-- All rules of engagement will appear under the menu **Evasion**. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:MenuROT( MenuTextFormat ) + self:F( MenuTextFormat ) + + self.Menu.ROT = self.Menu.ROT or {} + self.Menu.ROT[#self.Menu.ROT+1] = {} + + return self +end + +--- Defines a menu to let the escort resume its mission from a waypoint on its route. +-- All rules of engagement will appear under the menu **Resume mission from**. +-- @param #AI_ESCORT self +-- @return #AI_ESCORT +function AI_ESCORT:SetEscortMenuResumeMission( EscortGroup ) + self:F() + + if EscortGroup:IsAir() then + local EscortGroupName = EscortGroup:GetName() + EscortGroup.EscortMenuResumeMission = MENU_GROUP:New( self.PlayerGroup, "Resume from", EscortGroup.EscortMenu ) + end + + return self +end + + +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP OrbitGroup +-- @param Wrapper.Group#GROUP EscortGroup +-- @param #number OrbitHeight +-- @param #number OrbitSeconds +function AI_ESCORT:_HoldPosition( OrbitGroup, EscortGroup, OrbitHeight, OrbitSeconds ) + + local EscortUnit = self.PlayerUnit + + local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT + + self:SetFlightModeMission( EscortGroup ) + + local PointFrom = {} + local GroupVec3 = EscortGroup:GetUnit(1):GetVec3() + PointFrom = {} + PointFrom.x = GroupVec3.x + PointFrom.y = GroupVec3.z + PointFrom.speed = 250 + PointFrom.type = AI.Task.WaypointType.TURNING_POINT + PointFrom.alt = GroupVec3.y + PointFrom.alt_type = AI.Task.AltitudeType.BARO + + local OrbitPoint = OrbitUnit:GetVec2() + local PointTo = {} + PointTo.x = OrbitPoint.x + PointTo.y = OrbitPoint.y + PointTo.speed = 250 + PointTo.type = AI.Task.WaypointType.TURNING_POINT + PointTo.alt = OrbitHeight + PointTo.alt_type = AI.Task.AltitudeType.BARO + PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 ) + + local Points = { PointFrom, PointTo } + + EscortGroup:OptionROEHoldFire() + EscortGroup:OptionROTPassiveDefense() + + EscortGroup:SetTask( EscortGroup:TaskRoute( Points ), 1 ) + EscortGroup:MessageTypeToGroup( "Orbiting at current location.", MESSAGE.Type.Information, EscortUnit:GetGroup() ) + +end + + +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP OrbitGroup +-- @param #number OrbitHeight +-- @param #number OrbitSeconds +function AI_ESCORT:_FlightHoldPosition( OrbitGroup, OrbitHeight, OrbitSeconds ) + + local EscortUnit = self.PlayerUnit + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup, OrbitGroup ) + if EscortGroup:IsAir() then + if OrbitGroup == nil then + OrbitGroup = EscortGroup + end + self:_HoldPosition( OrbitGroup, EscortGroup, OrbitHeight, OrbitSeconds ) + end + end, OrbitGroup + ) + +end + + + +function AI_ESCORT:_JoinUp( EscortGroup ) + + local EscortUnit = self.PlayerUnit + + self:SetFlightModeFormation( EscortGroup ) + + EscortGroup:MessageTypeToGroup( "Joining up!", MESSAGE.Type.Information, EscortUnit:GetGroup() ) +end + + +function AI_ESCORT:_FlightJoinUp() + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_JoinUp( EscortGroup ) + end + end + ) + +end + + +--- Lets the escort to join in a trail formation. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #nubmer YStart The start position on the Y-axis in meters for the first group. +-- @return #AI_ESCORT +function AI_ESCORT:_EscortFormationTrail( EscortGroup, XStart, XSpace, YStart ) + + self:FormationTrail( XStart, XSpace, YStart ) + +end + + +function AI_ESCORT:_FlightFormationTrail( XStart, XSpace, YStart ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_EscortFormationTrail( EscortGroup, XStart, XSpace, YStart ) + end + end + ) + +end + +--- Lets the escort to join in a stacked formation. +-- @param #AI_ESCORT self +-- @param #number XStart The start position on the X-axis in meters for the first group. +-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group. +-- @param #number YStart The start position on the Y-axis in meters for the first group. +-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group. +-- @return #AI_ESCORT +function AI_ESCORT:_EscortFormationStack( EscortGroup, XStart, XSpace, YStart, YSpace ) + + self:FormationStack( XStart, XSpace, YStart, YSpace ) + +end + + +function AI_ESCORT:_FlightFormationStack( XStart, XSpace, YStart, YSpace ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_EscortFormationStack( EscortGroup, XStart, XSpace, YStart, YSpace ) + end + end + ) + +end + + +function AI_ESCORT:_Flare( EscortGroup, Color, Message ) + + local EscortUnit = self.PlayerUnit + + EscortGroup:GetUnit(1):Flare( Color ) + EscortGroup:MessageTypeToGroup( Message, MESSAGE.Type.Information, EscortUnit:GetGroup() ) +end + + +function AI_ESCORT:_FlightFlare( Color, Message ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_Flare( EscortGroup, Color, Message ) + end + end + ) + +end + + + +function AI_ESCORT:_Smoke( EscortGroup, Color, Message ) + + local EscortUnit = self.PlayerUnit + + EscortGroup:GetUnit(1):Smoke( Color ) + EscortGroup:MessageTypeToGroup( Message, MESSAGE.Type.Information, EscortUnit:GetGroup() ) +end + +function AI_ESCORT:_FlightSmoke( Color, Message ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_Smoke( EscortGroup, Color, Message ) + end + end + ) + +end + + +function AI_ESCORT:_ReportNearbyTargetsNow( EscortGroup ) + + local EscortUnit = self.PlayerUnit + + self:_ReportTargetsScheduler( EscortGroup ) + +end + + +function AI_ESCORT:_FlightReportNearbyTargetsNow() + + self:_FlightReportTargetsScheduler() + +end + + + +function AI_ESCORT:_FlightSwitchReportNearbyTargets( ReportTargets ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + if EscortGroup:IsAir() then + self:_EscortSwitchReportNearbyTargets( EscortGroup, ReportTargets ) + end + end + ) + +end + +function AI_ESCORT:SetFlightReportType( ReportType ) + + self.FlightReportType = ReportType + +end + +function AI_ESCORT:GetFlightReportType() + + return self.FlightReportType + +end + +function AI_ESCORT:_FlightSwitchReportTypeAll() + + self:SetFlightReportType( self.__Enum.ReportType.All ) + self:SetFlightMenuReportType() + + local EscortGroup = self.EscortGroupSet:GetFirst() + EscortGroup:MessageTypeToGroup( "Reporting all targets.", MESSAGE.Type.Information, self.PlayerGroup ) + +end + +function AI_ESCORT:_FlightSwitchReportTypeAirborne() + + self:SetFlightReportType( self.__Enum.ReportType.Airborne ) + self:SetFlightMenuReportType() + + local EscortGroup = self.EscortGroupSet:GetFirst() + EscortGroup:MessageTypeToGroup( "Reporting airborne targets.", MESSAGE.Type.Information, self.PlayerGroup ) + +end + +function AI_ESCORT:_FlightSwitchReportTypeGroundRadar() + + self:SetFlightReportType( self.__Enum.ReportType.Ground ) + self:SetFlightMenuReportType() + + local EscortGroup = self.EscortGroupSet:GetFirst() + EscortGroup:MessageTypeToGroup( "Reporting ground radar targets.", MESSAGE.Type.Information, self.PlayerGroup ) + +end + +function AI_ESCORT:_FlightSwitchReportTypeGround() + + self:SetFlightReportType( self.__Enum.ReportType.Ground ) + self:SetFlightMenuReportType() + + local EscortGroup = self.EscortGroupSet:GetFirst() + EscortGroup:MessageTypeToGroup( "Reporting ground targets.", MESSAGE.Type.Information, self.PlayerGroup ) + +end + + +function AI_ESCORT:_ScanTargets( ScanDuration ) + + local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP + local EscortUnit = self.PlayerUnit + + self.FollowScheduler:Stop( self.FollowSchedule ) + + if EscortGroup:IsHelicopter() then + EscortGroup:PushTask( + EscortGroup:TaskControlled( + EscortGroup:TaskOrbitCircle( 200, 20 ), + EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) + ), 1 ) + elseif EscortGroup:IsAirPlane() then + EscortGroup:PushTask( + EscortGroup:TaskControlled( + EscortGroup:TaskOrbitCircle( 1000, 500 ), + EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil ) + ), 1 ) + end + + EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortUnit ) + + if self.EscortMode == AI_ESCORT.MODE.FOLLOW then + self.FollowScheduler:Start( self.FollowSchedule ) + end + +end + +--- @param Wrapper.Group#GROUP EscortGroup +-- @param #AI_ESCORT self +function AI_ESCORT.___Resume( EscortGroup, self ) + + self:F( { self=self } ) + + local PlayerGroup = self.PlayerGroup + + EscortGroup:OptionROEHoldFire() + EscortGroup:OptionROTVertical() + + EscortGroup:SetState( EscortGroup, "Mode", EscortGroup:GetState( EscortGroup, "PreviousMode" ) ) + + if EscortGroup:GetState( EscortGroup, "Mode" ) == self.__Enum.Mode.Mission then + EscortGroup:MessageTypeToGroup( "Resuming route.", MESSAGE.Type.Information, PlayerGroup ) + else + EscortGroup:MessageTypeToGroup( "Rejoining formation.", MESSAGE.Type.Information, PlayerGroup ) + end + +end + + +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +-- @param #number WayPoint +function AI_ESCORT:_ResumeMission( EscortGroup, WayPoint ) + + --self.FollowScheduler:Stop( self.FollowSchedule ) + + self:SetFlightModeMission( EscortGroup ) + + local WayPoints = EscortGroup.MissionRoute + self:T( WayPoint, WayPoints ) + + for WayPointIgnore = 1, WayPoint do + table.remove( WayPoints, 1 ) + end + + EscortGroup:SetTask( EscortGroup:TaskRoute( WayPoints ), 1 ) + + EscortGroup:MessageTypeToGroup( "Resuming mission from waypoint ", MESSAGE.Type.Information, self.PlayerGroup ) +end + + +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup The escort group that will attack the detected item. +-- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem +function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem ) + + self:F( EscortGroup ) + + self:SetFlightModeAttack( EscortGroup ) + + if EscortGroup:IsAir() then + EscortGroup:OptionROEOpenFire() + EscortGroup:OptionROTVertical() + EscortGroup:SetState( EscortGroup, "Escort", self ) + + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local Tasks = {} + local AttackUnitTasks = {} + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit, Tasks ) + if DetectedUnit:IsAlive() then + AttackUnitTasks[#AttackUnitTasks+1] = EscortGroup:TaskAttackUnit( DetectedUnit ) + end + end, Tasks + ) + + Tasks[#Tasks+1] = EscortGroup:TaskCombo( AttackUnitTasks ) + Tasks[#Tasks+1] = EscortGroup:TaskFunction( "AI_ESCORT.___Resume", self ) + + EscortGroup:PushTask( + EscortGroup:TaskCombo( + Tasks + ), 1 + ) + + else + + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local Tasks = {} + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit, Tasks ) + if DetectedUnit:IsAlive() then + Tasks[#Tasks+1] = EscortGroup:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 ) + end + end, Tasks + ) + + EscortGroup:PushTask( + EscortGroup:TaskCombo( + Tasks + ), 1 + ) + + end + + local DetectedTargetsReport = REPORT:New( "Engaging target:\n" ) + local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, self.PlayerGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) + local ReportSummary = DetectedItemReportSummary:Text(", ") + DetectedTargetsReport:AddIndent( ReportSummary, "-" ) + + EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text(), MESSAGE.Type.Information, self.PlayerGroup ) +end + + +function AI_ESCORT:_FlightAttackTarget( DetectedItem ) + + self.EscortGroupSet:ForEachGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup, DetectedItem ) + if EscortGroup:IsAir() then + self:_AttackTarget( EscortGroup, DetectedItem ) + end + end, DetectedItem + ) + +end + + +function AI_ESCORT:_FlightAttackNearestTarget( TargetType ) + + self.Detection:Detect() + self:_FlightReportTargetsScheduler() + + local EscortGroup = self.EscortGroupSet:GetFirst() + local AttackDetectedItem = nil + local DetectedItems = self.Detection:GetDetectedItems() + + for DetectedItemIndex, DetectedItem in UTILS.spairs( DetectedItems, function( t, a, b ) return self:Distance( self.PlayerUnit, t[a] ) < self:Distance( self.PlayerUnit, t[b] ) end ) do + + local DetectedItemSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local HasGround = DetectedItemSet:HasGroundUnits() > 0 + local HasAir = DetectedItemSet:HasAirUnits() > 0 + + local FlightReportType = self:GetFlightReportType() + + if ( TargetType and TargetType == self.__Enum.ReportType.Ground and HasGround ) or + ( TargetType and TargetType == self.__Enum.ReportType.Air and HasAir ) or + ( TargetType == nil ) then + AttackDetectedItem = DetectedItem + break + end + end + + if AttackDetectedItem then + self:_FlightAttackTarget( AttackDetectedItem ) + else + EscortGroup:MessageTypeToGroup( "Nothing to attack!", MESSAGE.Type.Information, self.PlayerGroup ) + end + +end + + +--- +--- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup The escort group that will attack the detected item. +-- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem +function AI_ESCORT:_AssistTarget( EscortGroup, DetectedItem ) + + local EscortUnit = self.PlayerUnit + + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local Tasks = {} + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit, Tasks ) + if DetectedUnit:IsAlive() then + Tasks[#Tasks+1] = EscortGroup:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 ) + end + end, Tasks + ) + + EscortGroup:SetTask( + EscortGroup:TaskCombo( + Tasks + ), 1 + ) + + + EscortGroup:MessageTypeToGroup( "Assisting attack!", MESSAGE.Type.Information, EscortUnit:GetGroup() ) + +end + +function AI_ESCORT:_ROE( EscortGroup, EscortROEFunction, EscortROEMessage ) + pcall( function() EscortROEFunction( EscortGroup ) end ) + EscortGroup:MessageTypeToGroup( EscortROEMessage, MESSAGE.Type.Information, self.PlayerGroup ) +end + + +function AI_ESCORT:_FlightROEHoldFire( EscortROEMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROE( EscortGroup, EscortGroup.OptionROEHoldFire, EscortROEMessage ) + end + ) +end + +function AI_ESCORT:_FlightROEOpenFire( EscortROEMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROE( EscortGroup, EscortGroup.OptionROEOpenFire, EscortROEMessage ) + end + ) +end + +function AI_ESCORT:_FlightROEReturnFire( EscortROEMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROE( EscortGroup, EscortGroup.OptionROEReturnFire, EscortROEMessage ) + end + ) +end + +function AI_ESCORT:_FlightROEWeaponFree( EscortROEMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROE( EscortGroup, EscortGroup.OptionROEWeaponFree, EscortROEMessage ) + end + ) +end + + +function AI_ESCORT:_ROT( EscortGroup, EscortROTFunction, EscortROTMessage ) + pcall( function() EscortROTFunction( EscortGroup ) end ) + EscortGroup:MessageTypeToGroup( EscortROTMessage, MESSAGE.Type.Information, self.PlayerGroup ) +end + + +function AI_ESCORT:_FlightROTNoReaction( EscortROTMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROT( EscortGroup, EscortGroup.OptionROTNoReaction, EscortROTMessage ) + end + ) +end + +function AI_ESCORT:_FlightROTPassiveDefense( EscortROTMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROT( EscortGroup, EscortGroup.OptionROTPassiveDefense, EscortROTMessage ) + end + ) +end + +function AI_ESCORT:_FlightROTEvadeFire( EscortROTMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROT( EscortGroup, EscortGroup.OptionROTEvadeFire, EscortROTMessage ) + end + ) +end + +function AI_ESCORT:_FlightROTVertical( EscortROTMessage ) + self.EscortGroupSet:ForEachGroupAlive( + --- @param Wrapper.Group#GROUP EscortGroup + function( EscortGroup ) + self:_ROT( EscortGroup, EscortGroup.OptionROTVertical, EscortROTMessage ) + end + ) +end + +--- Registers the waypoints +-- @param #AI_ESCORT self +-- @return #table +function AI_ESCORT:RegisterRoute() + self:F() + + local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP + + local TaskPoints = EscortGroup:GetTaskRoute() + + self:T( TaskPoints ) + + return TaskPoints +end + +--- Resume Scheduler. +-- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +function AI_ESCORT:_ResumeScheduler( EscortGroup ) + self:F( EscortGroup:GetName() ) + + if EscortGroup:IsAlive() and self.PlayerUnit:IsAlive() then + + + local EscortGroupName = EscortGroup:GetCallsign() + + if EscortGroup.EscortMenuResumeMission then + EscortGroup.EscortMenuResumeMission:RemoveSubMenus() + + local TaskPoints = EscortGroup.MissionRoute + + for WayPointID, WayPoint in pairs( TaskPoints ) do + local EscortVec3 = EscortGroup:GetVec3() + local Distance = ( ( WayPoint.x - EscortVec3.x )^2 + + ( WayPoint.y - EscortVec3.z )^2 + ) ^ 0.5 / 1000 + MENU_GROUP_COMMAND:New( self.PlayerGroup, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", EscortGroup.EscortMenuResumeMission, AI_ESCORT._ResumeMission, self, EscortGroup, WayPointID ) + end + end + end +end + + +--- Measure distance between coordinate player and coordinate detected item. +-- @param #AI_ESCORT self +function AI_ESCORT:Distance( PlayerUnit, DetectedItem ) + + local DetectedCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + local PlayerCoordinate = PlayerUnit:GetCoordinate() + + return DetectedCoordinate:Get3DDistance( PlayerCoordinate ) + +end + +--- Report Targets Scheduler. +-- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +function AI_ESCORT:_ReportTargetsScheduler( EscortGroup, Report ) + self:F( EscortGroup:GetName() ) + + if EscortGroup:IsAlive() and self.PlayerUnit:IsAlive() then + + local EscortGroupName = EscortGroup:GetCallsign() + + local DetectedTargetsReport = REPORT:New( "Reporting targets:\n" ) -- A new report to display the detected targets as a message to the player. + + + if EscortGroup.EscortMenuTargetAssistance then + EscortGroup.EscortMenuTargetAssistance:RemoveSubMenus() + end + + local DetectedItems = self.Detection:GetDetectedItems() + + local ClientEscortTargets = self.Detection + + local TimeUpdate = timer.getTime() + + local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu ) + + local DetectedTargets = false + + for DetectedItemIndex, DetectedItem in UTILS.spairs( DetectedItems, function( t, a, b ) return self:Distance( self.PlayerUnit, t[a] ) < self:Distance( self.PlayerUnit, t[b] ) end ) do + --for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do + + local DetectedItemSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local HasGround = DetectedItemSet:HasGroundUnits() > 0 + local HasGroundRadar = HasGround and DetectedItemSet:HasRadar() > 0 + local HasAir = DetectedItemSet:HasAirUnits() > 0 + + local FlightReportType = self:GetFlightReportType() + + + if ( FlightReportType == self.__Enum.ReportType.All ) or + ( FlightReportType == self.__Enum.ReportType.Airborne and HasAir ) or + ( FlightReportType == self.__Enum.ReportType.Ground and HasGround ) or + ( FlightReportType == self.__Enum.ReportType.GroundRadar and HasGroundRadar ) then + + DetectedTargets = true + + local DetectedMenu = self.Detection:DetectedItemReportMenu( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ):Text("\n") + + local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) + local ReportSummary = DetectedItemReportSummary:Text(", ") + DetectedTargetsReport:AddIndent( ReportSummary, "-" ) + + if EscortGroup:IsAir() then + + MENU_GROUP_COMMAND:New( self.PlayerGroup, + DetectedMenu, + EscortMenuAttackTargets, + AI_ESCORT._AttackTarget, + self, + EscortGroup, + DetectedItem + ):SetTag( "Escort" ):SetTime( TimeUpdate ) + else + if self.EscortMenuTargetAssistance then + local MenuTargetAssistance = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, EscortGroup.EscortMenuTargetAssistance ) + MENU_GROUP_COMMAND:New( self.PlayerGroup, + DetectedMenu, + MenuTargetAssistance, + AI_ESCORT._AssistTarget, + self, + EscortGroup, + DetectedItem + ) + end + end + end + end + + EscortMenuAttackTargets:RemoveSubMenus( TimeUpdate, "Escort" ) + + if Report then + if DetectedTargets then + EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text( "\n" ), MESSAGE.Type.Information, self.PlayerGroup ) + else + EscortGroup:MessageTypeToGroup( "No targets detected.", MESSAGE.Type.Information, self.PlayerGroup ) + end + end + + return true + end + + return false +end + +--- Report Targets Scheduler for the flight. The report is generated from the perspective of the player plane, and is reported by the first plane in the formation set. +-- @param #AI_ESCORT self +-- @param Wrapper.Group#GROUP EscortGroup +function AI_ESCORT:_FlightReportTargetsScheduler() + + self:F("FlightReportTargetScheduler") + + local EscortGroup = self.EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP + + local DetectedTargetsReport = REPORT:New( "Reporting your targets:\n" ) -- A new report to display the detected targets as a message to the player. + + if EscortGroup and ( self.PlayerUnit:IsAlive() and EscortGroup:IsAlive() ) then + + local TimeUpdate = timer.getTime() + + local DetectedItems = self.Detection:GetDetectedItems() + + local DetectedTargets = false + + local ClientEscortTargets = self.Detection + + for DetectedItemIndex, DetectedItem in UTILS.spairs( DetectedItems, function( t, a, b ) return self:Distance( self.PlayerUnit, t[a] ) < self:Distance( self.PlayerUnit, t[b] ) end ) do + + self:F("FlightReportTargetScheduler Targets") + + local DetectedItemSet = self.Detection:GetDetectedItemSet( DetectedItem ) + + local HasGround = DetectedItemSet:HasGroundUnits() > 0 + local HasGroundRadar = HasGround and DetectedItemSet:HasRadar() > 0 + local HasAir = DetectedItemSet:HasAirUnits() > 0 + + local FlightReportType = self:GetFlightReportType() + + + if ( FlightReportType == self.__Enum.ReportType.All ) or + ( FlightReportType == self.__Enum.ReportType.Airborne and HasAir ) or + ( FlightReportType == self.__Enum.ReportType.Ground and HasGround ) or + ( FlightReportType == self.__Enum.ReportType.GroundRadar and HasGroundRadar ) then + + + DetectedTargets = true -- There are detected targets, when the content of the for loop is executed. We use it to display a message. + + local DetectedItemReportMenu = self.Detection:DetectedItemReportMenu( DetectedItem, self.PlayerGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) + local ReportMenuText = DetectedItemReportMenu:Text(", ") + + MENU_GROUP_COMMAND:New( self.PlayerGroup, + ReportMenuText, + self.FlightMenuAttack, + AI_ESCORT._FlightAttackTarget, + self, + DetectedItem + ):SetTag( "Flight" ):SetTime( TimeUpdate ) + + local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, self.PlayerGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ) + local ReportSummary = DetectedItemReportSummary:Text(", ") + DetectedTargetsReport:AddIndent( ReportSummary, "-" ) + end + end + + self.FlightMenuAttack:RemoveSubMenus( TimeUpdate, "Flight" ) + + if DetectedTargets then + EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text( "\n" ), MESSAGE.Type.Information, self.PlayerGroup ) +-- else +-- EscortGroup:MessageTypeToGroup( "No targets detected.", MESSAGE.Type.Information, self.PlayerGroup ) + end + + return true + end + + return false +end + + diff --git a/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua b/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua new file mode 100644 index 000000000..26e89a728 --- /dev/null +++ b/Moose Development/Moose/AI/AI_Escort_Dispatcher.lua @@ -0,0 +1,185 @@ +--- **AI** - Models the automatic assignment of AI escorts to player flights. +-- +-- ## Features: +-- -- +-- * Provides the facilities to trigger escorts when players join flight slots. +-- * +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Escort_Dispatcher +-- @image MOOSE.JPG + + +--- @type AI_ESCORT_DISPATCHER +-- @extends Core.Fsm#FSM + + +--- Models the automatic assignment of AI escorts to player flights. +-- +-- === +-- +-- @field #AI_ESCORT_DISPATCHER +AI_ESCORT_DISPATCHER = { + ClassName = "AI_ESCORT_DISPATCHER", +} + +--- @field #list +AI_ESCORT_DISPATCHER.AI_Escorts = {} + + +--- Creates a new AI_ESCORT_DISPATCHER object. +-- @param #AI_ESCORT_DISPATCHER self +-- @param Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers for which escorts are spawned in. +-- @param Core.Spawn#SPAWN EscortSpawn The spawn object that will spawn in the Escorts. +-- @param Wrapper.Airbase#AIRBASE EscortAirbase The airbase where the escorts are spawned. +-- @param #string EscortName Name of the escort, which will also be the name of the escort menu. +-- @param #string EscortBriefing A text showing the briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @return #AI_ESCORT_DISPATCHER +-- @usage +-- +-- -- Create a new escort when a player joins an SU-25T plane. +-- Create a carrier set, which contains the player slots that can be joined by the players, for which escorts will be defined. +-- local Red_SU25T_CarrierSet = SET_GROUP:New():FilterPrefixes( "Red A2G Player Su-25T" ):FilterStart() +-- +-- -- Create a spawn object that will spawn in the escorts, once the player has joined the player slot. +-- local Red_SU25T_EscortSpawn = SPAWN:NewWithAlias( "Red A2G Su-25 Escort", "Red AI A2G SU-25 Escort" ):InitLimit( 10, 10 ) +-- +-- -- Create an airbase object, where the escorts will be spawned. +-- local Red_SU25T_Airbase = AIRBASE:FindByName( AIRBASE.Caucasus.Maykop_Khanskaya ) +-- +-- -- Park the airplanes at the airbase, visible before start. +-- Red_SU25T_EscortSpawn:ParkAtAirbase( Red_SU25T_Airbase, AIRBASE.TerminalType.OpenMedOrBig ) +-- +-- -- New create the escort dispatcher, using the carrier set, the escort spawn object at the escort airbase. +-- -- Provide a name of the escort, which will be also the name appearing on the radio menu for the group. +-- -- And a briefing to appear when the player joins the player slot. +-- Red_SU25T_EscortDispatcher = AI_ESCORT_DISPATCHER:New( Red_SU25T_CarrierSet, Red_SU25T_EscortSpawn, Red_SU25T_Airbase, "Escort Su-25", "You Su-25T is escorted by one Su-25. Use the radio menu to control the escorts." ) +-- +-- -- The dispatcher needs to be started using the :Start() method. +-- Red_SU25T_EscortDispatcher:Start() +function AI_ESCORT_DISPATCHER:New( CarrierSet, EscortSpawn, EscortAirbase, EscortName, EscortBriefing ) + + local self = BASE:Inherit( self, FSM:New() ) -- #AI_ESCORT_DISPATCHER + + self.CarrierSet = CarrierSet + self.EscortSpawn = EscortSpawn + self.EscortAirbase = EscortAirbase + self.EscortName = EscortName + self.EscortBriefing = EscortBriefing + + self:SetStartState( "Idle" ) + + self:AddTransition( "Monitoring", "Monitor", "Monitoring" ) + + self:AddTransition( "Idle", "Start", "Monitoring" ) + self:AddTransition( "Monitoring", "Stop", "Idle" ) + + -- Put a Dead event handler on CarrierSet, to ensure that when a carrier is destroyed, that all internal parameters are reset. + function self.CarrierSet.OnAfterRemoved( CarrierSet, From, Event, To, CarrierName, Carrier ) + self:F( { Carrier = Carrier:GetName() } ) + end + + return self +end + +function AI_ESCORT_DISPATCHER:onafterStart( From, Event, To ) + + self:HandleEvent( EVENTS.Birth ) + + self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventExit ) + self:HandleEvent( EVENTS.Crash, self.OnEventExit ) + self:HandleEvent( EVENTS.Dead, self.OnEventExit ) + +end + +--- @param #AI_ESCORT_DISPATCHER self +-- @param Core.Event#EVENTDATA EventData +function AI_ESCORT_DISPATCHER:OnEventExit( EventData ) + + local PlayerGroupName = EventData.IniGroupName + local PlayerGroup = EventData.IniGroup + local PlayerUnit = EventData.IniUnit + + self:I({EscortAirbase= self.EscortAirbase } ) + self:I({PlayerGroupName = PlayerGroupName } ) + self:I({PlayerGroup = PlayerGroup}) + self:I({FirstGroup = self.CarrierSet:GetFirst()}) + self:I({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )}) + + if self.CarrierSet:FindGroup( PlayerGroupName ) then + if self.AI_Escorts[PlayerGroupName] then + self.AI_Escorts[PlayerGroupName]:Stop() + self.AI_Escorts[PlayerGroupName] = nil + end + end + +end + +--- @param #AI_ESCORT_DISPATCHER self +-- @param Core.Event#EVENTDATA EventData +function AI_ESCORT_DISPATCHER:OnEventBirth( EventData ) + + local PlayerGroupName = EventData.IniGroupName + local PlayerGroup = EventData.IniGroup + local PlayerUnit = EventData.IniUnit + + self:I({EscortAirbase= self.EscortAirbase } ) + self:I({PlayerGroupName = PlayerGroupName } ) + self:I({PlayerGroup = PlayerGroup}) + self:I({FirstGroup = self.CarrierSet:GetFirst()}) + self:I({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )}) + + if self.CarrierSet:FindGroup( PlayerGroupName ) then + if not self.AI_Escorts[PlayerGroupName] then + local LeaderUnit = PlayerUnit + local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase, SPAWN.Takeoff.Hot ) + self:I({EscortGroup = EscortGroup}) + + self:ScheduleOnce( 1, + function( EscortGroup ) + local EscortSet = SET_GROUP:New() + EscortSet:AddGroup( EscortGroup ) + self.AI_Escorts[PlayerGroupName] = AI_ESCORT:New( LeaderUnit, EscortSet, self.EscortName, self.EscortBriefing ) + self.AI_Escorts[PlayerGroupName]:FormationTrail( 0, 100, 0 ) + if EscortGroup:IsHelicopter() then + self.AI_Escorts[PlayerGroupName]:MenusHelicopters() + else + self.AI_Escorts[PlayerGroupName]:MenusAirplanes() + end + self.AI_Escorts[PlayerGroupName]:__Start( 0.1 ) + end, EscortGroup + ) + end + end + +end + + +--- Start Trigger for AI_ESCORT_DISPATCHER +-- @function [parent=#AI_ESCORT_DISPATCHER] Start +-- @param #AI_ESCORT_DISPATCHER self + +--- Start Asynchronous Trigger for AI_ESCORT_DISPATCHER +-- @function [parent=#AI_ESCORT_DISPATCHER] __Start +-- @param #AI_ESCORT_DISPATCHER self +-- @param #number Delay + +--- Stop Trigger for AI_ESCORT_DISPATCHER +-- @function [parent=#AI_ESCORT_DISPATCHER] Stop +-- @param #AI_ESCORT_DISPATCHER self + +--- Stop Asynchronous Trigger for AI_ESCORT_DISPATCHER +-- @function [parent=#AI_ESCORT_DISPATCHER] __Stop +-- @param #AI_ESCORT_DISPATCHER self +-- @param #number Delay + + + + + + diff --git a/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua b/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua new file mode 100644 index 000000000..33c614d4a --- /dev/null +++ b/Moose Development/Moose/AI/AI_Escort_Dispatcher_Request.lua @@ -0,0 +1,146 @@ +--- **AI** - Models the assignment of AI escorts to player flights upon request using the radio menu. +-- +-- ## Features: +-- +-- * Provides the facilities to trigger escorts when players join flight units. +-- * Provide a menu for which escorts can be requested. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_ESCORT_DISPATCHER_REQUEST +-- @image MOOSE.JPG + + +--- @type AI_ESCORT_DISPATCHER_REQUEST +-- @extends Core.Fsm#FSM + + +--- Models the assignment of AI escorts to player flights upon request using the radio menu. +-- +-- === +-- +-- @field #AI_ESCORT_DISPATCHER_REQUEST +AI_ESCORT_DISPATCHER_REQUEST = { + ClassName = "AI_ESCORT_DISPATCHER_REQUEST", +} + +--- @field #list +AI_ESCORT_DISPATCHER_REQUEST.AI_Escorts = {} + + +--- Creates a new AI_ESCORT_DISPATCHER_REQUEST object. +-- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers for which escorts are requested. +-- @param Core.Spawn#SPAWN EscortSpawn The spawn object that will spawn in the Escorts. +-- @param Wrapper.Airbase#AIRBASE EscortAirbase The airbase where the escorts are spawned. +-- @param #string EscortName Name of the escort, which will also be the name of the escort menu. +-- @param #string EscortBriefing A text showing the briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @return #AI_ESCORT_DISPATCHER_REQUEST +function AI_ESCORT_DISPATCHER_REQUEST:New( CarrierSet, EscortSpawn, EscortAirbase, EscortName, EscortBriefing ) + + local self = BASE:Inherit( self, FSM:New() ) -- #AI_ESCORT_DISPATCHER_REQUEST + + self.CarrierSet = CarrierSet + self.EscortSpawn = EscortSpawn + self.EscortAirbase = EscortAirbase + self.EscortName = EscortName + self.EscortBriefing = EscortBriefing + + self:SetStartState( "Idle" ) + + self:AddTransition( "Monitoring", "Monitor", "Monitoring" ) + + self:AddTransition( "Idle", "Start", "Monitoring" ) + self:AddTransition( "Monitoring", "Stop", "Idle" ) + + -- Put a Dead event handler on CarrierSet, to ensure that when a carrier is destroyed, that all internal parameters are reset. + function self.CarrierSet.OnAfterRemoved( CarrierSet, From, Event, To, CarrierName, Carrier ) + self:F( { Carrier = Carrier:GetName() } ) + end + + return self +end + +function AI_ESCORT_DISPATCHER_REQUEST:onafterStart( From, Event, To ) + + self:HandleEvent( EVENTS.Birth ) + + self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventExit ) + self:HandleEvent( EVENTS.Crash, self.OnEventExit ) + self:HandleEvent( EVENTS.Dead, self.OnEventExit ) + +end + +--- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param Core.Event#EVENTDATA EventData +function AI_ESCORT_DISPATCHER_REQUEST:OnEventExit( EventData ) + + local PlayerGroupName = EventData.IniGroupName + local PlayerGroup = EventData.IniGroup + local PlayerUnit = EventData.IniUnit + + if self.CarrierSet:FindGroup( PlayerGroupName ) then + if self.AI_Escorts[PlayerGroupName] then + self.AI_Escorts[PlayerGroupName]:Stop() + self.AI_Escorts[PlayerGroupName] = nil + end + end + +end + +--- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param Core.Event#EVENTDATA EventData +function AI_ESCORT_DISPATCHER_REQUEST:OnEventBirth( EventData ) + + local PlayerGroupName = EventData.IniGroupName + local PlayerGroup = EventData.IniGroup + local PlayerUnit = EventData.IniUnit + + if self.CarrierSet:FindGroup( PlayerGroupName ) then + if not self.AI_Escorts[PlayerGroupName] then + local LeaderUnit = PlayerUnit + self:ScheduleOnce( 0.1, + function() + self.AI_Escorts[PlayerGroupName] = AI_ESCORT_REQUEST:New( LeaderUnit, self.EscortSpawn, self.EscortAirbase, self.EscortName, self.EscortBriefing ) + self.AI_Escorts[PlayerGroupName]:FormationTrail( 0, 100, 0 ) + if PlayerGroup:IsHelicopter() then + self.AI_Escorts[PlayerGroupName]:MenusHelicopters() + else + self.AI_Escorts[PlayerGroupName]:MenusAirplanes() + end + self.AI_Escorts[PlayerGroupName]:__Start( 0.1 ) + end + ) + end + end + +end + + +--- Start Trigger for AI_ESCORT_DISPATCHER_REQUEST +-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] Start +-- @param #AI_ESCORT_DISPATCHER_REQUEST self + +--- Start Asynchronous Trigger for AI_ESCORT_DISPATCHER_REQUEST +-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] __Start +-- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param #number Delay + +--- Stop Trigger for AI_ESCORT_DISPATCHER_REQUEST +-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] Stop +-- @param #AI_ESCORT_DISPATCHER_REQUEST self + +--- Stop Asynchronous Trigger for AI_ESCORT_DISPATCHER_REQUEST +-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] __Stop +-- @param #AI_ESCORT_DISPATCHER_REQUEST self +-- @param #number Delay + + + + + + diff --git a/Moose Development/Moose/AI/AI_Escort_Request.lua b/Moose Development/Moose/AI/AI_Escort_Request.lua new file mode 100644 index 000000000..0994a143c --- /dev/null +++ b/Moose Development/Moose/AI/AI_Escort_Request.lua @@ -0,0 +1,316 @@ +--- **Functional** -- Taking the lead of AI escorting your flight or of other AI, upon request using the menu. +-- +-- === +-- +-- ## Features: +-- +-- * Escort navigation commands. +-- * Escort hold at position commands. +-- * Escorts reporting detected targets. +-- * Escorts scanning targets in advance. +-- * Escorts attacking specific targets. +-- * Request assistance from other groups for attack. +-- * Manage rule of engagement of escorts. +-- * Manage the allowed evasion techniques of escorts. +-- * Make escort to execute a defined mission or path. +-- * Escort tactical situation reporting. +-- +-- === +-- +-- ## Missions: +-- +-- [ESC - Escorting](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/ESC%20-%20Escorting) +-- +-- === +-- +-- Allows you to interact with escorting AI on your flight and take the lead. +-- +-- Each escorting group can be commanded with a complete set of radio commands (radio menu in your flight, and then F10). +-- +-- The radio commands will vary according the category of the group. The richest set of commands are with helicopters and airPlanes. +-- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. +-- +-- Escorts detect targets using a built-in detection mechanism. The detected targets are reported at a specified time interval. +-- Once targets are reported, each escort has these targets as menu options to command the attack of these targets. +-- Targets are by default grouped per area of 5000 meters, but the kind of detection and the grouping range can be altered. +-- +-- Different formations can be selected in the Flight menu: Trail, Stack, Left Line, Right Line, Left Wing, Right Wing, Central Wing and Boxed formations are available. +-- The Flight menu also allows for a mass attack, where all of the escorts are commanded to attack a target. +-- +-- Escorts can emit flares to reports their location. They can be commanded to hold at a location, which can be their current or the leader location. +-- In this way, you can spread out the escorts over the battle field before a coordinated attack. +-- +-- But basically, the escort class provides 4 modes of operation, and depending on the mode, you are either leading the flight, or following the flight. +-- +-- ## Leading the flight +-- +-- When leading the flight, you are expected to guide the escorts towards the target areas, +-- and carefully coordinate the attack based on the threat levels reported, and the available weapons +-- carried by the escorts. Ground ships or ground troops can execute A-assisted attacks, when they have long-range ground precision weapons for attack. +-- +-- ## Following the flight +-- +-- Escorts can be commanded to execute a specific mission path. In this mode, the escorts are in the lead. +-- You as a player, are following the escorts, and are commanding them to progress the mission while +-- ensuring that the escorts survive. You are joining the escorts in the battlefield. They will detect and report targets +-- and you will ensure that the attacks are well coordinated, assigning the correct escort type for the detected target +-- type. Once the attack is finished, the escort will resume the mission it was assigned. +-- In other words, you can use the escorts for reconnaissance, and for guiding the attack. +-- Imagine you as a mi-8 pilot, assigned to pickup cargo. Two ka-50s are guiding the way, and you are +-- following. You are in control. The ka-50s detect targets, report them, and you command how the attack +-- will commence and from where. You can control where the escorts are holding position and which targets +-- are attacked first. You are in control how the ka-50s will follow their mission path. +-- +-- Escorts can act as part of a AI A2G dispatcher offensive. In this way, You was a player are in control. +-- The mission is defined by the A2G dispatcher, and you are responsible to join the flight and ensure that the +-- attack is well coordinated. +-- +-- It is with great proud that I present you this class, and I hope you will enjoy the functionality and the dynamism +-- it brings in your DCS world simulations. +-- +-- # RADIO MENUs that can be created: +-- +-- Find a summary below of the current available commands: +-- +-- ## Navigation ...: +-- +-- Escort group navigation functions: +-- +-- * **"Join-Up":** The escort group fill follow you in the assigned formation. +-- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. +-- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. +-- +-- ## Hold position ...: +-- +-- Escort group navigation functions: +-- +-- * **"At current location":** The escort group will hover above the ground at the position they were. The altitude can be specified as a parameter. +-- * **"At my location":** The escort group will hover or orbit at the position where you are. The escort will fly to your location and hold position. The altitude can be specified as a parameter. +-- +-- ## Report targets ...: +-- +-- Report targets will make the escort group to report any target that it identifies within detection range. Any detected target can be attacked using the "Attack Targets" menu function. (see below). +-- +-- * **"Report now":** Will report the current detected targets. +-- * **"Report targets on":** Will make the escorts to report the detected targets and will fill the "Attack Targets" menu list. +-- * **"Report targets off":** Will stop detecting targets. +-- +-- ## Attack targets ...: +-- +-- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. +-- This menu will be available in Flight menu or in each Escort menu. +-- +-- ## Scan targets ...: +-- +-- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or rejoin formation. +-- +-- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. +-- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. +-- +-- ## Request assistance from ...: +-- +-- This menu item will list all detected targets within a 15km range, similar as with the menu item **Attack Targets**. +-- This menu item allows to request attack support from other ground based escorts supporting the current escort. +-- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. +-- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. +-- +-- ## ROE ...: +-- +-- Sets the Rules of Engagement (ROE) of the escort group when in flight. +-- +-- * **"Hold Fire":** The escort group will hold fire. +-- * **"Return Fire":** The escort group will return fire. +-- * **"Open Fire":** The escort group will open fire on designated targets. +-- * **"Weapon Free":** The escort group will engage with any target. +-- +-- ## Evasion ...: +-- +-- Will define the evasion techniques that the escort group will perform during flight or combat. +-- +-- * **"Fight until death":** The escort group will have no reaction to threats. +-- * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed. +-- * **"Evade enemy fire":** The rescort group will evade enemy fire before firing. +-- * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres. +-- +-- ## Resume Mission ...: +-- +-- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint. +-- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission. +-- +-- === +-- +-- ### Authors: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Escort +-- @image Escorting.JPG + + + +--- @type AI_ESCORT_REQUEST +-- @extends AI.AI_Escort#AI_ESCORT + +--- AI_ESCORT_REQUEST class +-- +-- # AI_ESCORT_REQUEST construction methods. +-- +-- Create a new AI_ESCORT_REQUEST object with the @{#AI_ESCORT_REQUEST.New} method: +-- +-- * @{#AI_ESCORT_REQUEST.New}: Creates a new AI_ESCORT_REQUEST object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT}, with an optional briefing text. +-- +-- @usage +-- -- Declare a new EscortPlanes object as follows: +-- +-- -- First find the GROUP object and the CLIENT object. +-- local EscortUnit = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor. +-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client. +-- +-- -- Now use these 2 objects to construct the new EscortPlanes object. +-- EscortPlanes = AI_ESCORT_REQUEST:New( EscortUnit, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) +-- +-- @field #AI_ESCORT_REQUEST +AI_ESCORT_REQUEST = { + ClassName = "AI_ESCORT_REQUEST", +} + +--- AI_ESCORT_REQUEST.Mode class +-- @type AI_ESCORT_REQUEST.MODE +-- @field #number FOLLOW +-- @field #number MISSION + +--- MENUPARAM type +-- @type MENUPARAM +-- @field #AI_ESCORT_REQUEST ParamSelf +-- @field #Distance ParamDistance +-- @field #function ParamFunction +-- @field #string ParamMessage + +--- AI_ESCORT_REQUEST class constructor for an AI group +-- @param #AI_ESCORT_REQUEST self +-- @param Wrapper.Client#CLIENT EscortUnit The client escorted by the EscortGroup. +-- @param Core.Spawn#SPAWN EscortSpawn The spawn object of AI, escorting the EscortUnit. +-- @param Wrapper.Airbase#AIRBASE EscortAirbase The airbase where escorts will be spawned once requested. +-- @param #string EscortName Name of the escort. +-- @param #string EscortBriefing A text showing the AI_ESCORT_REQUEST briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown. +-- @return #AI_ESCORT_REQUEST +-- @usage +-- EscortSpawn = SPAWN:NewWithAlias( "Red A2G Escort Template", "Red A2G Escort AI" ):InitLimit( 10, 10 ) +-- EscortSpawn:ParkAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Sochi_Adler ), AIRBASE.TerminalType.OpenBig ) +-- +-- local EscortUnit = UNIT:FindByName( "Red A2G Pilot" ) +-- +-- Escort = AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, AIRBASE:FindByName(AIRBASE.Caucasus.Sochi_Adler), "A2G", "Briefing" ) +-- Escort:FormationTrail( 50, 100, 100 ) +-- Escort:Menus() +-- Escort:__Start( 5 ) +function AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, EscortAirbase, EscortName, EscortBriefing ) + + local EscortGroupSet = SET_GROUP:New():FilterDeads():FilterCrashes() + local self = BASE:Inherit( self, AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) ) -- #AI_ESCORT_REQUEST + + self.EscortGroupSet = EscortGroupSet + self.EscortSpawn = EscortSpawn + self.EscortAirbase = EscortAirbase + + self.LeaderGroup = self.PlayerUnit:GetGroup() + + self.Detection = DETECTION_AREAS:New( self.EscortGroupSet, 5000 ) + self.Detection:__Start( 30 ) + + self.SpawnMode = self.__Enum.Mode.Mission + + return self +end + +--- @param #AI_ESCORT_REQUEST self +function AI_ESCORT_REQUEST:SpawnEscort() + + local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase, SPAWN.Takeoff.Hot ) + + self:ScheduleOnce( 0.1, + function( EscortGroup ) + + EscortGroup:OptionROTVertical() + EscortGroup:OptionROEHoldFire() + + self.EscortGroupSet:AddGroup( EscortGroup ) + + local LeaderEscort = self.EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP + local Report = REPORT:New() + Report:Add( "Joining Up " .. self.EscortGroupSet:GetUnitTypeNames():Text( ", " ) .. " from " .. LeaderEscort:GetCoordinate():ToString( self.EscortUnit ) ) + LeaderEscort:MessageTypeToGroup( Report:Text(), MESSAGE.Type.Information, self.PlayerUnit ) + + self:SetFlightModeFormation( EscortGroup ) + self:FormationTrail() + + self:_InitFlightMenus() + self:_InitEscortMenus( EscortGroup ) + self:_InitEscortRoute( EscortGroup ) + + --- @param #AI_ESCORT self + -- @param Core.Event#EVENTDATA EventData + function EscortGroup:OnEventDeadOrCrash( EventData ) + self:F( { "EventDead", EventData } ) + self.EscortMenu:Remove() + end + + EscortGroup:HandleEvent( EVENTS.Dead, EscortGroup.OnEventDeadOrCrash ) + EscortGroup:HandleEvent( EVENTS.Crash, EscortGroup.OnEventDeadOrCrash ) + + end, EscortGroup + ) + +end + +--- @param #AI_ESCORT_REQUEST self +-- @param Core.Set#SET_GROUP EscortGroupSet +function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet ) + + self:F() + + if not self.MenuRequestEscort then + self.MainMenu = MENU_GROUP:New( self.PlayerGroup, self.EscortName ) + self.MenuRequestEscort = MENU_GROUP_COMMAND:New( self.LeaderGroup, "Request new escort ", self.MainMenu, + function() + self:SpawnEscort() + end + ) + end + + self:GetParent( self ).onafterStart( self, EscortGroupSet ) + + self:HandleEvent( EVENTS.Dead, self.OnEventDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self.OnEventDeadOrCrash ) + +end + +--- @param #AI_ESCORT_REQUEST self +-- @param Core.Set#SET_GROUP EscortGroupSet +function AI_ESCORT_REQUEST:onafterStop( EscortGroupSet ) + + self:F() + + EscortGroupSet:ForEachGroup( + --- @param Core.Group#GROUP EscortGroup + function( EscortGroup ) + EscortGroup:WayPointInitialize() + + EscortGroup:OptionROTVertical() + EscortGroup:OptionROEOpenFire() + end + ) + + self.Detection:Stop() + + self.MainMenu:Remove() + +end + +--- Set the spawn mode to be mission execution. +-- @param #AI_ESCORT_REQUEST self +function AI_ESCORT_REQUEST:SetEscortSpawnMission() + + self.SpawnMode = self.__Enum.Mode.Mission + +end diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 489e69cf3..2ecca0958 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -36,6 +36,7 @@ -- @field #boolean ReportTargets If true, nearby targets are reported. -- @Field DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the FollowGroup. -- @field DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the FollowGroup. +-- @field #number dtFollow Time step between position updates. --- Build large formations, make AI follow a @{Wrapper.Client#CLIENT} (player) leader or a @{Wrapper.Unit#UNIT} (AI) leader. @@ -106,12 +107,59 @@ AI_FORMATION = { FollowScheduler = nil, OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE, OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION, + dtFollow = 0.5, } ---- AI_FORMATION.Mode class --- @type AI_FORMATION.MODE --- @field #number FOLLOW --- @field #number MISSION +AI_FORMATION.__Enum = {} + +--- @type AI_FORMATION.__Enum.Formation +-- @field #number None +-- @field #number Line +-- @field #number Trail +-- @field #number Stack +-- @field #number LeftLine +-- @field #number RightLine +-- @field #number LeftWing +-- @field #number RightWing +-- @field #number Vic +-- @field #number Box +AI_FORMATION.__Enum.Formation = { + None = 0, + Mission = 1, + Line = 2, + Trail = 3, + Stack = 4, + LeftLine = 5, + RightLine = 6, + LeftWing = 7, + RightWing = 8, + Vic = 9, + Box = 10, +} + +--- @type AI_FORMATION.__Enum.Mode +-- @field #number Mission +-- @field #number Formation +AI_FORMATION.__Enum.Mode = { + Mission = "M", + Formation = "F", + Attack = "A", + Reconnaissance = "R", +} + +--- @type AI_FORMATION.__Enum.ReportType +-- @field #number All +-- @field #number Airborne +-- @field #number GroundRadar +-- @field #number Ground +AI_FORMATION.__Enum.ReportType = { + Airborne = "*", + Airborne = "A", + GroundRadar = "R", + Ground = "G", +} + + --- MENUPARAM type -- @type MENUPARAM @@ -125,6 +173,7 @@ AI_FORMATION = { -- @param Wrapper.Unit#UNIT FollowUnit The UNIT leading the FolllowGroupSet. -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. -- @param #string FollowName Name of the escort. +-- @param #string FollowBriefing Briefing. -- @return #AI_FORMATION self function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefing ) --R2.1 local self = BASE:Inherit( self, FSM_SET:New( FollowGroupSet ) ) @@ -133,13 +182,20 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin self.FollowUnit = FollowUnit -- Wrapper.Unit#UNIT self.FollowGroupSet = FollowGroupSet -- Core.Set#SET_GROUP + self.FollowGroupSet:ForEachGroup( + function( FollowGroup ) + self:E("Following") + FollowGroup:SetState( self, "Mode", self.__Enum.Mode.Formation ) + end + ) + self:SetFlightRandomization( 2 ) self:SetStartState( "None" ) self:AddTransition( "*", "Stop", "Stopped" ) - self:AddTransition( "None", "Start", "Following" ) + self:AddTransition( {"None", "Stopped"}, "Start", "Following" ) self:AddTransition( "*", "FormationLine", "*" ) --- FormationLine Handler OnBefore for AI_FORMATION @@ -620,6 +676,16 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin return self end + +--- Set time interval between updates of the formation. +-- @param #AI_FORMATION self +-- @param #number dt Time step in seconds between formation updates. Default is every 0.5 seconds. +-- @return #AI_FORMATION +function AI_FORMATION:SetFollowTimeInterval(dt) --R2.1 + self.dtFollow=dt or 0.5 + return self +end + --- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to. -- This allows to visualize where the escort is flying to. -- @param #AI_FORMATION self @@ -643,8 +709,15 @@ end -- @param #nubmer ZStart The start position on the Z-axis in meters for the first group. -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. -- @return #AI_FORMATION -function AI_FORMATION:onafterFormationLine( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) --R2.1 - self:F( { FollowGroupSet, From , Event ,To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace } ) +function AI_FORMATION:onafterFormationLine( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, Formation ) --R2.1 + self:F( { FollowGroupSet, From , Event ,To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, Formation } ) + + XStart = XStart or self.XStart + XSpace = XSpace or self.XSpace + YStart = YStart or self.YStart + YSpace = YSpace or self.YSpace + ZStart = ZStart or self.ZStart + ZSpace = ZSpace or self.ZSpace FollowGroupSet:Flush( self ) @@ -662,6 +735,8 @@ function AI_FORMATION:onafterFormationLine( FollowGroupSet, From , Event , To, X local Vec3 = PointVec3:GetVec3() FollowGroup:SetState( self, "FormationVec3", Vec3 ) i = i + 1 + + FollowGroup:SetState( FollowGroup, "Formation", Formation ) end return self @@ -680,7 +755,7 @@ end -- @return #AI_FORMATION function AI_FORMATION:onafterFormationTrail( FollowGroupSet, From , Event , To, XStart, XSpace, YStart ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,0,0) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,0,0, self.__Enum.Formation.Trail ) return self end @@ -699,7 +774,7 @@ end -- @return #AI_FORMATION function AI_FORMATION:onafterFormationStack( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,0,0) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,0,0, self.__Enum.Formation.Stack ) return self end @@ -720,7 +795,7 @@ end -- @return #AI_FORMATION function AI_FORMATION:onafterFormationLeftLine( FollowGroupSet, From , Event , To, XStart, YStart, ZStart, ZSpace ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,ZStart,ZSpace) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,-ZStart,-ZSpace, self.__Enum.Formation.LeftLine ) return self end @@ -739,7 +814,7 @@ end -- @return #AI_FORMATION function AI_FORMATION:onafterFormationRightLine( FollowGroupSet, From , Event , To, XStart, YStart, ZStart, ZSpace ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,-ZStart,-ZSpace) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,ZStart,ZSpace,self.__Enum.Formation.RightLine) return self end @@ -758,7 +833,7 @@ end -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. function AI_FORMATION:onafterFormationLeftWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, ZStart, ZSpace ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,ZStart,ZSpace) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,-ZStart,-ZSpace,self.__Enum.Formation.LeftWing) return self end @@ -778,7 +853,7 @@ end -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group. function AI_FORMATION:onafterFormationRightWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, ZStart, ZSpace ) --R2.1 - self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,-ZStart,-ZSpace) + self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,ZStart,ZSpace,self.__Enum.Formation.RightWing) return self end @@ -816,6 +891,7 @@ function AI_FORMATION:onafterFormationCenterWing( FollowGroupSet, From , Event , local Vec3 = PointVec3:GetVec3() FollowGroup:SetState( self, "FormationVec3", Vec3 ) i = i + 1 + FollowGroup:SetState( FollowGroup, "Formation", self.__Enum.Formation.Vic ) end return self @@ -875,6 +951,7 @@ function AI_FORMATION:onafterFormationBox( FollowGroupSet, From , Event , To, XS local Vec3 = PointVec3:GetVec3() FollowGroup:SetState( self, "FormationVec3", Vec3 ) i = i + 1 + FollowGroup:SetState( FollowGroup, "Formation", self.__Enum.Formation.Box ) end return self @@ -893,17 +970,126 @@ function AI_FORMATION:SetFlightRandomization( FlightRandomization ) --R2.1 end ---- @param Follow#AI_FORMATION self -function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 - self:F( ) +--- Gets your escorts to flight mode. +-- @param #AI_FORMATION self +-- @param Wrapper.Group#GROUP FollowGroup FollowGroup. +-- @return #AI_FORMATION +function AI_FORMATION:GetFlightMode( FollowGroup ) + + if FollowGroup then + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission ) + end + + + return FollowGroup:GetState( FollowGroup, "Mode" ) +end + + + +--- This sets your escorts to fly a mission. +-- @param #AI_FORMATION self +-- @param Wrapper.Group#GROUP FollowGroup FollowGroup. +-- @return #AI_FORMATION +function AI_FORMATION:SetFlightModeMission( FollowGroup ) + + if FollowGroup then + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission ) + else + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( FollowGroup ) + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission ) + end + ) + end + + + return self +end + + +--- This sets your escorts to execute an attack. +-- @param #AI_FORMATION self +-- @param Wrapper.Group#GROUP FollowGroup FollowGroup. +-- @return #AI_FORMATION +function AI_FORMATION:SetFlightModeAttack( FollowGroup ) + + if FollowGroup then + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Attack ) + else + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( FollowGroup ) + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Attack ) + end + ) + end + + + return self +end + + +--- This sets your escorts to fly in a formation. +-- @param #AI_FORMATION self +-- @param Wrapper.Group#GROUP FollowGroup FollowGroup. +-- @return #AI_FORMATION +function AI_FORMATION:SetFlightModeFormation( FollowGroup ) + + if FollowGroup then + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) + else + self.EscortGroupSet:ForSomeGroupAlive( + --- @param Core.Group#GROUP EscortGroup + function( FollowGroup ) + FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) ) + FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation ) + end + ) + end + + return self +end + + + + +--- Stop function. Formation will not be updated any more. +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. +-- @param #string From From state. +-- @param #string Event Event. +-- @pram #string To The to state. +function AI_FORMATION:onafterStop(FollowGroupSet, From, Event, To) --R2.1 + self:E("Stopping formation.") +end + +--- Follow event fuction. Check if coming from state "stopped". If so the transition is rejected. +-- @param #AI_FORMATION self +-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups. +-- @param #string From From state. +-- @param #string Event Event. +-- @pram #string To The to state. +function AI_FORMATION:onbeforeFollow( FollowGroupSet, From, Event, To ) --R2.1 + if From=="Stopped" then + return false -- Deny transition. + end + return true +end + +--- @param #AI_FORMATION self +function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 - self:T( { self.FollowUnit.UnitName, self.FollowUnit:IsAlive() } ) if self.FollowUnit:IsAlive() then local ClientUnit = self.FollowUnit - self:T( {ClientUnit.UnitName } ) - local CT1, CT2, CV1, CV2 CT1 = ClientUnit:GetState( self, "CT1" ) @@ -920,120 +1106,139 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 ClientUnit:SetState( self, "CV1", CV2 ) end - FollowGroupSet:ForEachGroup( + FollowGroupSet:ForEachGroupAlive( --- @param Wrapper.Group#GROUP FollowGroup -- @param Wrapper.Unit#UNIT ClientUnit function( FollowGroup, Formation, ClientUnit, CT1, CV1, CT2, CV2 ) + + if FollowGroup:GetState( FollowGroup, "Mode" ) == self.__Enum.Mode.Formation then - FollowGroup:OptionROTEvadeFire() - FollowGroup:OptionROEReturnFire() + self:T({Mode=FollowGroup:GetState( FollowGroup, "Mode" )}) - local GroupUnit = FollowGroup:GetUnit( 1 ) - 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", GroupUnit:GetPointVec3() ) - 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 ) - + FollowGroup:OptionROTEvadeFire() + FollowGroup:OptionROEReturnFire() + + local GroupUnit = FollowGroup:GetUnit( 1 ) + local FollowFormation = FollowGroup:GetState( self, "FormationVec3" ) + if FollowFormation then + local FollowDistance = FollowFormation.x + local GT1 = GroupUnit:GetState( self, "GT1" ) - local GT2 = timer.getTime() - local GV1 = GroupUnit:GetState( self, "GV1" ) - local GV2 = GroupUnit:GetPointVec3() - GV2:AddX( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) - GV2:AddY( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) - GV2:AddZ( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) - 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 - + + if CT1 == nil or CT1 == 0 or GT1 == nil or GT1 == 0 then + GroupUnit:SetState( self, "GV1", GroupUnit:GetPointVec3() ) + 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 = GroupUnit:GetPointVec3() + GV2:AddX( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) + GV2:AddY( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) + GV2:AddZ( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) ) + GroupUnit:SetState( self, "GT1", GT2 ) + GroupUnit:SetState( self, "GV1", GV2 ) + + + local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5 + local GT = GT2 - GT1 + + + -- Calculate the distance + local GDv = { x = GV2.x - CV1.x, y = GV2.y - CV1.y, z = GV2.z - CV1.z } + local Alpha_T = math.atan2( GDv.x, GDv.z ) - math.atan2( CDv.x, CDv.z ) + local Alpha_R = ( Alpha_T < 0 ) and Alpha_T + 2 * math.pi or Alpha_T + local Position = math.cos( Alpha_R ) + local GD = ( ( GDv.x )^2 + ( GDv.z )^2 ) ^ 0.5 + local Distance = GD * Position + - CS * 0.5 + + -- Calculate the group direction vector + local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z } + + -- Calculate GH2, GH2 with the same height as CV2. + local GH2 = { x = GV2.x, y = CV2.y + FollowFormation.y, z = GV2.z } + + -- Calculate the angle of GV to the orthonormal plane + local alpha = math.atan2( GV.x, GV.z ) + + local GVx = FollowFormation.z * math.cos( Ca ) + FollowFormation.x * math.sin( Ca ) + local GVz = FollowFormation.x * math.cos( Ca ) - FollowFormation.z * math.sin( Ca ) + + + -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. + -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) + local Inclination = ( Distance + FollowFormation.x ) / 10 + if Inclination < -30 then + Inclination = - 30 + end + local CVI = { x = CV2.x + CS * 10 * math.sin(Ca), + y = GH2.y + Inclination, -- + FollowFormation.y, + y = GH2.y, + z = CV2.z + CS * 10 * math.cos(Ca), + } + + -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction. + local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z } + + -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s. + -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed. + -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right... + local DVu = { x = DV.x / FollowDistance, y = DV.y, z = DV.z / FollowDistance } + + -- Now we can calculate the group destination vector GDV. + local GDV = { x = CVI.x, y = CVI.y, z = CVI.z } + + local ADDx = FollowFormation.x * math.cos(alpha) - FollowFormation.z * math.sin(alpha) + local ADDz = FollowFormation.z * math.cos(alpha) + FollowFormation.x * math.sin(alpha) + + local GDV_Formation = { + x = GDV.x - GVx, + y = GDV.y, + z = GDV.z - GVz + } + + 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 - -- 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 ) + if Distance > -10000 then + Speed = - ( Distance + FollowFormation.x ) / 60 + end + + if Distance > -2500 then + Speed = - ( Distance + FollowFormation.x ) / 20 + end + + local GS = Speed + CS - - -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2. - -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2)) - local CVI = { x = CV2.x + CS * 10 * math.sin(Ca), - y = GH2.y - ( Distance + FollowFormation.x ) / 5, -- + 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 - } - - if self.SmokeDirectionVector == true then - trigger.action.smoke( GDV, trigger.smokeColor.Green ) - trigger.action.smoke( GDV_Formation, trigger.smokeColor.White ) + 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 - - - - local Time = 60 - - local Speed = - ( Distance + FollowFormation.x ) / Time - local GS = Speed + CS - if Speed < 0 then - Speed = 0 - end - - -- 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, self, ClientUnit, CT1, CV1, CT2, CV2 ) - - self:__Follow( -0.5 ) + + self:__Follow( -self.dtFollow ) end end diff --git a/Moose Development/Moose/AI/AI_Patrol.lua b/Moose Development/Moose/AI/AI_Patrol.lua index c4c0330bf..767dc4400 100644 --- a/Moose Development/Moose/AI/AI_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Patrol.lua @@ -178,8 +178,8 @@ function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltit self.PatrolMinSpeed = PatrolMinSpeed self.PatrolMaxSpeed = PatrolMaxSpeed - -- defafult PatrolAltType to "RADIO" if not specified - self.PatrolAltType = PatrolAltType or "RADIO" + -- defafult PatrolAltType to "BARO" if not specified + self.PatrolAltType = PatrolAltType or "BARO" self:SetRefreshTimeInterval( 30 ) @@ -636,7 +636,7 @@ function AI_PATROL_ZONE:onafterStart( Controllable, From, Event, To ) self.Controllable:OnReSpawn( function( PatrolGroup ) - self:E( "ReSpawn" ) + self:T( "ReSpawn" ) self:__Reset( 1 ) self:__Route( 5 ) end @@ -667,21 +667,27 @@ function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To ) if TargetObject and TargetObject:isExist() and TargetObject.id_ < 50000000 then local TargetUnit = UNIT:Find( TargetObject ) - local TargetUnitName = TargetUnit:GetName() - if self.DetectionZone then - if TargetUnit:IsInZone( self.DetectionZone ) then - self:T( {"Detected ", TargetUnit } ) + -- Check that target is alive due to issue https://github.com/FlightControl-Master/MOOSE/issues/1234 + if TargetUnit and TargetUnit:IsAlive() then + + local TargetUnitName = TargetUnit:GetName() + + if self.DetectionZone then + if TargetUnit:IsInZone( self.DetectionZone ) then + self:T( {"Detected ", TargetUnit } ) + if self.DetectedUnits[TargetUnit] == nil then + self.DetectedUnits[TargetUnit] = true + end + Detected = true + end + else if self.DetectedUnits[TargetUnit] == nil then self.DetectedUnits[TargetUnit] = true end - Detected = true + Detected = true end - else - if self.DetectedUnits[TargetUnit] == nil then - self.DetectedUnits[TargetUnit] = true - end - Detected = true + end end end @@ -735,7 +741,7 @@ function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) -- This will make the plane fly immediately to the patrol zone. if self.Controllable:InAir() == false then - self:E( "Not in the air, finding route path within PatrolZone" ) + self:T( "Not in the air, finding route path within PatrolZone" ) local CurrentVec2 = self.Controllable:GetVec2() --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() @@ -750,7 +756,7 @@ function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To ) ) PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint else - self:E( "In the air, finding route path within PatrolZone" ) + self:T( "In the air, finding route path within PatrolZone" ) local CurrentVec2 = self.Controllable:GetVec2() --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1). local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude() @@ -825,7 +831,7 @@ function AI_PATROL_ZONE:onafterStatus() local Fuel = self.Controllable:GetFuelMin() if Fuel < self.PatrolFuelThresholdPercentage then - self:E( self.Controllable:GetName() .. " is out of fuel:" .. Fuel .. ", RTB!" ) + self:I( self.Controllable:GetName() .. " is out of fuel:" .. Fuel .. ", RTB!" ) local OldAIControllable = self.Controllable local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) @@ -839,7 +845,7 @@ function AI_PATROL_ZONE:onafterStatus() -- TODO: Check GROUP damage function. local Damage = self.Controllable:GetLife() if Damage <= self.PatrolDamageThreshold then - self:E( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" ) + self:I( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" ) RTB = true end @@ -900,7 +906,6 @@ end function AI_PATROL_ZONE:OnCrash( EventData ) if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then - self:E( self.Controllable:GetUnits() ) if #self.Controllable:GetUnits() == 1 then self:__Crash( 1, EventData ) end diff --git a/Moose Development/Moose/Actions/Act_Account.lua b/Moose Development/Moose/Actions/Act_Account.lua index b0d26c9ea..835b52871 100644 --- a/Moose Development/Moose/Actions/Act_Account.lua +++ b/Moose Development/Moose/Actions/Act_Account.lua @@ -1,34 +1,34 @@ --- **Actions** - ACT_ACCOUNT_ classes **account for** (detect, count & report) various DCS events occuring on @{Wrapper.Unit}s. --- +-- -- ![Banner Image](..\Presentations\ACT_ACCOUNT\Dia1.JPG) --- --- === --- +-- +-- === +-- -- @module Actions.Account -- @image MOOSE.JPG do -- ACT_ACCOUNT - + --- # @{#ACT_ACCOUNT} FSM class, extends @{Core.Fsm#FSM_PROCESS} - -- - -- ## ACT_ACCOUNT state machine: - -- + -- + -- ## ACT_ACCOUNT state machine: + -- -- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. -- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. - -- Each derived class follows exactly the same process, using the same events and following the same state transitions, + -- Each derived class follows exactly the same process, using the same events and following the same state transitions, -- but will have **different implementation behaviour** upon each event or state transition. - -- - -- ### ACT_ACCOUNT States - -- + -- + -- ### ACT_ACCOUNT States + -- -- * **Asigned**: The player is assigned. -- * **Waiting**: Waiting for an event. -- * **Report**: Reporting. -- * **Account**: Account for an event. -- * **Accounted**: All events have been accounted for, end of the process. -- * **Failed**: Failed the process. - -- - -- ### ACT_ACCOUNT Events - -- + -- + -- ### ACT_ACCOUNT Events + -- -- * **Start**: Start the process. -- * **Wait**: Wait for an event. -- * **Report**: Report the status of the accounting. @@ -36,32 +36,32 @@ do -- ACT_ACCOUNT -- * **More**: More targets. -- * **NoMore (*)**: No more targets. -- * **Fail (*)**: The action process has failed. - -- + -- -- (*) End states of the process. - -- + -- -- ### ACT_ACCOUNT state transition methods: - -- + -- -- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. -- There are 2 moments when state transition methods will be called by the state machine: - -- - -- * **Before** the state transition. - -- The state transition method needs to start with the name **OnBefore + the name of the state**. + -- + -- * **Before** the state transition. + -- The state transition method needs to start with the name **OnBefore + the name of the state**. -- If the state transition method returns false, then the processing of the state transition will not be done! - -- If you want to change the behaviour of the AIControllable at this event, return false, + -- If you want to change the behaviour of the AIControllable at this event, return false, -- but then you'll need to specify your own logic using the AIControllable! - -- - -- * **After** the state transition. - -- The state transition method needs to start with the name **OnAfter + the name of the state**. + -- + -- * **After** the state transition. + -- The state transition method needs to start with the name **OnAfter + the name of the state**. -- These state transition methods need to provide a return value, which is specified at the function description. - -- + -- -- @type ACT_ACCOUNT -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends Core.Fsm#FSM_PROCESS - ACT_ACCOUNT = { + ACT_ACCOUNT = { ClassName = "ACT_ACCOUNT", TargetSetUnit = nil, } - + --- Creates a new DESTROY process. -- @param #ACT_ACCOUNT self -- @return #ACT_ACCOUNT @@ -69,7 +69,7 @@ do -- ACT_ACCOUNT -- Inherits from BASE local self = BASE:Inherit( self, FSM_PROCESS:New() ) -- Core.Fsm#FSM_PROCESS - + self:AddTransition( "Assigned", "Start", "Waiting" ) self:AddTransition( "*", "Wait", "Waiting" ) self:AddTransition( "*", "Report", "Report" ) @@ -79,16 +79,16 @@ do -- ACT_ACCOUNT self:AddTransition( { "Account", "AccountForPlayer", "AccountForOther" }, "More", "Wait" ) self:AddTransition( { "Account", "AccountForPlayer", "AccountForOther" }, "NoMore", "Accounted" ) self:AddTransition( "*", "Fail", "Failed" ) - + self:AddEndState( "Failed" ) - - self:SetStartState( "Assigned" ) - + + self:SetStartState( "Assigned" ) + return self end --- Process Events - + --- StateMachine callback function -- @param #ACT_ACCOUNT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -104,7 +104,7 @@ do -- ACT_ACCOUNT self:__Wait( 1 ) end - + --- StateMachine callback function -- @param #ACT_ACCOUNT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -112,17 +112,17 @@ do -- ACT_ACCOUNT -- @param #string From -- @param #string To function ACT_ACCOUNT:onenterWaiting( ProcessUnit, From, Event, To ) - + if self.DisplayCount >= self.DisplayInterval then self:Report() self.DisplayCount = 1 else self.DisplayCount = self.DisplayCount + 1 end - + return true -- Process always the event. end - + --- StateMachine callback function -- @param #ACT_ACCOUNT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -130,30 +130,30 @@ do -- ACT_ACCOUNT -- @param #string From -- @param #string To function ACT_ACCOUNT:onafterEvent( ProcessUnit, From, Event, To, Event ) - + self:__NoMore( 1 ) end - + end -- ACT_ACCOUNT do -- ACT_ACCOUNT_DEADS --- # @{#ACT_ACCOUNT_DEADS} FSM class, extends @{Core.Fsm.Account#ACT_ACCOUNT} - -- + -- -- The ACT_ACCOUNT_DEADS class accounts (detects, counts and reports) successful kills of DCS units. -- The process is given a @{Set} of units that will be tracked upon successful destruction. -- The process will end after each target has been successfully destroyed. -- Each successful dead will trigger an Account state transition that can be scored, modified or administered. - -- - -- + -- + -- -- ## ACT_ACCOUNT_DEADS constructor: - -- + -- -- * @{#ACT_ACCOUNT_DEADS.New}(): Creates a new ACT_ACCOUNT_DEADS object. - -- + -- -- @type ACT_ACCOUNT_DEADS -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends #ACT_ACCOUNT - ACT_ACCOUNT_DEADS = { + ACT_ACCOUNT_DEADS = { ClassName = "ACT_ACCOUNT_DEADS", } @@ -165,24 +165,24 @@ do -- ACT_ACCOUNT_DEADS function ACT_ACCOUNT_DEADS:New() -- Inherits from BASE local self = BASE:Inherit( self, ACT_ACCOUNT:New() ) -- #ACT_ACCOUNT_DEADS - + self.DisplayInterval = 30 self.DisplayCount = 30 self.DisplayMessage = true self.DisplayTime = 10 -- 10 seconds is the default self.DisplayCategory = "HQ" -- Targets is the default display category - + return self end - + function ACT_ACCOUNT_DEADS:Init( FsmAccount ) - - self.Task = self:GetTask() + + self.Task = self:GetTask() self.TaskName = self.Task:GetName() end --- Process Events - + --- StateMachine callback function -- @param #ACT_ACCOUNT_DEADS self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -190,12 +190,12 @@ do -- ACT_ACCOUNT_DEADS -- @param #string From -- @param #string To function ACT_ACCOUNT_DEADS:onenterReport( ProcessUnit, Task, From, Event, To ) - + local MessageText = "Your group with assigned " .. self.TaskName .. " task has " .. Task.TargetSetUnit:GetUnitTypesText() .. " targets left to be destroyed." self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) end - - + + --- StateMachine callback function -- @param #ACT_ACCOUNT_DEADS self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -206,7 +206,7 @@ do -- ACT_ACCOUNT_DEADS -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:onafterEvent( ProcessUnit, Task, From, Event, To, EventData ) self:T( { ProcessUnit:GetName(), Task:GetName(), From, Event, To, EventData } ) - + if Task.TargetSetUnit:FindUnit( EventData.IniUnitName ) then local PlayerName = ProcessUnit:GetPlayerName() local PlayerHit = self.PlayerHits and self.PlayerHits[EventData.IniUnitName] @@ -228,14 +228,14 @@ do -- ACT_ACCOUNT_DEADS -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:onenterAccountForPlayer( ProcessUnit, Task, From, Event, To, EventData ) self:T( { ProcessUnit:GetName(), Task:GetName(), From, Event, To, EventData } ) - + local TaskGroup = ProcessUnit:GetGroup() Task.TargetSetUnit:Remove( EventData.IniUnitName ) - + local MessageText = "You have destroyed a target.\nYour group assigned with task " .. self.TaskName .. " has\n" .. Task.TargetSetUnit:Count() .. " targets ( " .. Task.TargetSetUnit:GetUnitTypesText() .. " ) left to be destroyed." self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) - + local PlayerName = ProcessUnit:GetPlayerName() Task:AddProgress( PlayerName, "Destroyed " .. EventData.IniTypeName, timer.getTime(), 1 ) @@ -256,10 +256,10 @@ do -- ACT_ACCOUNT_DEADS -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:onenterAccountForOther( ProcessUnit, Task, From, Event, To, EventData ) self:T( { ProcessUnit:GetName(), Task:GetName(), From, Event, To, EventData } ) - + local TaskGroup = ProcessUnit:GetGroup() Task.TargetSetUnit:Remove( EventData.IniUnitName ) - + local MessageText = "One of the task targets has been destroyed.\nYour group assigned with task " .. self.TaskName .. " has\n" .. Task.TargetSetUnit:Count() .. " targets ( " .. Task.TargetSetUnit:GetUnitTypesText() .. " ) left to be destroyed." self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) @@ -270,9 +270,9 @@ do -- ACT_ACCOUNT_DEADS end end - + --- DCS Events - + --- @param #ACT_ACCOUNT_DEADS self -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:OnEventHit( EventData ) @@ -282,8 +282,8 @@ do -- ACT_ACCOUNT_DEADS self.PlayerHits = self.PlayerHits or {} self.PlayerHits[EventData.TgtDCSUnitName] = EventData.IniPlayerName end - end - + end + --- @param #ACT_ACCOUNT_DEADS self -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:onfuncEventDead( EventData ) @@ -295,7 +295,7 @@ do -- ACT_ACCOUNT_DEADS end --- DCS Events - + --- @param #ACT_ACCOUNT_DEADS self -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:onfuncEventCrash( EventData ) diff --git a/Moose Development/Moose/Actions/Act_Assign.lua b/Moose Development/Moose/Actions/Act_Assign.lua index 969009ab1..c6748cba8 100644 --- a/Moose Development/Moose/Actions/Act_Assign.lua +++ b/Moose Development/Moose/Actions/Act_Assign.lua @@ -1,82 +1,82 @@ --- (SP) (MP) (FSM) Accept or reject process for player (task) assignments. --- +-- -- === --- +-- -- # @{#ACT_ASSIGN} FSM template class, extends @{Core.Fsm#FSM_PROCESS} --- +-- -- ## ACT_ASSIGN state machine: --- +-- -- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. -- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, -- but will have **different implementation behaviour** upon each event or state transition. --- +-- -- ### ACT_ASSIGN **Events**: --- +-- -- These are the events defined in this class: --- +-- -- * **Start**: Start the tasking acceptance process. -- * **Assign**: Assign the task. -- * **Reject**: Reject the task.. --- +-- -- ### ACT_ASSIGN **Event methods**: --- +-- -- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. -- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- +-- -- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- -- ### ACT_ASSIGN **States**: --- +-- -- * **UnAssigned**: The player has not accepted the task. -- * **Assigned (*)**: The player has accepted the task. -- * **Rejected (*)**: The player has not accepted the task. -- * **Waiting**: The process is awaiting player feedback. -- * **Failed (*)**: The process has failed. --- +-- -- (*) End states of the process. --- +-- -- ### ACT_ASSIGN state transition methods: --- +-- -- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. -- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. -- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, +-- If you want to change the behaviour of the AIControllable at this event, return false, -- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. -- These state transition methods need to provide a return value, which is specified at the function description. --- +-- -- === --- +-- -- # 1) @{#ACT_ASSIGN_ACCEPT} class, extends @{Core.Fsm.Assign#ACT_ASSIGN} --- +-- -- The ACT_ASSIGN_ACCEPT class accepts by default a task for a player. No player intervention is allowed to reject the task. --- +-- -- ## 1.1) ACT_ASSIGN_ACCEPT constructor: --- +-- -- * @{#ACT_ASSIGN_ACCEPT.New}(): Creates a new ACT_ASSIGN_ACCEPT object. --- +-- -- === --- +-- -- # 2) @{#ACT_ASSIGN_MENU_ACCEPT} class, extends @{Core.Fsm.Assign#ACT_ASSIGN} --- +-- -- The ACT_ASSIGN_MENU_ACCEPT class accepts a task when the player accepts the task through an added menu option. -- This assignment type is useful to conditionally allow the player to choose whether or not he would accept the task. -- The assignment type also allows to reject the task. --- +-- -- ## 2.1) ACT_ASSIGN_MENU_ACCEPT constructor: -- ----------------------------------------- --- +-- -- * @{#ACT_ASSIGN_MENU_ACCEPT.New}(): Creates a new ACT_ASSIGN_MENU_ACCEPT object. --- +-- -- === --- +-- -- @module Actions.Assign -- @image MOOSE.JPG @@ -89,11 +89,11 @@ do -- ACT_ASSIGN -- @field Wrapper.Unit#UNIT ProcessUnit -- @field Core.Zone#ZONE_BASE TargetZone -- @extends Core.Fsm#FSM_PROCESS - ACT_ASSIGN = { + ACT_ASSIGN = { ClassName = "ACT_ASSIGN", } - - + + --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. -- @param #ACT_ASSIGN self -- @return #ACT_ASSIGN The task acceptance process. @@ -106,16 +106,16 @@ do -- ACT_ASSIGN self:AddTransition( "Waiting", "Assign", "Assigned" ) self:AddTransition( "Waiting", "Reject", "Rejected" ) self:AddTransition( "*", "Fail", "Failed" ) - + self:AddEndState( "Assigned" ) self:AddEndState( "Rejected" ) self:AddEndState( "Failed" ) - - self:SetStartState( "UnAssigned" ) - + + self:SetStartState( "UnAssigned" ) + return self end - + end -- ACT_ASSIGN @@ -128,26 +128,26 @@ do -- ACT_ASSIGN_ACCEPT -- @field Wrapper.Unit#UNIT ProcessUnit -- @field Core.Zone#ZONE_BASE TargetZone -- @extends #ACT_ASSIGN - ACT_ASSIGN_ACCEPT = { + ACT_ASSIGN_ACCEPT = { ClassName = "ACT_ASSIGN_ACCEPT", } - - + + --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted. -- @param #ACT_ASSIGN_ACCEPT self -- @param #string TaskBriefing function ACT_ASSIGN_ACCEPT:New( TaskBriefing ) - + local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_ACCEPT self.TaskBriefing = TaskBriefing - + return self end function ACT_ASSIGN_ACCEPT:Init( FsmAssign ) - - self.TaskBriefing = FsmAssign.TaskBriefing + + self.TaskBriefing = FsmAssign.TaskBriefing end --- StateMachine callback function @@ -157,8 +157,8 @@ do -- ACT_ASSIGN_ACCEPT -- @param #string From -- @param #string To function ACT_ASSIGN_ACCEPT:onafterStart( ProcessUnit, Task, From, Event, To ) - - self:__Assign( 1 ) + + self:__Assign( 1 ) end --- StateMachine callback function @@ -167,11 +167,11 @@ do -- ACT_ASSIGN_ACCEPT -- @param #string Event -- @param #string From -- @param #string To - function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, Task, From, Event, To ) - + function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, Task, From, Event, To, TaskGroup ) + self.Task:Assign( ProcessUnit, ProcessUnit:GetPlayerName() ) end - + end -- ACT_ASSIGN_ACCEPT @@ -183,7 +183,7 @@ do -- ACT_ASSIGN_MENU_ACCEPT -- @field Wrapper.Unit#UNIT ProcessUnit -- @field Core.Zone#ZONE_BASE TargetZone -- @extends #ACT_ASSIGN - ACT_ASSIGN_MENU_ACCEPT = { + ACT_ASSIGN_MENU_ACCEPT = { ClassName = "ACT_ASSIGN_MENU_ACCEPT", } @@ -197,7 +197,7 @@ do -- ACT_ASSIGN_MENU_ACCEPT local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_MENU_ACCEPT self.TaskBriefing = TaskBriefing - + return self end @@ -207,12 +207,12 @@ do -- ACT_ASSIGN_MENU_ACCEPT -- @param #string TaskBriefing -- @return #ACT_ASSIGN_MENU_ACCEPT self function ACT_ASSIGN_MENU_ACCEPT:Init( TaskBriefing ) - + self.TaskBriefing = TaskBriefing return self end - + --- StateMachine callback function -- @param #ACT_ASSIGN_MENU_ACCEPT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -222,30 +222,30 @@ do -- ACT_ASSIGN_MENU_ACCEPT function ACT_ASSIGN_MENU_ACCEPT:onafterStart( ProcessUnit, Task, From, Event, To ) self:GetCommandCenter():MessageToGroup( "Task " .. self.Task:GetName() .. " has been assigned to you and your group!\nRead the briefing and use the Radio Menu (F10) / Task ... CONFIRMATION menu to accept or reject the task.\nYou have 2 minutes to accept, or the task assignment will be cancelled!", ProcessUnit:GetGroup(), 120 ) - - local TaskGroup = ProcessUnit:GetGroup() + + local TaskGroup = ProcessUnit:GetGroup() self.Menu = MENU_GROUP:New( TaskGroup, "Task " .. self.Task:GetName() .. " CONFIRMATION" ) self.MenuAcceptTask = MENU_GROUP_COMMAND:New( TaskGroup, "Accept task " .. self.Task:GetName(), self.Menu, self.MenuAssign, self, TaskGroup ) self.MenuRejectTask = MENU_GROUP_COMMAND:New( TaskGroup, "Reject task " .. self.Task:GetName(), self.Menu, self.MenuReject, self, TaskGroup ) - + self:__Reject( 120, TaskGroup ) end - + --- Menu function. -- @param #ACT_ASSIGN_MENU_ACCEPT self function ACT_ASSIGN_MENU_ACCEPT:MenuAssign( TaskGroup ) - + self:__Assign( -1, TaskGroup ) end - + --- Menu function. -- @param #ACT_ASSIGN_MENU_ACCEPT self function ACT_ASSIGN_MENU_ACCEPT:MenuReject( TaskGroup ) - + self:__Reject( -1, TaskGroup ) end - + --- StateMachine callback function -- @param #ACT_ASSIGN_MENU_ACCEPT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -253,10 +253,10 @@ do -- ACT_ASSIGN_MENU_ACCEPT -- @param #string From -- @param #string To function ACT_ASSIGN_MENU_ACCEPT:onafterAssign( ProcessUnit, Task, From, Event, To, TaskGroup ) - + self.Menu:Remove() end - + --- StateMachine callback function -- @param #ACT_ASSIGN_MENU_ACCEPT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -265,7 +265,7 @@ do -- ACT_ASSIGN_MENU_ACCEPT -- @param #string To function ACT_ASSIGN_MENU_ACCEPT:onafterReject( ProcessUnit, Task, From, Event, To, TaskGroup ) self:F( { TaskGroup = TaskGroup } ) - + self.Menu:Remove() --TODO: need to resolve this problem ... it has to do with the events ... --self.Task:UnAssignFromUnit( ProcessUnit )needs to become a callback funtion call upon the event @@ -279,7 +279,7 @@ do -- ACT_ASSIGN_MENU_ACCEPT -- @param #string From -- @param #string To function ACT_ASSIGN_MENU_ACCEPT:onenterAssigned( ProcessUnit, Task, From, Event, To, TaskGroup ) - + --self.Task:AssignToGroup( TaskGroup ) self.Task:Assign( ProcessUnit, ProcessUnit:GetPlayerName() ) end diff --git a/Moose Development/Moose/Actions/Act_Assist.lua b/Moose Development/Moose/Actions/Act_Assist.lua index f9cd5fc3a..fb0e81d6b 100644 --- a/Moose Development/Moose/Actions/Act_Assist.lua +++ b/Moose Development/Moose/Actions/Act_Assist.lua @@ -1,65 +1,65 @@ --- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. --- +-- -- ## ACT_ASSIST state machine: --- +-- -- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. -- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, -- but will have **different implementation behaviour** upon each event or state transition. --- +-- -- ### ACT_ASSIST **Events**: --- +-- -- These are the events defined in this class: --- +-- -- * **Start**: The process is started. -- * **Next**: The process is smoking the targets in the given zone. --- +-- -- ### ACT_ASSIST **Event methods**: --- +-- -- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. -- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- +-- -- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- -- ### ACT_ASSIST **States**: --- +-- -- * **None**: The controllable did not receive route commands. -- * **AwaitSmoke (*)**: The process is awaiting to smoke the targets in the zone. -- * **Smoking (*)**: The process is smoking the targets in the zone. -- * **Failed (*)**: The process has failed. --- +-- -- (*) End states of the process. --- +-- -- ### ACT_ASSIST state transition methods: --- +-- -- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. -- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. -- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, +-- If you want to change the behaviour of the AIControllable at this event, return false, -- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. -- These state transition methods need to provide a return value, which is specified at the function description. --- +-- -- === --- +-- -- # 1) @{#ACT_ASSIST_SMOKE_TARGETS_ZONE} class, extends @{Core.Fsm.Route#ACT_ASSIST} --- +-- -- The ACT_ASSIST_SMOKE_TARGETS_ZONE class implements the core functions to smoke targets in a @{Zone}. --- The targets are smoked within a certain range around each target, simulating a realistic smoking behaviour. +-- The targets are smoked within a certain range around each target, simulating a realistic smoking behaviour. -- At random intervals, a new target is smoked. --- +-- -- # 1.1) ACT_ASSIST_SMOKE_TARGETS_ZONE constructor: --- +-- -- * @{#ACT_ASSIST_SMOKE_TARGETS_ZONE.New}(): Creates a new ACT_ASSIST_SMOKE_TARGETS_ZONE object. --- +-- -- === --- +-- -- @module Actions.Assist -- @image MOOSE.JPG @@ -69,7 +69,7 @@ do -- ACT_ASSIST --- ACT_ASSIST class -- @type ACT_ASSIST -- @extends Core.Fsm#FSM_PROCESS - ACT_ASSIST = { + ACT_ASSIST = { ClassName = "ACT_ASSIST", } @@ -86,15 +86,15 @@ do -- ACT_ASSIST self:AddTransition( "Smoking", "Next", "AwaitSmoke" ) self:AddTransition( "*", "Stop", "Success" ) self:AddTransition( "*", "Fail", "Failed" ) - + self:AddEndState( "Failed" ) self:AddEndState( "Success" ) - - self:SetStartState( "None" ) + + self:SetStartState( "None" ) return self end - + --- Task Events --- StateMachine callback function @@ -104,17 +104,17 @@ do -- ACT_ASSIST -- @param #string From -- @param #string To function ACT_ASSIST:onafterStart( ProcessUnit, From, Event, To ) - + local ProcessGroup = ProcessUnit:GetGroup() local MissionMenu = self:GetMission():GetMenu( ProcessGroup ) - + local function MenuSmoke( MenuParam ) local self = MenuParam.self local SmokeColor = MenuParam.SmokeColor self.SmokeColor = SmokeColor self:__Next( 1 ) end - + self.Menu = MENU_GROUP:New( ProcessGroup, "Target acquisition", MissionMenu ) self.MenuSmokeBlue = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop blue smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Blue } ) self.MenuSmokeGreen = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop green smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Green } ) @@ -130,10 +130,10 @@ do -- ACT_ASSIST -- @param #string From -- @param #string To function ACT_ASSIST:onafterStop( ProcessUnit, From, Event, To ) - + self.Menu:Remove() -- When stopped, remove the menus end - + end do -- ACT_ASSIST_SMOKE_TARGETS_ZONE @@ -143,17 +143,17 @@ do -- ACT_ASSIST_SMOKE_TARGETS_ZONE -- @field Core.Set#SET_UNIT TargetSetUnit -- @field Core.Zone#ZONE_BASE TargetZone -- @extends #ACT_ASSIST - ACT_ASSIST_SMOKE_TARGETS_ZONE = { + ACT_ASSIST_SMOKE_TARGETS_ZONE = { ClassName = "ACT_ASSIST_SMOKE_TARGETS_ZONE", } - + -- function ACT_ASSIST_SMOKE_TARGETS_ZONE:_Destructor() -- self:E("_Destructor") --- +-- -- self.Menu:Remove() -- self:EventRemoveAll() -- end - + --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self -- @param Core.Set#SET_UNIT TargetSetUnit @@ -163,29 +163,29 @@ do -- ACT_ASSIST_SMOKE_TARGETS_ZONE self.TargetSetUnit = TargetSetUnit self.TargetZone = TargetZone - + return self end function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( FsmSmoke ) - + self.TargetSetUnit = FsmSmoke.TargetSetUnit self.TargetZone = FsmSmoke.TargetZone end - + --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self -- @param Core.Set#SET_UNIT TargetSetUnit -- @param Core.Zone#ZONE_BASE TargetZone -- @return #ACT_ASSIST_SMOKE_TARGETS_ZONE self function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( TargetSetUnit, TargetZone ) - + self.TargetSetUnit = TargetSetUnit self.TargetZone = TargetZone - + return self end - + --- StateMachine callback function -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit @@ -193,7 +193,7 @@ do -- ACT_ASSIST_SMOKE_TARGETS_ZONE -- @param #string From -- @param #string To function ACT_ASSIST_SMOKE_TARGETS_ZONE:onenterSmoking( ProcessUnit, From, Event, To ) - + self.TargetSetUnit:ForEachUnit( --- @param Wrapper.Unit#UNIT SmokeUnit function( SmokeUnit ) @@ -203,12 +203,12 @@ do -- ACT_ASSIST_SMOKE_TARGETS_ZONE if SmokeUnit:IsAlive() then SmokeUnit:Smoke( self.SmokeColor, 150 ) end - end, {}, math.random( 10, 60 ) + end, {}, math.random( 10, 60 ) ) end end ) - + end - -end \ No newline at end of file + +end diff --git a/Moose Development/Moose/Actions/Act_Route.lua b/Moose Development/Moose/Actions/Act_Route.lua index d719f1f7b..f2c5ebcda 100644 --- a/Moose Development/Moose/Actions/Act_Route.lua +++ b/Moose Development/Moose/Actions/Act_Route.lua @@ -1,20 +1,20 @@ --- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. --- +-- -- === --- +-- -- # @{#ACT_ROUTE} FSM class, extends @{Core.Fsm#FSM_PROCESS} --- +-- -- ## ACT_ROUTE state machine: --- +-- -- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. -- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below. --- Each derived class follows exactly the same process, using the same events and following the same state transitions, +-- Each derived class follows exactly the same process, using the same events and following the same state transitions, -- but will have **different implementation behaviour** upon each event or state transition. --- +-- -- ### ACT_ROUTE **Events**: --- +-- -- These are the events defined in this class: --- +-- -- * **Start**: The process is started. The process will go into the Report state. -- * **Report**: The process is reporting to the player the route to be followed. -- * **Route**: The process is routing the controllable. @@ -22,56 +22,56 @@ -- * **Arrive**: The controllable has arrived at a route point. -- * **More**: There are more route points that need to be followed. The process will go back into the Report state. -- * **NoMore**: There are no more route points that need to be followed. The process will go into the Success state. --- +-- -- ### ACT_ROUTE **Event methods**: --- +-- -- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process. -- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine: --- +-- -- * **Immediate**: The event method has exactly the name of the event. --- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. --- +-- * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed. +-- -- ### ACT_ROUTE **States**: --- +-- -- * **None**: The controllable did not receive route commands. -- * **Arrived (*)**: The controllable has arrived at a route point. -- * **Aborted (*)**: The controllable has aborted the route path. -- * **Routing**: The controllable is understay to the route point. -- * **Pausing**: The process is pausing the routing. AI air will go into hover, AI ground will stop moving. Players can fly around. --- * **Success (*)**: All route points were reached. +-- * **Success (*)**: All route points were reached. -- * **Failed (*)**: The process has failed. --- +-- -- (*) End states of the process. --- +-- -- ### ACT_ROUTE state transition methods: --- +-- -- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state. -- There are 2 moments when state transition methods will be called by the state machine: --- --- * **Before** the state transition. --- The state transition method needs to start with the name **OnBefore + the name of the state**. +-- +-- * **Before** the state transition. +-- The state transition method needs to start with the name **OnBefore + the name of the state**. -- If the state transition method returns false, then the processing of the state transition will not be done! --- If you want to change the behaviour of the AIControllable at this event, return false, +-- If you want to change the behaviour of the AIControllable at this event, return false, -- but then you'll need to specify your own logic using the AIControllable! --- --- * **After** the state transition. --- The state transition method needs to start with the name **OnAfter + the name of the state**. +-- +-- * **After** the state transition. +-- The state transition method needs to start with the name **OnAfter + the name of the state**. -- These state transition methods need to provide a return value, which is specified at the function description. --- +-- -- === --- +-- -- # 1) @{#ACT_ROUTE_ZONE} class, extends @{Core.Fsm.Route#ACT_ROUTE} --- +-- -- The ACT_ROUTE_ZONE class implements the core functions to route an AIR @{Wrapper.Controllable} player @{Wrapper.Unit} to a @{Zone}. --- The player receives on perioding times messages with the coordinates of the route to follow. +-- The player receives on perioding times messages with the coordinates of the route to follow. -- Upon arrival at the zone, a confirmation of arrival is sent, and the process will be ended. --- +-- -- # 1.1) ACT_ROUTE_ZONE constructor: --- +-- -- * @{#ACT_ROUTE_ZONE.New}(): Creates a new ACT_ROUTE_ZONE object. --- +-- -- === --- +-- -- @module Actions.Route -- @image MOOSE.JPG @@ -85,11 +85,11 @@ do -- ACT_ROUTE -- @field Core.Zone#ZONE_BASE Zone -- @field Core.Point#COORDINATE Coordinate -- @extends Core.Fsm#FSM_PROCESS - ACT_ROUTE = { + ACT_ROUTE = { ClassName = "ACT_ROUTE", } - - + + --- Creates a new routing state machine. The process will route a CLIENT to a ZONE until the CLIENT is within that ZONE. -- @param #ACT_ROUTE self -- @return #ACT_ROUTE self @@ -97,7 +97,7 @@ do -- ACT_ROUTE -- Inherits from BASE local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ROUTE" ) ) -- Core.Fsm#FSM_PROCESS - + self:AddTransition( "*", "Reset", "None" ) self:AddTransition( "None", "Start", "Routing" ) self:AddTransition( "*", "Report", "*" ) @@ -109,23 +109,23 @@ do -- ACT_ROUTE self:AddTransition( "*", "Fail", "Failed" ) self:AddTransition( "", "", "" ) self:AddTransition( "", "", "" ) - + self:AddEndState( "Arrived" ) self:AddEndState( "Failed" ) self:AddEndState( "Cancelled" ) - + self:SetStartState( "None" ) - - self:SetRouteMode( "C" ) - + + self:SetRouteMode( "C" ) + return self end - + --- Set a Cancel Menu item. -- @param #ACT_ROUTE self -- @return #ACT_ROUTE function ACT_ROUTE:SetMenuCancel( MenuGroup, MenuText, ParentMenu, MenuTime, MenuTag ) - + self.CancelMenuGroupCommand = MENU_GROUP_COMMAND:New( MenuGroup, MenuText, @@ -135,47 +135,47 @@ do -- ACT_ROUTE ):SetTime( MenuTime ):SetTag( MenuTag ) ParentMenu:SetTime( MenuTime ) - + ParentMenu:Remove( MenuTime, MenuTag ) return self end - + --- Set the route mode. -- There are 2 route modes supported: - -- + -- -- * SetRouteMode( "B" ): Route mode is Bearing and Range. -- * SetRouteMode( "C" ): Route mode is LL or MGRS according coordinate system setup. - -- + -- -- @param #ACT_ROUTE self -- @return #ACT_ROUTE function ACT_ROUTE:SetRouteMode( RouteMode ) - + self.RouteMode = RouteMode - return self + return self end - + --- Get the routing text to be displayed. -- The route mode determines the text displayed. -- @param #ACT_ROUTE self -- @param Wrapper.Unit#UNIT Controllable -- @return #string function ACT_ROUTE:GetRouteText( Controllable ) - + local RouteText = "" local Coordinate = nil -- Core.Point#COORDINATE - + if self.Coordinate then Coordinate = self.Coordinate end - + if self.Zone then Coordinate = self.Zone:GetPointVec3( self.Altitude ) Coordinate:SetHeading( self.Heading ) end - + local Task = self:GetTask() -- This is to dermine that the coordinates are for a specific task mode (A2A or A2G). local CC = self:GetTask():GetMission():GetCommandCenter() @@ -209,7 +209,7 @@ do -- ACT_ROUTE return RouteText end - + function ACT_ROUTE:MenuCancel() self:F("Cancelled") self.CancelMenuGroupCommand:Remove() @@ -225,11 +225,11 @@ do -- ACT_ROUTE -- @param #string From -- @param #string To function ACT_ROUTE:onafterStart( ProcessUnit, From, Event, To ) - + self:__Route( 1 ) end - + --- Check if the controllable has arrived. -- @param #ACT_ROUTE self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -237,7 +237,7 @@ do -- ACT_ROUTE function ACT_ROUTE:onfuncHasArrived( ProcessUnit ) return false end - + --- StateMachine callback function -- @param #ACT_ROUTE self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -245,7 +245,7 @@ do -- ACT_ROUTE -- @param #string From -- @param #string To function ACT_ROUTE:onbeforeRoute( ProcessUnit, From, Event, To ) - + if ProcessUnit:IsAlive() then local HasArrived = self:onfuncHasArrived( ProcessUnit ) -- Polymorphic if self.DisplayCount >= self.DisplayInterval then @@ -257,18 +257,18 @@ do -- ACT_ROUTE else self.DisplayCount = self.DisplayCount + 1 end - + if HasArrived then self:__Arrive( 1 ) else self:__Route( 1 ) end - + return HasArrived -- if false, then the event will not be executed... end - + return false - + end end -- ACT_ROUTE @@ -280,12 +280,12 @@ do -- ACT_ROUTE_POINT -- @type ACT_ROUTE_POINT -- @field Tasking.Task#TASK TASK -- @extends #ACT_ROUTE - ACT_ROUTE_POINT = { + ACT_ROUTE_POINT = { ClassName = "ACT_ROUTE_POINT", } - --- Creates a new routing state machine. + --- Creates a new routing state machine. -- The task will route a controllable to a Coordinate until the controllable is within the Range. -- @param #ACT_ROUTE_POINT self -- @param Core.Point#COORDINATE The Coordinate to Target. @@ -296,29 +296,29 @@ do -- ACT_ROUTE_POINT self.Coordinate = Coordinate self.Range = Range or 0 - + self.DisplayInterval = 30 self.DisplayCount = 30 self.DisplayMessage = true self.DisplayTime = 10 -- 10 seconds is the default - + return self end - - --- Creates a new routing state machine. + + --- Creates a new routing state machine. -- The task will route a controllable to a Coordinate until the controllable is within the Range. -- @param #ACT_ROUTE_POINT self function ACT_ROUTE_POINT:Init( FsmRoute ) - + self.Coordinate = FsmRoute.Coordinate self.Range = FsmRoute.Range or 0 - + self.DisplayInterval = 30 self.DisplayCount = 30 self.DisplayMessage = true self.DisplayTime = 10 -- 10 seconds is the default self:SetStartState("None") - end + end --- Set Coordinate -- @param #ACT_ROUTE_POINT self @@ -326,7 +326,7 @@ do -- ACT_ROUTE_POINT function ACT_ROUTE_POINT:SetCoordinate( Coordinate ) self:F2( { Coordinate } ) self.Coordinate = Coordinate - end + end --- Get Coordinate -- @param #ACT_ROUTE_POINT self @@ -334,7 +334,7 @@ do -- ACT_ROUTE_POINT function ACT_ROUTE_POINT:GetCoordinate() self:F2( { self.Coordinate } ) return self.Coordinate - end + end --- Set Range around Coordinate -- @param #ACT_ROUTE_POINT self @@ -342,16 +342,16 @@ do -- ACT_ROUTE_POINT function ACT_ROUTE_POINT:SetRange( Range ) self:F2( { Range } ) self.Range = Range or 10000 - end - + end + --- Get Range around Coordinate -- @param #ACT_ROUTE_POINT self -- @return #number The Range to consider the arrival. Default is 10000 meters. function ACT_ROUTE_POINT:GetRange() self:F2( { self.Range } ) return self.Range - end - + end + --- Method override to check if the controllable has arrived. -- @param #ACT_ROUTE_POINT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -360,7 +360,7 @@ do -- ACT_ROUTE_POINT if ProcessUnit:IsAlive() then local Distance = self.Coordinate:Get2DDistance( ProcessUnit:GetCoordinate() ) - + if Distance <= self.Range then local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", you have arrived." self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) @@ -370,9 +370,9 @@ do -- ACT_ROUTE_POINT return false end - + --- Task Events - + --- StateMachine callback function -- @param #ACT_ROUTE_POINT self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -380,9 +380,9 @@ do -- ACT_ROUTE_POINT -- @param #string From -- @param #string To function ACT_ROUTE_POINT:onafterReport( ProcessUnit, From, Event, To ) - + local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", " .. self:GetRouteText( ProcessUnit ) - + self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Update ) end @@ -397,7 +397,7 @@ do -- ACT_ROUTE_ZONE -- @field Wrapper.Unit#UNIT ProcessUnit -- @field Core.Zone#ZONE_BASE Zone -- @extends #ACT_ROUTE - ACT_ROUTE_ZONE = { + ACT_ROUTE_ZONE = { ClassName = "ACT_ROUTE_ZONE", } @@ -409,25 +409,25 @@ do -- ACT_ROUTE_ZONE local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_ZONE self.Zone = Zone - + self.DisplayInterval = 30 self.DisplayCount = 30 self.DisplayMessage = true self.DisplayTime = 10 -- 10 seconds is the default - + return self end - + function ACT_ROUTE_ZONE:Init( FsmRoute ) - + self.Zone = FsmRoute.Zone - + self.DisplayInterval = 30 self.DisplayCount = 30 self.DisplayMessage = true self.DisplayTime = 10 -- 10 seconds is the default - end - + end + --- Set Zone -- @param #ACT_ROUTE_ZONE self -- @param Core.Zone#ZONE_BASE Zone The Zone object where to route to. @@ -437,14 +437,14 @@ do -- ACT_ROUTE_ZONE self.Zone = Zone self.Altitude = Altitude self.Heading = Heading - end + end --- Get Zone -- @param #ACT_ROUTE_ZONE self -- @return Core.Zone#ZONE_BASE Zone The Zone object where to route to. function ACT_ROUTE_ZONE:GetZone() - return self.Zone - end + return self.Zone + end --- Method override to check if the controllable has arrived. -- @param #ACT_ROUTE self @@ -459,9 +459,9 @@ do -- ACT_ROUTE_ZONE return ProcessUnit:IsInZone( self.Zone ) end - + --- Task Events - + --- StateMachine callback function -- @param #ACT_ROUTE_ZONE self -- @param Wrapper.Unit#UNIT ProcessUnit @@ -470,7 +470,7 @@ do -- ACT_ROUTE_ZONE -- @param #string To function ACT_ROUTE_ZONE:onafterReport( ProcessUnit, From, Event, To ) self:F( { ProcessUnit = ProcessUnit } ) - + local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", " .. self:GetRouteText( ProcessUnit ) self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Update ) end diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index cca8fedeb..381c6859f 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -298,7 +298,8 @@ end -- -- -- @param #BASE self --- @param #BASE Child is the Child class from which the Parent class needs to be retrieved. +-- @param #BASE Child This is the Child class from which the Parent class needs to be retrieved. +-- @param #BASE FromClass (Optional) The class from which to get the parent. -- @return #BASE function BASE:GetParent( Child, FromClass ) @@ -595,18 +596,85 @@ do -- Event Handling -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when any unit begins firing a weapon that has a high rate of fire. Most common with aircraft cannons (GAU-8), autocannons, and machine guns. - -- initiator : The unit that is doing the shooing. + -- initiator : The unit that is doing the shooting. -- target: The unit that is being targeted. -- @function [parent=#BASE] OnEventShootingStart -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when any unit stops firing its weapon. Event will always correspond with a shooting start event. - -- initiator : The unit that was doing the shooing. + -- initiator : The unit that was doing the shooting. -- @function [parent=#BASE] OnEventShootingEnd -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. + --- Occurs when a new mark was added. + -- MarkID: ID of the mark. + -- @function [parent=#BASE] OnEventMarkAdded + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a mark was removed. + -- MarkID: ID of the mark. + -- @function [parent=#BASE] OnEventMarkRemoved + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when a mark text was changed. + -- MarkID: ID of the mark. + -- @function [parent=#BASE] OnEventMarkChange + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + + --- Unknown precisely what creates this event, likely tied into newer damage model. Will update this page when new information become available. + -- + -- * initiator: The unit that had the failure. + -- + -- @function [parent=#BASE] OnEventDetailedFailure + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any modification to the "Score" as seen on the debrief menu would occur. + -- There is no information on what values the score was changed to. Event is likely similar to player_comment in this regard. + -- @function [parent=#BASE] OnEventScore + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs on the death of a unit. Contains more and different information. Similar to unit_lost it will occur for aircraft before the aircraft crash event occurs. + -- + -- * initiator: The unit that killed the target + -- * target: Target Object + -- * weapon: Weapon Object + -- + -- @function [parent=#BASE] OnEventKill + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when any modification to the "Score" as seen on the debrief menu would occur. + -- There is no information on what values the score was changed to. Event is likely similar to player_comment in this regard. + -- @function [parent=#BASE] OnEventScore + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when the game thinks an object is destroyed. + -- + -- * initiator: The unit that is was destroyed. + -- + -- @function [parent=#BASE] OnEventUnitLost + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs shortly after the landing animation of an ejected pilot touching the ground and standing up. Event does not occur if the pilot lands in the water and sub combs to Davey Jones Locker. + -- + -- * initiator: Static object representing the ejected pilot. Place : Aircraft that the pilot ejected from. + -- * place: may not return as a valid object if the aircraft has crashed into the ground and no longer exists. + -- * subplace: is always 0 for unknown reasons. + -- + -- @function [parent=#BASE] OnEventLandingAfterEjection + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. + end @@ -746,9 +814,7 @@ do -- Scheduling if not self.Scheduler then self.Scheduler = SCHEDULER:New( self ) end - - self.Scheduler.SchedulerObject = self.Scheduler - + local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( self, SchedulerFunction, @@ -786,16 +852,15 @@ do -- Scheduling self.Scheduler = SCHEDULER:New( self ) end - self.Scheduler.SchedulerObject = self.Scheduler - - local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( + local ScheduleID = self.Scheduler:Schedule( self, SchedulerFunction, { ... }, Start, Repeat, RandomizeFactor, - Stop + Stop, + 4 ) self._.Schedules[#self._.Schedules+1] = ScheduleID @@ -809,8 +874,10 @@ do -- Scheduling function BASE:ScheduleStop( SchedulerFunction ) self:F3( { "ScheduleStop:" } ) - - _SCHEDULEDISPATCHER:Stop( self.Scheduler, self._.Schedules[SchedulerFunction] ) + + if self.Scheduler then + _SCHEDULEDISPATCHER:Stop( self.Scheduler, self._.Schedules[SchedulerFunction] ) + end end end @@ -869,6 +936,26 @@ end -- Log a trace (only shown when trace is on) -- TODO: Make trace function using variable parameters. +--- Set trace on. +-- @param #BASE self +-- @usage +-- -- Switch the tracing On +-- BASE:TraceOn() +function BASE:TraceOn() + self:TraceOnOff( true ) +end + +--- Set trace off. +-- @param #BASE self +-- @usage +-- -- Switch the tracing Off +-- BASE:TraceOff() +function BASE:TraceOff() + self:TraceOnOff( false ) +end + + + --- Set trace on or off -- Note that when trace is off, no BASE.Debug statement is performed, increasing performance! -- When Moose is loaded statically, (as one file), tracing is switched off by default. @@ -883,7 +970,13 @@ end -- -- Switch the tracing Off -- BASE:TraceOnOff( false ) function BASE:TraceOnOff( TraceOnOff ) - _TraceOnOff = TraceOnOff + if TraceOnOff==false then + self:I( "Tracing in MOOSE is OFF" ) + _TraceOnOff = false + else + self:I( "Tracing in MOOSE is ON" ) + _TraceOnOff = true + end end @@ -903,8 +996,8 @@ end -- @param #BASE self -- @param #number Level function BASE:TraceLevel( Level ) - _TraceLevel = Level - self:E( "Tracing level " .. Level ) + _TraceLevel = Level or 1 + self:I( "Tracing level " .. _TraceLevel ) end --- Trace all methods in MOOSE @@ -912,12 +1005,16 @@ end -- @param #boolean TraceAll true = trace all methods in MOOSE. function BASE:TraceAll( TraceAll ) - _TraceAll = TraceAll + if TraceAll==false then + _TraceAll=false + else + _TraceAll = true + end if _TraceAll then - self:E( "Tracing all methods in MOOSE " ) + self:I( "Tracing all methods in MOOSE " ) else - self:E( "Switched off tracing all methods in MOOSE" ) + self:I( "Switched off tracing all methods in MOOSE" ) end end @@ -927,7 +1024,7 @@ end function BASE:TraceClass( Class ) _TraceClass[Class] = true _TraceClassMethod[Class] = {} - self:E( "Tracing class " .. Class ) + self:I( "Tracing class " .. Class ) end --- Set tracing for a specific method of class @@ -940,7 +1037,7 @@ function BASE:TraceClassMethod( Class, Method ) _TraceClassMethod[Class].Method = {} end _TraceClassMethod[Class].Method[Method] = true - self:E( "Tracing method " .. Method .. " of class " .. Class ) + self:I( "Tracing method " .. Method .. " of class " .. Class ) end --- Trace a function call. This function is private. @@ -967,7 +1064,7 @@ function BASE:_F( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) if DebugInfoFrom then LineFrom = DebugInfoFrom.currentline end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) end end end @@ -1042,7 +1139,7 @@ function BASE:_T( Arguments, DebugInfoCurrentParam, DebugInfoFromParam ) if DebugInfoFrom then LineFrom = DebugInfoFrom.currentline end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) end end end @@ -1113,9 +1210,9 @@ function BASE:E( Arguments ) LineFrom = DebugInfoFrom.currentline end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) else - env.info( string.format( "%1s:%20s%05d(%s)" , "E", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%1s:%30s%05d(%s)" , "E", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) end end @@ -1141,9 +1238,9 @@ function BASE:I( Arguments ) LineFrom = DebugInfoFrom.currentline end - env.info( string.format( "%6d(%6d)/%1s:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "I", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)" , LineCurrent, LineFrom, "I", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) ) else - env.info( string.format( "%1s:%20s%05d(%s)" , "I", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) + env.info( string.format( "%1s:%30s%05d(%s)" , "I", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) ) end end diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index 1a3d7c7e5..f691fb143 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -81,6 +81,7 @@ DATABASE = { HITS = {}, DESTROYS = {}, ZONES = {}, + ZONES_GOAL = {}, } local _DATABASECoalition = @@ -186,6 +187,7 @@ end function DATABASE:AddUnit( DCSUnitName ) if not self.UNITS[DCSUnitName] then + self:T( { "Add UNIT:", DCSUnitName } ) local UnitRegister = UNIT:Register( DCSUnitName ) self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName ) @@ -245,12 +247,15 @@ end --- Adds a Airbase based on the Airbase Name in the DATABASE. -- @param #DATABASE self --- @param #string AirbaseName The name of the airbase +-- @param #string AirbaseName The name of the airbase. +-- @return Wrapper.Airbase#AIRBASE Airbase object. function DATABASE:AddAirbase( AirbaseName ) if not self.AIRBASES[AirbaseName] then self.AIRBASES[AirbaseName] = AIRBASE:Register( AirbaseName ) end + + return self.AIRBASES[AirbaseName] end @@ -304,16 +309,6 @@ do -- Zones self.ZONES[ZoneName] = nil end - - --- Finds an @{Zone} based on the zone name in the DATABASE. - -- @param #DATABASE self - -- @param #string ZoneName - -- @return Core.Zone#ZONE_BASE The found @{Zone}. - function DATABASE:FindZone( ZoneName ) - - local ZoneFound = self.ZONES[ZoneName] - return ZoneFound - end --- Private method that registers new ZONE_BASE derived objects within the DATABASE Object. @@ -348,7 +343,39 @@ do -- Zones end -- zone +do -- Zone_Goal + --- Finds a @{Zone} based on the zone name. + -- @param #DATABASE self + -- @param #string ZoneName The name of the zone. + -- @return Core.Zone#ZONE_BASE The found ZONE. + function DATABASE:FindZoneGoal( ZoneName ) + + local ZoneFound = self.ZONES_GOAL[ZoneName] + return ZoneFound + end + + --- Adds a @{Zone} based on the zone name in the DATABASE. + -- @param #DATABASE self + -- @param #string ZoneName The name of the zone. + -- @param Core.Zone#ZONE_BASE Zone The zone. + function DATABASE:AddZoneGoal( ZoneName, Zone ) + + if not self.ZONES_GOAL[ZoneName] then + self.ZONES_GOAL[ZoneName] = Zone + end + end + + + --- Deletes a @{Zone} from the DATABASE based on the zone name. + -- @param #DATABASE self + -- @param #string ZoneName The name of the zone. + function DATABASE:DeleteZoneGoal( ZoneName ) + + self.ZONES_GOAL[ZoneName] = nil + end + +end -- Zone_Goal do -- cargo --- Adds a Cargo based on the Cargo Name in the DATABASE. @@ -485,7 +512,7 @@ end function DATABASE:AddGroup( GroupName ) if not self.GROUPS[GroupName] then - self:I( { "Add GROUP:", GroupName } ) + self:T( { "Add GROUP:", GroupName } ) self.GROUPS[GroupName] = GROUP:Register( GroupName ) end @@ -497,7 +524,7 @@ end function DATABASE:AddPlayer( UnitName, PlayerName ) if PlayerName then - self:I( { "Add player for unit:", UnitName, PlayerName } ) + self:T( { "Add player for unit:", UnitName, PlayerName } ) self.PLAYERS[PlayerName] = UnitName self.PLAYERUNITS[PlayerName] = self:FindUnit( UnitName ) self.PLAYERSJOINED[PlayerName] = PlayerName @@ -509,7 +536,7 @@ end function DATABASE:DeletePlayer( UnitName, PlayerName ) if PlayerName then - self:I( { "Clean player:", PlayerName } ) + self:T( { "Clean player:", PlayerName } ) self.PLAYERS[PlayerName] = nil self.PLAYERUNITS[PlayerName] = nil end @@ -674,11 +701,11 @@ function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, Category UnitNames[#UnitNames+1] = self.Templates.Units[UnitTemplate.name].UnitName end - self:I( { Group = self.Templates.Groups[GroupTemplateName].GroupName, + self:T( { Group = self.Templates.Groups[GroupTemplateName].GroupName, Coalition = self.Templates.Groups[GroupTemplateName].CoalitionID, - Category = self.Templates.Groups[GroupTemplateName].CategoryID, - Country = self.Templates.Groups[GroupTemplateName].CountryID, - Units = UnitNames + Category = self.Templates.Groups[GroupTemplateName].CategoryID, + Country = self.Templates.Groups[GroupTemplateName].CountryID, + Units = UnitNames } ) end @@ -823,9 +850,9 @@ function DATABASE:_RegisterGroupsAndUnits() end end - self:I("Groups:") + self:T("Groups:") for GroupName, Group in pairs( self.GROUPS ) do - self:I( { "Group:", GroupName } ) + self:T( { "Group:", GroupName } ) end return self @@ -837,7 +864,7 @@ end function DATABASE:_RegisterClients() for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do - self:I( { "Register Client:", ClientName } ) + self:T( { "Register Client:", ClientName } ) self:AddClient( ClientName ) end @@ -855,7 +882,7 @@ function DATABASE:_RegisterStatics() if DCSStatic:isExist() then local DCSStaticName = DCSStatic:getName() - self:I( { "Register Static:", DCSStaticName } ) + self:T( { "Register Static:", DCSStaticName } ) self:AddStatic( DCSStaticName ) else self:E( { "Static does not exist: ", DCSStatic } ) @@ -869,16 +896,29 @@ end --- @param #DATABASE self function DATABASE:_RegisterAirbases() + --[[ local CoalitionsData = { AirbasesRed = coalition.getAirbases( coalition.side.RED ), AirbasesBlue = coalition.getAirbases( coalition.side.BLUE ), AirbasesNeutral = coalition.getAirbases( coalition.side.NEUTRAL ) } for CoalitionId, CoalitionData in pairs( CoalitionsData ) do for DCSAirbaseId, DCSAirbase in pairs( CoalitionData ) do local DCSAirbaseName = DCSAirbase:getName() - self:I( { "Register Airbase:", DCSAirbaseName, DCSAirbase:getID() } ) + self:T( { "Register Airbase:", DCSAirbaseName, DCSAirbase:getID() } ) self:AddAirbase( DCSAirbaseName ) end end + ]] + + for DCSAirbaseId, DCSAirbase in pairs(world.getAirbases()) do + local DCSAirbaseName = DCSAirbase:getName() + + -- This gives the incorrect value to be inserted into the airdromeID for DCS 2.5.6! + local airbaseID=DCSAirbase:getID() + + local airbase=self:AddAirbase( DCSAirbaseName ) + + self:I(string.format("Register Airbase: %s, getID=%d, GetID=%d (unique=%d)", DCSAirbaseName, DCSAirbase:getID(), airbase:GetID(), airbase:GetID(true))) + end return self end @@ -890,7 +930,7 @@ end -- @param #DATABASE self -- @param Core.Event#EVENTDATA Event function DATABASE:_EventOnBirth( Event ) - self:F2( { Event } ) + self:F( { Event } ) if Event.IniDCSUnit then if Event.IniObjectCategory == 3 then @@ -899,6 +939,12 @@ function DATABASE:_EventOnBirth( Event ) if Event.IniObjectCategory == 1 then self:AddUnit( Event.IniDCSUnitName ) self:AddGroup( Event.IniDCSGroupName ) + -- Add airbase if it was spawned later in the mission. + local DCSAirbase = Airbase.getByName(Event.IniDCSUnitName) + if DCSAirbase then + self:I(string.format("Adding airbase %s", tostring(Event.IniDCSUnitName))) + self:AddAirbase(Event.IniDCSUnitName) + end end end if Event.IniObjectCategory == 1 then @@ -907,6 +953,7 @@ function DATABASE:_EventOnBirth( Event ) local PlayerName = Event.IniUnit:GetPlayerName() if PlayerName then self:I( { "Player Joined:", PlayerName } ) + self:AddClient( Event.IniDCSUnitName ) if not self.PLAYERS[PlayerName] then self:AddPlayer( Event.IniUnitName, PlayerName ) end @@ -1027,7 +1074,8 @@ function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set ) return false end - local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) + --local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 ) + Schedule() return self end @@ -1166,7 +1214,7 @@ function DATABASE:OnEventNewZone( EventData ) self:F2( { EventData } ) if EventData.Zone then - self:AddZone( EventData.Zone ) + self:AddZone( EventData.Zone.ZoneName, EventData.Zone ) end end @@ -1222,7 +1270,7 @@ function DATABASE:_RegisterTemplates() local CoalitionSide = coalition.side[string.upper(CoalitionName)] if CoalitionName=="red" then - CoalitionSide=coalition.side.NEUTRAL + CoalitionSide=coalition.side.RED elseif CoalitionName=="blue" then CoalitionSide=coalition.side.BLUE else @@ -1330,7 +1378,7 @@ end -- What is he hitting? if Event.TgtCategory then - if Event.IniCoalition then -- A coalition object was hit, probably a static. + if Event.WeaponCoalition then -- A coalition object was hit, probably a static. -- A target got hit self.HITS[Event.TgtUnitName] = self.HITS[Event.TgtUnitName] or {} local Hit = self.HITS[Event.TgtUnitName] diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 386169cb0..156e26153 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -189,7 +189,9 @@ world.event.S_EVENT_NEW_CARGO = world.event.S_EVENT_MAX + 1000 world.event.S_EVENT_DELETE_CARGO = world.event.S_EVENT_MAX + 1001 world.event.S_EVENT_NEW_ZONE = world.event.S_EVENT_MAX + 1002 world.event.S_EVENT_DELETE_ZONE = world.event.S_EVENT_MAX + 1003 -world.event.S_EVENT_REMOVE_UNIT = world.event.S_EVENT_MAX + 1004 +world.event.S_EVENT_NEW_ZONE_GOAL = world.event.S_EVENT_MAX + 1004 +world.event.S_EVENT_DELETE_ZONE_GOAL = world.event.S_EVENT_MAX + 1005 +world.event.S_EVENT_REMOVE_UNIT = world.event.S_EVENT_MAX + 1006 --- The different types of events supported by MOOSE. @@ -219,14 +221,24 @@ EVENTS = { PlayerComment = world.event.S_EVENT_PLAYER_COMMENT, ShootingStart = world.event.S_EVENT_SHOOTING_START, ShootingEnd = world.event.S_EVENT_SHOOTING_END, + -- Added with DCS 2.5.1 MarkAdded = world.event.S_EVENT_MARK_ADDED, MarkChange = world.event.S_EVENT_MARK_CHANGE, MarkRemoved = world.event.S_EVENT_MARK_REMOVED, + -- Moose Events NewCargo = world.event.S_EVENT_NEW_CARGO, DeleteCargo = world.event.S_EVENT_DELETE_CARGO, NewZone = world.event.S_EVENT_NEW_ZONE, DeleteZone = world.event.S_EVENT_DELETE_ZONE, + NewZoneGoal = world.event.S_EVENT_NEW_ZONE_GOAL, + DeleteZoneGoal = world.event.S_EVENT_DELETE_ZONE_GOAL, RemoveUnit = world.event.S_EVENT_REMOVE_UNIT, + -- Added with DCS 2.5.6 + DetailedFailure = world.event.S_EVENT_DETAILED_FAILURE or -1, --We set this to -1 for backward compatibility to DCS 2.5.5 and earlier + Kill = world.event.S_EVENT_KILL or -1, + Score = world.event.S_EVENT_SCORE or -1, + UnitLost = world.event.S_EVENT_UNIT_LOST or -1, + LandingAfterEjection = world.event.S_EVENT_LANDING_AFTER_EJECTION or -1, } --- The Event structure @@ -272,10 +284,16 @@ EVENTS = { -- @field Wrapper.Airbase#AIRBASE Place The MOOSE airbase object. -- @field #string PlaceName The name of the airbase. -- --- @field weapon The weapon used during the event. --- @field Weapon --- @field WeaponName --- @field WeaponTgtDCSUnit +-- @field #table weapon The weapon used during the event. +-- @field #table Weapon +-- @field #string WeaponName Name of the weapon. +-- @field DCS#Unit WeaponTgtDCSUnit Target DCS unit of the weapon. +-- +-- @field Cargo.Cargo#CARGO Cargo The cargo object. +-- @field #string CargoName The name of the cargo object. +-- +-- @field Core.ZONE#ZONE Zone The zone object. +-- @field #string ZoneName The name of the zone. @@ -456,11 +474,47 @@ local _EVENTMETA = { Event = "OnEventDeleteZone", Text = "S_EVENT_DELETE_ZONE" }, + [EVENTS.NewZoneGoal] = { + Order = 1, + Event = "OnEventNewZoneGoal", + Text = "S_EVENT_NEW_ZONE_GOAL" + }, + [EVENTS.DeleteZoneGoal] = { + Order = 1, + Event = "OnEventDeleteZoneGoal", + Text = "S_EVENT_DELETE_ZONE_GOAL" + }, [EVENTS.RemoveUnit] = { Order = -1, Event = "OnEventRemoveUnit", Text = "S_EVENT_REMOVE_UNIT" }, + -- Added with DCS 2.5.6 + [EVENTS.DetailedFailure] = { + Order = 1, + Event = "OnEventDetailedFailure", + Text = "S_EVENT_DETAILED_FAILURE" + }, + [EVENTS.Kill] = { + Order = 1, + Event = "OnEventKill", + Text = "S_EVENT_KILL" + }, + [EVENTS.Score] = { + Order = 1, + Event = "OnEventScore", + Text = "S_EVENT_SCORE" + }, + [EVENTS.UnitLost] = { + Order = 1, + Event = "OnEventUnitLost", + Text = "S_EVENT_UNIT_LOST" + }, + [EVENTS.LandingAfterEjection] = { + Order = 1, + Event = "OnEventLandingAfterEjection", + Text = "S_EVENT_LANDING_AFTER_EJECTION" + }, } @@ -468,6 +522,9 @@ local _EVENTMETA = { -- @type EVENT.Events -- @field #number IniUnit +--- Create new event handler. +-- @param #EVENT self +-- @return #EVENT self function EVENT:New() local self = BASE:Inherit( self, BASE:New() ) self:F2() @@ -498,6 +555,8 @@ function EVENT:Init( EventID, EventClass ) if not self.Events[EventID][EventPriority][EventClass] then self.Events[EventID][EventPriority][EventClass] = {} end + + return self.Events[EventID][EventPriority][EventClass] end @@ -584,7 +643,7 @@ end -- @param EventID -- @return #EVENT function EVENT:OnEventGeneric( EventFunction, EventClass, EventID ) - self:F2( { EventID } ) + self:F2( { EventID, EventClass, EventFunction } ) local EventData = self:Init( EventID, EventClass ) EventData.EventFunction = EventFunction @@ -794,6 +853,38 @@ do -- Event Creation world.onEvent( Event ) end + --- Creation of a New ZoneGoal Event. + -- @param #EVENT self + -- @param Core.Functional#ZONE_GOAL ZoneGoal The ZoneGoal created. + function EVENT:CreateEventNewZoneGoal( ZoneGoal ) + self:F( { ZoneGoal } ) + + local Event = { + id = EVENTS.NewZoneGoal, + time = timer.getTime(), + ZoneGoal = ZoneGoal, + } + + world.onEvent( Event ) + end + + + --- Creation of a ZoneGoal Deletion Event. + -- @param #EVENT self + -- @param Core.ZoneGoal#ZONE_GOAL ZoneGoal The ZoneGoal created. + function EVENT:CreateEventDeleteZoneGoal( ZoneGoal ) + self:F( { ZoneGoal } ) + + local Event = { + id = EVENTS.DeleteZoneGoal, + time = timer.getTime(), + ZoneGoal = ZoneGoal, + } + + world.onEvent( Event ) + end + + --- Creation of a S_EVENT_PLAYER_ENTER_UNIT Event. -- @param #EVENT self -- @param Wrapper.Unit#UNIT PlayerUnit. @@ -826,260 +917,227 @@ function EVENT:onEvent( Event ) end + -- Get event meta data. local EventMeta = _EVENTMETA[Event.id] - - --self:E( { EventMeta.Text, Event } ) -- Activate the see all incoming events ... - - if self and - self.Events and - self.Events[Event.id] and - self.MissionEnd == false and - ( Event.initiator ~= nil or ( Event.initiator == nil and Event.id ~= EVENTS.PlayerLeaveUnit ) ) then - - if Event.id and Event.id == EVENTS.MissionEnd then - self.MissionEnd = true - end - - if Event.initiator then - - Event.IniObjectCategory = Event.initiator:getCategory() - - if Event.IniObjectCategory == Object.Category.UNIT then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniDCSGroup = Event.IniDCSUnit:getGroup() - Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) - if not Event.IniUnit then - -- Unit can be a CLIENT. Most likely this will be the case ... - Event.IniUnit = CLIENT:FindByName( Event.IniDCSUnitName, '', true ) - end - Event.IniDCSGroupName = "" - if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then - Event.IniDCSGroupName = Event.IniDCSGroup:getName() - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - if Event.IniGroup then - Event.IniGroupName = Event.IniDCSGroupName - end - end - Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - end - - if Event.IniObjectCategory == Object.Category.STATIC then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false ) - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - end - - if Event.IniObjectCategory == Object.Category.CARGO then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = CARGO:FindByName( Event.IniDCSUnitName ) - Event.IniCoalition = Event.IniDCSUnit:getCoalition() - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.IniDCSUnit:getTypeName() - end - - if Event.IniObjectCategory == Object.Category.SCENERY then - Event.IniDCSUnit = Event.initiator - Event.IniDCSUnitName = Event.IniDCSUnit:getName() - Event.IniUnitName = Event.IniDCSUnitName - Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator ) - Event.IniCategory = Event.IniDCSUnit:getDesc().category - Event.IniTypeName = Event.initiator:isExist() and Event.IniDCSUnit:getTypeName() or "SCENERY" -- TODO: Bug fix for 2.1! - end - end - - if Event.target then - - Event.TgtObjectCategory = Event.target:getCategory() - - if Event.TgtObjectCategory == Object.Category.UNIT then - Event.TgtDCSUnit = Event.target - Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) - Event.TgtDCSGroupName = "" - if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then - Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() - Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) - if Event.TgtGroup then - Event.TgtGroupName = Event.TgtDCSGroupName - end - end - Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName() - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - - if Event.TgtObjectCategory == Object.Category.STATIC then - Event.TgtDCSUnit = Event.target - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName, false ) - Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - - if Event.TgtObjectCategory == Object.Category.SCENERY then - Event.TgtDCSUnit = Event.target - Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() - Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = SCENERY:Register( Event.TgtDCSUnitName, Event.target ) - Event.TgtCategory = Event.TgtDCSUnit:getDesc().category - Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() - end - end - - if Event.weapon then - Event.Weapon = Event.weapon - Event.WeaponName = Event.Weapon:getTypeName() - Event.WeaponUNIT = CLIENT:Find( Event.Weapon, '', true ) -- Sometimes, the weapon is a player unit! - Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName() - Event.WeaponCoalition = Event.WeaponUNIT and Event.Weapon:getCoalition() - Event.WeaponCategory = Event.WeaponUNIT and Event.Weapon:getDesc().category - Event.WeaponTypeName = Event.WeaponUNIT and Event.Weapon:getTypeName() - --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() - end - - -- Place should be given for takeoff and landing events as well as base captured. It should be a DCS airbase. - if Event.place then - Event.Place=AIRBASE:Find(Event.place) - Event.PlaceName=Event.Place:GetName() - end - --- @FC: something like this should be added. ---[[ - if Event.idx then - Event.MarkID=Event.idx - Event.MarkVec3=Event.pos - Event.MarkCoordinate=COORDINATE:NewFromVec3(Event.pos) - Event.MarkText=Event.text - Event.MarkCoalition=Event.coalition - Event.MarkGroupID = Event.groupID - end -]] - - if Event.cargo then - Event.Cargo = Event.cargo - Event.CargoName = Event.cargo.Name - end - - if Event.zone then - Event.Zone = Event.zone - Event.ZoneName = Event.zone.ZoneName - end - - local PriorityOrder = EventMeta.Order - local PriorityBegin = PriorityOrder == -1 and 5 or 1 - local PriorityEnd = PriorityOrder == -1 and 1 or 5 - - if Event.IniObjectCategory ~= Object.Category.STATIC then - self:T( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) - end - - for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do - - if self.Events[Event.id][EventPriority] then - - -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. - for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do - - --if Event.IniObjectCategory ~= Object.Category.STATIC then - -- self:E( { "Evaluating: ", EventClass:GetClassNameAndID() } ) - --end - - Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) - Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) - - -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. - if EventData.EventUnit then - - -- So now the EventClass must be a UNIT class!!! We check if it is still "Alive". - if EventClass:IsAlive() or - Event.id == EVENTS.PlayerEnterUnit or - Event.id == EVENTS.Crash or - Event.id == EVENTS.Dead or - Event.id == EVENTS.RemoveUnit then - - local UnitName = EventClass:GetName() - - if ( EventMeta.Side == "I" and UnitName == Event.IniDCSUnitName ) or - ( EventMeta.Side == "T" and UnitName == Event.TgtDCSUnitName ) then - - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventFunction then - - if Event.IniObjectCategory ~= 3 then - self:F( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event ) - end, ErrorHandler ) - - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ EventMeta.Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:F( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - - local Result, Value = xpcall( - function() - return EventFunction( EventClass, Event ) - end, ErrorHandler ) - end - end - end - else - -- The EventClass is not alive anymore, we remove it from the EventHandlers... - self:RemoveEvent( EventClass, Event.id ) - end - else - - -- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP. - if EventData.EventGroup then - - -- So now the EventClass must be a GROUP class!!! We check if it is still "Alive". - if EventClass:IsAlive() or - Event.id == EVENTS.PlayerEnterUnit or - Event.id == EVENTS.Crash or - Event.id == EVENTS.Dead or - Event.id == EVENTS.RemoveUnit then - - -- We can get the name of the EventClass, which is now always a GROUP object. - local GroupName = EventClass:GetName() - if ( EventMeta.Side == "I" and GroupName == Event.IniDCSGroupName ) or - ( EventMeta.Side == "T" and GroupName == Event.TgtDCSGroupName ) then - + -- Check if this is a known event? + if EventMeta then + + if self and + self.Events and + self.Events[Event.id] and + self.MissionEnd == false and + ( Event.initiator ~= nil or ( Event.initiator == nil and Event.id ~= EVENTS.PlayerLeaveUnit ) ) then + + if Event.id and Event.id == EVENTS.MissionEnd then + self.MissionEnd = true + end + + if Event.initiator then + + Event.IniObjectCategory = Event.initiator:getCategory() + + if Event.IniObjectCategory == Object.Category.UNIT then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniDCSGroup = Event.IniDCSUnit:getGroup() + Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName ) + if not Event.IniUnit then + -- Unit can be a CLIENT. Most likely this will be the case ... + Event.IniUnit = CLIENT:FindByName( Event.IniDCSUnitName, '', true ) + end + Event.IniDCSGroupName = "" + if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then + Event.IniDCSGroupName = Event.IniDCSGroup:getName() + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + --if Event.IniGroup then + Event.IniGroupName = Event.IniDCSGroupName + --end + end + Event.IniPlayerName = Event.IniDCSUnit:getPlayerName() + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + Event.IniCategory = Event.IniDCSUnit:getDesc().category + end + + if Event.IniObjectCategory == Object.Category.STATIC then + if Event.id==31 then + --env.info("FF event 31") + -- Event.initiator is a Static object representing the pilot. But getName() error due to DCS bug. + Event.IniDCSUnit = Event.initiator + local ID=Event.initiator.id_ + Event.IniDCSUnitName = string.format("Ejected Pilot ID %s", tostring(ID)) + Event.IniUnitName = Event.IniDCSUnitName + Event.IniCoalition = 0 + Event.IniCategory = 0 + Event.IniTypeName = "Ejected Pilot" + else + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false ) + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniCategory = Event.IniDCSUnit:getDesc().category + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + end + end + + if Event.IniObjectCategory == Object.Category.CARGO then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = CARGO:FindByName( Event.IniDCSUnitName ) + Event.IniCoalition = Event.IniDCSUnit:getCoalition() + Event.IniCategory = Event.IniDCSUnit:getDesc().category + Event.IniTypeName = Event.IniDCSUnit:getTypeName() + end + + if Event.IniObjectCategory == Object.Category.SCENERY then + Event.IniDCSUnit = Event.initiator + Event.IniDCSUnitName = Event.IniDCSUnit:getName() + Event.IniUnitName = Event.IniDCSUnitName + Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator ) + Event.IniCategory = Event.IniDCSUnit:getDesc().category + Event.IniTypeName = Event.initiator:isExist() and Event.IniDCSUnit:getTypeName() or "SCENERY" -- TODO: Bug fix for 2.1! + end + end + + if Event.target then + + Event.TgtObjectCategory = Event.target:getCategory() + + if Event.TgtObjectCategory == Object.Category.UNIT then + Event.TgtDCSUnit = Event.target + Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup() + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName ) + Event.TgtDCSGroupName = "" + if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then + Event.TgtDCSGroupName = Event.TgtDCSGroup:getName() + Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) + --if Event.TgtGroup then + Event.TgtGroupName = Event.TgtDCSGroupName + --end + end + Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName() + Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + end + + if Event.TgtObjectCategory == Object.Category.STATIC then + Event.TgtDCSUnit = Event.target + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName, false ) + Event.TgtCoalition = Event.TgtDCSUnit:getCoalition() + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + end + + if Event.TgtObjectCategory == Object.Category.SCENERY then + Event.TgtDCSUnit = Event.target + Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() + Event.TgtUnitName = Event.TgtDCSUnitName + Event.TgtUnit = SCENERY:Register( Event.TgtDCSUnitName, Event.target ) + Event.TgtCategory = Event.TgtDCSUnit:getDesc().category + Event.TgtTypeName = Event.TgtDCSUnit:getTypeName() + end + end + + if Event.weapon then + Event.Weapon = Event.weapon + Event.WeaponName = Event.Weapon:getTypeName() + Event.WeaponUNIT = CLIENT:Find( Event.Weapon, '', true ) -- Sometimes, the weapon is a player unit! + Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName() + Event.WeaponCoalition = Event.WeaponUNIT and Event.Weapon:getCoalition() + Event.WeaponCategory = Event.WeaponUNIT and Event.Weapon:getDesc().category + Event.WeaponTypeName = Event.WeaponUNIT and Event.Weapon:getTypeName() + --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget() + end + + -- Place should be given for takeoff and landing events as well as base captured. It should be a DCS airbase. + if Event.place then + if Event.id==EVENTS.LandingAfterEjection then + -- Place is here the UNIT of which the pilot ejected. + --local name=Event.place:getName() -- This returns a DCS error "Airbase doesn't exit" :( + -- However, this is not a big thing, as the aircraft the pilot ejected from is usually long crashed before the ejected pilot touches the ground. + --Event.Place=UNIT:Find(Event.place) + else + Event.Place=AIRBASE:Find(Event.place) + Event.PlaceName=Event.Place:GetName() + end + end + + -- Mark points. + if Event.idx then + Event.MarkID=Event.idx + Event.MarkVec3=Event.pos + Event.MarkCoordinate=COORDINATE:NewFromVec3(Event.pos) + Event.MarkText=Event.text + Event.MarkCoalition=Event.coalition + Event.MarkGroupID = Event.groupID + end + + if Event.cargo then + Event.Cargo = Event.cargo + Event.CargoName = Event.cargo.Name + end + + if Event.zone then + Event.Zone = Event.zone + Event.ZoneName = Event.zone.ZoneName + end + + local PriorityOrder = EventMeta.Order + local PriorityBegin = PriorityOrder == -1 and 5 or 1 + local PriorityEnd = PriorityOrder == -1 and 1 or 5 + + if Event.IniObjectCategory ~= Object.Category.STATIC then + self:F( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) + end + + for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do + + if self.Events[Event.id][EventPriority] then + + -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called. + for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do + + --if Event.IniObjectCategory ~= Object.Category.STATIC then + -- self:E( { "Evaluating: ", EventClass:GetClassNameAndID() } ) + --end + + Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName ) + Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName ) + + -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT. + if EventData.EventUnit then + + -- So now the EventClass must be a UNIT class!!! We check if it is still "Alive". + if EventClass:IsAlive() or + Event.id == EVENTS.PlayerEnterUnit or + Event.id == EVENTS.Crash or + Event.id == EVENTS.Dead or + Event.id == EVENTS.RemoveUnit then + + local UnitName = EventClass:GetName() + + if ( EventMeta.Side == "I" and UnitName == Event.IniDCSUnitName ) or + ( EventMeta.Side == "T" and UnitName == Event.TgtDCSUnitName ) then + -- First test if a EventFunction is Set, otherwise search for the default function if EventData.EventFunction then - + if Event.IniObjectCategory ~= 3 then - self:F( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) + self:F( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) end - + local Result, Value = xpcall( function() - return EventData.EventFunction( EventClass, Event, unpack( EventData.Params ) ) + return EventData.EventFunction( EventClass, Event ) end, ErrorHandler ) else @@ -1090,74 +1148,130 @@ function EVENT:onEvent( Event ) -- Now call the default event function. if Event.IniObjectCategory ~= 3 then - self:F( { "Calling " .. EventMeta.Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) + self:F( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) end - + local Result, Value = xpcall( function() - return EventFunction( EventClass, Event, unpack( EventData.Params ) ) + return EventFunction( EventClass, Event ) end, ErrorHandler ) end end end else -- The EventClass is not alive anymore, we remove it from the EventHandlers... - --self:RemoveEvent( EventClass, Event.id ) + self:RemoveEvent( EventClass, Event.id ) end - else - - -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. - -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. - if not EventData.EventUnit then - -- First test if a EventFunction is Set, otherwise search for the default function - if EventData.EventFunction then - - -- There is an EventFunction defined, so call the EventFunction. - if Event.IniObjectCategory ~= 3 then - self:F2( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) - end - local Result, Value = xpcall( - function() - return EventData.EventFunction( EventClass, Event ) - end, ErrorHandler ) - else - - -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. - local EventFunction = EventClass[ EventMeta.Event ] - if EventFunction and type( EventFunction ) == "function" then - - -- Now call the default event function. - if Event.IniObjectCategory ~= 3 then - self:F2( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + else + + --- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP. + if EventData.EventGroup then + + -- So now the EventClass must be a GROUP class!!! We check if it is still "Alive". + if EventClass:IsAlive() or + Event.id == EVENTS.PlayerEnterUnit or + Event.id == EVENTS.Crash or + Event.id == EVENTS.Dead or + Event.id == EVENTS.RemoveUnit then + + -- We can get the name of the EventClass, which is now always a GROUP object. + local GroupName = EventClass:GetName() + + if ( EventMeta.Side == "I" and GroupName == Event.IniDCSGroupName ) or + ( EventMeta.Side == "T" and GroupName == Event.TgtDCSGroupName ) then + + -- First test if a EventFunction is Set, otherwise search for the default function + if EventData.EventFunction then + + if Event.IniObjectCategory ~= 3 then + self:F( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } ) + end + + local Result, Value = xpcall( + function() + return EventData.EventFunction( EventClass, Event, unpack( EventData.Params ) ) + end, ErrorHandler ) + + else + + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. + local EventFunction = EventClass[ EventMeta.Event ] + if EventFunction and type( EventFunction ) == "function" then + + -- Now call the default event function. + if Event.IniObjectCategory ~= 3 then + self:F( { "Calling " .. EventMeta.Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } ) + end + + local Result, Value = xpcall( + function() + return EventFunction( EventClass, Event, unpack( EventData.Params ) ) + end, ErrorHandler ) + end end - + end + else + -- The EventClass is not alive anymore, we remove it from the EventHandlers... + --self:RemoveEvent( EventClass, Event.id ) + end + else + + -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction. + -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon. + if not EventData.EventUnit then + + -- First test if a EventFunction is Set, otherwise search for the default function + if EventData.EventFunction then + + -- There is an EventFunction defined, so call the EventFunction. + if Event.IniObjectCategory ~= 3 then + self:F2( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + end local Result, Value = xpcall( function() - local Result, Value = EventFunction( EventClass, Event ) - return Result, Value + return EventData.EventFunction( EventClass, Event ) end, ErrorHandler ) + else + + -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object. + local EventFunction = EventClass[ EventMeta.Event ] + if EventFunction and type( EventFunction ) == "function" then + + -- Now call the default event function. + if Event.IniObjectCategory ~= 3 then + self:F2( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } ) + end + + local Result, Value = xpcall( + function() + local Result, Value = EventFunction( EventClass, Event ) + return Result, Value + end, ErrorHandler ) + end end + end - end end end end end - end - - -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. - -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. - -- And this is a problem because it will remove all entries from the SET_CARGOs. - -- To prevent this from happening, the Cargo object has a flag NoDestroy. - -- When true, the SET_CARGO won't Remove the Cargo object from the set. - -- But we need to switch that flag off after the event handlers have been called. - if Event.id == EVENTS.DeleteCargo then - Event.Cargo.NoDestroy = nil + + -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. + -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. + -- And this is a problem because it will remove all entries from the SET_CARGOs. + -- To prevent this from happening, the Cargo object has a flag NoDestroy. + -- When true, the SET_CARGO won't Remove the Cargo object from the set. + -- But we need to switch that flag off after the event handlers have been called. + if Event.id == EVENTS.DeleteCargo then + Event.Cargo.NoDestroy = nil + end + else + self:T( { EventMeta.Text, Event } ) end else - self:T( { EventMeta.Text, Event } ) + self:E(string.format("WARNING: Could not get EVENTMETA data for event ID=%d! Is this an unknown/new DCS event?", tostring(Event.id))) end Event = nil @@ -1173,7 +1287,7 @@ EVENTHANDLER = { --- The EVENTHANDLER constructor -- @param #EVENTHANDLER self --- @return #EVENTHANDLER +-- @return #EVENTHANDLER self function EVENTHANDLER:New() self = BASE:Inherit( self, BASE:New() ) -- #EVENTHANDLER return self diff --git a/Moose Development/Moose/Core/Fsm.lua b/Moose Development/Moose/Core/Fsm.lua index ba8e3c4de..09d447f65 100644 --- a/Moose Development/Moose/Core/Fsm.lua +++ b/Moose Development/Moose/Core/Fsm.lua @@ -594,7 +594,17 @@ do -- FSM return errmsg end if self[handler] then - self:T( "*** FSM *** " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] ) + if step == "onafter" or step == "OnAfter" then + self:T( ":::>" .. step .. params[2] .. " : " .. params[1] .. " >> " .. params[2] .. ">" .. step .. params[2] .. "()" .. " >> " .. params[3] ) + elseif step == "onbefore" or step == "OnBefore" then + self:T( ":::>" .. step .. params[2] .. " : " .. params[1] .. " >> " .. step .. params[2] .. "()" .. ">" .. params[2] .. " >> " .. params[3] ) + elseif step == "onenter" or step == "OnEnter" then + self:T( ":::>" .. step .. params[3] .. " : " .. params[1] .. " >> " .. params[2] .. " >> " .. step .. params[3] .. "()" .. ">" .. params[3] ) + elseif step == "onleave" or step == "OnLeave" then + self:T( ":::>" .. step .. params[1] .. " : " .. params[1] .. ">" .. step .. params[1] .. "()" .. " >> " .. params[2] .. " >> " .. params[3] ) + else + self:T( ":::>" .. step .. " : " .. params[1] .. " >> " .. params[2] .. " >> " .. params[3] ) + end self._EventSchedules[EventName] = nil local Result, Value = xpcall( function() return self[handler]( self, unpack( params ) ) end, ErrorHandler ) return Value @@ -717,14 +727,17 @@ do -- FSM if DelaySeconds ~= nil then if DelaySeconds < 0 then -- Only call the event ONCE! DelaySeconds = math.abs( DelaySeconds ) - if not self._EventSchedules[EventName] then - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) + if not self._EventSchedules[EventName] then + CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4, true ) self._EventSchedules[EventName] = CallID + self:T2(string.format("NEGATIVE Event %s delayed by %.1f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) else + self:T2(string.format("NEGATIVE Event %s delayed by %.1f sec CANCELLED as we already have such an event in the queue.", EventName, DelaySeconds)) -- reschedule end else - CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 ) + CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4, true ) + self:T2(string.format("Event %s delayed by %.1f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID))) end else error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." ) diff --git a/Moose Development/Moose/Core/Goal.lua b/Moose Development/Moose/Core/Goal.lua index 4165e7f3f..c145d6de4 100644 --- a/Moose Development/Moose/Core/Goal.lua +++ b/Moose Development/Moose/Core/Goal.lua @@ -15,6 +15,7 @@ -- === -- -- ### Author: **FlightControl** +-- ### Contributions: **funkyfranky** -- -- === -- @@ -142,6 +143,7 @@ do -- Goal -- @param #GOAL self -- @param #string PlayerName The name of the player. function GOAL:AddPlayerContribution( PlayerName ) + self:F({PlayerName}) self.Players[PlayerName] = self.Players[PlayerName] or 0 self.Players[PlayerName] = self.Players[PlayerName] + 1 self.TotalContributions = self.TotalContributions + 1 diff --git a/Moose Development/Moose/Core/Menu.lua b/Moose Development/Moose/Core/Menu.lua index fa7ab6a72..e968b9350 100644 --- a/Moose Development/Moose/Core/Menu.lua +++ b/Moose Development/Moose/Core/Menu.lua @@ -238,7 +238,7 @@ do -- MENU_BASE self.Path = ( self.ParentMenu and "@" .. table.concat( self.MenuParentPath or {}, "@" ) or "" ) .. "@" .. self.MenuText self.Menus = {} self.MenuCount = 0 - self.MenuTime = timer.getTime() + self.MenuStamp = timer.getTime() self.MenuRemoveParent = false if self.ParentMenu then @@ -285,13 +285,31 @@ do -- MENU_BASE function MENU_BASE:GetMenu( MenuText ) return self.Menus[MenuText] end + + --- Sets a menu stamp for later prevention of menu removal. + -- @param #MENU_BASE self + -- @param MenuStamp + -- @return #MENU_BASE + function MENU_BASE:SetStamp( MenuStamp ) + self.MenuStamp = MenuStamp + return self + end + + + --- Gets a menu stamp for later prevention of menu removal. + -- @param #MENU_BASE self + -- @return MenuStamp + function MENU_BASE:GetStamp() + return timer.getTime() + end + --- Sets a time stamp for later prevention of menu removal. -- @param #MENU_BASE self - -- @param MenuTime + -- @param MenuStamp -- @return #MENU_BASE - function MENU_BASE:SetTime( MenuTime ) - self.MenuTime = MenuTime + function MENU_BASE:SetTime( MenuStamp ) + self.MenuStamp = MenuStamp return self end @@ -443,7 +461,7 @@ do -- MENU_MISSION --- Removes the main menu and the sub menus recursively of this MENU_MISSION. -- @param #MENU_MISSION self -- @return #nil - function MENU_MISSION:Remove( MenuTime, MenuTag ) + function MENU_MISSION:Remove( MenuStamp, MenuTag ) MENU_INDEX:PrepareMission() local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) @@ -451,7 +469,7 @@ do -- MENU_MISSION if MissionMenu == self then self:RemoveSubMenus() - if not MenuTime or self.MenuTime ~= MenuTime then + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then self:F( { Text = self.MenuText, Path = self.MenuPath } ) if self.MenuPath ~= nil then @@ -537,7 +555,7 @@ do -- MENU_MISSION_COMMAND local MissionMenu = MENU_INDEX:HasMissionMenu( Path ) if MissionMenu == self then - if not MenuTime or self.MenuTime ~= MenuTime then + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then self:F( { Text = self.MenuText, Path = self.MenuPath } ) if self.MenuPath ~= nil then @@ -666,7 +684,7 @@ do -- MENU_COALITION --- Removes the main menu and the sub menus recursively of this MENU_COALITION. -- @param #MENU_COALITION self -- @return #nil - function MENU_COALITION:Remove( MenuTime, MenuTag ) + function MENU_COALITION:Remove( MenuStamp, MenuTag ) MENU_INDEX:PrepareCoalition( self.Coalition ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) @@ -674,7 +692,7 @@ do -- MENU_COALITION if CoalitionMenu == self then self:RemoveSubMenus() - if not MenuTime or self.MenuTime ~= MenuTime then + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then self:F( { Coalition = self.Coalition, Text = self.MenuText, Path = self.MenuPath } ) if self.MenuPath ~= nil then @@ -758,14 +776,14 @@ do -- MENU_COALITION_COMMAND --- Removes a radio command item for a coalition -- @param #MENU_COALITION_COMMAND self -- @return #nil - function MENU_COALITION_COMMAND:Remove( MenuTime, MenuTag ) + function MENU_COALITION_COMMAND:Remove( MenuStamp, MenuTag ) MENU_INDEX:PrepareCoalition( self.Coalition ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( self.Coalition, Path ) if CoalitionMenu == self then - if not MenuTime or self.MenuTime ~= MenuTime then + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then self:F( { Coalition = self.Coalition, Text = self.MenuText, Path = self.MenuPath } ) if self.MenuPath ~= nil then @@ -907,13 +925,13 @@ do --- Removes the sub menus recursively of this MENU_GROUP. -- @param #MENU_GROUP self - -- @param MenuTime + -- @param MenuStamp -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #MENU_GROUP self - function MENU_GROUP:RemoveSubMenus( MenuTime, MenuTag ) + function MENU_GROUP:RemoveSubMenus( MenuStamp, MenuTag ) for MenuText, Menu in pairs( self.Menus or {} ) do - Menu:Remove( MenuTime, MenuTag ) + Menu:Remove( MenuStamp, MenuTag ) end self.Menus = nil @@ -923,18 +941,18 @@ do --- Removes the main menu and sub menus recursively of this MENU_GROUP. -- @param #MENU_GROUP self - -- @param MenuTime + -- @param MenuStamp -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #nil - function MENU_GROUP:Remove( MenuTime, MenuTag ) + function MENU_GROUP:Remove( MenuStamp, MenuTag ) MENU_INDEX:PrepareGroup( self.Group ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) if GroupMenu == self then - self:RemoveSubMenus( MenuTime, MenuTag ) - if not MenuTime or self.MenuTime ~= MenuTime then + self:RemoveSubMenus( MenuStamp, MenuTag ) + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then if self.MenuPath ~= nil then self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) @@ -1014,17 +1032,17 @@ do --- Removes a menu structure for a group. -- @param #MENU_GROUP_COMMAND self - -- @param MenuTime + -- @param MenuStamp -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #nil - function MENU_GROUP_COMMAND:Remove( MenuTime, MenuTag ) + function MENU_GROUP_COMMAND:Remove( MenuStamp, MenuTag ) MENU_INDEX:PrepareGroup( self.Group ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) if GroupMenu == self then - if not MenuTime or self.MenuTime ~= MenuTime then + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then if self.MenuPath ~= nil then self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) @@ -1136,13 +1154,13 @@ do --- Removes the sub menus recursively of this MENU_GROUP_DELAYED. -- @param #MENU_GROUP_DELAYED self - -- @param MenuTime + -- @param MenuStamp -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #MENU_GROUP_DELAYED self - function MENU_GROUP_DELAYED:RemoveSubMenus( MenuTime, MenuTag ) + function MENU_GROUP_DELAYED:RemoveSubMenus( MenuStamp, MenuTag ) for MenuText, Menu in pairs( self.Menus or {} ) do - Menu:Remove( MenuTime, MenuTag ) + Menu:Remove( MenuStamp, MenuTag ) end self.Menus = nil @@ -1152,18 +1170,18 @@ do --- Removes the main menu and sub menus recursively of this MENU_GROUP. -- @param #MENU_GROUP_DELAYED self - -- @param MenuTime + -- @param MenuStamp -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #nil - function MENU_GROUP_DELAYED:Remove( MenuTime, MenuTag ) + function MENU_GROUP_DELAYED:Remove( MenuStamp, MenuTag ) MENU_INDEX:PrepareGroup( self.Group ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) if GroupMenu == self then - self:RemoveSubMenus( MenuTime, MenuTag ) - if not MenuTime or self.MenuTime ~= MenuTime then + self:RemoveSubMenus( MenuStamp, MenuTag ) + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then if self.MenuPath ~= nil then self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) @@ -1263,17 +1281,17 @@ do --- Removes a menu structure for a group. -- @param #MENU_GROUP_COMMAND_DELAYED self - -- @param MenuTime + -- @param MenuStamp -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set. -- @return #nil - function MENU_GROUP_COMMAND_DELAYED:Remove( MenuTime, MenuTag ) + function MENU_GROUP_COMMAND_DELAYED:Remove( MenuStamp, MenuTag ) MENU_INDEX:PrepareGroup( self.Group ) local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText ) local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path ) if GroupMenu == self then - if not MenuTime or self.MenuTime ~= MenuTime then + if not MenuStamp or self.MenuStamp ~= MenuStamp then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then if self.MenuPath ~= nil then self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index 528ac9a84..5139d50a2 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -210,6 +210,7 @@ do -- COORDINATE FromParkingAreaHot = "From Parking Area Hot", FromRunway = "From Runway", Landing = "Landing", + LandingReFuAr = "LandingReFuAr", } --- @field COORDINATE.WaypointType @@ -219,6 +220,7 @@ do -- COORDINATE TakeOff = "TakeOffParkingHot", TurningPoint = "Turning Point", Land = "Land", + LandingReFuAr = "LandingReFuAr", } @@ -342,20 +344,20 @@ do -- COORDINATE return x - Precision <= self.x and x + Precision >= self.x and z - Precision <= self.z and z + Precision >= self.z end - --- Returns if the 2 coordinates are at the same 2D position. + --- Scan/find objects (units, statics, scenery) within a certain radius around the coordinate using the world.searchObjects() DCS API function. -- @param #COORDINATE self -- @param #number radius (Optional) Scan radius in meters. Default 100 m. -- @param #boolean scanunits (Optional) If true scan for units. Default true. -- @param #boolean scanstatics (Optional) If true scan for static objects. Default true. -- @param #boolean scanscenery (Optional) If true scan for scenery objects. Default false. - -- @return True if units were found. - -- @return True if statics were found. - -- @return True if scenery objects were found. - -- @return Unit objects found. - -- @return Static objects found. - -- @return Scenery objects found. + -- @return #boolean True if units were found. + -- @return #boolean True if statics were found. + -- @return #boolean True if scenery objects were found. + -- @return #table Table of MOOSE @[#Wrapper.Unit#UNIT} objects found. + -- @return #table Table of DCS static objects found. + -- @return #table Table of DCS scenery objects found. function COORDINATE:ScanObjects(radius, scanunits, scanstatics, scanscenery) - self:F(string.format("Scanning in radius %.1f m.", radius)) + self:F(string.format("Scanning in radius %.1f m.", radius or 100)) local SphereSearch = { id = world.VolumeType.SPHERE, @@ -405,18 +407,17 @@ do -- COORDINATE local ObjectCategory = ZoneObject:getCategory() -- Check for unit or static objects - --if (ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive()) then - if (ObjectCategory == Object.Category.UNIT and ZoneObject:isExist()) then + if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() then table.insert(Units, UNIT:Find(ZoneObject)) gotunits=true - elseif (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then + elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then table.insert(Statics, ZoneObject) gotstatics=true - elseif ObjectCategory == Object.Category.SCENERY then + elseif ObjectCategory==Object.Category.SCENERY then table.insert(Scenery, ZoneObject) gotscenery=true @@ -436,13 +437,56 @@ do -- COORDINATE end for _,static in pairs(Statics) do self:T(string.format("Scan found static %s", static:getName())) + _DATABASE:AddStatic(static:getName()) end for _,scenery in pairs(Scenery) do - self:T(string.format("Scan found scenery %s", scenery:getTypeName())) + self:T(string.format("Scan found scenery %s typename=%s", scenery:getName(), scenery:getTypeName())) + SCENERY:Register(scenery:getName(), scenery) end return gotunits, gotstatics, gotscenery, Units, Statics, Scenery end + + --- Scan/find UNITS within a certain radius around the coordinate using the world.searchObjects() DCS API function. + -- @param #COORDINATE self + -- @param #number radius (Optional) Scan radius in meters. Default 100 m. + -- @return Core.Set#SET_UNIT Set of units. + function COORDINATE:ScanUnits(radius) + + local _,_,_,units=self:ScanObjects(radius, true, false, false) + + local set=SET_UNIT:New() + + for _,unit in pairs(units) do + set:AddUnit(unit) + end + + return set + end + + --- Find the closest unit to the COORDINATE within a certain radius. + -- @param #COORDINATE self + -- @param #number radius Scan radius in meters. Default 100 m. + -- @return Wrapper.Unit#UNIT The closest unit or #nil if no unit is inside the given radius. + function COORDINATE:FindClosestUnit(radius) + + local units=self:ScanUnits(radius) + + local umin=nil --Wrapper.Unit#UNIT + local dmin=math.huge + for _,_unit in pairs(units.Set) do + local unit=_unit --Wrapper.Unit#UNIT + local coordinate=unit:GetCoordinate() + local d=self:Get2DDistance(coordinate) + if d0 then + SCHEDULER:New(nil, self.Explosion, {self,ExplosionIntensity}, Delay) + else + trigger.action.explosion( self:GetVec3(), ExplosionIntensity ) + end + return self end --- Creates an illumination bomb at the point. -- @param #COORDINATE self - -- @param #number power + -- @param #number power Power of illumination bomb in Candela. + -- @return #COORDINATE self function COORDINATE:IlluminationBomb(power) self:F2() trigger.action.illuminationBomb( self:GetVec3(), power ) @@ -1752,7 +1939,7 @@ do -- COORDINATE --- Returns if a Coordinate is in a certain Radius of this Coordinate in 2D plane using the X and Z axis. -- @param #COORDINATE self - -- @param #COORDINATE ToCoordinate The coordinate that will be tested if it is in the radius of this coordinate. + -- @param #COORDINATE Coordinate The coordinate that will be tested if it is in the radius of this coordinate. -- @param #number Radius The radius of the circle on the 2D plane around this coordinate. -- @return #boolean true if in the Radius. function COORDINATE:IsInRadius( Coordinate, Radius ) @@ -1800,12 +1987,12 @@ do -- COORDINATE -- @param #COORDINATE FromCoordinate The coordinate to measure the distance and the bearing from. -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The BR text. - function COORDINATE:ToStringBRA( FromCoordinate, Settings ) + function COORDINATE:ToStringBRA( FromCoordinate, Settings, Language ) local DirectionVec3 = FromCoordinate:GetDirectionVec3( self ) local AngleRadians = self:GetAngleRadians( DirectionVec3 ) local Distance = FromCoordinate:Get2DDistance( self ) local Altitude = self:GetAltitudeText() - return "BRA, " .. self:GetBRAText( AngleRadians, Distance, Settings ) + return "BRA, " .. self:GetBRAText( AngleRadians, Distance, Settings, Language ) end --- Return a BULLS string out of the BULLS of the coalition to the COORDINATE. @@ -1857,7 +2044,7 @@ do -- COORDINATE local LL_Accuracy = Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy local lat, lon = coord.LOtoLL( self:GetVec3() ) - return "LL DMS, " .. UTILS.tostringLL( lat, lon, LL_Accuracy, true ) + return "LL DMS " .. UTILS.tostringLL( lat, lon, LL_Accuracy, true ) end --- Provides a Lat Lon string in Degree Decimal Minute format. @@ -1868,7 +2055,7 @@ do -- COORDINATE local LL_Accuracy = Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy local lat, lon = coord.LOtoLL( self:GetVec3() ) - return "LL DDM, " .. UTILS.tostringLL( lat, lon, LL_Accuracy, false ) + return "LL DDM " .. UTILS.tostringLL( lat, lon, LL_Accuracy, false ) end --- Provides a MGRS string @@ -1880,7 +2067,7 @@ do -- COORDINATE local MGRS_Accuracy = Settings and Settings.MGRS_Accuracy or _SETTINGS.MGRS_Accuracy local lat, lon = coord.LOtoLL( self:GetVec3() ) local MGRS = coord.LLtoMGRS( lat, lon ) - return "MGRS, " .. UTILS.tostringMGRS( MGRS, MGRS_Accuracy ) + return "MGRS " .. UTILS.tostringMGRS( MGRS, MGRS_Accuracy ) end --- Provides a coordinate string of the point, based on a coordinate format system: @@ -1956,7 +2143,7 @@ do -- COORDINATE -- @param Wrapper.Controllable#CONTROLLABLE Controllable -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The coordinate Text in the configured coordinate system. - function COORDINATE:ToStringA2A( Controllable, Settings ) -- R2.2 + function COORDINATE:ToStringA2A( Controllable, Settings, Language ) -- R2.2 self:F2( { Controllable = Controllable and Controllable:GetName() } ) @@ -1965,23 +2152,23 @@ do -- COORDINATE if Settings:IsA2A_BRAA() then if Controllable then local Coordinate = Controllable:GetCoordinate() - return self:ToStringBRA( Coordinate, Settings ) + return self:ToStringBRA( Coordinate, Settings, Language ) else - return self:ToStringMGRS( Settings ) + return self:ToStringMGRS( Settings, Language ) end end if Settings:IsA2A_BULLS() then local Coalition = Controllable:GetCoalition() - return self:ToStringBULLS( Coalition, Settings ) + return self:ToStringBULLS( Coalition, Settings, Language ) end if Settings:IsA2A_LL_DMS() then - return self:ToStringLLDMS( Settings ) + return self:ToStringLLDMS( Settings, Language ) end if Settings:IsA2A_LL_DDM() then - return self:ToStringLLDDM( Settings ) + return self:ToStringLLDDM( Settings, Language ) end if Settings:IsA2A_MGRS() then - return self:ToStringMGRS( Settings ) + return self:ToStringMGRS( Settings, Language ) end return nil @@ -1992,37 +2179,38 @@ do -- COORDINATE -- * Uses default settings in COORDINATE. -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. -- @param #COORDINATE self - -- @param Wrapper.Controllable#CONTROLLABLE Controllable + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The controllable to retrieve the settings from, otherwise the default settings will be chosen. -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @param Tasking.Task#TASK Task The task for which coordinates need to be calculated. -- @return #string The coordinate Text in the configured coordinate system. function COORDINATE:ToString( Controllable, Settings, Task ) - self:F2( { Controllable = Controllable and Controllable:GetName() } ) +-- self:E( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS - local ModeA2A = false - self:E('A2A false') + local ModeA2A = nil if Task then - self:E('Task ' .. Task.ClassName ) if Task:IsInstanceOf( TASK_A2A ) then ModeA2A = true - self:E('A2A true') else if Task:IsInstanceOf( TASK_A2G ) then ModeA2A = false else if Task:IsInstanceOf( TASK_CARGO ) then ModeA2A = false - else - ModeA2A = false end + if Task:IsInstanceOf( TASK_CAPTURE_ZONE ) then + ModeA2A = false + end end end - else - local IsAir = Controllable and Controllable:IsAirPlane() or false + end + + + if ModeA2A == nil then + local IsAir = Controllable and ( Controllable:IsAirPlane() or Controllable:IsHelicopter() ) or false if IsAir then ModeA2A = true else diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 954fc51a9..575b45044 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -9,37 +9,37 @@ -- -- The Radio contains 2 classes : RADIO and BEACON -- --- What are radio communications in DCS ? +-- What are radio communications in DCS? -- -- * Radio transmissions consist of **sound files** that are broadcasted on a specific **frequency** (e.g. 115MHz) and **modulation** (e.g. AM), -- * They can be **subtitled** for a specific **duration**, the **power** in Watts of the transmiter's antenna can be set, and the transmission can be **looped**. -- --- How to supply DCS my own Sound Files ? +-- How to supply DCS my own Sound Files? -- -- * Your sound files need to be encoded in **.ogg** or .wav, -- * Your sound files should be **as tiny as possible**. It is suggested you encode in .ogg with low bitrate and sampling settings, -- * They need to be added in .\l10n\DEFAULT\ in you .miz file (wich can be decompressed like a .zip file), --- * For simplicty sake, you can **let DCS' Mission Editor add the file** itself, by creating a new Trigger with the action "Sound to Country", and choosing your sound file and a country you don't use in your mission. +-- * For simplicity sake, you can **let DCS' Mission Editor add the file** itself, by creating a new Trigger with the action "Sound to Country", and choosing your sound file and a country you don't use in your mission. -- -- Due to weird DCS quirks, **radio communications behave differently** if sent by a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or by any other @{Wrapper.Positionable#POSITIONABLE} -- --- * If the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, DCS will set the power of the transmission automatically, +-- * If the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, DCS will set the power of the transmission automatically, -- * If the transmitter is any other @{Wrapper.Positionable#POSITIONABLE}, the transmisison can't be subtitled or looped. -- -- Note that obviously, the **frequency** and the **modulation** of the transmission are important only if the players are piloting an **Advanced System Modelling** enabled aircraft, -- like the A10C or the Mirage 2000C. They will **hear the transmission** if they are tuned on the **right frequency and modulation** (and if they are close enough - more on that below). --- If a FC3 airacraft is used, it will **hear every communication, whatever the frequency and the modulation** is set to. The same is true for TACAN beacons. If your aircaft isn't compatible, +-- If an FC3 aircraft is used, it will **hear every communication, whatever the frequency and the modulation** is set to. The same is true for TACAN beacons. If your aircraft isn't compatible, -- you won't hear/be able to use the TACAN beacon informations. -- -- === -- --- ### Author: Hugues "Grey_Echo" Bousquet +-- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky -- -- @module Core.Radio -- @image Core_Radio.JPG ---- Models the radio capabilty. +--- Models the radio capability. -- -- ## RADIO usage -- @@ -56,34 +56,35 @@ -- * @{#RADIO.SetModulation}() : Sets the modulation of your transmission. -- * @{#RADIO.SetLoop}() : Choose if you want the transmission to be looped. If you need your transmission to be looped, you might need a @{#BEACON} instead... -- --- Additional Methods to set relevant parameters if the transmiter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} +-- Additional Methods to set relevant parameters if the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} -- -- * @{#RADIO.SetSubtitle}() : Set both the subtitle and its duration, -- * @{#RADIO.NewUnitTransmission}() : Shortcut to set all the relevant parameters in one method call -- --- Additional Methods to set relevant parameters if the transmiter is any other @{Wrapper.Positionable#POSITIONABLE} +-- Additional Methods to set relevant parameters if the transmitter is any other @{Wrapper.Positionable#POSITIONABLE} -- -- * @{#RADIO.SetPower}() : Sets the power of the antenna in Watts -- * @{#RADIO.NewGenericTransmission}() : Shortcut to set all the relevant parameters in one method call -- --- What is this power thing ? +-- What is this power thing? -- -- * If your transmission is sent by a @{Wrapper.Positionable#POSITIONABLE} other than a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, you can set the power of the antenna, -- * Otherwise, DCS sets it automatically, depending on what's available on your Unit, --- * If the player gets **too far** from the transmiter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, +-- * If the player gets **too far** from the transmitter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**, -- * This an automated DCS calculation you have no say on, --- * For reference, a standard VOR station has a 100W antenna, a standard AA TACAN has a 120W antenna, and civilian ATC's antenna usually range between 300 and 500W, +-- * For reference, a standard VOR station has a 100 W antenna, a standard AA TACAN has a 120 W antenna, and civilian ATC's antenna usually range between 300 and 500 W, -- * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. -- -- @type RADIO --- @field Positionable#POSITIONABLE Positionable The transmiter --- @field #string FileName Name of the sound file --- @field #number Frequency Frequency of the transmission in Hz --- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM) --- @field #string Subtitle Subtitle of the transmission --- @field #number SubtitleDuration Duration of the Subtitle in seconds --- @field #number Power Power of the antenna is Watts --- @field #boolean Loop (default true) +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will transmit the radio calls. +-- @field #string FileName Name of the sound file played. +-- @field #number Frequency Frequency of the transmission in Hz. +-- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM). +-- @field #string Subtitle Subtitle of the transmission. +-- @field #number SubtitleDuration Duration of the Subtitle in seconds. +-- @field #number Power Power of the antenna is Watts. +-- @field #boolean Loop Transmission is repeated (default true). +-- @field #string alias Name of the radio transmitter. -- @extends Core.Base#BASE RADIO = { ClassName = "RADIO", @@ -93,19 +94,19 @@ RADIO = { Subtitle = "", SubtitleDuration = 0, Power = 100, - Loop = true, + Loop = false, + alias=nil, } ---- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast --- If you want to create a RADIO, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetRadio}() instead +--- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast. +-- If you want to create a RADIO, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetRadio}() instead. -- @param #RADIO self -- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #RADIO Radio --- @return #nil If Positionable is invalid +-- @return #RADIO The RADIO object or #nil if Positionable is invalid. function RADIO:New(Positionable) + + -- Inherit base local self = BASE:Inherit( self, BASE:New() ) -- Core.Radio#RADIO - - self.Loop = true -- default Loop to true (not sure the above RADIO definition actually is working) self:F(Positionable) if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid @@ -113,11 +114,27 @@ function RADIO:New(Positionable) return self end - self:E({"The passed positionable is invalid, no RADIO created", Positionable}) + self:E({error="The passed positionable is invalid, no RADIO created!", positionable=Positionable}) return nil end ---- Check validity of the filename passed and sets RADIO.FileName +--- Set alias of the transmitter. +-- @param #RADIO self +-- @param #string alias Name of the radio transmitter. +-- @return #RADIO self +function RADIO:SetAlias(alias) + self.alias=tostring(alias) + return self +end + +--- Get alias of the transmitter. +-- @param #RADIO self +-- @return #string Name of the transmitter. +function RADIO:GetAlias() + return tostring(self.alias) +end + +--- Set the file name for the radio transmission. -- @param #RADIO self -- @param #string FileName File name of the sound file (i.e. "Noise.ogg") -- @return #RADIO self @@ -125,49 +142,63 @@ function RADIO:SetFileName(FileName) self:F2(FileName) if type(FileName) == "string" then + if FileName:find(".ogg") or FileName:find(".wav") then if not FileName:find("l10n/DEFAULT/") then FileName = "l10n/DEFAULT/" .. FileName end + self.FileName = FileName return self end end - self:E({"File name invalid. Maybe something wrong with the extension ?", self.FileName}) + self:E({"File name invalid. Maybe something wrong with the extension?", FileName}) return self end ---- Check validity of the frequency passed and sets RADIO.Frequency +--- Set the frequency for the radio transmission. +-- If the transmitting positionable is a unit or group, this also set the command "SetFrequency" with the defined frequency and modulation. -- @param #RADIO self --- @param #number Frequency in MHz (Ranges allowed for radio transmissions in DCS : 30-88 / 108-152 / 225-400MHz) +-- @param #number Frequency Frequency in MHz. Ranges allowed for radio transmissions in DCS : 30-87.995 / 108-173.995 / 225-399.975MHz. -- @return #RADIO self function RADIO:SetFrequency(Frequency) self:F2(Frequency) + if type(Frequency) == "number" then + -- If frequency is in range - if (Frequency >= 30 and Frequency < 88) or (Frequency >= 108 and Frequency < 152) or (Frequency >= 225 and Frequency < 400) then - self.Frequency = Frequency * 1000000 -- Conversion in Hz + if (Frequency >= 30 and Frequency <= 87.995) or (Frequency >= 108 and Frequency <= 173.995) or (Frequency >= 225 and Frequency <= 399.975) then + + -- Convert frequency from MHz to Hz + self.Frequency = Frequency * 1000000 + -- If the RADIO is attached to a UNIT or a GROUP, we need to send the DCS Command "SetFrequency" to change the UNIT or GROUP frequency if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ + + local commandSetFrequency={ id = "SetFrequency", params = { - frequency = self.Frequency, + frequency = self.Frequency, modulation = self.Modulation, } - }) + } + + self:T2(commandSetFrequency) + self.Positionable:SetCommand(commandSetFrequency) end + return self end end - self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", self.Frequency}) + + self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", Frequency}) return self end ---- Check validity of the frequency passed and sets RADIO.Modulation +--- Set AM or FM modulation of the radio transmitter. -- @param #RADIO self --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM +-- @param #number Modulation Modulation is either radio.modulation.AM or radio.modulation.FM. -- @return #RADIO self function RADIO:SetModulation(Modulation) self:F2(Modulation) @@ -183,23 +214,24 @@ end --- Check validity of the power passed and sets RADIO.Power -- @param #RADIO self --- @param #number Power in W +-- @param #number Power Power in W. -- @return #RADIO self function RADIO:SetPower(Power) self:F2(Power) + if type(Power) == "number" then self.Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that - return self + else + self:E({"Power is invalid. Power unchanged.", self.Power}) end - self:E({"Power is invalid. Power unchanged.", self.Power}) + return self end ---- Check validity of the loop passed and sets RADIO.Loop +--- Set message looping on or off. -- @param #RADIO self --- @param #boolean Loop +-- @param #boolean Loop If true, message is repeated indefinitely. -- @return #RADIO self --- @usage function RADIO:SetLoop(Loop) self:F2(Loop) if type(Loop) == "boolean" then @@ -232,13 +264,12 @@ function RADIO:SetSubtitle(Subtitle, SubtitleDuration) self:E({"Subtitle is invalid. Subtitle reset.", self.Subtitle}) end if type(SubtitleDuration) == "number" then - if math.floor(math.abs(SubtitleDuration)) == SubtitleDuration then - self.SubtitleDuration = SubtitleDuration - return self - end + self.SubtitleDuration = SubtitleDuration + else + self.SubtitleDuration = 0 + self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) end - self.SubtitleDuration = 0 - self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration}) + return self end --- Create a new transmission, that is to say, populate the RADIO with relevant data @@ -246,10 +277,10 @@ end -- but it will work with a UNIT or a GROUP anyway. -- Only the #RADIO and the Filename are mandatory -- @param #RADIO self --- @param #string FileName --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #number Power in W +-- @param #string FileName Name of the sound file that will be transmitted. +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Modulation of frequency, which is either radio.modulation.AM or radio.modulation.FM. +-- @param #number Power Power in W. -- @return #RADIO self function RADIO:NewGenericTransmission(FileName, Frequency, Modulation, Power, Loop) self:F({FileName, Frequency, Modulation, Power}) @@ -269,31 +300,43 @@ end -- but it will work for any @{Wrapper.Positionable#POSITIONABLE}. -- Only the RADIO and the Filename are mandatory. -- @param #RADIO self --- @param #string FileName --- @param #string Subtitle --- @param #number SubtitleDuration in s --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #boolean Loop +-- @param #string FileName Name of sound file. +-- @param #string Subtitle Subtitle to be displayed with sound file. +-- @param #number SubtitleDuration Duration of subtitle display in seconds. +-- @param #number Frequency Frequency in MHz. +-- @param #number Modulation Modulation which can be either radio.modulation.AM or radio.modulation.FM +-- @param #boolean Loop If true, loop message. -- @return #RADIO self function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop) self:F({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop}) + -- Set file name. self:SetFileName(FileName) - local Duration = 5 - if SubtitleDuration then Duration = SubtitleDuration end - -- SubtitleDuration argument was missing, adding it - if Subtitle then self:SetSubtitle(Subtitle, Duration) end - -- self:SetSubtitleDuration is non existent, removing faulty line - -- if SubtitleDuration then self:SetSubtitleDuration(SubtitleDuration) end - if Frequency then self:SetFrequency(Frequency) end - if Modulation then self:SetModulation(Modulation) end - if Loop then self:SetLoop(Loop) end + + -- Set modulation AM/FM. + if Modulation then + self:SetModulation(Modulation) + end + + -- Set frequency. + if Frequency then + self:SetFrequency(Frequency) + end + + -- Set subtitle. + if Subtitle then + self:SetSubtitle(Subtitle, SubtitleDuration or 0) + end + + -- Set Looping. + if Loop then + self:SetLoop(Loop) + end return self end ---- Actually Broadcast the transmission +--- Broadcast the transmission. -- * The Radio has to be populated with the new transmission before broadcasting. -- * Please use RADIO setters or either @{#RADIO.NewGenericTransmission} or @{#RADIO.NewUnitTransmission} -- * This class is in fact pretty smart, it determines the right DCS function to use depending on the type of POSITIONABLE @@ -302,31 +345,38 @@ end -- * If your POSITIONABLE is a UNIT or a GROUP, the Power is ignored. -- * If your POSITIONABLE is not a UNIT or a GROUP, the Subtitle, SubtitleDuration are ignored -- @param #RADIO self +-- @param #boolean viatrigger Use trigger.action.radioTransmission() in any case, i.e. also for UNITS and GROUPS. -- @return #RADIO self -function RADIO:Broadcast() - self:F() +function RADIO:Broadcast(viatrigger) + self:F({viatrigger=viatrigger}) - -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system - if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self:T2("Broadcasting from a UNIT or a GROUP") - self.Positionable:SetCommand({ + -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system. + if (self.Positionable.ClassName=="UNIT" or self.Positionable.ClassName=="GROUP") and (not viatrigger) then + self:T("Broadcasting from a UNIT or a GROUP") + + local commandTransmitMessage={ id = "TransmitMessage", params = { file = self.FileName, duration = self.SubtitleDuration, subtitle = self.Subtitle, loop = self.Loop, - } - }) + }} + + self:T3(commandTransmitMessage) + self.Positionable:SetCommand(commandTransmitMessage) else -- If the POSITIONABLE is anything else, we revert to the general singleton function -- I need to give it a unique name, so that the transmission can be stopped later. I use the class ID - self:T2("Broadcasting from a POSITIONABLE") + self:T("Broadcasting from a POSITIONABLE") trigger.action.radioTransmission(self.FileName, self.Positionable:GetPositionVec3(), self.Modulation, self.Loop, self.Frequency, self.Power, tostring(self.ID)) end + return self end + + --- Stops a transmission -- This function is especially usefull to stop the broadcast of looped transmissions -- @param #RADIO self @@ -335,10 +385,10 @@ function RADIO:StopBroadcast() self:F() -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then - self.Positionable:SetCommand({ - id = "StopTransmission", - params = {} - }) + + local commandStopTransmission={id="StopTransmission", params={}} + + self.Positionable:SetCommand(commandStopTransmission) else -- Else, we use the appropriate singleton funciton trigger.action.stopRadioTransmission(tostring(self.ID)) @@ -364,24 +414,91 @@ end -- Use @{#BEACON:StopRadioBeacon}() to stop it. -- -- @type BEACON +-- @field #string ClassName Name of the class "BEACON". +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will receive radio capabilities. -- @extends Core.Base#BASE BEACON = { ClassName = "BEACON", + Positionable = nil, + name=nil, } ---- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.AATACAN} or @{#BEACON.Generic} +--- Beacon types supported by DCS. +-- @type BEACON.Type +-- @field #number NULL +-- @field #number VOR +-- @field #number DME +-- @field #number VOR_DME +-- @field #number TACAN +-- @field #number VORTAC +-- @field #number RSBN +-- @field #number BROADCAST_STATION +-- @field #number HOMER +-- @field #number AIRPORT_HOMER +-- @field #number AIRPORT_HOMER_WITH_MARKER +-- @field #number ILS_FAR_HOMER +-- @field #number ILS_NEAR_HOMER +-- @field #number ILS_LOCALIZER +-- @field #number ILS_GLIDESLOPE +-- @field #number NAUTICAL_HOMER +-- @field #number ICLS +BEACON.Type={ + NULL = 0, + VOR = 1, + DME = 2, + VOR_DME = 3, + TACAN = 4, + VORTAC = 5, + RSBN = 32, + BROADCAST_STATION = 1024, + HOMER = 8, + AIRPORT_HOMER = 4104, + AIRPORT_HOMER_WITH_MARKER = 4136, + ILS_FAR_HOMER = 16408, + ILS_NEAR_HOMER = 16456, + ILS_LOCALIZER = 16640, + ILS_GLIDESLOPE = 16896, + NAUTICAL_HOMER = 32776, + ICLS = 131584, +} + +--- Beacon systems supported by DCS. https://wiki.hoggitworld.com/view/DCS_command_activateBeacon +-- @type BEACON.System +-- @field #number PAR_10 +-- @field #number RSBN_5 +-- @field #number TACAN +-- @field #number TACAN_TANKER +-- @field #number ILS_LOCALIZER (This is the one to be used for AA TACAN Tanker!) +-- @field #number ILS_GLIDESLOPE +-- @field #number BROADCAST_STATION +BEACON.System={ + PAR_10 = 1, + RSBN_5 = 2, + TACAN = 3, + TACAN_TANKER = 4, + ILS_LOCALIZER = 5, + ILS_GLIDESLOPE = 6, + BROADCAST_STATION = 7, +} + +--- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.ActivateTACAN} etc. -- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead. -- @param #BEACON self -- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #BEACON Beacon --- @return #nil If Positionable is invalid +-- @return #BEACON Beacon object or #nil if the positionable is invalid. function BEACON:New(Positionable) - local self = BASE:Inherit(self, BASE:New()) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) --#BEACON + -- Debug. self:F(Positionable) + -- Set positionable. if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid self.Positionable = Positionable + self.name=Positionable:GetName() + self:I(string.format("New BEACON %s", tostring(self.name))) return self end @@ -390,44 +507,95 @@ function BEACON:New(Positionable) end ---- Converts a TACAN Channel/Mode couple into a frequency in Hz +--- Activates a TACAN BEACON. -- @param #BEACON self --- @param #number TACANChannel --- @param #string TACANMode --- @return #number Frequecy --- @return #nil if parameters are invalid -function BEACON:_TACANToFrequency(TACANChannel, TACANMode) - self:F3({TACANChannel, TACANMode}) - - if type(TACANChannel) ~= "number" then - if TACANMode ~= "X" and TACANMode ~= "Y" then - return nil -- error in arguments - end +-- @param #number Channel TACAN channel, i.e. the "10" part in "10Y". +-- @param #string Mode TACAN mode, i.e. the "Y" part in "10Y". +-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +-- @usage +-- -- Let's create a TACAN Beacon for a tanker +-- local myUnit = UNIT:FindByName("MyUnit") +-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon +-- +-- myBeacon:ActivateTACAN(20, "Y", "TEXACO", true) -- Activate the beacon +function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) + self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration}) + + -- Get frequency. + local Frequency=UTILS.TACANToFrequency(Channel, Mode) + + -- Check. + if not Frequency then + self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) + return self end --- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. --- I have no idea what it does but it seems to work - local A = 1151 -- 'X', channel >= 64 - local B = 64 -- channel >= 64 + -- Beacon type. + local Type=BEACON.Type.TACAN - if TACANChannel < 64 then - B = 1 - end + -- Beacon system. + local System=BEACON.System.TACAN - if TACANMode == 'Y' then - A = 1025 - if TACANChannel < 64 then - A = 1088 - end - else -- 'X' - if TACANChannel < 64 then - A = 962 + -- Check if unit is an aircraft and set system accordingly. + local AA=self.Positionable:IsAir() + if AA then + System=5 --NOTE: 5 is how you cat the correct tanker behaviour! --BEACON.System.TACAN_TANKER + -- Check if "Y" mode is selected for aircraft. + if Mode~="Y" then + self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.", self.Positionable}) end end - return (A + TACANChannel - B) * 1000000 + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug. + self:I({string.format("BEACON Activating TACAN %s: Channel=%d%s, Morse=%s, Bearing=%s, Duration=%s!", tostring(self.name), Channel, Mode, Message, tostring(Bearing), tostring(Duration))}) + + -- Start beacon. + self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing) + + -- Stop sheduler. + if Duration then + self.Positionable:DeactivateBeacon(Duration) + end + + return self end +--- Activates an ICLS BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system. +-- @param #BEACON self +-- @param #number Channel ICLS channel. +-- @param #string Callsign The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +function BEACON:ActivateICLS(Channel, Callsign, Duration) + self:F({Channel=Channel, Callsign=Callsign, Duration=Duration}) + + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug + self:T2({"ICLS BEACON started!"}) + + -- Start beacon. + self.Positionable:CommandActivateICLS(Channel, UnitID, Callsign) + + -- Stop sheduler + if Duration then -- Schedule the stop of the BEACON if asked by the MD + self.Positionable:DeactivateBeacon(Duration) + end + + return self +end + + + + + --- Activates a TACAN BEACON on an Aircraft. -- @param #BEACON self @@ -480,7 +648,7 @@ function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration) }) if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD - SCHEDULER:New( nil, + SCHEDULER:New(nil, function() self:StopAATACAN() end, {}, BeaconDuration) @@ -591,4 +759,45 @@ function BEACON:StopRadioBeacon() self:F() -- The unique name of the transmission is the class ID trigger.action.stopRadioTransmission(tostring(self.ID)) -end \ No newline at end of file + return self +end + +--- Converts a TACAN Channel/Mode couple into a frequency in Hz +-- @param #BEACON self +-- @param #number TACANChannel +-- @param #string TACANMode +-- @return #number Frequecy +-- @return #nil if parameters are invalid +function BEACON:_TACANToFrequency(TACANChannel, TACANMode) + self:F3({TACANChannel, TACANMode}) + + if type(TACANChannel) ~= "number" then + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end + + diff --git a/Moose Development/Moose/Core/RadioQueue.lua b/Moose Development/Moose/Core/RadioQueue.lua new file mode 100644 index 000000000..1ff08c747 --- /dev/null +++ b/Moose Development/Moose/Core/RadioQueue.lua @@ -0,0 +1,577 @@ +--- **Core** - Queues Radio Transmissions. +-- +-- === +-- +-- ## Features: +-- +-- * Managed Radio Transmissions. +-- +-- === +-- +-- ### Authors: funkyfranky +-- +-- @module Core.RadioQueue +-- @image Core_Radio.JPG + +--- Manages radio transmissions. +-- +-- @type RADIOQUEUE +-- @field #string ClassName Name of the class "RADIOQUEUE". +-- @field #boolean Debug Debug mode. More info. +-- @field #string lid ID for dcs.log. +-- @field #number frequency The radio frequency in Hz. +-- @field #number modulation The radio modulation. Either radio.modulation.AM or radio.modulation.FM. +-- @field Core.Scheduler#SCHEDULER scheduler The scheduler. +-- @field #string RQid The radio queue scheduler ID. +-- @field #table queue The queue of transmissions. +-- @field #string alias Name of the radio. +-- @field #number dt Time interval in seconds for checking the radio queue. +-- @field #number delay Time delay before starting the radio queue. +-- @field #number Tlast Time (abs) when the last transmission finished. +-- @field Core.Point#COORDINATE sendercoord Coordinate from where transmissions are broadcasted. +-- @field #number sendername Name of the sending unit or static. +-- @field #boolean senderinit Set frequency was initialized. +-- @field #number power Power of radio station in Watts. Default 100 W. +-- @field #table numbers Table of number transmission parameters. +-- @field #boolean checking Scheduler is checking the radio queue. +-- @field #boolean schedonce Call ScheduleOnce instead of normal scheduler. +-- @extends Core.Base#BASE +RADIOQUEUE = { + ClassName = "RADIOQUEUE", + Debug = nil, + lid = nil, + frequency = nil, + modulation = nil, + scheduler = nil, + RQid = nil, + queue = {}, + alias = nil, + dt = nil, + delay = nil, + Tlast = nil, + sendercoord = nil, + sendername = nil, + senderinit = nil, + power = nil, + numbers = {}, + checking = nil, + schedonce = nil, +} + +--- Radio queue transmission data. +-- @type RADIOQUEUE.Transmission +-- @field #string filename Name of the file to be transmitted. +-- @field #string path Path in miz file where the file is located. +-- @field #number duration Duration in seconds. +-- @field #string subtitle Subtitle of the transmission. +-- @field #number subduration Duration of the subtitle being displayed. +-- @field #number Tstarted Mission time (abs) in seconds when the transmission started. +-- @field #boolean isplaying If true, transmission is currently playing. +-- @field #number Tplay Mission time (abs) in seconds when the transmission should be played. +-- @field #number interval Interval in seconds before next transmission. + + +--- Create a new RADIOQUEUE object for a given radio frequency/modulation. +-- @param #RADIOQUEUE self +-- @param #number frequency The radio frequency in MHz. +-- @param #number modulation (Optional) The radio modulation. Default radio.modulation.AM. +-- @param #string alias (Optional) Name of the radio queue. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:New(frequency, modulation, alias) + + -- Inherit base + local self=BASE:Inherit(self, BASE:New()) -- #RADIOQUEUE + + self.alias=alias or "My Radio" + + self.lid=string.format("RADIOQUEUE %s | ", self.alias) + + if frequency==nil then + self:E(self.lid.."ERROR: No frequency specified as first parameter!") + return nil + end + + -- Frequency in Hz. + self.frequency=frequency*1000000 + + -- Modulation. + self.modulation=modulation or radio.modulation.AM + + -- Set radio power. + self:SetRadioPower() + + -- Scheduler. + self.scheduler=SCHEDULER:New() + self.scheduler:NoTrace() + + return self +end + +--- Start the radio queue. +-- @param #RADIOQUEUE self +-- @param #number delay (Optional) Delay in seconds, before the radio queue is started. Default 1 sec. +-- @param #number dt (Optional) Time step in seconds for checking the queue. Default 0.01 sec. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:Start(delay, dt) + + -- Delay before start. + self.delay=delay or 1 + + -- Time interval for queue check. + self.dt=dt or 0.01 + + -- Debug message. + self:I(self.lid..string.format("Starting RADIOQUEUE %s on Frequency %.2f MHz [modulation=%d] in %.1f seconds (dt=%.3f sec)", self.alias, self.frequency/1000000, self.modulation, self.delay, self.dt)) + + -- Start Scheduler. + if self.schedonce then + self:_CheckRadioQueueDelayed(delay) + else + self.RQid=self.scheduler:Schedule(nil, RADIOQUEUE._CheckRadioQueue, {self}, delay, dt) + end + + return self +end + +--- Stop the radio queue. Stop scheduler and delete queue. +-- @param #RADIOQUEUE self +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:Stop() + self:I(self.lid.."Stopping RADIOQUEUE.") + self.scheduler:Stop(self.RQid) + self.queue={} + return self +end + +--- Set coordinate from where the transmission is broadcasted. +-- @param #RADIOQUEUE self +-- @param Core.Point#COORDINATE coordinate Coordinate of the sender. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetSenderCoordinate(coordinate) + self.sendercoord=coordinate + return self +end + +--- Set name of unit or static from which transmissions are made. +-- @param #RADIOQUEUE self +-- @param #string name Name of the unit or static used for transmissions. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetSenderUnitName(name) + self.sendername=name + return self +end + +--- Set radio power. Note that this only applies if no relay unit is used. +-- @param #RADIOQUEUE self +-- @param #number power Radio power in Watts. Default 100 W. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetRadioPower(power) + self.power=power or 100 + return self +end + +--- Set parameters of a digit. +-- @param #RADIOQUEUE self +-- @param #number digit The digit 0-9. +-- @param #string filename The name of the sound file. +-- @param #number duration The duration of the sound file in seconds. +-- @param #string path The directory within the miz file where the sound is located. Default "l10n/DEFAULT/". +-- @param #string subtitle Subtitle of the transmission. +-- @param #number subduration Duration [sec] of the subtitle being displayed. Default 5 sec. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetDigit(digit, filename, duration, path, subtitle, subduration) + + local transmission={} --#RADIOQUEUE.Transmission + transmission.filename=filename + transmission.duration=duration + transmission.path=path or "l10n/DEFAULT/" + transmission.subtitle=nil + transmission.subduration=nil + + -- Convert digit to string in case it is given as a number. + if type(digit)=="number" then + digit=tostring(digit) + end + + -- Set transmission. + self.numbers[digit]=transmission + + return self +end + +--- Add a transmission to the radio queue. +-- @param #RADIOQUEUE self +-- @param #RADIOQUEUE.Transmission transmission The transmission data table. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:AddTransmission(transmission) + self:F({transmission=transmission}) + + -- Init. + transmission.isplaying=false + transmission.Tstarted=nil + + -- Add to queue. + table.insert(self.queue, transmission) + + -- Start checking. + if self.schedonce and not self.checking then + self:_CheckRadioQueueDelayed() + end + + return self +end + +--- Add a transmission to the radio queue. +-- @param #RADIOQUEUE self +-- @param #string filename Name of the sound file. Usually an ogg or wav file type. +-- @param #number duration Duration in seconds the file lasts. +-- @param #number path Directory path inside the miz file where the sound file is located. Default "l10n/DEFAULT/". +-- @param #number tstart Start time (abs) seconds. Default now. +-- @param #number interval Interval in seconds after the last transmission finished. +-- @param #string subtitle Subtitle of the transmission. +-- @param #number subduration Duration [sec] of the subtitle being displayed. Default 5 sec. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:NewTransmission(filename, duration, path, tstart, interval, subtitle, subduration) + + -- Sanity checks. + if not filename then + self:E(self.lid.."ERROR: No filename specified.") + return nil + end + if type(filename)~="string" then + self:E(self.lid.."ERROR: Filename specified is NOT a string.") + return nil + end + + if not duration then + self:E(self.lid.."ERROR: No duration of transmission specified.") + return nil + end + if type(duration)~="number" then + self:E(self.lid.."ERROR: Duration specified is NOT a number.") + return nil + end + + + local transmission={} --#RADIOQUEUE.Transmission + transmission.filename=filename + transmission.duration=duration + transmission.path=path or "l10n/DEFAULT/" + transmission.Tplay=tstart or timer.getAbsTime() + transmission.subtitle=subtitle + transmission.interval=interval or 0 + if transmission.subtitle then + transmission.subduration=subduration or 5 + else + transmission.subduration=nil + end + + -- Add transmission to queue. + self:AddTransmission(transmission) + + return self +end + +--- Convert a number (as string) into a radio transmission. +-- E.g. for board number or headings. +-- @param #RADIOQUEUE self +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +-- @param #number interval Interval between the next call. +-- @return #number Duration of the call in seconds. +function RADIOQUEUE:Number2Transmission(number, delay, interval) + + --- Split string into characters. + local function _split(str) + local chars={} + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + return chars + end + + -- Split string into characters. + local numbers=_split(number) + + local wait=0 + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + -- Radio call. + local transmission=UTILS.DeepCopy(self.numbers[n]) --#RADIOQUEUE.Transmission + + transmission.Tplay=timer.getAbsTime()+(delay or 0) + + if interval and i==1 then + transmission.interval=interval + end + + self:AddTransmission(transmission) + + -- Add up duration of the number. + wait=wait+transmission.duration + end + + -- Return the total duration of the call. + return wait +end + + +--- Broadcast radio message. +-- @param #RADIOQUEUE self +-- @param #RADIOQUEUE.Transmission transmission The transmission. +function RADIOQUEUE:Broadcast(transmission) + + -- Get unit sending the transmission. + local sender=self:_GetRadioSender() + + -- Construct file name. + local filename=string.format("%s%s", transmission.path, transmission.filename) + + if sender then + + -- Broadcasting from aircraft. Only players tuned in to the right frequency will see the message. + self:T(self.lid..string.format("Broadcasting from aircraft %s", sender:GetName())) + + + if not self.senderinit then + + -- Command to set the Frequency for the transmission. + local commandFrequency={ + id="SetFrequency", + params={ + frequency=self.frequency, -- Frequency in Hz. + modulation=self.modulation, + }} + + -- Set commend for frequency + sender:SetCommand(commandFrequency) + + self.senderinit=true + end + + -- Set subtitle only if duration>0 sec. + local subtitle=nil + local duration=nil + if transmission.subtitle and transmission.subduration and transmission.subduration>0 then + subtitle=transmission.subtitle + duration=transmission.subduration + end + + -- Command to tranmit the call. + local commandTransmit={ + id = "TransmitMessage", + params = { + file=filename, + duration=duration, + subtitle=subtitle, + loop=false, + }} + + -- Set command for radio transmission. + sender:SetCommand(commandTransmit) + + -- Debug message. + local text=string.format("file=%s, freq=%.2f MHz, duration=%.2f sec, subtitle=%s", filename, self.frequency/1000000, transmission.duration, transmission.subtitle or "") + MESSAGE:New(text, 2, "RADIOQUEUE "..self.alias):ToAllIf(self.Debug) + + else + + -- Broadcasting from carrier. No subtitle possible. Need to send messages to players. + self:T(self.lid..string.format("Broadcasting via trigger.action.radioTransmission().")) + + -- Position from where to transmit. + local vec3=nil + + -- Try to get positon from sender unit/static. + if self.sendername then + local coord=self:_GetRadioSenderCoord() + if coord then + vec3=coord:GetVec3() + end + end + + -- Try to get fixed positon. + if self.sendercoord and not vec3 then + vec3=self.sendercoord:GetVec3() + end + + -- Transmit via trigger. + if vec3 then + self:T("Sending") + self:T( { filename = filename, vec3 = vec3, modulation = self.modulation, frequency = self.frequency, power = self.power } ) + + -- Trigger transmission. + trigger.action.radioTransmission(filename, vec3, self.modulation, false, self.frequency, self.power) + + -- Debug message. + local text=string.format("file=%s, freq=%.2f MHz, duration=%.2f sec, subtitle=%s", filename, self.frequency/1000000, transmission.duration, transmission.subtitle or "") + MESSAGE:New(string.format(text, filename, transmission.duration, transmission.subtitle or ""), 5, "RADIOQUEUE "..self.alias):ToAllIf(self.Debug) + end + + end +end + +--- Start checking the radio queue. +-- @param #RADIOQUEUE self +-- @param #number delay Delay in seconds before checking. +function RADIOQUEUE:_CheckRadioQueueDelayed(delay) + self.checking=true + self:ScheduleOnce(delay or self.dt, RADIOQUEUE._CheckRadioQueue, self) +end + +--- Check radio queue for transmissions to be broadcasted. +-- @param #RADIOQUEUE self +function RADIOQUEUE:_CheckRadioQueue() + --env.info("FF check radio queue "..self.alias) + + -- Check if queue is empty. + if #self.queue==0 then + -- Queue is now empty. Nothing to else to do. + self.checking=false + return + end + + -- Get current abs time. + local time=timer.getAbsTime() + + local playing=false + local next=nil --#RADIOQUEUE.Transmission + local remove=nil + for i,_transmission in ipairs(self.queue) do + local transmission=_transmission --#RADIOQUEUE.Transmission + + -- Check if transmission time has passed. + if time>=transmission.Tplay then + + -- Check if transmission is currently playing. + if transmission.isplaying then + + -- Check if transmission is finished. + if time>=transmission.Tstarted+transmission.duration then + + -- Transmission over. + transmission.isplaying=false + + -- Remove ith element in queue. + remove=i + + -- Store time last transmission finished. + self.Tlast=time + + else -- still playing + + -- Transmission is still playing. + playing=true + + end + + else -- not playing yet + + local Tlast=self.Tlast + + if transmission.interval==nil then + + -- Not playing ==> this will be next. + if next==nil then + next=transmission + end + + else + + if Tlast==nil or time-Tlast>=transmission.interval then + next=transmission + else + + end + end + + -- We got a transmission or one with an interval that is not due yet. No need for anything else. + if next or Tlast then + break + end + + end + + else + + -- Transmission not due yet. + + end + end + + -- Found a new transmission. + if next~=nil and not playing then + self:Broadcast(next) + next.isplaying=true + next.Tstarted=time + end + + -- Remove completed calls from queue. + if remove then + table.remove(self.queue, remove) + end + + -- Check queue. + if self.schedonce then + self:_CheckRadioQueueDelayed() + end + +end + +--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. +-- @param #RADIOQUEUE self +-- @return Wrapper.Unit#UNIT Sending aircraft unit or nil if was not setup, is not an aircraft or is not alive. +function RADIOQUEUE:_GetRadioSender() + + -- Check if we have a sending aircraft. + local sender=nil --Wrapper.Unit#UNIT + + -- Try the general default. + if self.sendername then + -- First try to find a unit + sender=UNIT:FindByName(self.sendername) + + -- Check that sender is alive and an aircraft. + if sender and sender:IsAlive() and sender:IsAir() then + return sender + end + + end + + return nil +end + +--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. +-- @param #RADIOQUEUE self +-- @return Core.Point#COORDINATE Coordinate of the sender unit. +function RADIOQUEUE:_GetRadioSenderCoord() + + local vec3=nil + + -- Try the general default. + if self.sendername then + + -- First try to find a unit + local sender=UNIT:FindByName(self.sendername) + + -- Check that sender is alive and an aircraft. + if sender and sender:IsAlive() then + return sender:GetCoordinate() + end + + -- Now try a static. + local sender=STATIC:FindByName( self.sendername, false ) + + -- Check that sender is alive and an aircraft. + if sender then + return sender:GetCoordinate() + end + + end + + return nil +end diff --git a/Moose Development/Moose/Core/RadioSpeech.lua b/Moose Development/Moose/Core/RadioSpeech.lua new file mode 100644 index 000000000..d4cf22af1 --- /dev/null +++ b/Moose Development/Moose/Core/RadioSpeech.lua @@ -0,0 +1,405 @@ +--- **Core** - Makes the radio talk. +-- +-- === +-- +-- ## Features: +-- +-- * Send text strings using a vocabulary that is converted in spoken language. +-- * Possiblity to implement multiple language. +-- +-- === +-- +-- ### Authors: FlightControl +-- +-- @module Core.RadioSpeech +-- @image Core_Radio.JPG + +--- Makes the radio speak. +-- +-- # RADIOSPEECH usage +-- +-- +-- @type RADIOSPEECH +-- @extends Core.RadioQueue#RADIOQUEUE +RADIOSPEECH = { + ClassName = "RADIOSPEECH", + Vocabulary = { + EN = {}, + DE = {}, + RU = {}, + } +} + + +RADIOSPEECH.Vocabulary.EN = { + ["1"] = { "1", 0.25 }, + ["2"] = { "2", 0.25 }, + ["3"] = { "3", 0.30 }, + ["4"] = { "4", 0.35 }, + ["5"] = { "5", 0.35 }, + ["6"] = { "6", 0.42 }, + ["7"] = { "7", 0.38 }, + ["8"] = { "8", 0.20 }, + ["9"] = { "9", 0.32 }, + ["10"] = { "10", 0.35 }, + ["11"] = { "11", 0.40 }, + ["12"] = { "12", 0.42 }, + ["13"] = { "13", 0.38 }, + ["14"] = { "14", 0.42 }, + ["15"] = { "15", 0.42 }, + ["16"] = { "16", 0.52 }, + ["17"] = { "17", 0.59 }, + ["18"] = { "18", 0.40 }, + ["19"] = { "19", 0.47 }, + ["20"] = { "20", 0.38 }, + ["30"] = { "30", 0.29 }, + ["40"] = { "40", 0.35 }, + ["50"] = { "50", 0.32 }, + ["60"] = { "60", 0.44 }, + ["70"] = { "70", 0.48 }, + ["80"] = { "80", 0.26 }, + ["90"] = { "90", 0.36 }, + ["100"] = { "100", 0.55 }, + ["200"] = { "200", 0.55 }, + ["300"] = { "300", 0.61 }, + ["400"] = { "400", 0.60 }, + ["500"] = { "500", 0.61 }, + ["600"] = { "600", 0.65 }, + ["700"] = { "700", 0.70 }, + ["800"] = { "800", 0.54 }, + ["900"] = { "900", 0.60 }, + ["1000"] = { "1000", 0.60 }, + ["2000"] = { "2000", 0.61 }, + ["3000"] = { "3000", 0.64 }, + ["4000"] = { "4000", 0.62 }, + ["5000"] = { "5000", 0.69 }, + ["6000"] = { "6000", 0.69 }, + ["7000"] = { "7000", 0.75 }, + ["8000"] = { "8000", 0.59 }, + ["9000"] = { "9000", 0.65 }, + + ["chevy"] = { "chevy", 0.35 }, + ["colt"] = { "colt", 0.35 }, + ["springfield"] = { "springfield", 0.65 }, + ["dodge"] = { "dodge", 0.35 }, + ["enfield"] = { "enfield", 0.5 }, + ["ford"] = { "ford", 0.32 }, + ["pontiac"] = { "pontiac", 0.55 }, + ["uzi"] = { "uzi", 0.28 }, + + ["degrees"] = { "degrees", 0.5 }, + ["kilometers"] = { "kilometers", 0.65 }, + ["km"] = { "kilometers", 0.65 }, + ["miles"] = { "miles", 0.45 }, + ["meters"] = { "meters", 0.41 }, + ["mi"] = { "miles", 0.45 }, + ["feet"] = { "feet", 0.29 }, + + ["br"] = { "br", 1.1 }, + ["bra"] = { "bra", 0.3 }, + + + ["returning to base"] = { "returning_to_base", 0.85 }, + ["on route to ground target"] = { "on_route_to_ground_target", 1.05 }, + ["intercepting bogeys"] = { "intercepting_bogeys", 1.00 }, + ["engaging ground target"] = { "engaging_ground_target", 1.20 }, + ["engaging bogeys"] = { "engaging_bogeys", 0.81 }, + ["wheels up"] = { "wheels_up", 0.42 }, + ["landing at base"] = { "landing at base", 0.8 }, + ["patrolling"] = { "patrolling", 0.55 }, + + ["for"] = { "for", 0.31 }, + ["and"] = { "and", 0.31 }, + ["at"] = { "at", 0.3 }, + ["dot"] = { "dot", 0.26 }, + ["defender"] = { "defender", 0.45 }, +} + +RADIOSPEECH.Vocabulary.RU = { + ["1"] = { "1", 0.34 }, + ["2"] = { "2", 0.30 }, + ["3"] = { "3", 0.23 }, + ["4"] = { "4", 0.51 }, + ["5"] = { "5", 0.31 }, + ["6"] = { "6", 0.44 }, + ["7"] = { "7", 0.25 }, + ["8"] = { "8", 0.43 }, + ["9"] = { "9", 0.45 }, + ["10"] = { "10", 0.53 }, + ["11"] = { "11", 0.66 }, + ["12"] = { "12", 0.70 }, + ["13"] = { "13", 0.66 }, + ["14"] = { "14", 0.80 }, + ["15"] = { "15", 0.65 }, + ["16"] = { "16", 0.75 }, + ["17"] = { "17", 0.74 }, + ["18"] = { "18", 0.85 }, + ["19"] = { "19", 0.80 }, + ["20"] = { "20", 0.58 }, + ["30"] = { "30", 0.51 }, + ["40"] = { "40", 0.51 }, + ["50"] = { "50", 0.67 }, + ["60"] = { "60", 0.76 }, + ["70"] = { "70", 0.68 }, + ["80"] = { "80", 0.84 }, + ["90"] = { "90", 0.71 }, + ["100"] = { "100", 0.35 }, + ["200"] = { "200", 0.59 }, + ["300"] = { "300", 0.53 }, + ["400"] = { "400", 0.70 }, + ["500"] = { "500", 0.50 }, + ["600"] = { "600", 0.58 }, + ["700"] = { "700", 0.64 }, + ["800"] = { "800", 0.77 }, + ["900"] = { "900", 0.75 }, + ["1000"] = { "1000", 0.87 }, + ["2000"] = { "2000", 0.83 }, + ["3000"] = { "3000", 0.84 }, + ["4000"] = { "4000", 1.00 }, + ["5000"] = { "5000", 0.77 }, + ["6000"] = { "6000", 0.90 }, + ["7000"] = { "7000", 0.77 }, + ["8000"] = { "8000", 0.92 }, + ["9000"] = { "9000", 0.87 }, + + ["Ñтепени"] = { "degrees", 0.5 }, + ["километров"] = { "kilometers", 0.65 }, + ["km"] = { "kilometers", 0.65 }, + ["миль"] = { "miles", 0.45 }, + ["mi"] = { "miles", 0.45 }, + ["метры"] = { "meters", 0.41 }, + ["m"] = { "meters", 0.41 }, + ["ноги"] = { "feet", 0.37 }, + + ["br"] = { "br", 1.1 }, + ["bra"] = { "bra", 0.3 }, + + + ["возвращаÑÑÑŒ на базу"] = { "returning_to_base", 1.40 }, + ["на пути к наземной цели"] = { "on_route_to_ground_target", 1.45 }, + ["перехват Ñамолетов"] = { "intercepting_bogeys", 1.22 }, + ["поражение наземной цели"] = { "engaging_ground_target", 1.53 }, + ["захватывающие Ñамолеты"] = { "engaging_bogeys", 1.68 }, + ["колеÑа вверх"] = { "wheels_up", 0.92 }, + ["поÑадка на базу"] = { "landing at base", 1.04 }, + ["патрулирующий"] = { "patrolling", 0.96 }, + + ["за"] = { "for", 0.27 }, + ["и"] = { "and", 0.17 }, + ["в"] = { "at", 0.19 }, + ["dot"] = { "dot", 0.51 }, + ["defender"] = { "defender", 0.45 }, +} + +--- Create a new RADIOSPEECH object for a given radio frequency/modulation. +-- @param #RADIOSPEECH self +-- @param #number frequency The radio frequency in MHz. +-- @param #number modulation (Optional) The radio modulation. Default radio.modulation.AM. +-- @return #RADIOSPEECH self The RADIOSPEECH object. +function RADIOSPEECH:New(frequency, modulation) + + -- Inherit base + local self = BASE:Inherit( self, RADIOQUEUE:New( frequency, modulation ) ) -- #RADIOSPEECH + + self.Language = "EN" + + self:BuildTree() + + return self +end + +function RADIOSPEECH:SetLanguage( Langauge ) + + self.Language = Langauge +end + + +--- Add Sentence to the Speech collection. +-- @param #RADIOSPEECH self +-- @param #string RemainingSentence The remaining sentence during recursion. +-- @param #table Speech The speech node. +-- @param #string Sentence The full sentence. +-- @param #string Data The speech data. +-- @return #RADIOSPEECH self The RADIOSPEECH object. +function RADIOSPEECH:AddSentenceToSpeech( RemainingSentence, Speech, Sentence, Data ) + + self:I( { RemainingSentence, Speech, Sentence, Data } ) + + local Token, RemainingSentence = RemainingSentence:match( "^ *([^ ]+)(.*)" ) + self:I( { Token = Token, RemainingSentence = RemainingSentence } ) + + -- Is there a Token? + if Token then + + -- We check if the Token is already in the Speech collection. + if not Speech[Token] then + + -- There is not yet a vocabulary registered for this. + Speech[Token] = {} + + if RemainingSentence and RemainingSentence ~= "" then + -- We use recursion to iterate through the complete Sentence, and make a chain of Tokens. + -- The last Speech node in the collection contains the Sentence and the Data to be spoken. + -- This to ensure that during the actual speech: + -- - Complete sentences are being understood. + -- - Words without speech are ignored. + -- - Incorrect sequence of words are ignored. + Speech[Token].Next = {} + self:AddSentenceToSpeech( RemainingSentence, Speech[Token].Next, Sentence, Data ) + else + -- There is no remaining sentence, so we add speech to the Sentence. + -- The recursion stops here. + Speech[Token].Sentence = Sentence + Speech[Token].Data = Data + end + end + end +end + +--- Build the tree structure based on the language words, in order to find the correct sentences and to ignore incomprehensible words. +-- @param #RADIOSPEECH self +-- @return #RADIOSPEECH self The RADIOSPEECH object. +function RADIOSPEECH:BuildTree() + + self.Speech = {} + + for Language, Sentences in pairs( self.Vocabulary ) do + self:I( { Language = Language, Sentences = Sentences }) + self.Speech[Language] = {} + for Sentence, Data in pairs( Sentences ) do + self:I( { Sentence = Sentence, Data = Data } ) + self:AddSentenceToSpeech( Sentence, self.Speech[Language], Sentence, Data ) + end + end + + self:I( { Speech = self.Speech } ) + + return self +end + +--- Speak a sentence. +-- @param #RADIOSPEECH self +-- @param #string Sentence The sentence to be spoken. +function RADIOSPEECH:SpeakWords( Sentence, Speech, Language ) + + local OriginalSentence = Sentence + + -- lua does not parse UTF-8, so the match statement will fail on cyrillic using %a. + -- therefore, the only way to parse the statement is to use blank, comma or dot as a delimiter. + -- and then check if the character can be converted to a number or not. + local Word, RemainderSentence = Sentence:match( "^[., ]*([^ .,]+)(.*)" ) + + self:I( { Word = Word, Speech = Speech[Word], RemainderSentence = RemainderSentence } ) + + + if Word then + if Word ~= "" and tonumber(Word) == nil then + + -- Construct of words + Word = Word:lower() + if Speech[Word] then + -- The end of the sentence has been reached. Now Speech.Next should be nil, otherwise there is an error. + if Speech[Word].Next == nil then + self:I( { Sentence = Speech[Word].Sentence, Data = Speech[Word].Data } ) + self:NewTransmission( Speech[Word].Data[1] .. ".wav", Speech[Word].Data[2], Language .. "/" ) + else + if RemainderSentence and RemainderSentence ~= "" then + return self:SpeakWords( RemainderSentence, Speech[Word].Next, Language ) + end + end + end + return RemainderSentence + end + return OriginalSentence + else + return "" + end + +end + +--- Speak a sentence. +-- @param #RADIOSPEECH self +-- @param #string Sentence The sentence to be spoken. +function RADIOSPEECH:SpeakDigits( Sentence, Speech, Langauge ) + + local OriginalSentence = Sentence + + -- lua does not parse UTF-8, so the match statement will fail on cyrillic using %a. + -- therefore, the only way to parse the statement is to use blank, comma or dot as a delimiter. + -- and then check if the character can be converted to a number or not. + local Digits, RemainderSentence = Sentence:match( "^[., ]*([^ .,]+)(.*)" ) + + self:I( { Digits = Digits, Speech = Speech[Digits], RemainderSentence = RemainderSentence } ) + + if Digits then + if Digits ~= "" and tonumber( Digits ) ~= nil then + + -- Construct numbers + local Number = tonumber( Digits ) + local Multiple = nil + while Number >= 0 do + if Number > 1000 then + Multiple = math.floor( Number / 1000 ) * 1000 + elseif Number > 100 then + Multiple = math.floor( Number / 100 ) * 100 + elseif Number > 20 then + Multiple = math.floor( Number / 10 ) * 10 + elseif Number >= 0 then + Multiple = Number + end + Sentence = tostring( Multiple ) + if Speech[Sentence] then + self:I( { Speech = Speech[Sentence].Sentence, Data = Speech[Sentence].Data } ) + self:NewTransmission( Speech[Sentence].Data[1] .. ".wav", Speech[Sentence].Data[2], Langauge .. "/" ) + end + Number = Number - Multiple + Number = ( Number == 0 ) and -1 or Number + end + return RemainderSentence + end + return OriginalSentence + else + return "" + end + +end + + + +--- Speak a sentence. +-- @param #RADIOSPEECH self +-- @param #string Sentence The sentence to be spoken. +function RADIOSPEECH:Speak( Sentence, Language ) + + self:I( { Sentence, Language } ) + + local Language = Language or "EN" + + self:I( { Language = Language } ) + + -- If there is no node for Speech, then we start at the first nodes of the language. + local Speech = self.Speech[Language] + + self:I( { Speech = Speech, Language = Language } ) + + self:NewTransmission( "_In.wav", 0.52, Language .. "/" ) + + repeat + + Sentence = self:SpeakWords( Sentence, Speech, Language ) + + self:I( { Sentence = Sentence } ) + + Sentence = self:SpeakDigits( Sentence, Speech, Language ) + + self:I( { Sentence = Sentence } ) + +-- Sentence = self:SpeakSymbols( Sentence, Speech ) +-- +-- self:I( { Sentence = Sentence } ) + + until not Sentence or Sentence == "" + + self:NewTransmission( "_Out.wav", 0.28, Language .. "/" ) + +end diff --git a/Moose Development/Moose/Core/Report.lua b/Moose Development/Moose/Core/Report.lua index 35800c60e..bd860996b 100644 --- a/Moose Development/Moose/Core/Report.lua +++ b/Moose Development/Moose/Core/Report.lua @@ -70,11 +70,12 @@ function REPORT:Add( Text ) return self end ---- Add a new line to a REPORT. +--- Add a new line to a REPORT, but indented. A separator character can be specified to separate the reported lines visually. -- @param #REPORT self --- @param #string Text +-- @param #string Text The report text. +-- @param #string Separator (optional) The start of each report line can begin with an optional separator character. This can be a "-", or "#", or "*". You're free to choose what you find the best. -- @return #REPORT -function REPORT:AddIndent( Text, Separator ) --R2.1 +function REPORT:AddIndent( Text, Separator ) self.Report[#self.Report+1] = ( ( Separator and Separator .. string.rep( " ", self.Indent - 1 ) ) or string.rep(" ", self.Indent ) ) .. Text:gsub("\n","\n"..string.rep( " ", self.Indent ) ) return self end diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index 8173c85c8..bfd1dabb4 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -4,7 +4,7 @@ -- -- Takes care of the creation and dispatching of scheduled functions for SCHEDULER objects. -- --- This class is tricky and needs some thorought explanation. +-- This class is tricky and needs some thorough explanation. -- SCHEDULE classes are used to schedule functions for objects, or as persistent objects. -- The SCHEDULEDISPATCHER class ensures that: -- @@ -13,9 +13,10 @@ -- - Scheduled functions are automatically removed when the schedule is finished, according the SCHEDULER object parameters. -- -- The SCHEDULEDISPATCHER class will manage SCHEDULER object in memory during garbage collection: --- - When a SCHEDULER object is not attached to another object (that is, it's first :Schedule() parameter is nil), then the SCHEDULER --- object is _persistent_ within memory. +-- +-- - When a SCHEDULER object is not attached to another object (that is, it's first :Schedule() parameter is nil), then the SCHEDULER object is _persistent_ within memory. -- - When a SCHEDULER object *is* attached to another object, then the SCHEDULER object is _not persistent_ within memory after a garbage collection! +-- -- The none persistency of SCHEDULERS attached to objects is required to allow SCHEDULER objects to be garbage collectged, when the parent object is also desroyed or nillified and garbage collected. -- Even when there are pending timer scheduled functions to be executed for the SCHEDULER object, -- these will not be executed anymore when the SCHEDULER object has been destroyed. @@ -33,13 +34,41 @@ -- @module Core.ScheduleDispatcher -- @image Core_Schedule_Dispatcher.JPG +--- SCHEDULEDISPATCHER class. +-- @type SCHEDULEDISPATCHER +-- @field #string ClassName Name of the class. +-- @field #number CallID Call ID counter. +-- @field #table PersistentSchedulers Persistant schedulers. +-- @field #table ObjectSchedulers Schedulers that only exist as long as the master object exists. +-- @field #table Schedule Meta table setmetatable( {}, { __mode = "k" } ). +-- @extends Core.Base#BASE + --- The SCHEDULEDISPATCHER structure -- @type SCHEDULEDISPATCHER SCHEDULEDISPATCHER = { - ClassName = "SCHEDULEDISPATCHER", - CallID = 0, + ClassName = "SCHEDULEDISPATCHER", + CallID = 0, + PersistentSchedulers = {}, + ObjectSchedulers = {}, + Schedule = nil, } +--- Player data table holding all important parameters of each player. +-- @type SCHEDULEDISPATCHER.ScheduleData +-- @field #function Function The schedule function to be called. +-- @field #table Arguments Schedule function arguments. +-- @field #number Start Start time in seconds. +-- @field #number Repeat Repeat time intervall in seconds. +-- @field #number Randomize Randomization factor [0,1]. +-- @field #number Stop Stop time in seconds. +-- @field #number StartTime Time in seconds when the scheduler is created. +-- @field #number ScheduleID Schedule ID. +-- @field #function CallHandler Function to be passed to the DCS timer.scheduleFunction(). +-- @field #boolean ShowTrace If true, show tracing info. + +--- Create a new schedule dispatcher object. +-- @param #SCHEDULEDISPATCHER self +-- @return #SCHEDULEDISPATCHER self function SCHEDULEDISPATCHER:New() local self = BASE:Inherit( self, BASE:New() ) self:F3() @@ -51,15 +80,28 @@ end -- It is constructed as such that a garbage collection is executed on the weak tables, when the Scheduler is nillified. -- Nothing of this code should be modified without testing it thoroughly. -- @param #SCHEDULEDISPATCHER self --- @param Core.Scheduler#SCHEDULER Scheduler -function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop ) - self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop } ) +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +-- @param #function ScheduleFunction Scheduler function. +-- @param #table ScheduleArguments Table of arguments passed to the ScheduleFunction. +-- @param #number Start Start time in seconds. +-- @param #number Repeat Repeat interval in seconds. +-- @param #number Randomize Radomization factor [0,1]. +-- @param #number Stop Stop time in seconds. +-- @param #number TraceLevel Trace level [0,3]. +-- @param Core.Fsm#FSM Fsm Finite state model. +-- @return #string Call ID or nil. +function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel, Fsm ) + self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel, Fsm } ) + -- Increase counter. self.CallID = self.CallID + 1 + + -- Create ID. local CallID = self.CallID .. "#" .. ( Scheduler.MasterObject and Scheduler.MasterObject.GetClassNameAndID and Scheduler.MasterObject:GetClassNameAndID() or "" ) or "" + + self:T2(string.format("Adding schedule #%d CallID=%s", self.CallID, CallID)) - -- Initialize the ObjectSchedulers array, which is a weakly coupled table. - -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array. + -- Initialize PersistentSchedulers self.PersistentSchedulers = self.PersistentSchedulers or {} -- Initialize the ObjectSchedulers array, which is a weakly coupled table. @@ -76,19 +118,60 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr self.Schedule = self.Schedule or setmetatable( {}, { __mode = "k" } ) self.Schedule[Scheduler] = self.Schedule[Scheduler] or {} - self.Schedule[Scheduler][CallID] = {} + self.Schedule[Scheduler][CallID] = {} --#SCHEDULEDISPATCHER.ScheduleData self.Schedule[Scheduler][CallID].Function = ScheduleFunction self.Schedule[Scheduler][CallID].Arguments = ScheduleArguments self.Schedule[Scheduler][CallID].StartTime = timer.getTime() + ( Start or 0 ) - self.Schedule[Scheduler][CallID].Start = Start + .1 + self.Schedule[Scheduler][CallID].Start = Start + 0.1 self.Schedule[Scheduler][CallID].Repeat = Repeat or 0 self.Schedule[Scheduler][CallID].Randomize = Randomize or 0 self.Schedule[Scheduler][CallID].Stop = Stop + + + -- This section handles the tracing of the scheduled calls. + -- Because these calls will be executed with a delay, we inspect the place where these scheduled calls are initiated. + -- The Info structure contains the output of the debug.getinfo() calls, which inspects the call stack for the function name, line number and source name. + -- The call stack has many levels, and the correct semantical function call depends on where in the code AddSchedule was "used". + -- - Using SCHEDULER:New() + -- - Using Schedule:AddSchedule() + -- - Using Fsm:__Func() + -- - Using Class:ScheduleOnce() + -- - Using Class:ScheduleRepeat() + -- - ... + -- So for each of these scheduled call variations, AddSchedule is the workhorse which will schedule the call. + -- But the correct level with the correct semantical function location will differ depending on the above scheduled call invocation forms. + -- That's where the field TraceLevel contains optionally the level in the call stack where the call information is obtained. + -- The TraceLevel field indicates the correct level where the semantical scheduled call was invoked within the source, ensuring that function name, line number and source name are correct. + -- There is one quick ... + -- The FSM class models scheduled calls using the __Func syntax. However, these functions are "tailed". + -- There aren't defined anywhere within the source code, but rather implemented as triggers within the FSM logic, + -- and using the onbefore, onafter, onenter, onleave prefixes. (See the FSM for details). + -- Therefore, in the call stack, at the TraceLevel these functions are mentioned as "tail calls", and the Info.name field will be nil as a result. + -- To obtain the correct function name for FSM object calls, the function is mentioned in the call stack at a higher stack level. + -- So when function name stored in Info.name is nil, then I inspect the function name within the call stack one level higher. + -- So this little piece of code does its magic wonderfully, preformance overhead is neglectible, as scheduled calls don't happen that often. + + local Info = {} + + if debug then + TraceLevel = TraceLevel or 2 + Info = debug.getinfo( TraceLevel, "nlS" ) + local name_fsm = debug.getinfo( TraceLevel - 1, "n" ).name -- #string + if name_fsm then + Info.name = name_fsm + end + end self:T3( self.Schedule[Scheduler][CallID] ) - self.Schedule[Scheduler][CallID].CallHandler = function( CallID ) - --self:E( CallID ) + --- Function passed to the DCS timer.scheduleFunction() + self.Schedule[Scheduler][CallID].CallHandler = function( Params ) + + local CallID = Params.CallID + local Info = Params.Info or {} + local Source = Info.source or "?" + local Line = Info.currentline or "?" + local Name = Info.name or "?" local ErrorHandler = function( errmsg ) env.info( "Error in timer function: " .. errmsg ) @@ -98,7 +181,8 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr return errmsg end - local Scheduler = self.ObjectSchedulers[CallID] + -- Get object or persistant scheduler object. + local Scheduler = self.ObjectSchedulers[CallID] --Core.Scheduler#SCHEDULER if not Scheduler then Scheduler = self.PersistentSchedulers[CallID] end @@ -107,30 +191,42 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr if Scheduler then - local MasterObject = tostring(Scheduler.MasterObject) - local Schedule = self.Schedule[Scheduler][CallID] + local MasterObject = tostring(Scheduler.MasterObject) + + -- Schedule object. + local Schedule = self.Schedule[Scheduler][CallID] --#SCHEDULEDISPATCHER.ScheduleData --self:T3( { Schedule = Schedule } ) - local SchedulerObject = Scheduler.SchedulerObject - --local ScheduleObjectName = Scheduler.SchedulerObject:GetNameAndClassID() - local ScheduleFunction = Schedule.Function - local ScheduleArguments = Schedule.Arguments - local Start = Schedule.Start - local Repeat = Schedule.Repeat or 0 - local Randomize = Schedule.Randomize or 0 - local Stop = Schedule.Stop or 0 - local ScheduleID = Schedule.ScheduleID + local SchedulerObject = Scheduler.MasterObject --Scheduler.SchedulerObject Now is this the Maste or Scheduler object? + local ShowTrace = Scheduler.ShowTrace + + local ScheduleFunction = Schedule.Function + local ScheduleArguments = Schedule.Arguments or {} + local Start = Schedule.Start + local Repeat = Schedule.Repeat or 0 + local Randomize = Schedule.Randomize or 0 + local Stop = Schedule.Stop or 0 + local ScheduleID = Schedule.ScheduleID + + + local Prefix = ( Repeat == 0 ) and "--->" or "+++>" local Status, Result --self:E( { SchedulerObject = SchedulerObject } ) if SchedulerObject then local function Timer() + if ShowTrace then + SchedulerObject:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" ) + end return ScheduleFunction( SchedulerObject, unpack( ScheduleArguments ) ) end Status, Result = xpcall( Timer, ErrorHandler ) else local function Timer() + if ShowTrace then + self:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" ) + end return ScheduleFunction( unpack( ScheduleArguments ) ) end Status, Result = xpcall( Timer, ErrorHandler ) @@ -139,39 +235,39 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr local CurrentTime = timer.getTime() local StartTime = Schedule.StartTime - self:F3( { Master = MasterObject, CurrentTime = CurrentTime, StartTime = StartTime, Start = Start, Repeat = Repeat, Randomize = Randomize, Stop = Stop } ) + -- Debug info. + self:F3( { CallID=CallID, ScheduleID=ScheduleID, Master = MasterObject, CurrentTime = CurrentTime, StartTime = StartTime, Start = Start, Repeat = Repeat, Randomize = Randomize, Stop = Stop } ) if Status and (( Result == nil ) or ( Result and Result ~= false ) ) then + if Repeat ~= 0 and ( ( Stop == 0 ) or ( Stop ~= 0 and CurrentTime <= StartTime + Stop ) ) then - local ScheduleTime = - CurrentTime + - Repeat + - math.random( - - ( Randomize * Repeat / 2 ), - ( Randomize * Repeat / 2 ) - ) + - 0.01 + local ScheduleTime = CurrentTime + Repeat + math.random(- ( Randomize * Repeat / 2 ), ( Randomize * Repeat / 2 )) + 0.0001 -- Accuracy --self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } ) return ScheduleTime -- returns the next time the function needs to be called. else self:Stop( Scheduler, CallID ) end + else self:Stop( Scheduler, CallID ) end else - self:E( "Scheduled obsolete call for CallID: " .. CallID ) + self:I( "<<<>" .. Name .. ":" .. Line .. " (" .. Source .. ")" ) end return nil end - self:Start( Scheduler, CallID ) + self:Start( Scheduler, CallID, Info ) return CallID end +--- Remove schedule. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +-- @param #table CallID Call ID. function SCHEDULEDISPATCHER:RemoveSchedule( Scheduler, CallID ) self:F2( { Remove = CallID, Scheduler = Scheduler } ) @@ -181,46 +277,80 @@ function SCHEDULEDISPATCHER:RemoveSchedule( Scheduler, CallID ) end end -function SCHEDULEDISPATCHER:Start( Scheduler, CallID ) +--- Start dispatcher. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +-- @param #table CallID (Optional) Call ID. +-- @param #string Info (Optional) Debug info. +function SCHEDULEDISPATCHER:Start( Scheduler, CallID, Info ) self:F2( { Start = CallID, Scheduler = Scheduler } ) - + if CallID then - local Schedule = self.Schedule[Scheduler] + + local Schedule = self.Schedule[Scheduler][CallID] --#SCHEDULEDISPATCHER.ScheduleData + -- Only start when there is no ScheduleID defined! -- This prevents to "Start" the scheduler twice with the same CallID... - if not Schedule[CallID].ScheduleID then - Schedule[CallID].StartTime = timer.getTime() -- Set the StartTime field to indicate when the scheduler started. - Schedule[CallID].ScheduleID = timer.scheduleFunction( - Schedule[CallID].CallHandler, - CallID, - timer.getTime() + Schedule[CallID].Start - ) + if not Schedule.ScheduleID then + + -- Current time in seconds. + local Tnow=timer.getTime() + + Schedule.StartTime = Tnow -- Set the StartTime field to indicate when the scheduler started. + + -- Start DCS schedule function https://wiki.hoggitworld.com/view/DCS_func_scheduleFunction + Schedule.ScheduleID = timer.scheduleFunction(Schedule.CallHandler, { CallID = CallID, Info = Info }, Tnow + Schedule.Start) + + self:T(string.format("Starting scheduledispatcher Call ID=%s ==> Schedule ID=%s", tostring(CallID), tostring(Schedule.ScheduleID))) end + else + + -- Recursive. for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do - self:Start( Scheduler, CallID ) -- Recursive + self:Start( Scheduler, CallID, Info ) -- Recursive end + end end +--- Stop dispatcher. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +-- @param #table CallID Call ID. function SCHEDULEDISPATCHER:Stop( Scheduler, CallID ) self:F2( { Stop = CallID, Scheduler = Scheduler } ) if CallID then - local Schedule = self.Schedule[Scheduler] - -- Only stop when there is a ScheduleID defined for the CallID. - -- So, when the scheduler was stopped before, do nothing. - if Schedule[CallID].ScheduleID then - timer.removeFunction( Schedule[CallID].ScheduleID ) - Schedule[CallID].ScheduleID = nil + + local Schedule = self.Schedule[Scheduler][CallID] --#SCHEDULEDISPATCHER.ScheduleData + + -- Only stop when there is a ScheduleID defined for the CallID. So, when the scheduler was stopped before, do nothing. + if Schedule.ScheduleID then + + self:T(string.format("scheduledispatcher stopping scheduler CallID=%s, ScheduleID=%s", tostring(CallID), tostring(Schedule.ScheduleID))) + + -- Remove schedule function https://wiki.hoggitworld.com/view/DCS_func_removeFunction + timer.removeFunction(Schedule.ScheduleID) + + Schedule.ScheduleID = nil + + else + self:T(string.format("Error no ScheduleID for CallID=%s", tostring(CallID))) end + else + for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do self:Stop( Scheduler, CallID ) -- Recursive end + end end +--- Clear all schedules by stopping all dispatchers. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. function SCHEDULEDISPATCHER:Clear( Scheduler ) self:F2( { Scheduler = Scheduler } ) @@ -229,5 +359,19 @@ function SCHEDULEDISPATCHER:Clear( Scheduler ) end end +--- Shopw tracing info. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +function SCHEDULEDISPATCHER:ShowTrace( Scheduler ) + self:F2( { Scheduler = Scheduler } ) + Scheduler.ShowTrace = true +end +--- No tracing info. +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +function SCHEDULEDISPATCHER:NoTrace( Scheduler ) + self:F2( { Scheduler = Scheduler } ) + Scheduler.ShowTrace = false +end diff --git a/Moose Development/Moose/Core/Scheduler.lua b/Moose Development/Moose/Core/Scheduler.lua index 2b5e7e03b..781b90ebe 100644 --- a/Moose Development/Moose/Core/Scheduler.lua +++ b/Moose Development/Moose/Core/Scheduler.lua @@ -43,7 +43,9 @@ --- The SCHEDULER class -- @type SCHEDULER --- @field #number ScheduleID the ID of the scheduler. +-- @field #table Schedules Table of schedules. +-- @field #table MasterObject Master object. +-- @field #boolean ShowTrace Trace info if true. -- @extends Core.Base#BASE @@ -69,53 +71,53 @@ -- -- * @{#SCHEDULER.New}( nil ): Setup a new SCHEDULER object, which is persistently executed after garbage collection. -- --- SchedulerObject = SCHEDULER:New() --- SchedulerID = SchedulerObject:Schedule( nil, ScheduleFunction, {} ) +-- MasterObject = SCHEDULER:New() +-- SchedulerID = MasterObject:Schedule( nil, ScheduleFunction, {} ) -- --- The above example creates a new SchedulerObject, but does not schedule anything. --- A separate schedule is created by using the SchedulerObject using the method :Schedule..., which returns a ScheduleID +-- The above example creates a new MasterObject, but does not schedule anything. +-- A separate schedule is created by using the MasterObject using the method :Schedule..., which returns a ScheduleID -- -- ### Construct a SCHEDULER object without a volatile schedule, but volatile to the Object existence... -- -- * @{#SCHEDULER.New}( Object ): Setup a new SCHEDULER object, which is linked to the Object. When the Object is nillified or destroyed, the SCHEDULER object will also be destroyed and stopped after garbage collection. -- -- ZoneObject = ZONE:New( "ZoneName" ) --- SchedulerObject = SCHEDULER:New( ZoneObject ) --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {} ) +-- MasterObject = SCHEDULER:New( ZoneObject ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {} ) -- ... -- ZoneObject = nil -- garbagecollect() -- --- The above example creates a new SchedulerObject, but does not schedule anything, and is bound to the existence of ZoneObject, which is a ZONE. --- A separate schedule is created by using the SchedulerObject using the method :Schedule()..., which returns a ScheduleID +-- The above example creates a new MasterObject, but does not schedule anything, and is bound to the existence of ZoneObject, which is a ZONE. +-- A separate schedule is created by using the MasterObject using the method :Schedule()..., which returns a ScheduleID -- Later in the logic, the ZoneObject is put to nil, and garbage is collected. --- As a result, the ScheduleObject will cancel any planned schedule. +-- As a result, the MasterObject will cancel any planned schedule. -- -- ### Construct a SCHEDULER object with a persistent schedule. -- -- * @{#SCHEDULER.New}( nil, Function, FunctionArguments, Start, ... ): Setup a new persistent SCHEDULER object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. -- --- SchedulerObject, SchedulerID = SCHEDULER:New( nil, ScheduleFunction, {} ) +-- MasterObject, SchedulerID = SCHEDULER:New( nil, ScheduleFunction, {} ) -- --- The above example creates a new SchedulerObject, and does schedule the first schedule as part of the call. --- Note that 2 variables are returned here: SchedulerObject, ScheduleID... +-- The above example creates a new MasterObject, and does schedule the first schedule as part of the call. +-- Note that 2 variables are returned here: MasterObject, ScheduleID... -- -- ### Construct a SCHEDULER object without a schedule, but volatile to the Object existence... -- -- * @{#SCHEDULER.New}( Object, Function, FunctionArguments, Start, ... ): Setup a new SCHEDULER object, linked to Object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters. -- -- ZoneObject = ZONE:New( "ZoneName" ) --- SchedulerObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} ) --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {} ) +-- MasterObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {} ) -- ... -- ZoneObject = nil -- garbagecollect() -- --- The above example creates a new SchedulerObject, and schedules a method call (ScheduleFunction), +-- The above example creates a new MasterObject, and schedules a method call (ScheduleFunction), -- and is bound to the existence of ZoneObject, which is a ZONE object (ZoneObject). --- Both a ScheduleObject and a SchedulerID variable are returned. +-- Both a MasterObject and a SchedulerID variable are returned. -- Later in the logic, the ZoneObject is put to nil, and garbage is collected. --- As a result, the ScheduleObject will cancel the planned schedule. +-- As a result, the MasterObject will cancel the planned schedule. -- -- ## SCHEDULER timer stopping and (re-)starting. -- @@ -125,15 +127,15 @@ -- * @{#SCHEDULER.Stop}(): Stop the schedules within the SCHEDULER object. If a CallID is provided to :Stop(), then only the schedule referenced by CallID will be stopped. -- -- ZoneObject = ZONE:New( "ZoneName" ) --- SchedulerObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} ) --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 10 ) +-- MasterObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 10 ) -- ... --- SchedulerObject:Stop( SchedulerID ) +-- MasterObject:Stop( SchedulerID ) -- ... --- SchedulerObject:Start( SchedulerID ) +-- MasterObject:Start( SchedulerID ) -- --- The above example creates a new SchedulerObject, and does schedule the first schedule as part of the call. --- Note that 2 variables are returned here: SchedulerObject, ScheduleID... +-- The above example creates a new MasterObject, and does schedule the first schedule as part of the call. +-- Note that 2 variables are returned here: MasterObject, ScheduleID... -- Later in the logic, the repeating schedule with SchedulerID is stopped. -- A bit later, the repeating schedule with SchedulerId is (re)-started. -- @@ -145,32 +147,32 @@ -- Consider the following code fragment of the SCHEDULER object creation. -- -- ZoneObject = ZONE:New( "ZoneName" ) --- SchedulerObject = SCHEDULER:New( ZoneObject ) +-- MasterObject = SCHEDULER:New( ZoneObject ) -- -- Several parameters can be specified that influence the behaviour of a Schedule. -- -- ### A single schedule, immediately executed -- --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {} ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {} ) -- -- The above example schedules a new ScheduleFunction call to be executed asynchronously, within milleseconds ... -- -- ### A single schedule, planned over time -- --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10 ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10 ) -- -- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds ... -- -- ### A schedule with a repeating time interval, planned over time -- --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60 ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60 ) -- -- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, -- and repeating 60 every seconds ... -- -- ### A schedule with a repeating time interval, planned over time, with time interval randomization -- --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5 ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5 ) -- -- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, -- and repeating 60 seconds, with a 50% time interval randomization ... @@ -180,7 +182,7 @@ -- -- ### A schedule with a repeating time interval, planned over time, with time interval randomization, and stop after a time interval -- --- SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5, 300 ) +-- SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5, 300 ) -- -- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds, -- The schedule will repeat every 60 seconds. @@ -191,13 +193,15 @@ -- -- @field #SCHEDULER SCHEDULER = { - ClassName = "SCHEDULER", - Schedules = {}, + ClassName = "SCHEDULER", + Schedules = {}, + MasterObject = nil, + ShowTrace = nil, } --- SCHEDULER constructor. -- @param #SCHEDULER self --- @param #table SchedulerObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. +-- @param #table MasterObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. -- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. -- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. @@ -205,50 +209,51 @@ SCHEDULER = { -- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. -- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. -- @return #SCHEDULER self. --- @return #number The ScheduleID of the planned schedule. -function SCHEDULER:New( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) +-- @return #table The ScheduleID of the planned schedule. +function SCHEDULER:New( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) local self = BASE:Inherit( self, BASE:New() ) -- #SCHEDULER self:F2( { Start, Repeat, RandomizeFactor, Stop } ) local ScheduleID = nil - self.MasterObject = SchedulerObject + self.MasterObject = MasterObject + self.ShowTrace = false if SchedulerFunction then - ScheduleID = self:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) + ScheduleID = self:Schedule( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, 3 ) end return self, ScheduleID end ---function SCHEDULER:_Destructor() --- --self:E("_Destructor") --- --- _SCHEDULEDISPATCHER:RemoveSchedule( self.CallID ) ---end - --- Schedule a new time event. Note that the schedule will only take place if the scheduler is *started*. Even for a single schedule event, the scheduler needs to be started also. -- @param #SCHEDULER self --- @param #table SchedulerObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. +-- @param #table MasterObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference. -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments. -- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }. -- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called. --- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function. +-- @param #number Repeat Specifies the time interval in seconds when the scheduler will call the event function. -- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat. --- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped. --- @return #number The ScheduleID of the planned schedule. -function SCHEDULER:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop ) +-- @param #number Stop Time interval in seconds after which the scheduler will be stoppe. +-- @param #number TraceLevel Trace level [0,3]. Default 3. +-- @param Core.Fsm#FSM Fsm Finite state model. +-- @return #table The ScheduleID of the planned schedule. +function SCHEDULER:Schedule( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, TraceLevel, Fsm ) self:F2( { Start, Repeat, RandomizeFactor, Stop } ) self:T3( { SchedulerArguments } ) + -- Debug info. local ObjectName = "-" - if SchedulerObject and SchedulerObject.ClassName and SchedulerObject.ClassID then - ObjectName = SchedulerObject.ClassName .. SchedulerObject.ClassID + if MasterObject and MasterObject.ClassName and MasterObject.ClassID then + ObjectName = MasterObject.ClassName .. MasterObject.ClassID end - self:F3( { "Schedule :", ObjectName, tostring( SchedulerObject ), Start, Repeat, RandomizeFactor, Stop } ) - self.SchedulerObject = SchedulerObject + self:F3( { "Schedule :", ObjectName, tostring( MasterObject ), Start, Repeat, RandomizeFactor, Stop } ) + -- Set master object. + self.MasterObject = MasterObject + + -- Add schedule. local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( self, SchedulerFunction, @@ -256,7 +261,9 @@ function SCHEDULER:Schedule( SchedulerObject, SchedulerFunction, SchedulerArgume Start, Repeat, RandomizeFactor, - Stop + Stop, + TraceLevel or 3, + Fsm ) self.Schedules[#self.Schedules+1] = ScheduleID @@ -266,49 +273,47 @@ end --- (Re-)Starts the schedules or a specific schedule if a valid ScheduleID is provided. -- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. +-- @param #string ScheduleID (Optional) The ScheduleID of the planned (repeating) schedule. function SCHEDULER:Start( ScheduleID ) self:F3( { ScheduleID } ) - + self:T(string.format("Starting scheduler ID=%s", tostring(ScheduleID))) _SCHEDULEDISPATCHER:Start( self, ScheduleID ) end --- Stops the schedules or a specific schedule if a valid ScheduleID is provided. -- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. +-- @param #string ScheduleID (Optional) The ScheduleID of the planned (repeating) schedule. function SCHEDULER:Stop( ScheduleID ) self:F3( { ScheduleID } ) - + self:T(string.format("Stopping scheduler ID=%s", tostring(ScheduleID))) _SCHEDULEDISPATCHER:Stop( self, ScheduleID ) end --- Removes a specific schedule if a valid ScheduleID is provided. -- @param #SCHEDULER self --- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. +-- @param #string ScheduleID (optional) The ScheduleID of the planned (repeating) schedule. function SCHEDULER:Remove( ScheduleID ) self:F3( { ScheduleID } ) - - _SCHEDULEDISPATCHER:Remove( self, ScheduleID ) + self:T(string.format("Removing scheduler ID=%s", tostring(ScheduleID))) + _SCHEDULEDISPATCHER:RemoveSchedule( self, ScheduleID ) end --- Clears all pending schedules. -- @param #SCHEDULER self function SCHEDULER:Clear() self:F3( ) - + self:T(string.format("Clearing scheduler")) _SCHEDULEDISPATCHER:Clear( self ) end +--- Show tracing for this scheduler. +-- @param #SCHEDULER self +function SCHEDULER:ShowTrace() + _SCHEDULEDISPATCHER:ShowTrace( self ) +end - - - - - - - - - - - - +--- No tracing for this scheduler. +-- @param #SCHEDULER self +function SCHEDULER:NoTrace() + _SCHEDULEDISPATCHER:NoTrace( self ) +end diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 0864fb6c1..58cb5c386 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -125,11 +125,25 @@ do -- SET_BASE self.Index = {} self.CallScheduler = SCHEDULER:New( self ) - + self:SetEventPriority( 2 ) return self end + + --- Clear the Objects in the Set. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:Clear() + + for Name, Object in pairs( self.Set ) do + self:Remove( Name ) + end + + return self + end + + --- Finds an @{Core.Base#BASE} object based on the object Name. -- @param #SET_BASE self @@ -148,7 +162,7 @@ do -- SET_BASE function SET_BASE:GetSet() self:F2() - return self.Set + return self.Set or {} end --- Gets a list of the Names of the Objects in the Set. @@ -327,6 +341,25 @@ do -- SET_BASE return self end + --- Define the SET iterator **"limit"**. + -- @param #SET_BASE self + -- @param #number Limit Defines how many objects are evaluated of the set as part of the Some iterators. The default is 1. + -- @return #SET_BASE self + function SET_BASE:SetSomeIteratorLimit( Limit ) + + self.SomeIteratorLimit = Limit or 1 + + return self + end + + --- Get the SET iterator **"limit"**. + -- @param #SET_BASE self + -- @return #number Defines how many objects are evaluated of the set as part of the Some iterators. + function SET_BASE:GetSomeIteratorLimit() + + return self.SomeIteratorLimit or self:Count() + end + --- Filters for the defined collection. -- @param #SET_BASE self @@ -351,7 +384,6 @@ do -- SET_BASE for ObjectName, Object in pairs( self.Database ) do if self:IsIncludeObject( Object ) then - self:E( { "Adding Object:", ObjectName } ) self:Add( ObjectName, Object ) end end @@ -409,9 +441,9 @@ do -- SET_BASE for ObjectID, ObjectData in pairs( self.Set ) do if NearestObject == nil then NearestObject = ObjectData - ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetVec2() ) + ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) else - local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetVec2() ) + local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) if Distance < ClosestDistance then NearestObject = ObjectData ClosestDistance = Distance @@ -525,6 +557,10 @@ do -- SET_BASE --- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters. -- @param #SET_BASE self -- @param #function IteratorFunction The function that will be called. + -- @param #table arg Arguments of the IteratorFunction. + -- @param #SET_BASE Set (Optional) The set to use. Default self:GetSet(). + -- @param #function Function (Optional) A function returning a #boolean true/false. Only if true, the IteratorFunction is called. + -- @param #table FunctionArguments (Optional) Function arguments. -- @return #SET_BASE self function SET_BASE:ForEach( IteratorFunction, arg, Set, Function, FunctionArguments ) self:F3( arg ) @@ -532,6 +568,63 @@ do -- SET_BASE Set = Set or self:GetSet() arg = arg or {} + local function CoRoutine() + local Count = 0 + for ObjectID, ObjectData in pairs( Set ) do + local Object = ObjectData + self:T3( Object ) + if Function then + if Function( unpack( FunctionArguments or {} ), Object ) == true then + IteratorFunction( Object, unpack( arg ) ) + end + else + IteratorFunction( Object, unpack( arg ) ) + end + Count = Count + 1 + -- if Count % self.YieldInterval == 0 then + -- coroutine.yield( false ) + -- end + end + return true + end + + -- local co = coroutine.create( CoRoutine ) + local co = CoRoutine + + local function Schedule() + + -- local status, res = coroutine.resume( co ) + local status, res = co() + self:T3( { status, res } ) + + if status == false then + error( res ) + end + if res == false then + return true -- resume next time the loop + end + + return false + end + + --self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) + Schedule() + + return self + end + + --- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters. + -- @param #SET_BASE self + -- @param #function IteratorFunction The function that will be called. + -- @return #SET_BASE self + function SET_BASE:ForSome( IteratorFunction, arg, Set, Function, FunctionArguments ) + self:F3( arg ) + + Set = Set or self:GetSet() + arg = arg or {} + + local Limit = self:GetSomeIteratorLimit() + local function CoRoutine() local Count = 0 for ObjectID, ObjectData in pairs( Set ) do @@ -545,6 +638,9 @@ do -- SET_BASE IteratorFunction( Object, unpack( arg ) ) end Count = Count + 1 + if Count >= Limit then + break + end -- if Count % self.YieldInterval == 0 then -- coroutine.yield( false ) -- end @@ -831,7 +927,40 @@ do -- SET_GROUP return AliveSet.Set or {} end + + --- Returns a report of of unit types. + -- @param #SET_GROUP self + -- @return Core.Report#REPORT A report of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found. + function SET_GROUP:GetUnitTypeNames() + self:F2() + local MT = {} -- Message Text + local UnitTypes = {} + + local ReportUnitTypes = REPORT:New() + + for GroupID, GroupData in pairs( self:GetSet() ) do + local Units = GroupData:GetUnits() + for UnitID, UnitData in pairs( Units ) do + if UnitData:IsAlive() then + local UnitType = UnitData:GetTypeName() + + if not UnitTypes[UnitType] then + UnitTypes[UnitType] = 1 + else + UnitTypes[UnitType] = UnitTypes[UnitType] + 1 + end + end + end + end + + for UnitTypeID, UnitType in pairs( UnitTypes ) do + ReportUnitTypes:Add( UnitType .. " of " .. UnitTypeID ) + end + + return ReportUnitTypes + end + --- Add a GROUP to SET_GROUP. -- Note that for each unit in the group that is set, a default cargo bay limit is initialized. -- @param Core.Set#SET_GROUP self @@ -1130,7 +1259,7 @@ do -- SET_GROUP --- Iterate the SET_GROUP and call an iterator function for each GROUP object, providing the GROUP and optional parameters. -- @param #SET_GROUP self - -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @param #function IteratorFunction The function that will be called for all GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. -- @return #SET_GROUP self function SET_GROUP:ForEachGroup( IteratorFunction, ... ) self:F2( arg ) @@ -1140,6 +1269,18 @@ do -- SET_GROUP return self end + --- Iterate the SET_GROUP and call an iterator function for some GROUP objects, providing the GROUP and optional parameters. + -- @param #SET_GROUP self + -- @param #function IteratorFunction The function that will be called for some GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForSomeGroup( IteratorFunction, ... ) + self:F2( arg ) + + self:ForSome( IteratorFunction, arg, self:GetSet() ) + + return self + end + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP object, providing the GROUP and optional parameters. -- @param #SET_GROUP self -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. @@ -1152,6 +1293,18 @@ do -- SET_GROUP return self end + --- Iterate the SET_GROUP and call an iterator function for some **alive** GROUP objects, providing the GROUP and optional parameters. + -- @param #SET_GROUP self + -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForSomeGroupAlive( IteratorFunction, ... ) + self:F2( arg ) + + self:ForSome( IteratorFunction, arg, self:GetAliveSet() ) + + return self + end + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. -- @param #SET_GROUP self -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. @@ -1421,6 +1574,35 @@ do -- SET_GROUP return Count end + --- Iterate the SET_GROUP and count how many GROUPs and UNITs are alive. + -- @param #SET_GROUP self + -- @return #number The number of GROUPs alive. + -- @return #number The number of UNITs alive. + function SET_GROUP:CountAlive() + local CountG = 0 + local CountU = 0 + + local Set = self:GetSet() + + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + if GroupData and GroupData:IsAlive() then + + CountG = CountG + 1 + + --Count Units. + for _,_unit in pairs(GroupData:GetUnits()) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + CountU=CountU+1 + end + end + end + + end + + return CountG,CountU + end + ----- Iterate the SET_GROUP and call an interator function for each **alive** player, providing the Group of the player and optional parameters. ---- @param #SET_GROUP self ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a GROUP parameter. @@ -2004,20 +2186,6 @@ do -- SET_UNIT return IsNotInZone end - - --- Check if minimal one element of the SET_UNIT is in the Zone. - -- @param #SET_UNIT self - -- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter. - -- @return #SET_UNIT self - function SET_UNIT:ForEachUnitInZone( IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self:GetSet() ) - - return self - end - - end @@ -2033,6 +2201,54 @@ do -- SET_UNIT return self end + + --- Get the SET of the SET_UNIT **sorted per Threat Level**. + -- + -- @param #SET_UNIT self + -- @param #number FromThreatLevel The TreatLevel to start the evaluation **From** (this must be a value between 0 and 10). + -- @param #number ToThreatLevel The TreatLevel to stop the evaluation **To** (this must be a value between 0 and 10). + -- @return #SET_UNIT self + -- @usage + -- + -- + function SET_UNIT:GetSetPerThreatLevel( FromThreatLevel, ToThreatLevel ) + self:F2( arg ) + + local ThreatLevelSet = {} + + if self:Count() ~= 0 then + for UnitName, UnitObject in pairs( self.Set ) do + local Unit = UnitObject -- Wrapper.Unit#UNIT + + local ThreatLevel = Unit:GetThreatLevel() + ThreatLevelSet[ThreatLevel] = ThreatLevelSet[ThreatLevel] or {} + ThreatLevelSet[ThreatLevel].Set = ThreatLevelSet[ThreatLevel].Set or {} + ThreatLevelSet[ThreatLevel].Set[UnitName] = UnitObject + self:F( { ThreatLevel = ThreatLevel, ThreatLevelSet = ThreatLevelSet[ThreatLevel].Set } ) + end + + + local OrderedPerThreatLevelSet = {} + + local ThreatLevelIncrement = FromThreatLevel <= ToThreatLevel and 1 or -1 + + + for ThreatLevel = FromThreatLevel, ToThreatLevel, ThreatLevelIncrement do + self:F( { ThreatLevel = ThreatLevel } ) + local ThreatLevelItem = ThreatLevelSet[ThreatLevel] + if ThreatLevelItem then + for UnitName, UnitObject in pairs( ThreatLevelItem.Set ) do + table.insert( OrderedPerThreatLevelSet, UnitObject ) + end + end + end + + return OrderedPerThreatLevelSet + end + + end + + --- Iterate the SET_UNIT **sorted *per Threat Level** and call an interator function for each **alive** UNIT, providing the UNIT and optional parameters. -- -- @param #SET_UNIT self @@ -2393,6 +2609,23 @@ do -- SET_UNIT return GroundUnitCount end + --- Returns if the @{Set} has air targets. + -- @param #SET_UNIT self + -- @return #number The amount of air targets in the Set. + function SET_UNIT:HasAirUnits() + self:F2() + + local AirUnitCount = 0 + for UnitID, UnitData in pairs( self:GetSet() ) do + local UnitTest = UnitData -- Wrapper.Unit#UNIT + if UnitTest:IsAir() then + AirUnitCount = AirUnitCount + 1 + end + end + + return AirUnitCount + end + --- Returns if the @{Set} has friendly ground units. -- @param #SET_UNIT self -- @return #number The amount of ground targets in the Set. @@ -5249,4 +5482,324 @@ do -- SET_ZONE return nil end -end \ No newline at end of file +end + +do -- SET_ZONE_GOAL + + --- @type SET_ZONE_GOAL + -- @extends Core.Set#SET_BASE + + --- Mission designers can use the @{Core.Set#SET_ZONE_GOAL} class to build sets of zones of various types. + -- + -- ## SET_ZONE_GOAL constructor + -- + -- Create a new SET_ZONE_GOAL object with the @{#SET_ZONE_GOAL.New} method: + -- + -- * @{#SET_ZONE_GOAL.New}: Creates a new SET_ZONE_GOAL object. + -- + -- ## Add or Remove ZONEs from SET_ZONE_GOAL + -- + -- ZONEs can be added and removed using the @{Core.Set#SET_ZONE_GOAL.AddZonesByName} and @{Core.Set#SET_ZONE_GOAL.RemoveZonesByName} respectively. + -- These methods take a single ZONE name or an array of ZONE names to be added or removed from SET_ZONE_GOAL. + -- + -- ## SET_ZONE_GOAL filter criteria + -- + -- You can set filter criteria to build the collection of zones in SET_ZONE_GOAL. + -- Filter criteria are defined by: + -- + -- * @{#SET_ZONE_GOAL.FilterPrefixes}: Builds the SET_ZONE_GOAL with the zones having a certain text pattern of prefix. + -- + -- Once the filter criteria have been set for the SET_ZONE_GOAL, you can start filtering using: + -- + -- * @{#SET_ZONE_GOAL.FilterStart}: Starts the filtering of the zones within the SET_ZONE_GOAL. + -- + -- ## SET_ZONE_GOAL iterators + -- + -- Once the filters have been defined and the SET_ZONE_GOAL has been built, you can iterate the SET_ZONE_GOAL with the available iterator methods. + -- The iterator methods will walk the SET_ZONE_GOAL set, and call for each airbase within the set a function that you provide. + -- The following iterator methods are currently available within the SET_ZONE_GOAL: + -- + -- * @{#SET_ZONE_GOAL.ForEachZone}: Calls a function for each zone it finds within the SET_ZONE_GOAL. + -- + -- === + -- @field #SET_ZONE_GOAL SET_ZONE_GOAL + SET_ZONE_GOAL = { + ClassName = "SET_ZONE_GOAL", + Zones = {}, + Filter = { + Prefixes = nil, + }, + FilterMeta = { + }, + } + + + --- Creates a new SET_ZONE_GOAL object, building a set of zones. + -- @param #SET_ZONE_GOAL self + -- @return #SET_ZONE_GOAL self + -- @usage + -- -- Define a new SET_ZONE_GOAL Object. The DatabaseSet will contain a reference to all Zones. + -- DatabaseSet = SET_ZONE_GOAL:New() + function SET_ZONE_GOAL:New() + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.ZONES_GOAL ) ) + + return self + end + + --- Add ZONEs to SET_ZONE_GOAL. + -- @param Core.Set#SET_ZONE_GOAL self + -- @param Core.Zone#ZONE_BASE Zone A ZONE_BASE object. + -- @return self + function SET_ZONE_GOAL:AddZone( Zone ) + + self:Add( Zone:GetName(), Zone ) + + return self + end + + + --- Remove ZONEs from SET_ZONE_GOAL. + -- @param Core.Set#SET_ZONE_GOAL self + -- @param Core.Zone#ZONE_BASE RemoveZoneNames A single name or an array of ZONE_BASE names. + -- @return self + function SET_ZONE_GOAL:RemoveZonesByName( RemoveZoneNames ) + + local RemoveZoneNamesArray = ( type( RemoveZoneNames ) == "table" ) and RemoveZoneNames or { RemoveZoneNames } + + for RemoveZoneID, RemoveZoneName in pairs( RemoveZoneNamesArray ) do + self:Remove( RemoveZoneName ) + end + + return self + end + + + --- Finds a Zone based on the Zone Name. + -- @param #SET_ZONE_GOAL self + -- @param #string ZoneName + -- @return Core.Zone#ZONE_BASE The found Zone. + function SET_ZONE_GOAL:FindZone( ZoneName ) + + local ZoneFound = self.Set[ZoneName] + return ZoneFound + end + + + --- Get a random zone from the set. + -- @param #SET_ZONE_GOAL self + -- @return Core.Zone#ZONE_BASE The random Zone. + -- @return #nil if no zone in the collection. + function SET_ZONE_GOAL:GetRandomZone() + + if self:Count() ~= 0 then + + local Index = self.Index + local ZoneFound = nil -- Core.Zone#ZONE_BASE + + -- Loop until a zone has been found. + -- The :GetZoneMaybe() call will evaluate the probability for the zone to be selected. + -- If the zone is not selected, then nil is returned by :GetZoneMaybe() and the loop continues! + while not ZoneFound do + local ZoneRandom = math.random( 1, #Index ) + ZoneFound = self.Set[Index[ZoneRandom]]:GetZoneMaybe() + end + + return ZoneFound + end + + return nil + end + + + --- Set a zone probability. + -- @param #SET_ZONE_GOAL self + -- @param #string ZoneName The name of the zone. + function SET_ZONE_GOAL:SetZoneProbability( ZoneName, ZoneProbability ) + local Zone = self:FindZone( ZoneName ) + Zone:SetZoneProbability( ZoneProbability ) + end + + + + + --- Builds a set of zones of defined zone prefixes. + -- All the zones starting with the given prefixes will be included within the set. + -- @param #SET_ZONE_GOAL self + -- @param #string Prefixes The prefix of which the zone name starts with. + -- @return #SET_ZONE_GOAL self + function SET_ZONE_GOAL:FilterPrefixes( Prefixes ) + if not self.Filter.Prefixes then + self.Filter.Prefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.Prefixes[Prefix] = Prefix + end + return self + end + + + --- Starts the filtering. + -- @param #SET_ZONE_GOAL self + -- @return #SET_ZONE_GOAL self + function SET_ZONE_GOAL:FilterStart() + + if _DATABASE then + + -- We initialize the first set. + for ObjectName, Object in pairs( self.Database ) do + if self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + else + self:RemoveZonesByName( ObjectName ) + end + end + end + + self:HandleEvent( EVENTS.NewZoneGoal ) + self:HandleEvent( EVENTS.DeleteZoneGoal ) + + return self + end + + --- Stops the filtering for the defined collection. + -- @param #SET_ZONE_GOAL self + -- @return #SET_ZONE_GOAL self + function SET_ZONE_GOAL:FilterStop() + + self:UnHandleEvent( EVENTS.NewZoneGoal ) + self:UnHandleEvent( EVENTS.DeleteZoneGoal ) + + return self + end + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_ZONE_GOAL self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the AIRBASE + -- @return #table The AIRBASE + function SET_ZONE_GOAL:AddInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_ZONE_GOAL self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the AIRBASE + -- @return #table The AIRBASE + function SET_ZONE_GOAL:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Iterate the SET_ZONE_GOAL and call an interator function for each ZONE, providing the ZONE and optional parameters. + -- @param #SET_ZONE_GOAL self + -- @param #function IteratorFunction The function that will be called when there is an alive ZONE in the SET_ZONE_GOAL. The function needs to accept a AIRBASE parameter. + -- @return #SET_ZONE_GOAL self + function SET_ZONE_GOAL:ForEachZone( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + + --- + -- @param #SET_ZONE_GOAL self + -- @param Core.Zone#ZONE_BASE MZone + -- @return #SET_ZONE_GOAL self + function SET_ZONE_GOAL:IsIncludeObject( MZone ) + self:F2( MZone ) + + local MZoneInclude = true + + if MZone then + local MZoneName = MZone:GetName() + + if self.Filter.Prefixes then + local MZonePrefix = false + for ZonePrefixId, ZonePrefix in pairs( self.Filter.Prefixes ) do + self:T3( { "Prefix:", string.find( MZoneName, ZonePrefix, 1 ), ZonePrefix } ) + if string.find( MZoneName, ZonePrefix, 1 ) then + MZonePrefix = true + end + end + self:T( { "Evaluated Prefix", MZonePrefix } ) + MZoneInclude = MZoneInclude and MZonePrefix + end + end + + self:T2( MZoneInclude ) + return MZoneInclude + end + + --- Handles the OnEventNewZone event for the Set. + -- @param #SET_ZONE_GOAL self + -- @param Core.Event#EVENTDATA EventData + function SET_ZONE_GOAL:OnEventNewZoneGoal( EventData ) + + self:I( { "New Zone Capture Coalition", EventData } ) + self:I( { "Zone Capture Coalition", EventData.ZoneGoal } ) + + if EventData.ZoneGoal then + if EventData.ZoneGoal and self:IsIncludeObject( EventData.ZoneGoal ) then + self:I( { "Adding Zone Capture Coalition", EventData.ZoneGoal.ZoneName, EventData.ZoneGoal } ) + self:Add( EventData.ZoneGoal.ZoneName , EventData.ZoneGoal ) + end + end + end + + --- Handles the OnDead or OnCrash event for alive units set. + -- @param #SET_ZONE_GOAL self + -- @param Core.Event#EVENTDATA EventData + function SET_ZONE_GOAL:OnEventDeleteZoneGoal( EventData ) --R2.1 + self:F3( { EventData } ) + + if EventData.ZoneGoal then + local Zone = _DATABASE:FindZone( EventData.ZoneGoal.ZoneName ) + if Zone and Zone.ZoneName then + + -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. + -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. + -- And this is a problem because it will remove all entries from the SET_ZONE_GOALs. + -- To prevent this from happening, the Zone object has a flag NoDestroy. + -- When true, the SET_ZONE_GOAL won't Remove the Zone object from the set. + -- This flag is switched off after the event handlers have been called in the EVENT class. + self:F( { ZoneNoDestroy=Zone.NoDestroy } ) + if Zone.NoDestroy then + else + self:Remove( Zone.ZoneName ) + end + end + end + end + + --- Validate if a coordinate is in one of the zones in the set. + -- Returns the ZONE object where the coordiante is located. + -- If zones overlap, the first zone that validates the test is returned. + -- @param #SET_ZONE_GOAL self + -- @param Core.Point#COORDINATE Coordinate The coordinate to be searched. + -- @return Core.Zone#ZONE_BASE The zone that validates the coordinate location. + -- @return #nil No zone has been found. + function SET_ZONE_GOAL:IsCoordinateInZone( Coordinate ) + + for _, Zone in pairs( self:GetSet() ) do + local Zone = Zone -- Core.Zone#ZONE_BASE + if Zone:IsCoordinateInZone( Coordinate ) then + return Zone + end + end + + return nil + end + +end diff --git a/Moose Development/Moose/Core/Settings.lua b/Moose Development/Moose/Core/Settings.lua index 1f504271d..3a557de65 100644 --- a/Moose Development/Moose/Core/Settings.lua +++ b/Moose Development/Moose/Core/Settings.lua @@ -1,9 +1,9 @@ --- **Core** - Manages various settings for running missions, consumed by moose classes and provides a menu system for players to tweak settings in running missions. -- -- === --- +-- -- ## Features: --- +-- -- * Provide a settings menu system to the players. -- * Provide a player settings menu and an overall mission settings menu. -- * Mission settings provide default settings, while player settings override mission settings. @@ -11,19 +11,19 @@ -- * Provide a menu to select between different coordinate formats for A2A coordinates. -- * Provide a menu to select between different message time duration options. -- * Provide a menu to select between different metric systems. --- +-- -- === --- +-- -- The documentation of the SETTINGS class can be found further in this document. --- +-- -- === --- +-- -- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- ### Authors: --- +-- +-- ### Contributions: +-- +-- ### Authors: +-- -- * **FlightControl**: Design & Programming -- -- @module Core.Settings @@ -34,174 +34,202 @@ -- @extends Core.Base#BASE --- Takes care of various settings that influence the behaviour of certain functionalities and classes within the MOOSE framework. --- +-- -- === --- +-- -- The SETTINGS class takes care of various settings that influence the behaviour of certain functionalities and classes within the MOOSE framework. -- SETTINGS can work on 2 levels: --- --- - **Default settings**: A running mission has **Default settings**. +-- +-- - **Default settings**: A running mission has **Default settings**. -- - **Player settings**: For each player its own **Player settings** can be defined, overriding the **Default settings**. --- +-- -- So, when there isn't any **Player setting** defined for a player for a specific setting, or, the player cannot be identified, the **Default setting** will be used instead. --- +-- -- # 1) \_SETTINGS object --- +-- -- MOOSE defines by default a singleton object called **\_SETTINGS**. Use this object to modify all the **Default settings** for a running mission. -- For each player, MOOSE will automatically allocate also a **player settings** object, and will expose a radio menu to allow the player to adapt the settings to his own preferences. --- +-- -- # 2) SETTINGS Menu --- +-- -- Settings can be adapted by the Players and by the Mission Administrator through **radio menus, which are automatically available in the mission**. -- These menus can be found **on level F10 under "Settings"**. There are two kinds of menus generated by the system. --- +-- -- ## 2.1) Default settings menu --- +-- -- A menu is created automatically per Command Center that allows to modify the **Default** settings. -- So, when joining a CC unit, a menu will be available that allows to change the settings parameters **FOR ALL THE PLAYERS**! -- Note that the **Default settings** will only be used when a player has not choosen its own settings. --- +-- -- ## 2.2) Player settings menu --- +-- -- A menu is created automatically per Player Slot (group) that allows to modify the **Player** settings. -- So, when joining a slot, a menu wil be available that allows to change the settings parameters **FOR THE PLAYER ONLY**! -- Note that when a player has not chosen a specific setting, the **Default settings** will be used. --- +-- -- ## 2.3) Show or Hide the Player Setting menus --- +-- -- Of course, it may be requried not to show any setting menus. In this case, a method is available on the **\_SETTINGS object**. -- Use @{#SETTINGS.SetPlayerMenuOff}() to hide the player menus, and use @{#SETTINGS.SetPlayerMenuOn}() show the player menus. -- Note that when this method is used, any player already in a slot will not have its menus visibility changed. --- The option will only have effect when a player enters a new slot or changes a slot. --- +-- The option will only have effect when a player enters a new slot or changes a slot. +-- -- Example: --- +-- -- _SETTINGS:SetPlayerMenuOff() -- will disable the player menus. -- _SETTINGS:SetPlayerMenuOn() -- will enable the player menus. -- -- But only when a player exits and reenters the slot these settings will have effect! --- --- +-- +-- -- # 3) Settings --- +-- -- There are different settings that are managed and applied within the MOOSE framework. -- See below a comprehensive description of each. --- +-- -- ## 3.1) **A2G coordinates** display formatting --- +-- -- ### 3.1.1) A2G coordinates setting **types** --- +-- -- Will customize which display format is used to indicate A2G coordinates in text as part of the Command Center communications. --- +-- -- - A2G BR: [Bearing Range](https://en.wikipedia.org/wiki/Bearing_(navigation)). -- - A2G MGRS: The [Military Grid Reference System](https://en.wikipedia.org/wiki/Military_Grid_Reference_System). The accuracy can also be adapted. -- - A2G LL DMS: Lattitude Longitude [Degrees Minutes Seconds](https://en.wikipedia.org/wiki/Geographic_coordinate_conversion). The accuracy can also be adapted. -- - A2G LL DDM: Lattitude Longitude [Decimal Degrees Minutes](https://en.wikipedia.org/wiki/Decimal_degrees). The accuracy can also be adapted. --- +-- -- ### 3.1.2) A2G coordinates setting **menu** --- +-- -- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot. --- +-- -- ### 3.1.3) A2G coordinates setting **methods** --- +-- -- There are different methods that can be used to change the **System settings** using the \_SETTINGS object. --- +-- -- - @{#SETTINGS.SetA2G_BR}(): Enable the BR display formatting by default. -- - @{#SETTINGS.SetA2G_MGRS}(): Enable the MGRS display formatting by default. Use @{SETTINGS.SetMGRS_Accuracy}() to adapt the accuracy of the MGRS formatting. --- - @{#SETTINGS.SetA2G_LL_DMS}(): Enable the LL DMS display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. +-- - @{#SETTINGS.SetA2G_LL_DMS}(): Enable the LL DMS display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. -- - @{#SETTINGS.SetA2G_LL_DDM}(): Enable the LL DDM display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. --- +-- -- ### 3.1.4) A2G coordinates setting - additional notes --- --- One additional note on BR. In a situation when a BR coordinate should be given, +-- +-- One additional note on BR. In a situation when a BR coordinate should be given, -- but there isn't any player context (no player unit to reference from), the MGRS formatting will be applied! --- +-- -- ## 3.2) **A2A coordinates** formatting --- +-- -- ### 3.2.1) A2A coordinates setting **types** --- +-- -- Will customize which display format is used to indicate A2A coordinates in text as part of the Command Center communications. --- +-- -- - A2A BRAA: [Bearing Range Altitude Aspect](https://en.wikipedia.org/wiki/Bearing_(navigation)). -- - A2A MGRS: The [Military Grid Reference System](https://en.wikipedia.org/wiki/Military_Grid_Reference_System). The accuracy can also be adapted. -- - A2A LL DMS: Lattitude Longitude [Degrees Minutes Seconds](https://en.wikipedia.org/wiki/Geographic_coordinate_conversion). The accuracy can also be adapted. -- - A2A LL DDM: Lattitude Longitude [Decimal Degrees and Minutes](https://en.wikipedia.org/wiki/Decimal_degrees). The accuracy can also be adapted. -- - A2A BULLS: [Bullseye](http://falcon4.wikidot.com/concepts:bullseye). --- +-- -- ### 3.2.2) A2A coordinates setting **menu** --- +-- -- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot. --- +-- -- ### 3.2.3) A2A coordinates setting **methods** --- +-- -- There are different methods that can be used to change the **System settings** using the \_SETTINGS object. --- +-- -- - @{#SETTINGS.SetA2A_BRAA}(): Enable the BR display formatting by default. -- - @{#SETTINGS.SetA2A_MGRS}(): Enable the MGRS display formatting by default. Use @{SETTINGS.SetMGRS_Accuracy}() to adapt the accuracy of the MGRS formatting. --- - @{#SETTINGS.SetA2A_LL_DMS}(): Enable the LL DMS display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. +-- - @{#SETTINGS.SetA2A_LL_DMS}(): Enable the LL DMS display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. -- - @{#SETTINGS.SetA2A_LL_DDM}(): Enable the LL DDM display formatting by default. Use @{SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting. -- - @{#SETTINGS.SetA2A_BULLS}(): Enable the BULLSeye display formatting by default. --- +-- -- ### 3.2.4) A2A coordinates settings - additional notes --- --- One additional note on BRAA. In a situation when a BRAA coordinate should be given, +-- +-- One additional note on BRAA. In a situation when a BRAA coordinate should be given, -- but there isn't any player context (no player unit to reference from), the MGRS formatting will be applied! --- +-- -- ## 3.3) **Measurements** formatting --- +-- -- ### 3.3.1) Measurements setting **types** --- +-- -- Will customize the measurements system being used as part as part of the Command Center communications. --- +-- -- - **Metrics** system: Applies the [Metrics system](https://en.wikipedia.org/wiki/Metric_system) ... --- - **Imperial** system: Applies the [Imperial system](https://en.wikipedia.org/wiki/Imperial_units) ... --- +-- - **Imperial** system: Applies the [Imperial system](https://en.wikipedia.org/wiki/Imperial_units) ... +-- -- ### 3.3.2) Measurements setting **menu** --- +-- -- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot. --- +-- -- ### 3.3.3) Measurements setting **methods** --- +-- -- There are different methods that can be used to change the **Default settings** using the \_SETTINGS object. --- +-- -- - @{#SETTINGS.SetMetric}(): Enable the Metric system. -- - @{#SETTINGS.SetImperial}(): Enable the Imperial system. --- +-- -- ## 3.4) **Message** display times --- +-- -- ### 3.4.1) Message setting **types** --- +-- -- There are various **Message Types** that will influence the duration how long a message will appear as part of the Command Center communications. --- +-- -- - **Update** message: A short update message. -- - **Information** message: Provides new information **while** executing a mission. -- - **Briefing** message: Provides a complete briefing **before** executing a mission. -- - **Overview report**: Provides a short report overview, the summary of the report. -- - **Detailed report**: Provides a complete report. --- +-- -- ### 3.4.2) Message setting **menu** --- +-- -- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot. --- +-- -- Each Message Type has specific timings that will be applied when the message is displayed. -- The Settings Menu will provide for each Message Type a selection of proposed durations from which can be choosen. -- So the player can choose its own amount of seconds how long a message should be displayed of a certain type. -- Note that **Update** messages can be chosen not to be displayed at all! --- +-- -- ### 3.4.3) Message setting **methods** --- +-- -- There are different methods that can be used to change the **System settings** using the \_SETTINGS object. --- --- - @{#SETTINGS.SetMessageTime}(): Define for a specific @{Message.MESSAGE.MessageType} the duration to be displayed in seconds. --- - @{#SETTINGS.GetMessageTime}(): Retrieves for a specific @{Message.MESSAGE.MessageType} the duration to be displayed in seconds. --- +-- +-- - @{#SETTINGS.SetMessageTime}(): Define for a specific @{Message.MESSAGE.MessageType} the duration to be displayed in seconds. +-- - @{#SETTINGS.GetMessageTime}(): Retrieves for a specific @{Message.MESSAGE.MessageType} the duration to be displayed in seconds. +-- +-- ## 3.5) **Era** of the battle +-- +-- The threat level metric is scaled according the era of the battle. A target that is AAA, will pose a much greather threat in WWII than on modern warfare. +-- Therefore, there are 4 era that are defined within the settings: +-- +-- - **WWII** era: Use for warfare with equipment during the world war II time. +-- - **Korea** era: Use for warfare with equipment during the Korea war time. +-- - **Cold War** era: Use for warfare with equipment during the cold war time. +-- - **Modern** era: Use for warfare with modern equipment in the 2000s. +-- +-- There are different API defined that you can use with the _SETTINGS object to configure your mission script to work in one of the 4 era: +-- @{#SETTINGS.SetEraWWII}(), @{#SETTINGS.SetEraKorea}(), @{#SETTINGS.SetEraCold}(), @{#SETTINGS.SetEraModern}() +-- -- === --- +-- -- @field #SETTINGS SETTINGS = { ClassName = "SETTINGS", ShowPlayerMenu = true, + MenuShort = false, + MenuStatic = false, } +SETTINGS.__Enum = {} + +--- @type SETTINGS.__Enum.Era +-- @field #number WWII +-- @field #number Korea +-- @field #number Cold +-- @field #number Modern +SETTINGS.__Enum.Era = { + WWII = 1, + Korea = 2, + Cold = 3, + Modern = 4, +} do -- SETTINGS @@ -209,7 +237,7 @@ do -- SETTINGS --- SETTINGS constructor. -- @param #SETTINGS self -- @return #SETTINGS - function SETTINGS:Set( PlayerName ) + function SETTINGS:Set( PlayerName ) if PlayerName == nil then local self = BASE:Inherit( self, BASE:New() ) -- #SETTINGS @@ -223,6 +251,7 @@ do -- SETTINGS self:SetMessageTime( MESSAGE.Type.Information, 30 ) self:SetMessageTime( MESSAGE.Type.Overview, 60 ) self:SetMessageTime( MESSAGE.Type.Update, 15 ) + self:SetEraModern() return self else local Settings = _DATABASE:GetPlayerSettings( PlayerName ) @@ -233,14 +262,28 @@ do -- SETTINGS return Settings end end - - + + --- Set short text for menus on (*true*) or off (*false*). + -- Short text are better suited for, e.g., VR. + -- @param #SETTINGS self + -- @param #boolean onoff If *true* use short menu texts. If *false* long ones (default). + function SETTINGS:SetMenutextShort(onoff) + _SETTINGS.MenuShort = onoff + end + + --- Set menu to be static. + -- @param #SETTINGS self + -- @param #boolean onoff If *true* menu is static. If *false* menu will be updated after changes (default). + function SETTINGS:SetMenuStatic(onoff) + _SETTINGS.MenuStatic = onoff + end + --- Sets the SETTINGS metric. -- @param #SETTINGS self function SETTINGS:SetMetric() self.Metric = true end - + --- Gets if the SETTINGS is metric. -- @param #SETTINGS self -- @return #boolean true if metric. @@ -253,7 +296,7 @@ do -- SETTINGS function SETTINGS:SetImperial() self.Metric = false end - + --- Gets if the SETTINGS is imperial. -- @param #SETTINGS self -- @return #boolean true if imperial. @@ -290,7 +333,7 @@ do -- SETTINGS function SETTINGS:GetMGRS_Accuracy() return self.MGRS_Accuracy or _SETTINGS:GetMGRS_Accuracy() end - + --- Sets the SETTINGS Message Display Timing of a MessageType -- @param #SETTINGS self -- @param Core.Message#MESSAGE MessageType The type of the message. @@ -299,8 +342,8 @@ do -- SETTINGS self.MessageTypeTimings = self.MessageTypeTimings or {} self.MessageTypeTimings[MessageType] = MessageTime end - - + + --- Gets the SETTINGS Message Display Timing of a MessageType -- @param #SETTINGS self -- @param Core.Message#MESSAGE MessageType The type of the message. @@ -436,40 +479,77 @@ do -- SETTINGS end --- @param #SETTINGS self + -- @param Wrapper.Group#GROUP MenuGroup Group for which to add menus. + -- @param #table RootMenu Root menu table -- @return #SETTINGS function SETTINGS:SetSystemMenu( MenuGroup, RootMenu ) local MenuText = "System Settings" - + local MenuTime = timer.getTime() - + local SettingsMenu = MENU_GROUP:New( MenuGroup, MenuText, RootMenu ):SetTime( MenuTime ) - local A2GCoordinateMenu = MENU_GROUP:New( MenuGroup, "A2G Coordinate System", SettingsMenu ):SetTime( MenuTime ) - - - if not self:IsA2G_LL_DMS() then - MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime ) + ------- + -- A2G Coordinate System + ------- + + local text="A2G Coordinate System" + if _SETTINGS.MenuShort then + text="A2G Coordinates" end - + local A2GCoordinateMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) + + -- Set LL DMS + if not self:IsA2G_LL_DMS() then + local text="Lat/Lon Degree Min Sec (LL DMS)" + if _SETTINGS.MenuShort then + text="LL DMS" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime ) + end + + -- Set LL DDM if not self:IsA2G_LL_DDM() then + local text="Lat/Lon Degree Dec Min (LL DDM)" + if _SETTINGS.MenuShort then + text="LL DDM" + end MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime ) end - + + -- Set LL DMS accuracy. if self:IsA2G_LL_DDM() then + local text1="LL DDM Accuracy 1" + local text2="LL DDM Accuracy 2" + local text3="LL DDM Accuracy 3" + if _SETTINGS.MenuShort then + text1="LL DDM" + end MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 1", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 2", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 3", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) end - + + -- Set BR. if not self:IsA2G_BR() then - MENU_GROUP_COMMAND:New( MenuGroup, "Bearing, Range (BR)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "BR" ):SetTime( MenuTime ) + local text="Bearing, Range (BR)" + if _SETTINGS.MenuShort then + text="BR" + end + MENU_GROUP_COMMAND:New( MenuGroup, text , A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "BR" ):SetTime( MenuTime ) end - + + -- Set MGRS. if not self:IsA2G_MGRS() then - MENU_GROUP_COMMAND:New( MenuGroup, "Military Grid (MGRS)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime ) + local text="Military Grid (MGRS)" + if _SETTINGS.MenuShort then + text="MGRS" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime ) end - + + -- Set MGRS accuracy. if self:IsA2G_MGRS() then MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 1", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 2", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) @@ -478,32 +558,61 @@ do -- SETTINGS MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 5", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 5 ):SetTime( MenuTime ) end - local A2ACoordinateMenu = MENU_GROUP:New( MenuGroup, "A2A Coordinate System", SettingsMenu ):SetTime( MenuTime ) + ------- + -- A2A Coordinate System + ------- + + local text="A2A Coordinate System" + if _SETTINGS.MenuShort then + text="A2A Coordinates" + end + local A2ACoordinateMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) if not self:IsA2A_LL_DMS() then - MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime ) + local text="Lat/Lon Degree Min Sec (LL DMS)" + if _SETTINGS.MenuShort then + text="LL DMS" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime ) end if not self:IsA2A_LL_DDM() then - MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime ) + local text="Lat/Lon Degree Dec Min (LL DDM)" + if _SETTINGS.MenuShort then + text="LL DDM" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime ) end - if self:IsA2A_LL_DDM() then - MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 1", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 2", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) - MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 3", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) - end + if self:IsA2A_LL_DDM() or self:IsA2A_LL_DMS() then + MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 0", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 0 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 1", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 2", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime ) + MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 3", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) + end if not self:IsA2A_BULLS() then - MENU_GROUP_COMMAND:New( MenuGroup, "Bullseye (BULLS)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BULLS" ):SetTime( MenuTime ) + local text="Bullseye (BULLS)" + if _SETTINGS.MenuShort then + text="Bulls" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BULLS" ):SetTime( MenuTime ) end - + if not self:IsA2A_BRAA() then - MENU_GROUP_COMMAND:New( MenuGroup, "Bearing Range Altitude Aspect (BRAA)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BRAA" ):SetTime( MenuTime ) + local text="Bearing Range Altitude Aspect (BRAA)" + if _SETTINGS.MenuShort then + text="BRAA" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BRAA" ):SetTime( MenuTime ) end - + if not self:IsA2A_MGRS() then - MENU_GROUP_COMMAND:New( MenuGroup, "Military Grid (MGRS)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime ) + local text="Military Grid (MGRS)" + if _SETTINGS.MenuShort then + text="MGRS" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime ) end if self:IsA2A_MGRS() then @@ -512,19 +621,35 @@ do -- SETTINGS MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 3", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 4", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 4 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 5", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 5 ):SetTime( MenuTime ) - end - - local MetricsMenu = MENU_GROUP:New( MenuGroup, "Measures and Weights System", SettingsMenu ):SetTime( MenuTime ) - - if self:IsMetric() then - MENU_GROUP_COMMAND:New( MenuGroup, "Imperial (Miles,Feet)", MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, false ):SetTime( MenuTime ) end - + + local text="Measures and Weights System" + if _SETTINGS.MenuShort then + text="Unit System" + end + local MetricsMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) + + if self:IsMetric() then + local text="Imperial (Miles,Feet)" + if _SETTINGS.MenuShort then + text="Imperial" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, false ):SetTime( MenuTime ) + end + if self:IsImperial() then - MENU_GROUP_COMMAND:New( MenuGroup, "Metric (Kilometers,Meters)", MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, true ):SetTime( MenuTime ) - end - - local MessagesMenu = MENU_GROUP:New( MenuGroup, "Messages and Reports", SettingsMenu ):SetTime( MenuTime ) + local text="Metric (Kilometers,Meters)" + if _SETTINGS.MenuShort then + text="Metric" + end + MENU_GROUP_COMMAND:New( MenuGroup, text, MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, true ):SetTime( MenuTime ) + end + + local text="Messages and Reports" + if _SETTINGS.MenuShort then + text="Messages & Reports" + end + local MessagesMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime ) local UpdateMessagesMenu = MENU_GROUP:New( MenuGroup, "Update Messages", MessagesMenu ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "Off", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 0 ):SetTime( MenuTime ) @@ -562,10 +687,10 @@ do -- SETTINGS MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 60 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "2 minutes", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 120 ):SetTime( MenuTime ) MENU_GROUP_COMMAND:New( MenuGroup, "3 minutes", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 180 ):SetTime( MenuTime ) - + SettingsMenu:Remove( MenuTime ) - + return self end @@ -591,8 +716,6 @@ do -- SETTINGS self.ShowPlayerMenu = false end - - --- Updates the menu of the player seated in the PlayerUnit. -- @param #SETTINGS self -- @param Wrapper.Client#CLIENT PlayerUnit @@ -600,136 +723,200 @@ do -- SETTINGS function SETTINGS:SetPlayerMenu( PlayerUnit ) if _SETTINGS.ShowPlayerMenu == true then - + local PlayerGroup = PlayerUnit:GetGroup() local PlayerName = PlayerUnit:GetPlayerName() local PlayerNames = PlayerGroup:GetPlayerNames() - + local PlayerMenu = MENU_GROUP:New( PlayerGroup, 'Settings "' .. PlayerName .. '"' ) - + self.PlayerMenu = PlayerMenu - - local A2GCoordinateMenu = MENU_GROUP:New( PlayerGroup, "A2G Coordinate System", PlayerMenu ) - - if not self:IsA2G_LL_DMS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" ) + + self:I(string.format("Setting menu for player %s", tostring(PlayerName))) + + local submenu = MENU_GROUP:New( PlayerGroup, "LL Accuracy", PlayerMenu ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 0 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 0 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 1 Decimal", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 2 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 3 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "LL 4 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 ) + + local submenu = MENU_GROUP:New( PlayerGroup, "MGRS Accuracy", PlayerMenu ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 0", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 0 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 1", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 2", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 3", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 4", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 5", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 5 ) + + ------ + -- A2G Coordinate System + ------ + + local text="A2G Coordinate System" + if _SETTINGS.MenuShort then + text="A2G Coordinates" end - - if not self:IsA2G_LL_DDM() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" ) + local A2GCoordinateMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) + + if not self:IsA2G_LL_DMS() or _SETTINGS.MenuStatic then + local text="Lat/Lon Degree Min Sec (LL DMS)" + if _SETTINGS.MenuShort then + text="A2G LL DMS" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" ) end - - if self:IsA2G_LL_DDM() then - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 1", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 2", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 3", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) + + if not self:IsA2G_LL_DDM() or _SETTINGS.MenuStatic then + local text="Lat/Lon Degree Dec Min (LL DDM)" + if _SETTINGS.MenuShort then + text="A2G LL DDM" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" ) end - - if not self:IsA2G_BR() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Bearing, Range (BR)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "BR" ) + + if not self:IsA2G_BR() or _SETTINGS.MenuStatic then + local text="Bearing, Range (BR)" + if _SETTINGS.MenuShort then + text="A2G BR" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "BR" ) end - - if not self:IsA2G_MGRS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" ) - end - - if self:IsA2G_MGRS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 1", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 2", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 3", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 4", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 5", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 5 ) + + if not self:IsA2G_MGRS() or _SETTINGS.MenuStatic then + local text="Military Grid (MGRS)" + if _SETTINGS.MenuShort then + text="A2G MGRS" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" ) end - - local A2ACoordinateMenu = MENU_GROUP:New( PlayerGroup, "A2A Coordinate System", PlayerMenu ) - - - if not self:IsA2A_LL_DMS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" ) + + ------ + -- A2A Coordinates Menu + ------ + + local text="A2A Coordinate System" + if _SETTINGS.MenuShort then + text="A2A Coordinates" end - - if not self:IsA2A_LL_DDM() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" ) + local A2ACoordinateMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) + + + if not self:IsA2A_LL_DMS() or _SETTINGS.MenuStatic then + local text="Lat/Lon Degree Min Sec (LL DMS)" + if _SETTINGS.MenuShort then + text="A2A LL DMS" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" ) end - - if self:IsA2A_LL_DDM() then - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 1", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 2", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 3", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) + + if not self:IsA2A_LL_DDM() or _SETTINGS.MenuStatic then + local text="Lat/Lon Degree Dec Min (LL DDM)" + if _SETTINGS.MenuShort then + text="A2A LL DDM" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" ) end - - if not self:IsA2A_BULLS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Bullseye (BULLS)", A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BULLS" ) + + if not self:IsA2A_BULLS() or _SETTINGS.MenuStatic then + local text="Bullseye (BULLS)" + if _SETTINGS.MenuShort then + text="A2A BULLS" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BULLS" ) end - - if not self:IsA2A_BRAA() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Bearing Range Altitude Aspect (BRAA)", A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BRAA" ) + + if not self:IsA2A_BRAA() or _SETTINGS.MenuStatic then + local text="Bearing Range Altitude Aspect (BRAA)" + if _SETTINGS.MenuShort then + text="A2A BRAA" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BRAA" ) end - - if not self:IsA2A_MGRS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS)", A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" ) + + if not self:IsA2A_MGRS() or _SETTINGS.MenuStatic then + local text="Military Grid (MGRS)" + if _SETTINGS.MenuShort then + text="A2A MGRS" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" ) end - - if self:IsA2A_MGRS() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 1", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 2", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 3", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 4", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 5", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 5 ) - end - - local MetricsMenu = MENU_GROUP:New( PlayerGroup, "Measures and Weights System", PlayerMenu ) - - if self:IsMetric() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Imperial (Miles,Feet)", MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, false ) + + --- + -- Unit system + --- + + local text="Measures and Weights System" + if _SETTINGS.MenuShort then + text="Unit System" end - - if self:IsImperial() then - MENU_GROUP_COMMAND:New( PlayerGroup, "Metric (Kilometers,Meters)", MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, true ) - end - - - local MessagesMenu = MENU_GROUP:New( PlayerGroup, "Messages and Reports", PlayerMenu ) - + local MetricsMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) + + if self:IsMetric() or _SETTINGS.MenuStatic then + local text="Imperial (Miles,Feet)" + if _SETTINGS.MenuShort then + text="Imperial" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, false ) + end + + if self:IsImperial() or _SETTINGS.MenuStatic then + local text="Metric (Kilometers,Meters)" + if _SETTINGS.MenuShort then + text="Metric" + end + MENU_GROUP_COMMAND:New( PlayerGroup, text, MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, true ) + end + + --- + -- Messages and Reports + --- + + local text="Messages and Reports" + if _SETTINGS.MenuShort then + text="Messages & Reports" + end + local MessagesMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu ) + local UpdateMessagesMenu = MENU_GROUP:New( PlayerGroup, "Update Messages", MessagesMenu ) - MENU_GROUP_COMMAND:New( PlayerGroup, "Off", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 0 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "5 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 5 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "10 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 10 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 15 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 30 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 60 ) - - local InformationMessagesMenu = MENU_GROUP:New( PlayerGroup, "Information Messages", MessagesMenu ) - MENU_GROUP_COMMAND:New( PlayerGroup, "5 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 5 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "10 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 10 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 15 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 30 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 60 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 120 ) - + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates Off", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 0 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 5 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 5 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 10 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 10 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 15 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 15 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 30 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 30 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 1 min", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 60 ) + + local InformationMessagesMenu = MENU_GROUP:New( PlayerGroup, "Info Messages", MessagesMenu ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 5 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 5 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 10 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 10 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 15 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 15 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 30 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 30 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 1 min", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 60 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Info 2 min", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 120 ) + local BriefingReportsMenu = MENU_GROUP:New( PlayerGroup, "Briefing Reports", MessagesMenu ) - MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 15 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 30 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 60 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 120 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "3 minutes", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 180 ) - + MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 15 sec", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 15 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 30 sec", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 30 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 1 min", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 60 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 2 min", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 120 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 3 min", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 180 ) + local OverviewReportsMenu = MENU_GROUP:New( PlayerGroup, "Overview Reports", MessagesMenu ) - MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 15 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 30 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 60 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 120 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "3 minutes", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 180 ) - + MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 15 sec", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 15 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 30 sec", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 30 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 1 min", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 60 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 2 min", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 120 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 3 min", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 180 ) + local DetailedReportsMenu = MENU_GROUP:New( PlayerGroup, "Detailed Reports", MessagesMenu ) - MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 15 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 30 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 60 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 120 ) - MENU_GROUP_COMMAND:New( PlayerGroup, "3 minutes", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 180 ) - - end - + MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 15 sec", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 15 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 30 sec", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 30 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 1 min", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 60 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 2 min", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 120 ) + MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 3 min", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 180 ) + + end + return self end @@ -743,7 +930,7 @@ do -- SETTINGS self.PlayerMenu:Remove() self.PlayerMenu = nil end - + return self end @@ -795,40 +982,50 @@ do -- SETTINGS BASE:E( {self, PlayerUnit:GetName(), A2GSystem} ) self.A2GSystem = A2GSystem MESSAGE:New( string.format( "Settings: A2G format set to %s for player %s.", A2GSystem, PlayerName ), 5 ):ToGroup( PlayerGroup ) - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) + if _SETTINGS.MenuStatic==false then + self:RemovePlayerMenu(PlayerUnit) + self:SetPlayerMenu(PlayerUnit) + end end - + --- @param #SETTINGS self function SETTINGS:MenuGroupA2ASystem( PlayerUnit, PlayerGroup, PlayerName, A2ASystem ) self.A2ASystem = A2ASystem MESSAGE:New( string.format( "Settings: A2A format set to %s for player %s.", A2ASystem, PlayerName ), 5 ):ToGroup( PlayerGroup ) - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) + if _SETTINGS.MenuStatic==false then + self:RemovePlayerMenu(PlayerUnit) + self:SetPlayerMenu(PlayerUnit) + end end - + --- @param #SETTINGS self function SETTINGS:MenuGroupLL_DDM_AccuracySystem( PlayerUnit, PlayerGroup, PlayerName, LL_Accuracy ) self.LL_Accuracy = LL_Accuracy - MESSAGE:New( string.format( "Settings: A2G LL format accuracy set to %d for player %s.", LL_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup ) - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) + MESSAGE:New( string.format( "Settings: LL format accuracy set to %d decimal places for player %s.", LL_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup ) + if _SETTINGS.MenuStatic==false then + self:RemovePlayerMenu(PlayerUnit) + self:SetPlayerMenu(PlayerUnit) + end end - + --- @param #SETTINGS self function SETTINGS:MenuGroupMGRS_AccuracySystem( PlayerUnit, PlayerGroup, PlayerName, MGRS_Accuracy ) self.MGRS_Accuracy = MGRS_Accuracy - MESSAGE:New( string.format( "Settings: A2G MGRS format accuracy set to %d for player %s.", MGRS_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup ) - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) + MESSAGE:New( string.format( "Settings: MGRS format accuracy set to %d for player %s.", MGRS_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup ) + if _SETTINGS.MenuStatic==false then + self:RemovePlayerMenu(PlayerUnit) + self:SetPlayerMenu(PlayerUnit) + end end --- @param #SETTINGS self function SETTINGS:MenuGroupMWSystem( PlayerUnit, PlayerGroup, PlayerName, MW ) self.Metric = MW MESSAGE:New( string.format( "Settings: Measurement format set to %s for player %s.", MW and "Metric" or "Imperial", PlayerName ), 5 ):ToGroup( PlayerGroup ) - self:RemovePlayerMenu(PlayerUnit) - self:SetPlayerMenu(PlayerUnit) + if _SETTINGS.MenuStatic==false then + self:RemovePlayerMenu(PlayerUnit) + self:SetPlayerMenu(PlayerUnit) + end end --- @param #SETTINGS self @@ -836,9 +1033,48 @@ do -- SETTINGS self:SetMessageTime( MessageType, MessageTime ) MESSAGE:New( string.format( "Settings: Default message time set for %s to %d.", MessageType, MessageTime ), 5 ):ToGroup( PlayerGroup ) end - + end + --- Configures the era of the mission to be WWII. + -- @param #SETTINGS self + -- @return #SETTINGS self + function SETTINGS:SetEraWWII() + + self.Era = SETTINGS.__Enum.Era.WWII + + end + + --- Configures the era of the mission to be Korea. + -- @param #SETTINGS self + -- @return #SETTINGS self + function SETTINGS:SetEraKorea() + + self.Era = SETTINGS.__Enum.Era.Korea + + end + + + --- Configures the era of the mission to be Cold war. + -- @param #SETTINGS self + -- @return #SETTINGS self + function SETTINGS:SetEraCold() + + self.Era = SETTINGS.__Enum.Era.Cold + + end + + + --- Configures the era of the mission to be Modern war. + -- @param #SETTINGS self + -- @return #SETTINGS self + function SETTINGS:SetEraModern() + + self.Era = SETTINGS.__Enum.Era.Modern + + end + + + + end - - diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 466e6a70f..ebcfb1f99 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -8,7 +8,7 @@ -- * Schedule spawning of new groups. -- * Put limits on the amount of groups that can be spawned, and the amount of units that can be alive at the same time. -- * Randomize the spawning location between different zones. --- * Randomize the intial positions within the zones. +-- * Randomize the initial positions within the zones. -- * Spawn in array formation. -- * Spawn uncontrolled (for planes or helos only). -- * Clean up inactive helicopters that "crashed". @@ -16,7 +16,7 @@ -- * Spawn late activated. -- * Spawn with or without an initial delay. -- * Respawn after landing, on the runway or at the ramp after engine shutdown. --- * Spawn with custom heading. +-- * Spawn with custom heading, both for a group formation and for the units in the group. -- * Spawn with different skills. -- * Spawn with different liveries. -- * Spawn with an inner and outer radius to set the initial position. @@ -214,9 +214,8 @@ -- ### **Scheduled** spawning methods -- -- * @{#SPAWN.SpawnScheduled}(): Spawn groups at scheduled but randomized intervals. --- * @{#SPAWN.SpawnScheduledStart}(): Start or continue to spawn groups at scheduled time intervals. --- * @{#SPAWN.SpawnScheduledStop}(): Stop the spawning of groups at scheduled time intervals. --- +--- * @{#SPAWN.SpawnScheduleStart}(): Start or continue to spawn groups at scheduled time intervals. +-- * @{#SPAWN.SpawnScheduleStop}(): Stop the spawning of groups at scheduled time intervals. -- -- -- ## Retrieve alive GROUPs spawned by the SPAWN object @@ -260,7 +259,7 @@ -- immediately when :SpawnScheduled() is initiated. The methods @{#SPAWN.InitDelayOnOff}() and @{#SPAWN.InitDelayOn}() can be used to -- activate a delay before the first @{Wrapper.Group} is spawned. For completeness, a method @{#SPAWN.InitDelayOff}() is also available, that -- can be used to switch off the initial delay. Because there is no delay by default, this method would only be used when a --- @{#SPAWN.SpawnScheduledStop}() ; @{#SPAWN.SpawnScheduledStart}() sequence would have been used. +-- @{#SPAWN.SpawnScheduleStop}() ; @{#SPAWN.SpawnScheduleStart}() sequence would have been used. -- -- -- @field #SPAWN SPAWN @@ -319,9 +318,15 @@ function SPAWN:New( SpawnTemplatePrefix ) self.SpawnUnControlled = false self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. self.DelayOnOff = false -- No intial delay when spawning the first group. - self.Grouping = nil -- No grouping. + self.SpawnGrouping = nil -- No grouping. self.SpawnInitLivery = nil -- No special livery. self.SpawnInitSkill = nil -- No special skill. + self.SpawnInitFreq = nil -- No special frequency. + self.SpawnInitModu = nil -- No special modulation. + self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnInitModex = nil + self.SpawnInitAirbase = nil + self.TweakedTemplate = false -- Check if the user is using self made template. self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else @@ -367,9 +372,15 @@ function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) self.SpawnUnControlled = false self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. self.DelayOnOff = false -- No intial delay when spawning the first group. - self.Grouping = nil -- No grouping. + self.SpawnGrouping = nil -- No grouping. self.SpawnInitLivery = nil -- No special livery. self.SpawnInitSkill = nil -- No special skill. + self.SpawnInitFreq = nil -- No special frequency. + self.SpawnInitModu = nil -- No special modulation. + self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnInitModex = nil + self.SpawnInitAirbase = nil + self.TweakedTemplate = false -- Check if the user is using self made template. self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else @@ -398,7 +409,11 @@ end function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPrefix ) local self = BASE:Inherit( self, BASE:New() ) self:F( { SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPrefix } ) - + if SpawnAliasPrefix == nil or SpawnAliasPrefix == "" then + BASE:I("ERROR: in function NewFromTemplate, required paramter SpawnAliasPrefix is not set") + return nil + end + if SpawnTemplate then self.SpawnTemplate = SpawnTemplate -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! self.SpawnTemplatePrefix = SpawnTemplatePrefix @@ -421,7 +436,13 @@ function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPr self.Grouping = nil -- No grouping. self.SpawnInitLivery = nil -- No special livery. self.SpawnInitSkill = nil -- No special skill. - + self.SpawnInitFreq = nil -- No special frequency. + self.SpawnInitModu = nil -- No special modulation. + self.SpawnInitRadio = nil -- No radio comms setting. + self.SpawnInitModex = nil + self.SpawnInitAirbase = nil + self.TweakedTemplate = true -- Check if the user is using self made template. + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else error( "There is no template provided for SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) @@ -434,7 +455,8 @@ function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPr end ---- Limits the Maximum amount of Units that can be alive at the same time, and the maximum amount of groups that can be spawned. +--- Stops any more repeat spawns from happening once the UNIT count of Alive units, spawned by the same SPAWN object, exceeds the first parameter. Also can stop spawns from happening once a total GROUP still alive is met. +-- Exceptionally powerful when combined with SpawnSchedule for Respawning. -- Note that this method is exceptionally important to balance the performance of the mission. Depending on the machine etc, a mission can only process a maximum amount of units. -- If the time interval must be short, but there should not be more Units or Groups alive than a maximum amount of units, then this method should be used... -- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed. @@ -491,6 +513,24 @@ function SPAWN:InitLateActivated( LateActivated ) return self end +--- Set spawns to happen at a particular airbase. Only for aircraft, of course. +-- @param #SPAWN self +-- @param #string AirbaseName Name of the airbase. +-- @param #number Takeoff (Optional) Takeoff type. Can be SPAWN.Takeoff.Hot (default), SPAWN.Takeoff.Cold or SPAWN.Takeoff.Runway. +-- @param #number TerminalTyple (Optional) The terminal type. +-- @return #SPAWN self +function SPAWN:InitAirbase( AirbaseName, Takeoff, TerminalType ) + self:F( ) + + self.SpawnInitAirbase=AIRBASE:FindByName(AirbaseName) + + self.SpawnInitTakeoff=Takeoff or SPAWN.Takeoff.Hot + + self.SpawnInitTerminalType=TerminalType + + return self +end + --- Defines the Heading for the new spawned units. -- The heading can be given as one fixed degree, or can be randomized between minimum and maximum degrees. @@ -518,6 +558,41 @@ function SPAWN:InitHeading( HeadingMin, HeadingMax ) end +--- Defines the heading of the overall formation of the new spawned group. +-- The heading can be given as one fixed degree, or can be randomized between minimum and maximum degrees. +-- The Group's formation as laid out in its template will be rotated around the first unit in the group +-- Group individual units facings will rotate to match. If InitHeading is also applied to this SPAWN then that will take precedence for individual unit facings. +-- Note that InitGroupHeading does *not* rotate the groups route; only its initial facing! +-- @param #SPAWN self +-- @param #number HeadingMin The minimum or fixed heading in degrees. +-- @param #number HeadingMax (optional) The maximum heading in degrees. This there is no maximum heading, then the heading for the group will be HeadingMin. +-- @param #number unitVar (optional) Individual units within the group will have their heading randomized by +/- unitVar degrees. Default is zero. +-- @return #SPAWN self +-- @usage +-- +-- mySpawner = SPAWN:New( ... ) +-- +-- -- Spawn the Group with the formation rotated +100 degrees around unit #1, compared to the mission template. +-- mySpawner:InitGroupHeading( 100 ) +-- +-- Spawn the Group with the formation rotated units between +100 and +150 degrees around unit #1, compared to the mission template, and with individual units varying by +/- 10 degrees from their templated facing. +-- mySpawner:InitGroupHeading( 100, 150, 10 ) +-- +-- Spawn the Group with the formation rotated -60 degrees around unit #1, compared to the mission template, but with all units facing due north regardless of how they were laid out in the template. +-- mySpawner:InitGroupHeading(-60):InitHeading(0) +-- or +-- mySpawner:InitHeading(0):InitGroupHeading(-60) +-- +function SPAWN:InitGroupHeading( HeadingMin, HeadingMax, unitVar ) + self:F({HeadingMin=HeadingMin, HeadingMax=HeadingMax, unitVar=unitVar}) + + self.SpawnInitGroupHeadingMin = HeadingMin + self.SpawnInitGroupHeadingMax = HeadingMax + self.SpawnInitGroupUnitVar = unitVar + return self +end + + --- Sets the coalition of the spawned group. Note that it might be necessary to also set the country explicitly! -- @param #SPAWN self -- @param DCS#coalition.side Coalition Coalition of the group as number of enumerator: @@ -537,7 +612,7 @@ end --- Sets the country of the spawn group. Note that the country determins the coalition of the group depending on which country is defined to be on which side for each specific mission! -- @param #SPAWN self --- @param #DCS.country Country Country id as number or enumerator: +-- @param #number Country Country id as number or enumerator: -- -- * @{DCS#country.id.RUSSIA} -- * @{DCS#county.id.USA} @@ -597,6 +672,55 @@ function SPAWN:InitSkill( Skill ) return self end +--- Sets the radio comms on or off. Same as checking/unchecking the COMM box in the mission editor. +-- @param #SPAWN self +-- @param #number switch If true (or nil), enables the radio comms. If false, disables the radio for the spawned group. +-- @return #SPAWN self +function SPAWN:InitRadioCommsOnOff(switch) + self:F({switch=switch} ) + self.SpawnInitRadio=switch or true + return self +end + +--- Sets the radio frequency of the group. +-- @param #SPAWN self +-- @param #number frequency The frequency in MHz. +-- @return #SPAWN self +function SPAWN:InitRadioFrequency(frequency) + self:F({frequency=frequency} ) + + self.SpawnInitFreq=frequency + + return self +end + +--- Set radio modulation. Default is AM. +-- @param #SPAWN self +-- @param #string modulation Either "FM" or "AM". If no value is given, modulation is set to AM. +-- @return #SPAWN self +function SPAWN:InitRadioModulation(modulation) + self:F({modulation=modulation}) + if modulation and modulation:lower()=="fm" then + self.SpawnInitModu=radio.modulation.FM + else + self.SpawnInitModu=radio.modulation.AM + end + return self +end + +--- Sets the modex of the first unit of the group. If more units are in the group, the number is increased by one with every unit. +-- @param #SPAWN self +-- @param #number modex Modex of the first unit. +-- @return #SPAWN self +function SPAWN:InitModex(modex) + + if modex then + self.SpawnInitModex=tonumber(modex) + end + + return self +end + --- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behaviour of groups. -- @param #SPAWN self @@ -692,9 +816,9 @@ end -- Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5', -- 'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10', -- 'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' } --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 ) function SPAWN:InitRandomizeTemplate( SpawnTemplatePrefixTable ) self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } ) @@ -728,9 +852,9 @@ end -- Spawn_US_PlatoonSet = SET_GROUP:New():FilterPrefixes( "US Tank Platoon Templates" ):FilterOnce() -- -- --- Now use the Spawn_US_PlatoonSet to define the templates using InitRandomizeTemplateSet. --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 ) function SPAWN:InitRandomizeTemplateSet( SpawnTemplateSet ) -- R2.3 self:F( { self.SpawnTemplatePrefix } ) @@ -761,9 +885,9 @@ end -- -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and -- -- with a limit set of maximum 12 Units alive simulteneously and 150 Groups to be spawned during the whole mission. -- --- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) --- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) +-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 ) function SPAWN:InitRandomizeTemplatePrefixes( SpawnTemplatePrefixes ) --R2.3 self:F( { self.SpawnTemplatePrefix } ) @@ -855,11 +979,9 @@ end -- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. -- SpawnRU_SU34 = SPAWN -- :New( 'Su-34' ) --- :Schedule( 2, 3, 1800, 0.4 ) --- :SpawnUncontrolled() -- :InitRandomizeRoute( 1, 1, 3000 ) -- :InitRepeatOnLanding() --- +-- :Spawn() function SPAWN:InitRepeatOnLanding() self:F( { self.SpawnTemplatePrefix } ) @@ -879,11 +1001,10 @@ end -- -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically. -- SpawnRU_SU34 = SPAWN -- :New( 'Su-34' ) --- :Schedule( 2, 3, 1800, 0.4 ) -- :SpawnUncontrolled() -- :InitRandomizeRoute( 1, 1, 3000 ) -- :InitRepeatOnEngineShutDown() --- +-- :Spawn() function SPAWN:InitRepeatOnEngineShutDown() self:F( { self.SpawnTemplatePrefix } ) @@ -895,7 +1016,8 @@ function SPAWN:InitRepeatOnEngineShutDown() end ---- CleanUp groups when they are still alive, but inactive. +--- Delete groups that have not moved for X seconds - AIR ONLY!!! +-- DO NOT USE ON GROUPS THAT DO NOT MOVE OR YOUR SERVER WILL BURN IN HELL (Pikes - April 2020) -- When groups are still alive and have become inactive due to damage and are unable to contribute anything, then this group will be removed at defined intervals in seconds. -- @param #SPAWN self -- @param #string SpawnCleanUpInterval The interval to check for inactive groups within seconds. @@ -920,6 +1042,7 @@ end --- Makes the groups visible before start (like a batallion). -- The method will take the position of the group as the first position in the array. +-- CAUTION: this directive will NOT work with OnSpawnGroup function. -- @param #SPAWN self -- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. -- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. @@ -1054,7 +1177,12 @@ end -- Delay methods function SPAWN:Spawn() self:F( { self.SpawnTemplatePrefix, self.SpawnIndex, self.AliveUnits } ) - return self:SpawnWithIndex( self.SpawnIndex + 1 ) + if self.SpawnInitAirbase then + return self:SpawnAtAirbase(self.SpawnInitAirbase, self.SpawnInitTakeoff, nil, self.SpawnInitTerminalType) + else + return self:SpawnWithIndex( self.SpawnIndex + 1 ) + end + end --- Will re-spawn a group based on a given index. @@ -1069,7 +1197,7 @@ function SPAWN:ReSpawn( SpawnIndex ) SpawnIndex = 1 end --- TODO: This logic makes DCS crash and i don't know why (yet). +-- TODO: This logic makes DCS crash and i don't know why (yet). -- ED (Pikes -- not in the least bit scary to see this, right?) local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) local WayPoints = SpawnGroup and SpawnGroup.WayPoints or nil if SpawnGroup then @@ -1095,12 +1223,24 @@ function SPAWN:ReSpawn( SpawnIndex ) return SpawnGroup end + +--- Set the spawn index to a specified index number. +-- This method can be used to "reset" the spawn counter to a specific index number. +-- This will actually enable a respawn of groups from the specific index. +-- @param #SPAWN self +-- @param #string SpawnIndex The index of the group from where the spawning will start again. The default value would be 0, which means a complete reset of the spawnindex. +-- @return #SPAWN self +function SPAWN:SetSpawnIndex( SpawnIndex ) + self.SpawnIndex = SpawnIndex or 0 +end + + --- Will spawn a group with a specified index number. -- Uses @{DATABASE} global object defined in MOOSE. -- @param #SPAWN self -- @param #string SpawnIndex The index of the group to be spawned. -- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions. -function SPAWN:SpawnWithIndex( SpawnIndex ) +function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) self:F2( { SpawnTemplatePrefix = self.SpawnTemplatePrefix, SpawnIndex = SpawnIndex, AliveUnits = self.AliveUnits, SpawnMaxGroups = self.SpawnMaxGroups } ) if self:_GetSpawnIndex( SpawnIndex ) then @@ -1141,21 +1281,72 @@ function SPAWN:SpawnWithIndex( SpawnIndex ) end end - -- Get correct heading. - local function _Heading(course) + -- Get correct heading in Radians. + local function _Heading(courseDeg) local h - if course<=180 then - h=math.rad(course) + if courseDeg<=180 then + h=math.rad(courseDeg) else - h=-math.rad(360-course) + h=-math.rad(360-courseDeg) end return h end + + local Rad180 = math.rad(180) + local function _HeadingRad(courseRad) + if courseRad<=Rad180 then + return courseRad + else + return -((2*Rad180)-courseRad) + end + end + + -- Generate a random value somewhere between two floating point values. + local function _RandomInRange ( min, max ) + if min and max then + return min + ( math.random()*(max-min) ) + else + return min + end + end + + -- Apply InitGroupHeading rotation if requested. + -- We do this before InitHeading unit rotation so that can take precedence + -- NOTE: Does *not* rotate the groups route; only its initial facing. + if self.SpawnInitGroupHeadingMin and #SpawnTemplate.units > 0 then + + local pivotX = SpawnTemplate.units[1].x -- unit #1 is the pivot point + local pivotY = SpawnTemplate.units[1].y + + local headingRad = math.rad(_RandomInRange(self.SpawnInitGroupHeadingMin or 0,self.SpawnInitGroupHeadingMax)) + local cosHeading = math.cos(headingRad) + local sinHeading = math.sin(headingRad) + + local unitVarRad = math.rad(self.SpawnInitGroupUnitVar or 0) + + for UnitID = 1, #SpawnTemplate.units do + + if UnitID > 1 then -- don't rotate position of unit #1 + local unitXOff = SpawnTemplate.units[UnitID].x - pivotX -- rotate position offset around unit #1 + local unitYOff = SpawnTemplate.units[UnitID].y - pivotY + + SpawnTemplate.units[UnitID].x = pivotX + (unitXOff*cosHeading) - (unitYOff*sinHeading) + SpawnTemplate.units[UnitID].y = pivotY + (unitYOff*cosHeading) + (unitXOff*sinHeading) + end + + -- adjust heading of all units, including unit #1 + local unitHeading = SpawnTemplate.units[UnitID].heading + headingRad -- add group rotation to units default rotation + SpawnTemplate.units[UnitID].heading = _HeadingRad(_RandomInRange(unitHeading-unitVarRad, unitHeading+unitVarRad)) + SpawnTemplate.units[UnitID].psi = -SpawnTemplate.units[UnitID].heading + + end + + end - -- If Heading is given, point all the units towards the given Heading. + -- If Heading is given, point all the units towards the given Heading. Overrides any heading set in InitGroupHeading above. if self.SpawnInitHeadingMin then for UnitID = 1, #SpawnTemplate.units do - SpawnTemplate.units[UnitID].heading = _Heading(self.SpawnInitHeadingMax and math.random( self.SpawnInitHeadingMin, self.SpawnInitHeadingMax ) or self.SpawnInitHeadingMin) + SpawnTemplate.units[UnitID].heading = _Heading(_RandomInRange(self.SpawnInitHeadingMin, self.SpawnInitHeadingMax)) SpawnTemplate.units[UnitID].psi = -SpawnTemplate.units[UnitID].heading end end @@ -1173,6 +1364,28 @@ function SPAWN:SpawnWithIndex( SpawnIndex ) SpawnTemplate.units[UnitID].skill = self.SpawnInitSkill end end + + -- Set tail number. + if self.SpawnInitModex then + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].onboard_num = string.format("%03d", self.SpawnInitModex+(UnitID-1)) + end + end + + -- Set radio comms on/off. + if self.SpawnInitRadio then + SpawnTemplate.communication=self.SpawnInitRadio + end + + -- Set radio frequency. + if self.SpawnInitFreq then + SpawnTemplate.frequency=self.SpawnInitFreq + end + + -- Set radio modulation. + if self.SpawnInitModu then + SpawnTemplate.modulation=self.SpawnInitModu + end -- Set country, coaliton and categroy. SpawnTemplate.CategoryID = self.SpawnInitCategory or SpawnTemplate.CategoryID @@ -1180,14 +1393,16 @@ function SPAWN:SpawnWithIndex( SpawnIndex ) SpawnTemplate.CoalitionID = self.SpawnInitCoalition or SpawnTemplate.CoalitionID - if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then - if SpawnTemplate.route.points[1].type == "TakeOffParking" then - SpawnTemplate.uncontrolled = self.SpawnUnControlled - end - end +-- if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then +-- if SpawnTemplate.route.points[1].type == "TakeOffParking" then +-- SpawnTemplate.uncontrolled = self.SpawnUnControlled +-- end +-- end end - self:HandleEvent( EVENTS.Birth, self._OnBirth ) + if not NoBirth then + self:HandleEvent( EVENTS.Birth, self._OnBirth ) + end self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash ) self:HandleEvent( EVENTS.Crash, self._OnDeadOrCrash ) self:HandleEvent( EVENTS.RemoveUnit, self._OnDeadOrCrash ) @@ -1222,6 +1437,7 @@ function SPAWN:SpawnWithIndex( SpawnIndex ) --end end + self.SpawnGroups[self.SpawnIndex].Spawned = true return self.SpawnGroups[self.SpawnIndex].Group else @@ -1246,7 +1462,7 @@ end -- -- Low limit: 600 * ( 1 - 0.5 / 2 ) = 450 -- -- High limit: 600 * ( 1 + 0.5 / 2 ) = 750 -- -- Between these two values, a random amount of seconds will be choosen for each new spawn of the helicopters. --- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 ) +-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):SpawnScheduled( 600, 0.5 ) function SPAWN:SpawnScheduled( SpawnTime, SpawnTimeVariation ) self:F( { SpawnTime, SpawnTimeVariation } ) @@ -1341,7 +1557,7 @@ end -- @param Wrapper.Airbase#AIRBASE.TerminalType TerminalType (optional) The terminal type the aircraft should be spawned at. See @{Wrapper.Airbase#AIRBASE.TerminalType}. -- @param #boolean EmergencyAirSpawn (optional) If true (default), groups are spawned in air if there is no parking spot at the airbase. If false, nothing is spawned if no parking spot is available. -- @param #table Parkingdata (optional) Table holding the coordinates and terminal ids for all units of the group. Spawning will be forced to happen at exactily these spots! --- @return Wrapper.Group#GROUP that was spawned or nil when nothing was spawned. +-- @return Wrapper.Group#GROUP The group that was spawned or nil when nothing was spawned. -- @usage -- Spawn_Plane = SPAWN:New( "Plane" ) -- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Cold ) @@ -1376,12 +1592,23 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT EmergencyAirSpawn=true end + self:F( { SpawnIndex = self.SpawnIndex } ) + if self:_GetSpawnIndex( self.SpawnIndex + 1 ) then -- Get group template. local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate + self:F( { SpawnTemplate = SpawnTemplate } ) + if SpawnTemplate then + + -- Check if the aircraft with the specified SpawnIndex is already spawned. + -- If yes, ensure that the aircraft is spawned at the same aircraft spot. + + local GroupAlive = self:GetGroupFromIndex( self.SpawnIndex ) + + self:F( { GroupAlive = GroupAlive } ) -- Debug output self:T( { "Current point of ", self.SpawnTemplatePrefix, SpawnAirbase } ) @@ -1389,10 +1616,15 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT -- Template group, unit and its attributes. local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix) local TemplateUnit=TemplateGroup:GetUnit(1) + + -- General category of spawned group. + local group=TemplateGroup + local istransport=group:HasAttribute("Transports") and group:HasAttribute("Planes") + local isawacs=group:HasAttribute("AWACS") + local isfighter=group:HasAttribute("Fighters") or group:HasAttribute("Interceptors") or group:HasAttribute("Multirole fighters") or (group:HasAttribute("Bombers") and not group:HasAttribute("Strategic bombers")) + local isbomber=group:HasAttribute("Strategic bombers") + local istanker=group:HasAttribute("Tankers") local ishelo=TemplateUnit:HasAttribute("Helicopters") - local isbomber=TemplateUnit:HasAttribute("Bombers") - local istransport=TemplateUnit:HasAttribute("Transports") - local isfighter=TemplateUnit:HasAttribute("Battleplanes") -- Number of units in the group. With grouping this can actually differ from the template group size! local nunits=#SpawnTemplate.units @@ -1407,7 +1639,7 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT -- Get airbase ID and category. local AirbaseID = SpawnAirbase:GetID() - local AirbaseCategory = SpawnAirbase:GetDesc().category + local AirbaseCategory = SpawnAirbase:GetAirbaseCategory() self:F( { AirbaseCategory = AirbaseCategory } ) -- Set airdromeId. @@ -1452,15 +1684,25 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT local spots -- Spawn happens on ground, i.e. at an airbase, a FARP or a ship. - if spawnonground then + if spawnonground and not SpawnTemplate.parked then + -- Number of free parking spots. local nfree=0 -- Set terminal type. local termtype=TerminalType - if spawnonrunway then - termtype=AIRBASE.TerminalType.Runway + if spawnonrunway then + if spawnonship then + -- Looks like there are no runway spawn spots on the stennis! + if ishelo then + termtype=AIRBASE.TerminalType.HelicopterUsable + else + termtype=AIRBASE.TerminalType.OpenMedOrBig + end + else + termtype=AIRBASE.TerminalType.Runway + end end -- Scan options. Might make that input somehow. @@ -1502,10 +1744,7 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT else -- Fixed wing aircraft is spawned. if termtype==nil then - --TODO: Add some default cases for transport, bombers etc. if no explicit terminal type is provided. - --TODO: We don't want Bombers to spawn in shelters. But I don't know a good attribute for just fighers. - --TODO: Some attributes are "Helicopters", "Bombers", "Transports", "Battleplanes". Need to check it out. - if isbomber or istransport then + if isbomber or istransport or istanker or isawacs then -- First we fill the potentially bigger spots. self:T(string.format("Transport/bomber group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.OpenBig)) spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.OpenBig, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits) @@ -1532,9 +1771,9 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT -- Get parking data. local parkingdata=SpawnAirbase:GetParkingSpotsTable(termtype) - self:T2(string.format("Parking at %s, terminal type %s:", SpawnAirbase:GetName(), tostring(termtype))) + self:T(string.format("Parking at %s, terminal type %s:", SpawnAirbase:GetName(), tostring(termtype))) for _,_spot in pairs(parkingdata) do - self:T2(string.format("%s, Termin Index = %3d, Term Type = %03d, Free = %5s, TOAC = %5s, Term ID0 = %3d, Dist2Rwy = %4d", + self:T(string.format("%s, Termin Index = %3d, Term Type = %03d, Free = %5s, TOAC = %5s, Term ID0 = %3d, Dist2Rwy = %4d", SpawnAirbase:GetName(), _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy)) end self:T(string.format("%s at %s: free parking spots = %d - number of units = %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), nfree, nunits)) @@ -1625,9 +1864,407 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT end + if not SpawnTemplate.parked then + -- Translate the position of the Group Template to the Vec3. + + SpawnTemplate.parked = true + + for UnitID = 1, nunits do + self:T2('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + + -- Template of the current unit. + local UnitTemplate = SpawnTemplate.units[UnitID] + + -- Tranlate position and preserve the relative position/formation of all aircraft. + local SX = UnitTemplate.x + local SY = UnitTemplate.y + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = PointVec3.x + (SX-BX) + local TY = PointVec3.z + (SY-BY) + + if spawnonground then + + -- Ships and FARPS seem to have a build in queue. + if spawnonship or spawnonfarp or spawnonrunway then + + self:T(string.format("Group %s spawning at farp, ship or runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + -- Spawn on ship. We take only the position of the ship. + SpawnTemplate.units[UnitID].x = PointVec3.x --TX + SpawnTemplate.units[UnitID].y = PointVec3.z --TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + + else + + self:T(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + + -- Get coordinates of parking spot. + SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x + SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z + SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y + + --parkingspots[UnitID]:MarkToAll(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + end + + else + + self:T(string.format("Group %s spawning in air at %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. + SpawnTemplate.units[UnitID].x = TX + SpawnTemplate.units[UnitID].y = TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + + end + + -- Parking spot id. + UnitTemplate.parking = nil + UnitTemplate.parking_id = nil + if parkingindex[UnitID] then + UnitTemplate.parking = parkingindex[UnitID] + end + + -- Debug output. + self:T(string.format("Group %s unit number %d: Parking = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking))) + self:T(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking_id))) + self:T2('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + end + end + + -- Set gereral spawnpoint position. + SpawnPoint.x = PointVec3.x + SpawnPoint.y = PointVec3.z + SpawnPoint.alt = PointVec3.y + + SpawnTemplate.x = PointVec3.x + SpawnTemplate.y = PointVec3.z + + SpawnTemplate.uncontrolled = self.SpawnUnControlled + + -- Spawn group. + local GroupSpawned = self:SpawnWithIndex( self.SpawnIndex ) + + -- When spawned in the air, we need to generate a Takeoff Event. + if Takeoff == GROUP.Takeoff.Air then + for UnitID, UnitSpawned in pairs( GroupSpawned:GetUnits() ) do + SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() } , 5 ) + end + end + + -- Check if we accidentally spawned on the runway. Needs to be schedules, because group is not immidiately alive. + if Takeoff~=SPAWN.Takeoff.Runway and Takeoff~=SPAWN.Takeoff.Air and spawnonairport then + SCHEDULER:New(nil, AIRBASE.CheckOnRunWay, {SpawnAirbase, GroupSpawned, 75, true} , 1.0) + end + + return GroupSpawned + end + end + + return nil +end + +--- Spawn a group on an @{Wrapper.Airbase} at a specific parking spot. +-- @param #SPAWN self +-- @param Wrapper.Airbase#AIRBASE Airbase The @{Wrapper.Airbase} where to spawn the group. +-- @param #table Spots Table of parking spot IDs. Note that these in general are different from the numbering in the mission editor! +-- @param #SPAWN.Takeoff Takeoff (Optional) Takeoff type, i.e. either SPAWN.Takeoff.Cold or SPAWN.Takeoff.Hot. Default is Hot. +-- @return Wrapper.Group#GROUP The group that was spawned or nil when nothing was spawned. +function SPAWN:SpawnAtParkingSpot(Airbase, Spots, Takeoff) -- R2.5 + self:F({Airbase=Airbase, Spots=Spots, Takeoff=Takeoff}) + + -- Ensure that Spots parameter is a table. + if type(Spots)~="table" then + Spots={Spots} + end + + -- Get template group. + local group=GROUP:FindByName(self.SpawnTemplatePrefix) + + -- Get number of units in group. + local nunits=self.SpawnGrouping or #group:GetUnits() + + -- Quick check. + if nunits then + + -- Check that number of provided parking spots is large enough. + if #Spots=nunits then + return self:SpawnAtAirbase(Airbase, Takeoff, nil, nil, nil, Parkingdata) + else + self:E("ERROR: Could not find enough free parking spots!") + end + + + else + self:E("ERROR: Could not get number of units in group!") + end + + return nil +end + +--- Will park a group at an @{Wrapper.Airbase}. +-- +-- @param #SPAWN self +-- @param Wrapper.Airbase#AIRBASE SpawnAirbase The @{Wrapper.Airbase} where to spawn the group. +-- @param Wrapper.Airbase#AIRBASE.TerminalType TerminalType (optional) The terminal type the aircraft should be spawned at. See @{Wrapper.Airbase#AIRBASE.TerminalType}. +-- @param #table Parkingdata (optional) Table holding the coordinates and terminal ids for all units of the group. Spawning will be forced to happen at exactily these spots! +-- @return #nil Nothing is returned! +function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex ) + + self:F( { SpawnIndex = SpawnIndex, SpawnMaxGroups = self.SpawnMaxGroups } ) + + -- Get position of airbase. + local PointVec3 = SpawnAirbase:GetCoordinate() + self:T2(PointVec3) + + -- Set take off type. Default is hot. + local Takeoff = SPAWN.Takeoff.Cold + + -- Get group template. + local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate + + if SpawnTemplate then + + -- Check if the aircraft with the specified SpawnIndex is already spawned. + -- If yes, ensure that the aircraft is spawned at the same aircraft spot. + + local GroupAlive = self:GetGroupFromIndex( SpawnIndex ) + + -- Debug output + self:T( { "Current point of ", self.SpawnTemplatePrefix, SpawnAirbase } ) + + -- Template group, unit and its attributes. + local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix) + local TemplateUnit=TemplateGroup:GetUnit(1) + local ishelo=TemplateUnit:HasAttribute("Helicopters") + local isbomber=TemplateUnit:HasAttribute("Bombers") + local istransport=TemplateUnit:HasAttribute("Transports") + local isfighter=TemplateUnit:HasAttribute("Battleplanes") + + -- Number of units in the group. With grouping this can actually differ from the template group size! + local nunits=#SpawnTemplate.units + + -- First waypoint of the group. + local SpawnPoint = SpawnTemplate.route.points[1] + + -- These are only for ships and FARPS. + SpawnPoint.linkUnit = nil + SpawnPoint.helipadId = nil + SpawnPoint.airdromeId = nil + + -- Get airbase ID and category. + local AirbaseID = SpawnAirbase:GetID() + local AirbaseCategory = SpawnAirbase:GetAirbaseCategory() + self:F( { AirbaseCategory = AirbaseCategory } ) + + -- Set airdromeId. + if AirbaseCategory == Airbase.Category.SHIP then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.HELIPAD then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.AIRDROME then + SpawnPoint.airdromeId = AirbaseID + end + + -- Set waypoint type/action. + SpawnPoint.alt = 0 + SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + + -- Check if we spawn on ground. + local spawnonground=not (Takeoff==SPAWN.Takeoff.Air) + self:T({spawnonground=spawnonground, TOtype=Takeoff, TOair=Takeoff==SPAWN.Takeoff.Air}) + + -- Check where we actually spawn if we spawn on ground. + local spawnonship=false + local spawnonfarp=false + local spawnonrunway=false + local spawnonairport=false + if spawnonground then + if AirbaseCategory == Airbase.Category.SHIP then + spawnonship=true + elseif AirbaseCategory == Airbase.Category.HELIPAD then + spawnonfarp=true + elseif AirbaseCategory == Airbase.Category.AIRDROME then + spawnonairport=true + end + spawnonrunway=Takeoff==SPAWN.Takeoff.Runway + end + + -- Array with parking spots coordinates. + local parkingspots={} + local parkingindex={} + local spots + + -- Spawn happens on ground, i.e. at an airbase, a FARP or a ship. + if spawnonground and not SpawnTemplate.parked then + + + -- Number of free parking spots. + local nfree=0 + + -- Set terminal type. + local termtype=TerminalType + + -- Scan options. Might make that input somehow. + local scanradius=50 + local scanunits=true + local scanstatics=true + local scanscenery=false + local verysafe=false + + -- Number of free parking spots at the airbase. + if spawnonship or spawnonfarp or spawnonrunway then + -- These places work procedural and have some kind of build in queue ==> Less effort. + self:T(string.format("Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype, true) + spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype, true) + elseif Parkingdata~=nil then + -- Parking data explicitly set by user as input parameter. + nfree=#Parkingdata + spots=Parkingdata + else + if ishelo then + if termtype==nil then + -- Helo is spawned. Try exclusive helo spots first. + self:T(string.format("Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterOnly)) + spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits) + nfree=#spots + if nfree=1 then + + -- All units get the same spot. DCS takes care of the rest. + for i=1,nunits do + table.insert(parkingspots, spots[1].Coordinate) + table.insert(parkingindex, spots[1].TerminalID) + end + -- This is actually used... + PointVec3=spots[1].Coordinate + + else + -- If there is absolutely no spot ==> air start! + _notenough=true + end + + elseif spawnonairport then + + if nfree>=nunits then + + for i=1,nunits do + table.insert(parkingspots, spots[i].Coordinate) + table.insert(parkingindex, spots[i].TerminalID) + end + + else + -- Not enough spots for the whole group ==> air start! + _notenough=true + end + end + + -- Not enough spots ==> Prepare airstart. + if _notenough then + + if not self.SpawnUnControlled then + else + self:E(string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + return nil + end + end + + else + + end + + if not SpawnTemplate.parked then -- Translate the position of the Group Template to the Vec3. + + SpawnTemplate.parked = true + for UnitID = 1, nunits do - self:T2('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + self:F('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) -- Template of the current unit. local UnitTemplate = SpawnTemplate.units[UnitID] @@ -1687,33 +2324,86 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT self:T2(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking_id))) self:T2('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) end - - -- Set gereral spawnpoint position. - SpawnPoint.x = PointVec3.x - SpawnPoint.y = PointVec3.z - SpawnPoint.alt = PointVec3.y - - SpawnTemplate.x = PointVec3.x - SpawnTemplate.y = PointVec3.z - - -- Spawn group. - local GroupSpawned = self:SpawnWithIndex( self.SpawnIndex ) - - -- When spawned in the air, we need to generate a Takeoff Event. - if Takeoff == GROUP.Takeoff.Air then - for UnitID, UnitSpawned in pairs( GroupSpawned:GetUnits() ) do - SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() } , 5 ) - end - end - - -- Check if we accidentally spawned on the runway. Needs to be schedules, because group is not immidiately alive. - if Takeoff~=SPAWN.Takeoff.Runway and Takeoff~=SPAWN.Takeoff.Air and spawnonairport then - SCHEDULER:New(nil, AIRBASE.CheckOnRunWay, {SpawnAirbase, GroupSpawned, 75, true} , 1.0) - end - - return GroupSpawned end + + -- Set general spawnpoint position. + SpawnPoint.x = PointVec3.x + SpawnPoint.y = PointVec3.z + SpawnPoint.alt = PointVec3.y + + SpawnTemplate.x = PointVec3.x + SpawnTemplate.y = PointVec3.z + + SpawnTemplate.uncontrolled = true + + -- Spawn group. + local GroupSpawned = self:SpawnWithIndex( SpawnIndex, true ) + + -- When spawned in the air, we need to generate a Takeoff Event. + if Takeoff == GROUP.Takeoff.Air then + for UnitID, UnitSpawned in pairs( GroupSpawned:GetUnits() ) do + SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() } , 5 ) + end + end + + -- Check if we accidentally spawned on the runway. Needs to be schedules, because group is not immidiately alive. + if Takeoff~=SPAWN.Takeoff.Runway and Takeoff~=SPAWN.Takeoff.Air and spawnonairport then + SCHEDULER:New(nil, AIRBASE.CheckOnRunWay, {SpawnAirbase, GroupSpawned, 75, true} , 1.0) + end + end + +end + +--- Will park a group at an @{Wrapper.Airbase}. +-- This method is mostly advisable to be used if you want to simulate parking units at an airbase and be visible. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- +-- All groups that are in the spawn collection and that are alive, and not in the air, are parked. +-- +-- The @{Wrapper.Airbase#AIRBASE} object must refer to a valid airbase known in the sim. +-- You can use the following enumerations to search for the pre-defined airbases on the current known maps of DCS: +-- +-- * @{Wrapper.Airbase#AIRBASE.Caucasus}: The airbases on the Caucasus map. +-- * @{Wrapper.Airbase#AIRBASE.Nevada}: The airbases on the Nevada (NTTR) map. +-- * @{Wrapper.Airbase#AIRBASE.Normandy}: The airbases on the Normandy map. +-- +-- Use the method @{Wrapper.Airbase#AIRBASE.FindByName}() to retrieve the airbase object. +-- The known AIRBASE objects are automatically imported at mission start by MOOSE. +-- Therefore, there isn't any New() constructor defined for AIRBASE objects. +-- +-- Ships and Farps are added within the mission, and are therefore not known. +-- For these AIRBASE objects, there isn't an @{Wrapper.Airbase#AIRBASE} enumeration defined. +-- You need to provide the **exact name** of the airbase as the parameter to the @{Wrapper.Airbase#AIRBASE.FindByName}() method! +-- +-- @param #SPAWN self +-- @param Wrapper.Airbase#AIRBASE SpawnAirbase The @{Wrapper.Airbase} where to spawn the group. +-- @param Wrapper.Airbase#AIRBASE.TerminalType TerminalType (optional) The terminal type the aircraft should be spawned at. See @{Wrapper.Airbase#AIRBASE.TerminalType}. +-- @param #table Parkingdata (optional) Table holding the coordinates and terminal ids for all units of the group. Spawning will be forced to happen at exactily these spots! +-- @return #nil Nothing is returned! +-- @usage +-- Spawn_Plane = SPAWN:New( "Plane" ) +-- Spawn_Plane:ParkAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ) ) +-- +-- Spawn_Heli = SPAWN:New( "Heli") +-- +-- Spawn_Heli:ParkAtAirbase( AIRBASE:FindByName( "FARP Cold" ) ) +-- +-- Spawn_Heli:ParkAtAirbase( AIRBASE:FindByName( "Carrier" ) ) +-- +-- Spawn_Plane:ParkAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), AIRBASE.TerminalType.OpenBig ) +-- +function SPAWN:ParkAtAirbase( SpawnAirbase, TerminalType, Parkingdata ) -- R2.2, R2.4, R2.5 + self:F( { self.SpawnTemplatePrefix, SpawnAirbase, TerminalType } ) + + self:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, 1 ) + + for SpawnIndex = 2, self.SpawnMaxGroups do + self:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex ) + --self:ScheduleOnce( SpawnIndex * 0.1, SPAWN.ParkAircraft, self, SpawnAirbase, TerminalType, Parkingdata, SpawnIndex ) + end + + self:SetSpawnIndex( 0 ) return nil end @@ -2005,7 +2695,7 @@ end function SPAWN:InitUnControlled( UnControlled ) self:F2( { self.SpawnTemplatePrefix, UnControlled } ) - self.SpawnUnControlled = UnControlled or true + self.SpawnUnControlled = ( UnControlled == true ) and true or nil for SpawnGroupID = 1, self.SpawnMaxGroups do self.SpawnGroups[SpawnGroupID].UnControlled = self.SpawnUnControlled @@ -2114,10 +2804,9 @@ end -- -- Do actions with the GroupPlane object. -- end function SPAWN:GetLastAliveGroup() - self:F( { self.SpawnTemplatePrefixself.SpawnAliasPrefix } ) - - self.SpawnIndex = self:_GetLastIndex() - for SpawnIndex = self.SpawnIndex, 1, -1 do + self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) + + for SpawnIndex = self.SpawnCount, 1, -1 do -- Added local SpawnGroup = self:GetGroupFromIndex( SpawnIndex ) if SpawnGroup and SpawnGroup:IsAlive() then self.SpawnIndex = SpawnIndex @@ -2296,9 +2985,15 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) --R2.2 -- self.SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) -- end - local SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) - --local SpawnTemplate = self.SpawnTemplate - SpawnTemplate.name = self:SpawnGroupName( SpawnIndex ) + local SpawnTemplate + if self.TweakedTemplate ~= nil and self.TweakedTemplate == true then + BASE:I("WARNING: You are using a tweaked template.") + SpawnTemplate = self.SpawnTemplate + else + SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) + SpawnTemplate.name = self:SpawnGroupName( SpawnIndex ) + end + SpawnTemplate.groupId = nil --SpawnTemplate.lateActivation = false @@ -2340,6 +3035,21 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) --R2.2 SpawnTemplate.units[UnitID].unitId = nil end end + + -- Callsign + for UnitID = 1, #SpawnTemplate.units do + local Callsign = SpawnTemplate.units[UnitID].callsign + if Callsign then + if type(Callsign) ~= "number" then -- blue callsign + Callsign[2] = ( ( SpawnIndex - 1 ) % 10 ) + 1 + local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string + local CallsignLen = CallsignName:len() + SpawnTemplate.units[UnitID].callsign["name"] = CallsignName:sub(1,CallsignLen) .. SpawnTemplate.units[UnitID].callsign[2] .. SpawnTemplate.units[UnitID].callsign[3] + else + SpawnTemplate.units[UnitID].callsign = Callsign + SpawnIndex + end + end + end self:T3( { "Template:", SpawnTemplate } ) return SpawnTemplate @@ -2504,6 +3214,9 @@ function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, Spa end --- Get the next index of the groups to be spawned. This method is complicated, as it is used at several spaces. +-- @param #SPAWN self +-- @param #number SpawnIndex Spawn index. +-- @return #number self.SpawnIndex function SPAWN:_GetSpawnIndex( SpawnIndex ) self:F2( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #self.SpawnTemplate.units } ) @@ -2611,7 +3324,10 @@ function SPAWN:_OnLand( EventData ) if self.RepeatOnLanding then local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) self:T( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) + --self:ReSpawn( SpawnGroupIndex ) + -- Delay respawn by three seconds due to DCS 2.5.4.26368 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076 + -- Bug was initially only for engine shutdown event but after ED "fixed" it, it now happens on landing events. + SCHEDULER:New(nil, self.ReSpawn, {self, SpawnGroupIndex}, 3) end end end @@ -2637,7 +3353,9 @@ function SPAWN:_OnEngineShutDown( EventData ) if Landed and self.RepeatOnEngineShutDown then local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup ) self:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } ) - self:ReSpawn( SpawnGroupIndex ) + --self:ReSpawn( SpawnGroupIndex ) + -- Delay respawn by three seconds due to DCS 2.5.4 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076 + SCHEDULER:New(nil, self.ReSpawn, {self, SpawnGroupIndex}, 3) end end end @@ -2646,6 +3364,7 @@ end --- This function is called automatically by the Spawning scheduler. -- It is the internal worker method SPAWNing new Groups on the defined time intervals. +-- @param #SPAWN self function SPAWN:_Scheduler() self:F2( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } ) diff --git a/Moose Development/Moose/Core/SpawnStatic.lua b/Moose Development/Moose/Core/SpawnStatic.lua index 0081195a5..8f96a1c60 100644 --- a/Moose Development/Moose/Core/SpawnStatic.lua +++ b/Moose Development/Moose/Core/SpawnStatic.lua @@ -195,21 +195,42 @@ function SPAWNSTATIC:SpawnFromPointVec2( PointVec2, Heading, NewName ) --R2.1 end ---- Respawns the original @{Static}. +--- Creates a new @{Static} from a COORDINATE. -- @param #SPAWNSTATIC self +-- @param Core.Point#COORDINATE Coordinate The 3D coordinate where to spawn the static. +-- @param #number Heading (Optional) Heading The heading of the static, which is a number in degrees from 0 to 360. Default is 0 degrees. +-- @param #string NewName (Optional) The name of the new static. -- @return #SPAWNSTATIC -function SPAWNSTATIC:ReSpawn() +function SPAWNSTATIC:SpawnFromCoordinate(Coordinate, Heading, NewName) --R2.4 + self:F( { PointVec2, Heading, NewName } ) local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix ) if StaticTemplate then - + + Heading=Heading or 0 + local StaticUnitTemplate = StaticTemplate.units[1] + + StaticUnitTemplate.x = Coordinate.x + StaticUnitTemplate.y = Coordinate.z + StaticUnitTemplate.alt = Coordinate.y + StaticTemplate.route = nil StaticTemplate.groupId = nil + StaticTemplate.name = NewName or string.format("%s#%05d", self.SpawnTemplatePrefix, self.SpawnIndex ) + StaticUnitTemplate.name = StaticTemplate.name + StaticUnitTemplate.heading = ( Heading / 180 ) * math.pi + + _DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, CategoryID, CountryID) + + self:F({StaticTemplate = StaticTemplate}) + local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] ) + self.SpawnIndex = self.SpawnIndex + 1 + return _DATABASE:FindStatic(Static:getName()) end @@ -217,6 +238,36 @@ function SPAWNSTATIC:ReSpawn() end +--- Respawns the original @{Static}. +-- @param #SPAWNSTATIC self +-- @param #number delay Delay before respawn in seconds. +-- @return #SPAWNSTATIC +function SPAWNSTATIC:ReSpawn(delay) + + if delay and delay>0 then + self:ScheduleOnce(delay, SPAWNSTATIC.ReSpawn, self) + else + + local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix ) + + if StaticTemplate then + + local StaticUnitTemplate = StaticTemplate.units[1] + StaticTemplate.route = nil + StaticTemplate.groupId = nil + + local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] ) + + return _DATABASE:FindStatic(Static:getName()) + end + + return nil + end + + return self +end + + --- Creates the original @{Static} at a POINT_VEC2. -- @param #SPAWNSTATIC self -- @param Core.Point#COORDINATE Coordinate The 2D coordinate where to spawn the static. @@ -248,7 +299,7 @@ end -- @param #SPAWNSTATIC self -- @param Core.Zone#ZONE_BASE Zone The Zone where to spawn the static. -- @param #number Heading The heading of the static, which is a number in degrees from 0 to 360. --- @param #string (optional) The name of the new static. +-- @param #string NewName (optional) The name of the new static. -- @return #SPAWNSTATIC function SPAWNSTATIC:SpawnFromZone( Zone, Heading, NewName ) --R2.1 self:F( { Zone, Heading, NewName } ) diff --git a/Moose Development/Moose/Core/Spot.lua b/Moose Development/Moose/Core/Spot.lua index dbc3ea683..144c83ca3 100644 --- a/Moose Development/Moose/Core/Spot.lua +++ b/Moose Development/Moose/Core/Spot.lua @@ -126,6 +126,39 @@ do -- @param Wrapper.Positionable#POSITIONABLE Target -- @param #number LaserCode Laser code. -- @param #number Duration Duration of lasing in seconds. + + self:AddTransition( "Off", "LaseOnCoordinate", "On" ) + + --- LaseOnCoordinate Handler OnBefore for SPOT. + -- @function [parent=#SPOT] OnBeforeLaseOnCoordinate + -- @param #SPOT self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- LaseOnCoordinate Handler OnAfter for SPOT. + -- @function [parent=#SPOT] OnAfterLaseOnCoordinate + -- @param #SPOT self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- LaseOnCoordinate Trigger for SPOT. + -- @function [parent=#SPOT] LaseOnCoordinate + -- @param #SPOT self + -- @param Core.Point#COORDINATE Coordinate The coordinate to lase. + -- @param #number LaserCode Laser code. + -- @param #number Duration Duration of lasing in seconds. + + --- LaseOn Asynchronous Trigger for SPOT + -- @function [parent=#SPOT] __LaseOn + -- @param #SPOT self + -- @param #number Delay + -- @param Wrapper.Positionable#POSITIONABLE Target + -- @param #number LaserCode Laser code. + -- @param #number Duration Duration of lasing in seconds. + self:AddTransition( "On", "Lasing", "On" ) @@ -194,7 +227,8 @@ do return self end - --- @param #SPOT self + --- On after LaseOn event. Activates the laser spot. + -- @param #SPOT self -- @param From -- @param Event -- @param To @@ -226,6 +260,40 @@ do self:__Lasing( -1 ) end + + + --- On after LaseOnCoordinate event. Activates the laser spot. + -- @param #SPOT self + -- @param From + -- @param Event + -- @param To + -- @param Core.Point#COORDINATE Coordinate The coordinate at which the laser is pointing. + -- @param #number LaserCode Laser code. + -- @param #number Duration Duration of lasing in seconds. + function SPOT:onafterLaseOnCoordinate(From, Event, To, Coordinate, LaserCode, Duration) + self:F( { "LaseOnCoordinate", Coordinate, LaserCode, Duration } ) + + local function StopLase( self ) + self:LaseOff() + end + + self.Target = nil + self.TargetCoord=Coordinate + self.LaserCode = LaserCode + + self.Lasing = true + + local RecceDcsUnit = self.Recce:GetDCSObject() + + self.SpotIR = Spot.createInfraRed( RecceDcsUnit, { x = 0, y = 1, z = 0 }, Coordinate:GetVec3() ) + self.SpotLaser = Spot.createLaser( RecceDcsUnit, { x = 0, y = 1, z = 0 }, Coordinate:GetVec3(), LaserCode ) + + if Duration then + self.ScheduleID = self.LaseScheduler:Schedule( self, StopLase, {self}, Duration ) + end + + self:__Lasing(-1) + end --- @param #SPOT self -- @param Core.Event#EVENTDATA EventData @@ -246,10 +314,20 @@ do -- @param To function SPOT:onafterLasing( From, Event, To ) - if self.Target:IsAlive() then + if self.Target and self.Target:IsAlive() then self.SpotIR:setPoint( self.Target:GetPointVec3():AddY(1):AddY(math.random(-100,100)/100):AddX(math.random(-100,100)/100):GetVec3() ) self.SpotLaser:setPoint( self.Target:GetPointVec3():AddY(1):GetVec3() ) self:__Lasing( -0.2 ) + elseif self.TargetCoord then + + -- Wiggle the IR spot a bit. + local irvec3={x=self.TargetCoord.x+math.random(-100,100)/100, y=self.TargetCoord.y+math.random(-100,100)/100, z=self.TargetCoord.z} --#DCS.Vec3 + local lsvec3={x=self.TargetCoord.x, y=self.TargetCoord.y, z=self.TargetCoord.z} --#DCS.Vec3 + + self.SpotIR:setPoint(irvec3) + self.SpotLaser:setPoint(lsvec3) + + self:__Lasing(-0.25) else self:F( { "Target is not alive", self.Target:IsAlive() } ) end diff --git a/Moose Development/Moose/Core/UserFlag.lua b/Moose Development/Moose/Core/UserFlag.lua index 88c1d0f60..aac0c78ea 100644 --- a/Moose Development/Moose/Core/UserFlag.lua +++ b/Moose Development/Moose/Core/UserFlag.lua @@ -19,6 +19,8 @@ do -- UserFlag --- @type USERFLAG + -- @field #string ClassName Name of the class + -- @field #string UserFlagName Name of the flag. -- @extends Core.Base#BASE @@ -30,7 +32,8 @@ do -- UserFlag -- -- @field #USERFLAG USERFLAG = { - ClassName = "USERFLAG", + ClassName = "USERFLAG", + UserFlagName = nil, } --- USERFLAG Constructor. @@ -46,18 +49,29 @@ do -- UserFlag return self end + --- Get the userflag name. + -- @param #USERFLAG self + -- @return #string Name of the user flag. + function USERFLAG:GetName() + return self.UserFlagName + end --- Set the userflag to a given Number. -- @param #USERFLAG self -- @param #number Number The number value to be checked if it is the same as the userflag. + -- @param #number Delay Delay in seconds, before the flag is set. -- @return #USERFLAG The userflag instance. -- @usage -- local BlueVictory = USERFLAG:New( "VictoryBlue" ) -- BlueVictory:Set( 100 ) -- Set the UserFlag VictoryBlue to 100. -- - function USERFLAG:Set( Number ) --R2.3 + function USERFLAG:Set( Number, Delay ) --R2.3 - trigger.action.setUserFlag( self.UserFlagName, Number ) + if Delay and Delay>0 then + self:ScheduleOnce(Delay, USERFLAG.Set, self, Number) + else + trigger.action.setUserFlag( self.UserFlagName, Number ) + end return self end @@ -70,7 +84,7 @@ do -- UserFlag -- local BlueVictory = USERFLAG:New( "VictoryBlue" ) -- local BlueVictoryValue = BlueVictory:Get() -- Get the UserFlag VictoryBlue value. -- - function USERFLAG:Get( Number ) --R2.3 + function USERFLAG:Get() --R2.3 return trigger.misc.getUserFlag( self.UserFlagName ) end diff --git a/Moose Development/Moose/Core/UserSound.lua b/Moose Development/Moose/Core/UserSound.lua index a0547a5cf..b0f6fb393 100644 --- a/Moose Development/Moose/Core/UserSound.lua +++ b/Moose Development/Moose/Core/UserSound.lua @@ -118,15 +118,21 @@ do -- UserSound --- Play the usersound to the given @{Wrapper.Group}. -- @param #USERSOUND self -- @param Wrapper.Group#GROUP Group The @{Wrapper.Group} to play the usersound to. + -- @param #number Delay (Optional) Delay in seconds, before the sound is played. Default 0. -- @return #USERSOUND The usersound instance. -- @usage -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) -- local PlayerGroup = GROUP:FindByName( "PlayerGroup" ) -- Search for the active group named "PlayerGroup", that contains a human player. -- BlueVictory:ToGroup( PlayerGroup ) -- Play the sound that Blue has won to the player group. -- - function USERSOUND:ToGroup( Group ) --R2.3 - - trigger.action.outSoundForGroup( Group:GetID(), self.UserSoundFileName ) + function USERSOUND:ToGroup( Group, Delay ) --R2.3 + + Delay=Delay or 0 + if Delay>0 then + SCHEDULER:New(nil, USERSOUND.ToGroup,{self, Group}, Delay) + else + trigger.action.outSoundForGroup( Group:GetID(), self.UserSoundFileName ) + end return self end diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 244d5de66..8ef630eee 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -56,7 +56,7 @@ --- @type ZONE_BASE -- @field #string ZoneName Name of the zone. -- @field #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. --- @extends Core.Base#BASE +-- @extends Core.Fsm#FSM --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. @@ -120,7 +120,7 @@ ZONE_BASE = { -- @param #string ZoneName Name of the zone. -- @return #ZONE_BASE self function ZONE_BASE:New( ZoneName ) - local self = BASE:Inherit( self, BASE:New() ) + local self = BASE:Inherit( self, FSM:New() ) self:F( ZoneName ) self.ZoneName = ZoneName @@ -442,6 +442,33 @@ function ZONE_RADIUS:New( ZoneName, Vec2, Radius ) return self end +--- Mark the zone with markers on the F10 map. +-- @param #ZONE_RADIUS self +-- @param #number Points (Optional) The amount of points in the circle. Default 360. +-- @return #ZONE_RADIUS self +function ZONE_RADIUS:MarkZone(Points) + + local Point = {} + local Vec2 = self:GetVec2() + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + for Angle = 0, 360, (360 / Points ) do + + local Radial = Angle * RadialBase / 360 + + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + + COORDINATE:NewFromVec2(Point):MarkToAll(self:GetName()) + + end + +end + --- Bounds the zone with tires. -- @param #ZONE_RADIUS self -- @param #number Points (optional) The amount of points in the circle. Default 360. @@ -535,7 +562,7 @@ function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth, AddHeight ) local Vec2 = self:GetVec2() AddHeight = AddHeight or 0 - + Points = Points and Points or 360 local Angle @@ -618,6 +645,9 @@ function ZONE_RADIUS:GetVec3( Height ) end + + + --- Scan the zone for the presence of units of the given ObjectCategories. -- Note that after a zone has been scanned, the zone can be evaluated by: -- @@ -628,12 +658,12 @@ end -- * @{ZONE_RADIUS.IsNoneInZone}(): Scan if the zone is empty. -- @{#ZONE_RADIUS. -- @param #ZONE_RADIUS self --- @param ObjectCategories --- @param Coalition +-- @param ObjectCategories An array of categories of the objects to find in the zone. +-- @param UnitCategories An array of unit categories of the objects to find in the zone. -- @usage -- self.Zone:Scan() -- local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) -function ZONE_RADIUS:Scan( ObjectCategories ) +function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories ) self.ScanData = {} self.ScanData.Coalitions = {} @@ -655,15 +685,48 @@ function ZONE_RADIUS:Scan( ObjectCategories ) local function EvaluateZone( ZoneObject ) --if ZoneObject:isExist() then --FF: isExist always returns false for SCENERY objects since DCS 2.2 and still in DCS 2.5 - if ZoneObject then + if ZoneObject then + local ObjectCategory = ZoneObject:getCategory() - if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or - (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then + + --local name=ZoneObject:getName() + --env.info(string.format("Zone object %s", tostring(name))) + --self:E(ZoneObject) + + if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then + local CoalitionDCSUnit = ZoneObject:getCoalition() - self.ScanData.Coalitions[CoalitionDCSUnit] = true - self.ScanData.Units[ZoneObject] = ZoneObject - self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) + + local Include = false + if not UnitCategories then + -- Anythink found is included. + Include = true + else + -- Check if found object is in specified categories. + local CategoryDCSUnit = ZoneObject:getDesc().category + + for UnitCategoryID, UnitCategory in pairs( UnitCategories ) do + if UnitCategory == CategoryDCSUnit then + Include = true + break + end + end + + end + + if Include then + + local CoalitionDCSUnit = ZoneObject:getCoalition() + + -- This coalition is inside the zone. + self.ScanData.Coalitions[CoalitionDCSUnit] = true + + self.ScanData.Units[ZoneObject] = ZoneObject + + self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) + end end + if ObjectCategory == Object.Category.SCENERY then local SceneryType = ZoneObject:getTypeName() local SceneryName = ZoneObject:getName() @@ -671,21 +734,57 @@ function ZONE_RADIUS:Scan( ObjectCategories ) self.ScanData.Scenery[SceneryType][SceneryName] = SCENERY:Register( SceneryName, ZoneObject ) self:F2( { SCENERY = self.ScanData.Scenery[SceneryType][SceneryName] } ) end + end + return true end + -- Search objects. world.searchObjects( ObjectCategories, SphereSearch, EvaluateZone ) end - +--- Count the number of different coalitions inside the zone. +-- @param #ZONE_RADIUS self +-- @return #table Table of DCS units and DCS statics inside the zone. function ZONE_RADIUS:GetScannedUnits() return self.ScanData.Units end +--- Get a set of scanned units. +-- @param #ZONE_RADIUS self +-- @return Core.Set#SET_UNIT Set of units and statics inside the zone. +function ZONE_RADIUS:GetScannedSetUnit() + + local SetUnit = SET_UNIT:New() + + if self.ScanData then + for ObjectID, UnitObject in pairs( self.ScanData.Units ) do + local UnitObject = UnitObject -- DCS#Unit + if UnitObject:isExist() then + local FoundUnit = UNIT:FindByName( UnitObject:getName() ) + if FoundUnit then + SetUnit:AddUnit( FoundUnit ) + else + local FoundStatic = STATIC:FindByName( UnitObject:getName() ) + if FoundStatic then + SetUnit:AddUnit( FoundStatic ) + end + end + end + end + end + + return SetUnit +end + + +--- Count the number of different coalitions inside the zone. +-- @param #ZONE_RADIUS self +-- @return #number Counted coalitions. function ZONE_RADIUS:CountScannedCoalitions() local Count = 0 @@ -693,14 +792,25 @@ function ZONE_RADIUS:CountScannedCoalitions() for CoalitionID, Coalition in pairs( self.ScanData.Coalitions ) do Count = Count + 1 end + return Count end +--- Check if a certain coalition is inside a scanned zone. +-- @param #ZONE_RADIUS self +-- @param #number Coalition The coalition id, e.g. coalition.side.BLUE. +-- @return #boolean If true, the coalition is inside the zone. +function ZONE_RADIUS:CheckScannedCoalition( Coalition ) + if Coalition then + return self.ScanData.Coalitions[Coalition] + end + return nil +end --- Get Coalitions of the units in the Zone, or Check if there are units of the given Coalition in the Zone. --- Returns nil if there are none ot two Coalitions in the zone! +-- Returns nil if there are none to two Coalitions in the zone! -- Returns one Coalition if there are only Units of one Coalition in the Zone. --- Returns the Coalition for the given Coalition if there are units of the Coalition in the Zone +-- Returns the Coalition for the given Coalition if there are units of the Coalition in the Zone. -- @param #ZONE_RADIUS self -- @return #table function ZONE_RADIUS:GetScannedCoalition( Coalition ) @@ -725,20 +835,27 @@ function ZONE_RADIUS:GetScannedCoalition( Coalition ) end +--- Get scanned scenery type +-- @param #ZONE_RADIUS self +-- @return #table Table of DCS scenery type objects. function ZONE_RADIUS:GetScannedSceneryType( SceneryType ) return self.ScanData.Scenery[SceneryType] end +--- Get scanned scenery table +-- @param #ZONE_RADIUS self +-- @return #table Table of DCS scenery objects. function ZONE_RADIUS:GetScannedScenery() return self.ScanData.Scenery end --- Is All in Zone of Coalition? +-- Check if only the specifed coalition is inside the zone and noone else. -- @param #ZONE_RADIUS self --- @param Coalition --- @return #boolean +-- @param #number Coalition Coalition ID of the coalition which is checked to be the only one in the zone. +-- @return #boolean True, if **only** that coalition is inside the zone and no one else. -- @usage -- self.Zone:Scan() -- local IsGuarded = self.Zone:IsAllInZoneOfCoalition( self.Coalition ) @@ -750,11 +867,12 @@ end --- Is All in Zone of Other Coalition? +-- Check if only one coalition is inside the zone and the specified coalition is not the one. -- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated! -- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set. -- @param #ZONE_RADIUS self --- @param Coalition --- @return #boolean +-- @param #number Coalition Coalition ID of the coalition which is not supposed to be in the zone. +-- @return #boolean True, if and only if only one coalition is inside the zone and the specified coalition is not it. -- @usage -- self.Zone:Scan() -- local IsCaptured = self.Zone:IsAllInZoneOfOtherCoalition( self.Coalition ) @@ -766,11 +884,12 @@ end --- Is Some in Zone of Coalition? +-- Check if more than one coaltion is inside the zone and the specifed coalition is one of them. -- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated! -- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set. -- @param #ZONE_RADIUS self --- @param Coalition --- @return #boolean +-- @param #number Coalition ID of the coaliton which is checked to be inside the zone. +-- @return #boolean True if more than one coalition is inside the zone and the specified coalition is one of them. -- @usage -- self.Zone:Scan() -- local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) @@ -834,7 +953,6 @@ function ZONE_RADIUS:SearchZone( EvaluateFunction, ObjectCategories ) local function EvaluateZone( ZoneDCSUnit ) - env.info( ZoneDCSUnit:getName() ) local ZoneUnit = UNIT:Find( ZoneDCSUnit ) @@ -1323,7 +1441,7 @@ end function ZONE_POLYGON_BASE:Flush() self:F2() - self:E( { Polygon = self.ZoneName, Coordinates = self._.Polygon } ) + self:F( { Polygon = self.ZoneName, Coordinates = self._.Polygon } ) return self end @@ -1380,16 +1498,15 @@ end --- Smokes the zone boundaries in a color. -- @param #ZONE_POLYGON_BASE self -- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. -- @return #ZONE_POLYGON_BASE self -function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) +function ZONE_POLYGON_BASE:SmokeZone( SmokeColor, Segments ) self:F2( SmokeColor ) - local i - local j - local Segments = 10 + Segments=Segments or 10 - i = 1 - j = #self._.Polygon + local i=1 + local j=#self._.Polygon while i <= #self._.Polygon do self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) @@ -1410,6 +1527,42 @@ function ZONE_POLYGON_BASE:SmokeZone( SmokeColor ) end +--- Flare the zone boundaries in a color. +-- @param #ZONE_POLYGON_BASE self +-- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. +-- @param #number Segments (Optional) Number of segments within boundary line. Default 10. +-- @param DCS#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. +-- @param #number AddHeight (optional) The height to be added for the smoke. +-- @return #ZONE_POLYGON_BASE self +function ZONE_POLYGON_BASE:FlareZone( FlareColor, Segments, Azimuth, AddHeight ) + self:F2(FlareColor) + + Segments=Segments or 10 + + AddHeight = AddHeight or 0 + + local i=1 + local j=#self._.Polygon + + while i <= #self._.Polygon do + self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } ) + + local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x + local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y + + for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line. + local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments ) + local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments ) + POINT_VEC2:New( PointX, PointY, AddHeight ):Flare(FlareColor, Azimuth) + end + j = i + i = i + 1 + end + + return self +end + + --- Returns if a location is within the zone. diff --git a/Moose Development/Moose/Core/Zone_Detection.lua b/Moose Development/Moose/Core/Zone_Detection.lua new file mode 100644 index 000000000..96a508c0e --- /dev/null +++ b/Moose Development/Moose/Core/Zone_Detection.lua @@ -0,0 +1,203 @@ + +--- The ZONE_DETECTION class, defined by a zone name, a detection object and a radius. +-- @type ZONE_DETECTION +-- @field DCS#Vec2 Vec2 The current location of the zone. +-- @field DCS#Distance Radius The radius of the zone. +-- @extends #ZONE_BASE + +--- The ZONE_DETECTION class defined by a zone name, a location and a radius. +-- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. +-- +-- ## ZONE_DETECTION constructor +-- +-- * @{#ZONE_DETECTION.New}(): Constructor. +-- +-- @field #ZONE_DETECTION +ZONE_DETECTION = { + ClassName="ZONE_DETECTION", + } + +--- Constructor of @{#ZONE_DETECTION}, taking the zone name, the zone location and a radius. +-- @param #ZONE_DETECTION self +-- @param #string ZoneName Name of the zone. +-- @param Functional.Detection#DETECTION_BASE Detection The detection object defining the locations of the central detections. +-- @param DCS#Distance Radius The radius around the detections defining the combined zone. +-- @return #ZONE_DETECTION self +function ZONE_DETECTION:New( ZoneName, Detection, Radius ) + local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_DETECTION + self:F( { ZoneName, Detection, Radius } ) + + self.Detection = Detection + self.Radius = Radius + + return self +end + +--- Bounds the zone with tires. +-- @param #ZONE_DETECTION self +-- @param #number Points (optional) The amount of points in the circle. Default 360. +-- @param DCS#country.id CountryID The country id of the tire objects, e.g. country.id.USA for blue or country.id.RUSSIA for red. +-- @param #boolean UnBound (Optional) If true the tyres will be destroyed. +-- @return #ZONE_DETECTION self +function ZONE_DETECTION:BoundZone( Points, CountryID, UnBound ) + + local Point = {} + local Vec2 = self:GetVec2() + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + -- + for Angle = 0, 360, (360 / Points ) do + local Radial = Angle * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + + local CountryName = _DATABASE.COUNTRY_NAME[CountryID] + + local Tire = { + ["country"] = CountryName, + ["category"] = "Fortifications", + ["canCargo"] = false, + ["shape_name"] = "H-tyre_B_WF", + ["type"] = "Black_Tyre_WF", + --["unitId"] = Angle + 10000, + ["y"] = Point.y, + ["x"] = Point.x, + ["name"] = string.format( "%s-Tire #%0d", self:GetName(), Angle ), + ["heading"] = 0, + } -- end of ["group"] + + local Group = coalition.addStaticObject( CountryID, Tire ) + if UnBound and UnBound == true then + Group:destroy() + end + end + + return self +end + + +--- Smokes the zone boundaries in a color. +-- @param #ZONE_DETECTION self +-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. +-- @param #number Points (optional) The amount of points in the circle. +-- @param #number AddHeight (optional) The height to be added for the smoke. +-- @param #number AddOffSet (optional) The angle to be added for the smoking start position. +-- @return #ZONE_DETECTION self +function ZONE_DETECTION:SmokeZone( SmokeColor, Points, AddHeight, AngleOffset ) + self:F2( SmokeColor ) + + local Point = {} + local Vec2 = self:GetVec2() + + AddHeight = AddHeight or 0 + AngleOffset = AngleOffset or 0 + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + for Angle = 0, 360, 360 / Points do + local Radial = ( Angle + AngleOffset ) * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + POINT_VEC2:New( Point.x, Point.y, AddHeight ):Smoke( SmokeColor ) + end + + return self +end + + +--- Flares the zone boundaries in a color. +-- @param #ZONE_DETECTION self +-- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. +-- @param #number Points (optional) The amount of points in the circle. +-- @param DCS#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. +-- @param #number AddHeight (optional) The height to be added for the smoke. +-- @return #ZONE_DETECTION self +function ZONE_DETECTION:FlareZone( FlareColor, Points, Azimuth, AddHeight ) + self:F2( { FlareColor, Azimuth } ) + + local Point = {} + local Vec2 = self:GetVec2() + + AddHeight = AddHeight or 0 + + Points = Points and Points or 360 + + local Angle + local RadialBase = math.pi*2 + + for Angle = 0, 360, 360 / Points do + local Radial = Angle * RadialBase / 360 + Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius() + Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius() + POINT_VEC2:New( Point.x, Point.y, AddHeight ):Flare( FlareColor, Azimuth ) + end + + return self +end + +--- Returns the radius around the detected locations defining the combine zone. +-- @param #ZONE_DETECTION self +-- @return DCS#Distance The radius. +function ZONE_DETECTION:GetRadius() + self:F2( self.ZoneName ) + + self:T2( { self.Radius } ) + + return self.Radius +end + +--- Sets the radius around the detected locations defining the combine zone. +-- @param #ZONE_DETECTION self +-- @param DCS#Distance Radius The radius. +-- @return #ZONE_DETECTION self +function ZONE_DETECTION:SetRadius( Radius ) + self:F2( self.ZoneName ) + + self.Radius = Radius + self:T2( { self.Radius } ) + + return self.Radius +end + + + +--- Returns if a location is within the zone. +-- @param #ZONE_DETECTION self +-- @param DCS#Vec2 Vec2 The location to test. +-- @return #boolean true if the location is within the zone. +function ZONE_DETECTION:IsVec2InZone( Vec2 ) + self:F2( Vec2 ) + + local Coordinates = self.Detection:GetDetectedItemCoordinates() -- This returns a list of coordinates that define the (central) locations of the detections. + + for CoordinateID, Coordinate in pairs( Coordinates ) do + local ZoneVec2 = Coordinate:GetVec2() + if ZoneVec2 then + if (( Vec2.x - ZoneVec2.x )^2 + ( Vec2.y - ZoneVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then + return true + end + end + end + + return false +end + +--- Returns if a point is within the zone. +-- @param #ZONE_DETECTION self +-- @param DCS#Vec3 Vec3 The point to test. +-- @return #boolean true if the point is within the zone. +function ZONE_DETECTION:IsVec3InZone( Vec3 ) + self:F2( Vec3 ) + + local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) + + return InZone +end + diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index d4af6e979..206165bd3 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -1,6 +1,7 @@ ---- DCS API prototypes --- See [https://wiki.hoggitworld.com/view/Simulator_Scripting_Engine_Documentation](https://wiki.hoggitworld.com/view/Simulator_Scripting_Engine_Documentation) --- for further explanation and examples. +--- **DCS API** Prototypes +-- +-- See the [Simulator Scripting Engine Documentation](https://wiki.hoggitworld.com/view/Simulator_Scripting_Engine_Documentation) on Hoggit for further explanation and examples. +-- -- @module DCS -- @image MOOSE.JPG @@ -46,9 +47,13 @@ do -- world -- @field S_EVENT_PLAYER_COMMENT -- @field S_EVENT_SHOOTING_START [https://wiki.hoggitworld.com/view/DCS_event_shooting_start](https://wiki.hoggitworld.com/view/DCS_event_shooting_start) -- @field S_EVENT_SHOOTING_END [https://wiki.hoggitworld.com/view/DCS_event_shooting_end](https://wiki.hoggitworld.com/view/DCS_event_shooting_end) - -- @field S_EVENT_MARK ADDED [https://wiki.hoggitworld.com/view/DCS_event_mark_added](https://wiki.hoggitworld.com/view/DCS_event_mark_added) - -- @field S_EVENT_MARK CHANGE [https://wiki.hoggitworld.com/view/DCS_event_mark_change](https://wiki.hoggitworld.com/view/DCS_event_mark_change) - -- @field S_EVENT_MARK REMOVE [https://wiki.hoggitworld.com/view/DCS_event_mark_remove](https://wiki.hoggitworld.com/view/DCS_event_mark_remove) + -- @field S_EVENT_MARK ADDED [https://wiki.hoggitworld.com/view/DCS_event_mark_added](https://wiki.hoggitworld.com/view/DCS_event_mark_added) DCS>=2.5.1 + -- @field S_EVENT_MARK CHANGE [https://wiki.hoggitworld.com/view/DCS_event_mark_change](https://wiki.hoggitworld.com/view/DCS_event_mark_change) DCS>=2.5.1 + -- @field S_EVENT_MARK REMOVE [https://wiki.hoggitworld.com/view/DCS_event_mark_remove](https://wiki.hoggitworld.com/view/DCS_event_mark_remove) DCS>=2.5.1 + -- @field S_EVENT_KILL [https://wiki.hoggitworld.com/view/DCS_event_kill](https://wiki.hoggitworld.com/view/DCS_event_kill) DCS>=2.5.6 + -- @field S_EVENT_SCORE [https://wiki.hoggitworld.com/view/DCS_event_score](https://wiki.hoggitworld.com/view/DCS_event_score) DCS>=2.5.6 + -- @field S_EVENT_UNIT_LOST [https://wiki.hoggitworld.com/view/DCS_event_unit_lost](https://wiki.hoggitworld.com/view/DCS_event_unit_lost) DCS>=2.5.6 + -- @field S_EVENT_LANDING_AFTER_EJECTION [https://wiki.hoggitworld.com/view/DCS_event_landing_after_ejection](https://wiki.hoggitworld.com/view/DCS_event_landing_after_ejection) DCS>=2.5.6 -- @field S_EVENT_MAX --- The birthplace enumerator is used to define where an aircraft or helicopter has spawned in association with birth events. @@ -325,9 +330,27 @@ end -- coalition do -- Types --- @type Desc - -- @field #TypeName typeName type name - -- @field #string displayName localized display name - -- @field #table attributes object type attributes + -- @field #number speedMax0 Max speed in meters/second at zero altitude. + -- @field #number massEmpty Empty mass in kg. + -- @field #number tankerType Type of refueling system: 0=boom, 1=probe. + -- @field #number range Range in km(?). + -- @field #table box Bounding box. + -- @field #number Hmax Max height in meters. + -- @field #number Kmax ? + -- @field #number speedMax10K Max speed in meters/second at 10k altitude. + -- @field #number NyMin ? + -- @field #number NyMax ? + -- @field #number fuelMassMax Max fuel mass in kg. + -- @field #number speedMax10K Max speed in meters/second. + -- @field #number massMax Max mass of unit. + -- @field #number RCS ? + -- @field #number life Life points. + -- @field #number VyMax Max vertical velocity in m/s. + -- @field #number Kab ? + -- @field #table attributes Table of attributes. + -- @field #TypeName typeName Type Name. + -- @field #string displayName Localized display name. + -- @field #number category Unit category. --- A distance type -- @type Distance @@ -427,9 +450,15 @@ do -- Types -- @type TaskArray -- @list <#Task> + --- + --@type WaypointAir + --@field #boolean lateActivated + --@field #boolean uncontrolled end -- + + do -- Object --- [DCS Class Object](https://wiki.hoggitworld.com/view/DCS_Class_Object) @@ -527,6 +556,126 @@ do -- CoalitionObject end -- CoalitionObject +do -- Weapon + + --- [DCS Class Weapon](https://wiki.hoggitworld.com/view/DCS_Class_Weapon) + -- @type Weapon + -- @extends #CoalitionObject + -- @field #Weapon.flag flag enum stores weapon flags. Some of them are combination of another flags. + -- @field #Weapon.Category Category enum that stores weapon categories. + -- @field #Weapon.GuidanceType GuidanceType enum that stores guidance methods. Available only for guided weapon (Weapon.Category.MISSILE and some Weapon.Category.BOMB). + -- @field #Weapon.MissileCategory MissileCategory enum that stores missile category. Available only for missiles (Weapon.Category.MISSILE). + -- @field #Weapon.WarheadType WarheadType enum that stores warhead types. + -- @field #Weapon.Desc Desc The descriptor of a weapon. + + --- enum stores weapon flags. Some of them are combination of another flags. + -- @type Weapon.flag + -- @field LGB + -- @field TvGB + -- @field SNSGB + -- @field HEBomb + -- @field Penetrator + -- @field NapalmBomb + -- @field FAEBomb + -- @field ClusterBomb + -- @field Dispencer + -- @field CandleBomb + -- @field ParachuteBomb + -- @field GuidedBomb = LGB + TvGB + SNSGB + -- @field AnyUnguidedBomb = HEBomb + Penetrator + NapalmBomb + FAEBomb + ClusterBomb + Dispencer + CandleBomb + ParachuteBomb + -- @field AnyBomb = GuidedBomb + AnyUnguidedBomb + -- @field LightRocket + -- @field MarkerRocket + -- @field CandleRocket + -- @field HeavyRocket + -- @field AnyRocket = LightRocket + HeavyRocket + MarkerRocket + CandleRocket + -- @field AntiRadarMissile + -- @field AntiShipMissile + -- @field AntiTankMissile + -- @field FireAndForgetASM + -- @field LaserASM + -- @field TeleASM + -- @field CruiseMissile + -- @field GuidedASM = LaserASM + TeleASM + -- @field TacticASM = GuidedASM + FireAndForgetASM + -- @field AnyASM = AntiRadarMissile + AntiShipMissile + AntiTankMissile + FireAndForgetASM + GuidedASM + CruiseMissile + -- @field SRAAM + -- @field MRAAM + -- @field LRAAM + -- @field IR_AAM + -- @field SAR_AAM + -- @field AR_AAM + -- @field AnyAAM = IR_AAM + SAR_AAM + AR_AAM + SRAAM + MRAAM + LRAAM + -- @field AnyMissile = AnyASM + AnyAAM + -- @field AnyAutonomousMissile = IR_AAM + AntiRadarMissile + AntiShipMissile + FireAndForgetASM + CruiseMissile + -- @field GUN_POD + -- @field BuiltInCannon + -- @field Cannons = GUN_POD + BuiltInCannon + -- @field AnyAGWeapon = BuiltInCannon + GUN_POD + AnyBomb + AnyRocket + AnyASM + -- @field AnyAAWeapon = BuiltInCannon + GUN_POD + AnyAAM + -- @field UnguidedWeapon = Cannons + BuiltInCannon + GUN_POD + AnyUnguidedBomb + AnyRocket + -- @field GuidedWeapon = GuidedBomb + AnyASM + AnyAAM + -- @field AnyWeapon = AnyBomb + AnyRocket + AnyMissile + Cannons + -- @field MarkerWeapon = MarkerRocket + CandleRocket + CandleBomb + -- @field ArmWeapon = AnyWeapon - MarkerWeapon + + --- Weapon.Category enum that stores weapon categories. + -- @type Weapon.Category + -- @field SHELL + -- @field MISSILE + -- @field ROCKET + -- @field BOMB + + + --- Weapon.GuidanceType enum that stores guidance methods. Available only for guided weapon (Weapon.Category.MISSILE and some Weapon.Category.BOMB). + -- @type Weapon.GuidanceType + -- @field INS + -- @field IR + -- @field RADAR_ACTIVE + -- @field RADAR_SEMI_ACTIVE + -- @field RADAR_PASSIVE + -- @field TV + -- @field LASER + -- @field TELE + + + --- Weapon.MissileCategory enum that stores missile category. Available only for missiles (Weapon.Category.MISSILE). + -- @type Weapon.MissileCategory + -- @field AAM + -- @field SAM + -- @field BM + -- @field ANTI_SHIP + -- @field CRUISE + -- @field OTHER + + --- Weapon.WarheadType enum that stores warhead types. + -- @type Weapon.WarheadType + -- @field AP + -- @field HE + -- @field SHAPED_EXPLOSIVE + + --- Returns the unit that launched the weapon. + -- @function [parent=#Weapon] getLauncher + -- @param #Weapon self + -- @return #Unit + + --- returns target of the guided weapon. Unguided weapons and guided weapon that is targeted at the point on the ground will return nil. + -- @function [parent=#Weapon] getTarget + -- @param #Weapon self + -- @return #Object + + --- returns weapon descriptor. Descriptor type depends on weapon category. + -- @function [parent=#Weapon] getDesc + -- @param #Weapon self + -- @return #Weapon.Desc + + + + Weapon = {} --#Weapon + +end -- Weapon + + do -- Airbase --- [DCS Class Airbase](https://wiki.hoggitworld.com/view/DCS_Class_Airbase) @@ -1082,6 +1231,7 @@ do -- AI -- @field TAKEOFF -- @field TAKEOFF_PARKING -- @field TURNING_POINT + -- @field TAKEOFF_PARKING_HOT -- @field LAND --- @type AI.Task.TurnMethod @@ -1118,8 +1268,8 @@ do -- AI --- @type AI.Option.Naval -- @field #AI.Option.Naval.id id -- @field #AI.Option.Naval.val val - - --TODO: work on formation + + --- @type AI.Option.Air.id -- @field NO_OPTION -- @field ROE @@ -1128,7 +1278,34 @@ do -- AI -- @field FLARE_USING -- @field FORMATION -- @field RTB_ON_BINGO - -- @field SILENCE + -- @field SILENCE + -- @field RTB_ON_OUT_OF_AMMO + -- @field ECM_USING + -- @field PROHIBIT_AA + -- @field PROHIBIT_JETT + -- @field PROHIBIT_AB + -- @field PROHIBIT_AG + -- @field MISSILE_ATTACK + -- @field PROHIBIT_WP_PASS_REPORT + + --- @type AI.Option.Air.id.FORMATION + -- @field LINE_ABREAST + -- @field TRAIL + -- @field WEDGE + -- @field ECHELON_RIGHT + -- @field ECHELON_LEFT + -- @field FINGER_FOUR + -- @field SPREAD_FOUR + -- @field WW2_BOMBER_ELEMENT + -- @field WW2_BOMBER_ELEMENT_HEIGHT + -- @field WW2_FIGHTER_VIC + -- @field HEL_WEDGE + -- @field HEL_ECHELON + -- @field HEL_FRONT + -- @field HEL_COLUMN + -- @field COMBAT_BOX + -- @field JAVELIN_DOWN + --- @type AI.Option.Air.val -- @field #AI.Option.Air.val.ROE ROE @@ -1161,12 +1338,27 @@ do -- AI -- @field AGAINST_FIRED_MISSILE -- @field WHEN_FLYING_IN_SAM_WEZ -- @field WHEN_FLYING_NEAR_ENEMIES + + --- @type AI.Option.Air.val.ECM_USING + -- @field NEVER_USE + -- @field USE_IF_ONLY_LOCK_BY_RADAR + -- @field USE_IF_DETECTED_LOCK_BY_RADAR + -- @field ALWAYS_USE + + --- @type AI.Option.Air.val.MISSILE_ATTACK + -- @field MAX_RANGE + -- @field NEZ_RANGE + -- @field HALF_WAY_RMAX_NEZ + -- @field TARGET_THREAT_EST + -- @field RANDOM_RANGE + --- @type AI.Option.Ground.id -- @field NO_OPTION -- @field ROE @{#AI.Option.Ground.val.ROE} -- @field DISPERSE_ON_ATTACK true or false -- @field ALARM_STATE @{#AI.Option.Ground.val.ALARM_STATE} + -- @field ENGAGE_AIR_WEAPONS --- @type AI.Option.Ground.val -- @field #AI.Option.Ground.val.ROE ROE @@ -1196,7 +1388,4 @@ do -- AI AI = {} --#AI -end -- AI - - - +end -- AI \ No newline at end of file diff --git a/Moose Development/Moose/Functional/ATC_Ground.lua b/Moose Development/Moose/Functional/ATC_Ground.lua index 259bade99..455f3eaf5 100644 --- a/Moose Development/Moose/Functional/ATC_Ground.lua +++ b/Moose Development/Moose/Functional/ATC_Ground.lua @@ -125,6 +125,7 @@ end -- Atc_Ground = ATC_GROUND_CAUCAUS:New() -- Atc_Ground = ATC_GROUND_NEVADA:New() -- Atc_Ground = ATC_GROUND_NORMANDY:New() +-- Atc_Ground = ATC_GROUND_PERSIANGULF:New() -- -- -- Then use one of these methods... -- @@ -190,6 +191,7 @@ end -- Atc_Ground = ATC_GROUND_CAUCAUS:New() -- Atc_Ground = ATC_GROUND_NEVADA:New() -- Atc_Ground = ATC_GROUND_NORMANDY:New() +-- Atc_Ground = ATC_GROUND_PERSIANGULF:New() -- -- -- Then use one of these methods... -- @@ -273,7 +275,7 @@ function ATC_GROUND:_AirbaseMonitor() self:E( Taxi ) if Taxi == false then local Velocity = VELOCITY:New( AirbaseMeta.KickSpeed or self.KickSpeed ) - Client:Message( "Welcome at " .. AirbaseID .. ". The maximum taxiing speed is " .. + Client:Message( "Welcome to " .. AirbaseID .. ". The maximum taxiing speed is " .. Velocity:ToString() , 20, "ATC" ) Client:SetState( self, "Taxi", true ) end @@ -297,7 +299,7 @@ function ATC_GROUND:_AirbaseMonitor() end if Speeding == true then MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. - " is kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() + " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() Client:Destroy() Client:SetState( self, "Speeding", false ) Client:SetState( self, "Warnings", 0 ) @@ -329,7 +331,7 @@ function ATC_GROUND:_AirbaseMonitor() Velocity:ToString(), 5, "ATC" ) Client:SetState( self, "Warnings", SpeedingWarnings + 1 ) else - MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " is kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() + MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() --- @param Wrapper.Client#CLIENT Client Client:Destroy() Client:SetState( self, "Speeding", false ) @@ -361,7 +363,7 @@ function ATC_GROUND:_AirbaseMonitor() Client:Message( "Warning " .. OffRunwayWarnings .. "/3! Airbase traffic rule violation! Get back on the taxi immediately!", 5, "ATC" ) Client:SetState( self, "OffRunwayWarnings", OffRunwayWarnings + 1 ) else - MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " is kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() + MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll() --- @param Wrapper.Client#CLIENT Client Client:Destroy() Client:SetState( self, "IsOffRunway", false ) @@ -786,8 +788,6 @@ function ATC_GROUND_CAUCASUS:New( AirbaseNames ) -- Inherits from BASE local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, 0.05 ) - self:SetKickSpeedKmph( 50 ) self:SetMaximumKickSpeedKmph( 150 ) @@ -1000,6 +1000,15 @@ function ATC_GROUND_CAUCASUS:New( AirbaseNames ) end +--- Start SCHEDULER for ATC_GROUND_CAUCASUS object. +-- @param #ATC_GROUND_CAUCASUS self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return nothing +function ATC_GROUND_CAUCASUS:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) +end + --- @type ATC_GROUND_NEVADA @@ -1043,7 +1052,6 @@ end -- * `AIRBASE.Nevada.Laughlin_Airport` -- * `AIRBASE.Nevada.Lincoln_County` -- * `AIRBASE.Nevada.McCarran_International_Airport` --- * `AIRBASE.Nevada.Mellan_Airstrip` -- * `AIRBASE.Nevada.Mesquite` -- * `AIRBASE.Nevada.Mina_Airport_3Q0` -- * `AIRBASE.Nevada.Nellis_AFB` @@ -1087,8 +1095,7 @@ end -- -- -- Monitor specific airbases. -- ATC_Ground = ATC_GROUND_NEVADA:New( --- { AIRBASE.Nevada.Laughlin_Airport, --- AIRBASE.Nevada.Mellan_Airstrip, +-- { AIRBASE.Nevada.Laughlin_Airport, -- AIRBASE.Nevada.Lincoln_County, -- AIRBASE.Nevada.North_Las_Vegas, -- AIRBASE.Nevada.McCarran_International_Airport @@ -1377,8 +1384,6 @@ function ATC_GROUND_NEVADA:New( AirbaseNames ) -- Inherits from BASE local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, 0.05 ) - self:SetKickSpeedKmph( 50 ) self:SetMaximumKickSpeedKmph( 150 ) @@ -1542,6 +1547,16 @@ function ATC_GROUND_NEVADA:New( AirbaseNames ) return self end +--- Start SCHEDULER for ATC_GROUND_NEVADA object. +-- @param #ATC_GROUND_NEVADA self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return nothing +function ATC_GROUND_NEVADA:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) +end + + --- @type ATC_GROUND_NORMANDY -- @extends #ATC_GROUND @@ -1557,7 +1572,7 @@ end -- -- --- -- --- The default maximum speed for the airbases at Caucasus is **40 km/h**. Warnings are given if this speed limit is trespassed. +-- The default maximum speed for the airbases at Normandy is **40 km/h**. Warnings are given if this speed limit is trespassed. -- Players will be immediately kicked when driving faster than **100 km/h** on the taxi way. -- -- The ATC\_GROUND\_NORMANDY class monitors the speed of the airplanes at the airbase during taxi. @@ -1604,6 +1619,13 @@ end -- * `AIRBASE.Normandy.Sainte_Laurent_sur_Mer` -- * `AIRBASE.Normandy.Sommervieu` -- * `AIRBASE.Normandy.Tangmere` +-- * `AIRBASE.Normandy.Argentan` +-- * `AIRBASE.Normandy.Goulet` +-- * `AIRBASE.Normandy.Essay` +-- * `AIRBASE.Normandy.Hauterive` +-- * `AIRBASE.Normandy.Barville` +-- * `AIRBASE.Normandy.Conches` +-- * `AIRBASE.Normandy.Vrigny` -- -- # Installation -- @@ -1817,7 +1839,7 @@ ATC_GROUND_NORMANDY = { }, }, }, - [AIRBASE.Normandy.Ford] = { + [AIRBASE.Normandy.Ford_AF] = { PointsRunways = { [1] = { [1]={["y"]=-26506.13971428,["x"]=147514.39971429,}, @@ -2019,7 +2041,83 @@ ATC_GROUND_NORMANDY = { }, }, }, - }, + [AIRBASE.Normandy.Argentan] = { + PointsRunways = { + [1] = { + [1]={["y"]=22322.280338032,["x"]=-78607.309765269,}, + [2]={["y"]=23032.778713963,["x"]=-78967.17709893,}, + [3]={["y"]=23015.27074041,["x"]=-79008.02903722,}, + [4]={["y"]=22299.944963827,["x"]=-78650.366148928,}, + }, + }, + }, + [AIRBASE.Normandy.Goulet] = { + PointsRunways = { + [1] = { + [1]={["y"]=24901.788373185,["x"]=-89139.367511763,}, + [2]={["y"]=25459.965967043,["x"]=-89709.67940114,}, + [3]={["y"]=25422.459962713,["x"]=-89741.669816598,}, + [4]={["y"]=24857.663662208,["x"]=-89173.56416277,}, + }, + }, + }, + [AIRBASE.Normandy.Essay] = { + PointsRunways = { + [1] = { + [1]={["y"]=44610.072022849,["x"]=-105469.21149064,}, + [2]={["y"]=45417.939023956,["x"]=-105536.08535277,}, + [3]={["y"]=45412.558368383,["x"]=-105585.27991801,}, + [4]={["y"]=44602.38537203,["x"]=-105516.10006064,}, + }, + }, + }, + [AIRBASE.Normandy.Hauterive] = { + PointsRunways = { + [1] = { + [1]={["y"]=40617.185360953,["x"]=-107657.10147517,}, + [2]={["y"]=41114.628372034,["x"]=-108298.77015609,}, + [3]={["y"]=41080.006684855,["x"]=-108319.06562788,}, + [4]={["y"]=40584.558402807,["x"]=-107692.29370481,}, + }, + }, + }, + [AIRBASE.Normandy.Vrigny] = { + PointsRunways = { + [1] = { + [1]={["y"]=24892.131051827,["x"]=-89131.628297486,}, + [2]={["y"]=25469.738000575,["x"]=-89709.235246234,}, + [3]={["y"]=25418.869206793,["x"]=-89738.771965204,}, + [4]={["y"]=24859.312475193,["x"]=-89171.010589446,}, + }, + }, + }, + [AIRBASE.Normandy.Barville] = { + PointsRunways = { + [1] = { + [1]={["y"]=49027.850333166,["x"]=-109217.05049066,}, + [2]={["y"]=49755.022185805,["x"]=-110346.63783457,}, + [3]={["y"]=49682.657996586,["x"]=-110401.35222154,}, + [4]={["y"]=48921.951519675,["x"]=-109285.88471943,}, + }, + [2] = { + [1]={["y"]=48429.522036941,["x"]=-109818.90874734,}, + [2]={["y"]=49746.197284681,["x"]=-109954.81222465,}, + [3]={["y"]=49735.607403332,["x"]=-110032.47135455,}, + [4]={["y"]=48420.697135816,["x"]=-109900.09783768,}, + }, + }, + }, + [AIRBASE.Normandy.Conches] = { + PointsRunways = { + [1] = { + [1]={["y"]=95099.187473266,["x"]=-56389.619005858,}, + [2]={["y"]=95181.545025963,["x"]=-56465.440244849,}, + [3]={["y"]=94071.678958666,["x"]=-57627.596821795,}, + [4]={["y"]=94005.008558864,["x"]=-57558.31189651,}, + }, + }, + }, + }, } @@ -2031,8 +2129,6 @@ function ATC_GROUND_NORMANDY:New( AirbaseNames ) -- Inherits from BASE local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) -- #ATC_GROUND_NORMANDY - - self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, 0.05 ) self:SetKickSpeedKmph( 40 ) self:SetMaximumKickSpeedKmph( 100 ) @@ -2316,6 +2412,856 @@ function ATC_GROUND_NORMANDY:New( AirbaseNames ) end +--- Start SCHEDULER for ATC_GROUND_NORMANDY object. +-- @param #ATC_GROUND_NORMANDY self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return nothing +function ATC_GROUND_NORMANDY:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) +end + +--- @type ATC_GROUND_PERSIANGULF +-- @extends #ATC_GROUND - \ No newline at end of file +--- # ATC\_GROUND\_PERSIANGULF, extends @{#ATC_GROUND} +-- +-- The ATC\_GROUND\_PERSIANGULF class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- --- +-- +-- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG) +-- +-- --- +-- +-- The default maximum speed for the airbases at Persian Gulf is **50 km/h**. Warnings are given if this speed limit is trespassed. +-- Players will be immediately kicked when driving faster than **150 km/h** on the taxi way. +-- +-- The ATC\_GROUND\_PERSIANGULF class monitors the speed of the airplanes at the airbase during taxi. +-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned. +-- +-- The pilot will receive 3 times a warning during speeding. After the 3rd warning, if the pilot is still driving +-- faster than the maximum allowed speed, the pilot will be kicked. +-- +-- Different airbases have different maximum speeds, according safety regulations. +-- +-- # Airbases monitored +-- +-- The following airbases are monitored at the PersianGulf region. +-- Use the @{Wrapper.Airbase#AIRBASE.PersianGulf} enumeration to select the airbases to be monitored. +-- +-- * `AIRBASE.PersianGulf.Abu_Musa_Island_Airport` +-- * `AIRBASE.PersianGulf.Al_Dhafra_AB` +-- * `AIRBASE.PersianGulf.Al_Maktoum_Intl` +-- * `AIRBASE.PersianGulf.Al_Minhad_AB` +-- * `AIRBASE.PersianGulf.Bandar_Abbas_Intl` +-- * `AIRBASE.PersianGulf.Bandar_Lengeh` +-- * `AIRBASE.PersianGulf.Dubai_Intl` +-- * `AIRBASE.PersianGulf.Fujairah_Intl` +-- * `AIRBASE.PersianGulf.Havadarya` +-- * `AIRBASE.PersianGulf.Kerman_Airport` +-- * `AIRBASE.PersianGulf.Khasab` +-- * `AIRBASE.PersianGulf.Lar_Airbase` +-- * `AIRBASE.PersianGulf.Qeshm_Island` +-- * `AIRBASE.PersianGulf.Sharjah_Intl` +-- * `AIRBASE.PersianGulf.Shiraz_International_Airport` +-- * `AIRBASE.PersianGulf.Sir_Abu_Nuayr` +-- * `AIRBASE.PersianGulf.Sirri_Island` +-- * `AIRBASE.PersianGulf.Tunb_Island_AFB` +-- * `AIRBASE.PersianGulf.Tunb_Kochak` +-- * `AIRBASE.PersianGulf.Sas_Al_Nakheel_Airport` +-- * `AIRBASE.PersianGulf.Bandar_e_Jask_airfield` +-- * `AIRBASE.PersianGulf.Abu_Dhabi_International_Airport` +-- * `AIRBASE.PersianGulf.Al_Bateen_Airport` +-- * `AIRBASE.PersianGulf.Kish_International_Airport` +-- * `AIRBASE.PersianGulf.Al_Ain_International_Airport` +-- * `AIRBASE.PersianGulf.Lavan_Island_Airport` +-- * `AIRBASE.PersianGulf.Jiroft_Airport` +-- +-- # Installation +-- +-- ## In Single Player Missions +-- +-- ATC\_GROUND is fully functional in single player. +-- +-- ## In Multi Player Missions +-- +-- ATC\_GROUND is functional in multi player, however ... +-- +-- Due to a bug in DCS since release 1.5, the despawning of clients are not anymore working in multi player. +-- To **work around this problem**, a much better solution has been made, using the **slot blocker** script designed +-- by Ciribob. +-- +-- With the help of __Ciribob__, this script has been extended to also kick client players while in flight. +-- ATC\_GROUND is communicating with this modified script to kick players! +-- +-- Install the file **SimpleSlotBlockGameGUI.lua** on the server, following the installation instructions described by Ciribob. +-- +-- [Simple Slot Blocker from Ciribob & FlightControl](https://github.com/ciribob/DCS-SimpleSlotBlock) +-- +-- # Script it! +-- +-- ## 1. ATC_GROUND_PERSIANGULF Constructor +-- +-- Creates a new ATC_GROUND_PERSIANGULF object that will monitor pilots taxiing behaviour. +-- +-- -- This creates a new ATC_GROUND_PERSIANGULF object. +-- +-- -- Monitor for these clients the airbases. +-- AirbasePoliceCaucasus = ATC_GROUND_PERSIANGULF:New() +-- +-- ATC_Ground = ATC_GROUND_PERSIANGULF:New( +-- { AIRBASE.PersianGulf.Kerman_Airport, +-- AIRBASE.PersianGulf.Al_Minhad_AB +-- } +-- ) +-- +-- +-- ## 2. Set various options +-- +-- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes. +-- +-- ### 2.1 Speed limit at an airbase. +-- +-- * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour. +-- +-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately. +-- +-- * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. +-- * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour. +-- * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour. +-- +-- @field #ATC_GROUND_PERSIANGULF +ATC_GROUND_PERSIANGULF = { + ClassName = "ATC_GROUND_PERSIANGULF", + Airbases = { + [AIRBASE.PersianGulf.Abu_Musa_Island_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-122813.71002344,["x"]=-31689.936027827,}, + [2]={["y"]=-122827.82488722,["x"]=-31590.105445836,}, + [3]={["y"]=-122769.5689949,["x"]=-31583.176330891,}, + [4]={["y"]=-122726.96776968,["x"]=-31614.998932862,}, + [5]={["y"]=-121293.92414543,["x"]=-31467.947715689,}, + [6]={["y"]=-121296.4904843,["x"]=-31432.018971528,}, + [7]={["y"]=-121236.18152088,["x"]=-31424.576588809,}, + [8]={["y"]=-121190.50068902,["x"]=-31458.452261875,}, + [9]={["y"]=-119839.83654246,["x"]=-31319.356695194,}, + [10]={["y"]=-119824.69514313,["x"]=-31423.293419374,}, + [11]={["y"]=-119886.80054375,["x"]=-31430.22253432,}, + [12]={["y"]=-119932.22474173,["x"]=-31395.320325706,}, + [13]={["y"]=-122813.9472789,["x"]=-31689.81193251,}, + }, + }, + }, + [AIRBASE.PersianGulf.Al_Dhafra_AB] = { + PointsRunways = { + [1] = { + [1]={["y"]=-174672.06004916,["x"]=-209880.97145616,}, + [2]={["y"]=-174705.15693282,["x"]=-209923.15131918,}, + [3]={["y"]=-171819.05380065,["x"]=-212172.84298281,}, + [4]={["y"]=-171785.09826475,["x"]=-212129.87417284,}, + [5]={["y"]=-174671.96413454,["x"]=-209880.52453983,}, + }, + [2] = { + [1]={["y"]=-174351.95872272,["x"]=-211813.88516693,}, + [2]={["y"]=-174381.29169939,["x"]=-211851.81242636,}, + [3]={["y"]=-171493.65648904,["x"]=-214102.92235002,}, + [4]={["y"]=-171464.99693831,["x"]=-214062.78788361,}, + [5]={["y"]=-174351.8628081,["x"]=-211813.4382506,}, + }, + }, + }, + [AIRBASE.PersianGulf.Al_Maktoum_Intl] = { + PointsRunways = { + [1] = { + [1]={["y"]=-111879.49046471,["x"]=-138953.80105841,}, + [2]={["y"]=-111917.23447224,["x"]=-139018.2804046,}, + [3]={["y"]=-108092.98121312,["x"]=-141406.67838426,}, + [4]={["y"]=-108052.34416748,["x"]=-141341.82058294,}, + [5]={["y"]=-111879.5412879,["x"]=-138952.87693763,}, + }, + }, + }, + [AIRBASE.PersianGulf.Al_Minhad_AB] = { + PointsRunways = { + [1] = { + [1]={["y"]=-91070.628933035,["x"]=-125989.64095162,}, + [2]={["y"]=-91072.346560159,["x"]=-126040.59722299,}, + [3]={["y"]=-87098.282779771,["x"]=-126039.41747017,}, + [4]={["y"]=-87099.632735396,["x"]=-125991.26905291,}, + [5]={["y"]=-91071.031270042,["x"]=-125987.44617225,}, + }, + }, + }, + [AIRBASE.PersianGulf.Bandar_Abbas_Intl] = { + PointsRunways = { + [1] = { + [1]={["y"]=12988.484058788,["x"]=113979.99250505,}, + [2]={["y"]=13037.8836239,["x"]=113952.60241152,}, + [3]={["y"]=14877.313199902,["x"]=117414.37833333,}, + [4]={["y"]=14828.777486364,["x"]=117439.06043783,}, + [5]={["y"]=12988.939584604,["x"]=113979.52494386,}, + }, + [2] = { + [1]={["y"]=13203.406014284,["x"]=113848.44907555,}, + [2]={["y"]=13258.268500181,["x"]=113818.47303925,}, + [3]={["y"]=15315.015323566,["x"]=117694.27156647,}, + [4]={["y"]=15264.815746383,["x"]=117725.22168173,}, + [5]={["y"]=13203.861540099,["x"]=113847.98151436,}, + }, + }, + }, + [AIRBASE.PersianGulf.Bandar_Lengeh] = { + PointsRunways = { + [1] = { + [1]={["y"]=-142373.15541415,["x"]=41364.94047809,}, + [2]={["y"]=-142363.30071107,["x"]=41298.112282592,}, + [3]={["y"]=-142217.57151662,["x"]=41320.35666061,}, + [4]={["y"]=-142213.00856728,["x"]=41291.838227254,}, + [5]={["y"]=-142131.44584788,["x"]=41301.534494595,}, + [6]={["y"]=-142132.58658522,["x"]=41323.778872613,}, + [7]={["y"]=-142123.17550221,["x"]=41336.041798956,}, + [8]={["y"]=-139580.45381288,["x"]=41711.022304533,}, + [9]={["y"]=-139590.04241918,["x"]=41778.350996659,}, + [10]={["y"]=-139732.41237808,["x"]=41757.089304408,}, + [11]={["y"]=-139736.7897853,["x"]=41785.646675372,}, + [12]={["y"]=-139816.41690726,["x"]=41775.641173137,}, + [13]={["y"]=-139816.00001133,["x"]=41754.58792885,}, + [14]={["y"]=-139824.1294819,["x"]=41743.748634761,}, + [15]={["y"]=-142373.20183966,["x"]=41365.161507021,}, + }, + }, + }, + [AIRBASE.PersianGulf.Dubai_Intl] = { + PointsRunways = { + [1] = { + [1]={["y"]=-89693.511670714,["x"]=-100490.47082052,}, + [2]={["y"]=-89731.488328846,["x"]=-100555.50584758,}, + [3]={["y"]=-85706.437275049,["x"]=-103076.68123933,}, + [4]={["y"]=-85669.519216262,["x"]=-103010.44994755,}, + [5]={["y"]=-89693.036962487,["x"]=-100489.9961123,}, + }, + [2] = { + [1]={["y"]=-90797.505501889,["x"]=-99344.082465487,}, + [2]={["y"]=-90835.482160021,["x"]=-99409.11749254,}, + [3]={["y"]=-87210.216900398,["x"]=-101681.72494832,}, + [4]={["y"]=-87171.474397253,["x"]=-101619.20256393,}, + [5]={["y"]=-90797.030793662,["x"]=-99343.607757261,}, + }, + }, + }, + [AIRBASE.PersianGulf.Fujairah_Intl] = { + PointsRunways = { + [1] = { + [1]={["y"]=5808.8716147284,["x"]=-116602.15633995,}, + [2]={["y"]=5781.9885293892,["x"]=-116666.67574476,}, + [3]={["y"]=9435.1910907931,["x"]=-118192.91910235,}, + [4]={["y"]=9459.878635843,["x"]=-118134.40047704,}, + [5]={["y"]=5808.4078522575,["x"]=-116603.31550719,}, + }, + }, + }, + [AIRBASE.PersianGulf.Havadarya] = { + PointsRunways = { + [1] = { + [1]={["y"]=-7565.4887830428,["x"]=109074.13162774,}, + [2]={["y"]=-7557.8281079193,["x"]=109030.65729641,}, + [3]={["y"]=-4987.3556518085,["x"]=109524.49147773,}, + [4]={["y"]=-4996.215358578,["x"]=109566.57508489,}, + [5]={["y"]=-7565.4936338604,["x"]=109074.32262205,}, + }, + }, + }, + [AIRBASE.PersianGulf.Kerman_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=70375.468628778,["x"]=456046.12685302,}, + [2]={["y"]=70297.050081575,["x"]=456015.1578105,}, + [3]={["y"]=71814.291673715,["x"]=452165.51037702,}, + [4]={["y"]=71902.918622452,["x"]=452188.46411914,}, + [5]={["y"]=70860.465673482,["x"]=454829.89695989,}, + [6]={["y"]=70862.525255971,["x"]=454892.77675983,}, + [7]={["y"]=70816.157465062,["x"]=454922.77944807,}, + [8]={["y"]=70462.749176371,["x"]=455833.38051827,}, + [9]={["y"]=70483.400377364,["x"]=455901.17880077,}, + [10]={["y"]=70453.787334431,["x"]=455974.8217628,}, + [11]={["y"]=70405.860962315,["x"]=455961.57382254,}, + [12]={["y"]=70374.689338175,["x"]=456046.51649833,}, + }, + }, + }, + [AIRBASE.PersianGulf.Khasab] = { + PointsRunways = { + [1] = { + [1]={["y"]=-534.81827307392,["x"]=-1495.070060483,}, + [2]={["y"]=-434.82912685139,["x"]=-1519.8421462589,}, + [3]={["y"]=-405.55302547993,["x"]=-1413.0969766429,}, + [4]={["y"]=-424.92029254105,["x"]=-1352.0675653224,}, + [5]={["y"]=216.05735069389,["x"]=1206.9187095195,}, + [6]={["y"]=116.42961315781,["x"]=1229.9576238247,}, + [7]={["y"]=88.253643635887,["x"]=1123.7918160128,}, + [8]={["y"]=101.1741158476,["x"]=1042.6886109249,}, + [9]={["y"]=-535.31436058928,["x"]=-1494.8762081291,}, + }, + }, + }, + [AIRBASE.PersianGulf.Lar_Airbase] = { + PointsRunways = { + [1] = { + [1]={["y"]=-183987.5454359,["x"]=169021.72039309,}, + [2]={["y"]=-183988.41292374,["x"]=168955.27082471,}, + [3]={["y"]=-180847.92031188,["x"]=168930.46175795,}, + [4]={["y"]=-180806.58653731,["x"]=168888.39641215,}, + [5]={["y"]=-180740.37934087,["x"]=168886.56748407,}, + [6]={["y"]=-180735.62412787,["x"]=168932.65647164,}, + [7]={["y"]=-180685.14571291,["x"]=168934.11961411,}, + [8]={["y"]=-180682.5852136,["x"]=169001.78995301,}, + [9]={["y"]=-183987.48111493,["x"]=169021.35002828,}, + }, + }, + }, + [AIRBASE.PersianGulf.Qeshm_Island] = { + PointsRunways = { + [1] = { + [1]={["y"]=-35140.372717152,["x"]=63373.658918509,}, + [2]={["y"]=-35098.556715749,["x"]=63320.377239302,}, + [3]={["y"]=-34991.318905699,["x"]=63408.730403557,}, + [4]={["y"]=-34984.574389344,["x"]=63401.311435566,}, + [5]={["y"]=-34991.993357335,["x"]=63313.632722947,}, + [6]={["y"]=-34956.921872287,["x"]=63265.746656824,}, + [7]={["y"]=-34917.129225791,["x"]=63261.699947011,}, + [8]={["y"]=-34832.822771349,["x"]=63337.23853019,}, + [9]={["y"]=-34915.105870884,["x"]=63436.382920614,}, + [10]={["y"]=-34906.337999622,["x"]=63478.198922017,}, + [11]={["y"]=-32728.533668488,["x"]=65307.986209216,}, + [12]={["y"]=-32676.600892552,["x"]=65299.218337954,}, + [13]={["y"]=-32623.99366498,["x"]=65334.964274638,}, + [14]={["y"]=-32626.691471522,["x"]=65388.92040548,}, + [15]={["y"]=-31822.745121968,["x"]=66067.418750826,}, + [16]={["y"]=-31777.556862387,["x"]=66068.767654097,}, + [17]={["y"]=-31691.227053039,["x"]=65974.344425122,}, + [18]={["y"]=-31606.246146962,["x"]=66042.464040311,}, + [19]={["y"]=-31602.199437148,["x"]=66084.280041714,}, + [20]={["y"]=-31632.549760747,["x"]=66124.747139846,}, + [21]={["y"]=-31727.647441358,["x"]=66134.189462744,}, + [22]={["y"]=-31734.391957713,["x"]=66141.608430735,}, + [23]={["y"]=-31632.549760747,["x"]=66225.914885176,}, + [24]={["y"]=-31673.691310515,["x"]=66277.173209477,}, + [25]={["y"]=-35140.880825624,["x"]=63373.905965825,}, + }, + }, + }, + [AIRBASE.PersianGulf.Sharjah_Intl] = { + PointsRunways = { + [1] = { + [1]={["y"]=-71668.808658476,["x"]=-93980.156242153,}, + [2]={["y"]=-75307.847363315,["x"]=-91617.097584505,}, + [3]={["y"]=-75280.458023829,["x"]=-91574.709321014,}, + [4]={["y"]=-72249.697184234,["x"]=-93529.134331507,}, + [5]={["y"]=-72179.919581256,["x"]=-93526.199759419,}, + [6]={["y"]=-72138.183444896,["x"]=-93597.933743788,}, + [7]={["y"]=-71638.654062835,["x"]=-93927.584008321,}, + [8]={["y"]=-71668.325847279,["x"]=-93979.428115206,}, + }, + [2] = { + [1]={["y"]=-71553.225408723,["x"]=-93775.312323319,}, + [2]={["y"]=-75168.13829548,["x"]=-91426.51571111,}, + [3]={["y"]=-75125.388157445,["x"]=-91363.754870166,}, + [4]={["y"]=-71510.511081666,["x"]=-93703.252275385,}, + [5]={["y"]=-71552.247218027,["x"]=-93775.638386885,}, + }, + }, + }, + [AIRBASE.PersianGulf.Shiraz_International_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-353995.75579778,["x"]=382327.42294273,}, + [2]={["y"]=-354029.77009807,["x"]=382265.46199492,}, + [3]={["y"]=-349407.98049238,["x"]=379941.14030526,}, + [4]={["y"]=-349376.87025024,["x"]=380004.69408564,}, + [5]={["y"]=-353995.71101815,["x"]=382327.59771695,}, + }, + [2] = { + [1]={["y"]=-354056.29510012,["x"]=381845.97598829,}, + [2]={["y"]=-354091.48797289,["x"]=381783.6025623,}, + [3]={["y"]=-349650.64038107,["x"]=379550.92898242,}, + [4]={["y"]=-349624.41889127,["x"]=379614.92719482,}, + [5]={["y"]=-354056.25032049,["x"]=381846.15076251,}, + }, + }, + }, + [AIRBASE.PersianGulf.Sir_Abu_Nuayr] = { + PointsRunways = { + [1] = { + [1]={["y"]=-203367.3128691,["x"]=-103017.22553918,}, + [2]={["y"]=-203373.59664477,["x"]=-103054.92819323,}, + [3]={["y"]=-202578.27577922,["x"]=-103188.26018333,}, + [4]={["y"]=-202571.37254488,["x"]=-103151.01482599,}, + [5]={["y"]=-203367.65259839,["x"]=-103016.48202662,}, + [6]={["y"]=-203291.39594004,["x"]=-102985.49774228,}, + }, + }, + }, + [AIRBASE.PersianGulf.Sirri_Island] = { + PointsRunways = { + [1] = { + [1]={["y"]=-169713.12842428,["x"]=-27766.658020853,}, + [2]={["y"]=-169682.02009414,["x"]=-27726.583172021,}, + [3]={["y"]=-169727.21866794,["x"]=-27691.632048154,}, + [4]={["y"]=-169694.28043602,["x"]=-27650.276268081,}, + [5]={["y"]=-169763.08474269,["x"]=-27598.490047901,}, + [6]={["y"]=-169825.30140298,["x"]=-27607.090586235,}, + [7]={["y"]=-171614.98889813,["x"]=-26246.247907014,}, + [8]={["y"]=-171620.85326172,["x"]=-26187.105176343,}, + [9]={["y"]=-171686.10990337,["x"]=-26138.56820961,}, + [10]={["y"]=-171716.55468456,["x"]=-26178.745338885,}, + [11]={["y"]=-171764.9668776,["x"]=-26142.810515186,}, + [12]={["y"]=-171796.29599657,["x"]=-26183.416460911,}, + [13]={["y"]=-169713.5628285,["x"]=-27766.883787223,}, + }, + }, + }, + [AIRBASE.PersianGulf.Tunb_Island_AFB] = { + PointsRunways = { + [1] = { + [1]={["y"]=-92923.634698863,["x"]=9547.6862547173,}, + [2]={["y"]=-92963.030803298,["x"]=9565.7274614215,}, + [3]={["y"]=-92934.128053782,["x"]=9619.2987996964,}, + [4]={["y"]=-92970.946842975,["x"]=9640.1014155901,}, + [5]={["y"]=-92949.591945243,["x"]=9682.8112110532,}, + [6]={["y"]=-92899.518391942,["x"]=9699.7478540817,}, + [7]={["y"]=-91969.13471408,["x"]=11464.627292768,}, + [8]={["y"]=-91983.666755417,["x"]=11515.293058512,}, + [9]={["y"]=-91960.101282978,["x"]=11557.710908902,}, + [10]={["y"]=-91921.021874517,["x"]=11539.251288825,}, + [11]={["y"]=-91893.725202275,["x"]=11589.720675632,}, + [12]={["y"]=-91859.751646175,["x"]=11571.850192366,}, + [13]={["y"]=-92922.149728329,["x"]=9547.2937058617,}, + }, + }, + }, + [AIRBASE.PersianGulf.Tunb_Kochak] = { + PointsRunways = { + [1] = { + [1]={["y"]=-109925.50271188,["x"]=8974.5666013181,}, + [2]={["y"]=-109905.7382908,["x"]=8937.53274444,}, + [3]={["y"]=-109009.93726324,["x"]=9072.2234968343,}, + [4]={["y"]=-109040.82867587,["x"]=9104.9871291834,}, + [5]={["y"]=-109925.26515172,["x"]=8974.091480998,}, + }, + }, + }, + [AIRBASE.PersianGulf.Sas_Al_Nakheel_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-176230.75865538,["x"]=-188732.01369812,}, + [2]={["y"]=-176274.78045186,["x"]=-188744.8049371,}, + [3]={["y"]=-175692.03171595,["x"]=-190564.17145168,}, + [4]={["y"]=-175649.7486572,["x"]=-190550.58435053,}, + [5]={["y"]=-176230.66274076,["x"]=-188731.5667818,}, + }, + }, + }, + [AIRBASE.PersianGulf.Bandar_e_Jask_airfield] = { + PointsRunways = { + [1] = { + [1]={["y"]=155156.73167657,["x"]=-57837.031277333,}, + [2]={["y"]=155130.38996239,["x"]=-57790.475605714,}, + [3]={["y"]=157137.17872571,["x"]=-56710.411783359,}, + [4]={["y"]=157148.46631801,["x"]=-56688.071756941,}, + [5]={["y"]=157220.07198163,["x"]=-56649.035500253,}, + [6]={["y"]=157227.83220133,["x"]=-56662.204357931,}, + [7]={["y"]=157359.6383572,["x"]=-56590.481115222,}, + [8]={["y"]=157383.03659539,["x"]=-56633.044744502,}, + [9]={["y"]=155156.7940421,["x"]=-57837.149989814,}, + }, + }, + }, + [AIRBASE.PersianGulf.Abu_Dhabi_International_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-163964.56943899,["x"]=-189427.63621921,}, + [2]={["y"]=-164005.96838287,["x"]=-189478.90226888,}, + [3]={["y"]=-160798.22080495,["x"]=-192054.59531727,}, + [4]={["y"]=-160755.05282258,["x"]=-192002.58569997,}, + [5]={["y"]=-163964.47352437,["x"]=-189427.18930288,}, + }, + [2] = { + [1]={["y"]=-163615.44952024,["x"]=-187144.00786922,}, + [2]={["y"]=-163656.84846411,["x"]=-187195.27391888,}, + [3]={["y"]=-160452.71811093,["x"]=-189764.86593382,}, + [4]={["y"]=-160411.94568221,["x"]=-189715.47961171,}, + [5]={["y"]=-163615.35360562,["x"]=-187143.56095289,}, + }, + }, + }, + [AIRBASE.PersianGulf.Al_Bateen_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-183207.51774197,["x"]=-189871.8319832,}, + [2]={["y"]=-183240.61462564,["x"]=-189914.01184622,}, + [3]={["y"]=-180748.88998479,["x"]=-191943.30402837,}, + [4]={["y"]=-180711.83076051,["x"]=-191896.52435182,}, + [5]={["y"]=-183207.42182735,["x"]=-189871.38506688,}, + }, + }, + }, + [AIRBASE.PersianGulf.Kish_International_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-227330.79164594,["x"]=42691.91536494,}, + [2]={["y"]=-227321.58531968,["x"]=42758.113234714,}, + [3]={["y"]=-223235.73004619,["x"]=42313.579195302,}, + [4]={["y"]=-223240.99080406,["x"]=42247.819722016,}, + [5]={["y"]=-227330.67774245,["x"]=42691.785682556,}, + }, + [2] = { + [1]={["y"]=-227283.77911886,["x"]=42987.748941936,}, + [2]={["y"]=-227274.5727926,["x"]=43053.946811711,}, + [3]={["y"]=-222907.94761294,["x"]=42580.826755904,}, + [4]={["y"]=-222915.76510871,["x"]=42514.58376547,}, + [5]={["y"]=-227283.66521537,["x"]=42987.619259553,}, + }, + }, + }, + [AIRBASE.PersianGulf.Al_Ain_International_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-65165.315648901,["x"]=-209042.45716363,}, + [2]={["y"]=-65112.933878375,["x"]=-209048.84518442,}, + [3]={["y"]=-65672.013626755,["x"]=-213019.66479976,}, + [4]={["y"]=-65722.555424932,["x"]=-213013.91596964,}, + [5]={["y"]=-65165.400582791,["x"]=-209042.15059908,}, + }, + }, + }, + [AIRBASE.PersianGulf.Lavan_Island_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=-288099.83301495,["x"]=76353.443273049,}, + [2]={["y"]=-288119.51457685,["x"]=76302.756224611,}, + [3]={["y"]=-288070.96603401,["x"]=76283.898526152,}, + [4]={["y"]=-288085.61084238,["x"]=76247.386812114,}, + [5]={["y"]=-288032.04695421,["x"]=76224.316223573,}, + [6]={["y"]=-287991.12173627,["x"]=76245.38067398,}, + [7]={["y"]=-287489.96435675,["x"]=76037.610404141,}, + [8]={["y"]=-287497.65444594,["x"]=76017.686082159,}, + [9]={["y"]=-287453.61120787,["x"]=75998.111309685,}, + [10]={["y"]=-287419.70490555,["x"]=76007.199596905,}, + [11]={["y"]=-285642.24565503,["x"]=75279.787069797,}, + [12]={["y"]=-285625.46727862,["x"]=75239.239326815,}, + [13]={["y"]=-285570.23845628,["x"]=75217.217707782,}, + [14]={["y"]=-285555.20782742,["x"]=75252.172658628,}, + [15]={["y"]=-285505.92134673,["x"]=75231.199688121,}, + [16]={["y"]=-285484.28380792,["x"]=75284.258832895,}, + [17]={["y"]=-288099.97979219,["x"]=76354.32393647,}, + }, + }, + }, + [AIRBASE.PersianGulf.Jiroft_Airport] = { + PointsRunways = { + [1] = { + [1]={["y"]=140376.87310595,["x"]=283748.07558774,}, + [2]={["y"]=140299.43760975,["x"]=283655.81201779,}, + [3]={["y"]=143008.43807723,["x"]=281517.41347718,}, + [4]={["y"]=143052.6952428,["x"]=281573.25195709,}, + [5]={["y"]=142946.60213095,["x"]=281656.5960586,}, + [6]={["y"]=142975.14179847,["x"]=281687.20381796,}, + [7]={["y"]=142932.12548801,["x"]=281724.01585287,}, + [8]={["y"]=142870.49635092,["x"]=281719.05243244,}, + [9]={["y"]=140437.35783025,["x"]=283640.84253664,}, + [10]={["y"]=140433.27045062,["x"]=283705.80267729,}, + [11]={["y"]=140376.77702493,["x"]=283747.8442964,}, + }, + }, + }, + }, +} + + +--- Creates a new ATC_GROUND_PERSIANGULF object. +-- @param #ATC_GROUND_PERSIANGULF self +-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.PersianGulf enumerator). +-- @return #ATC_GROUND_PERSIANGULF self +function ATC_GROUND_PERSIANGULF:New( AirbaseNames ) + + -- Inherits from BASE + local self = BASE:Inherit( self, ATC_GROUND:New( self.Airbases, AirbaseNames ) ) -- #ATC_GROUND_PERSIANGULF + + self:SetKickSpeedKmph( 50 ) + self:SetMaximumKickSpeedKmph( 150 ) + + -- These lines here are for the demonstration mission. + -- They create in the dcs.log the coordinates of the runway polygons, that are then + -- taken by the moose designer from the dcs.log and reworked to define the + -- Airbases structure, which is part of the class. + -- When new airbases are added or airbases are changed on the map, + -- the MOOSE designer willde-comment this section and apply the changes in the demo + -- mission, and do a re-run to create a new dcs.log, and then add the changed coordinates + -- in the Airbases structure. + -- So, this needs to stay commented normally once a map has been finished. + + + --[[ + + -- Abu_Musa_Island_Airport + do + local VillagePrefix = "Abu_Musa_Island_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Al_Dhafra_AB + do + local VillagePrefix = "Al_Dhafra_AB" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Al_Maktoum_Intl + do + local VillagePrefix = "Al_Maktoum_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Al_Minhad_AB + do + local VillagePrefix = "Al_Minhad_AB" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Bandar_Abbas_Intl + do + local VillagePrefix = "Bandar_Abbas_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Bandar_Lengeh + do + local VillagePrefix = "Bandar_Lengeh" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Dubai_Intl + do + local VillagePrefix = "Dubai_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Fujairah_Intl + do + local VillagePrefix = "Fujairah_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Havadarya + do + local VillagePrefix = "Havadarya" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Kerman_Airport + do + local VillagePrefix = "Kerman_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Khasab + do + local VillagePrefix = "Khasab" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Lar_Airbase + do + local VillagePrefix = "Lar_Airbase" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Qeshm_Island + do + local VillagePrefix = "Qeshm_Island" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Sharjah_Intl + do + local VillagePrefix = "Sharjah_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Shiraz_International_Airport + do + local VillagePrefix = "Shiraz_International_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Sir_Abu_Nuayr + do + local VillagePrefix = "Sir_Abu_Nuayr" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Sirri_Island + do + local VillagePrefix = "Sirri_Island" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Tunb_Island_AFB + do + local VillagePrefix = "Tunb_Island_AFB" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Tunb_Kochak + do + local VillagePrefix = "Tunb_Kochak" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Sas_Al_Nakheel_Airport + do + local VillagePrefix = "Sas_Al_Nakheel_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Bandar_e_Jask_airfield + do + local VillagePrefix = "Bandar_e_Jask_airfield" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Abu_Dhabi_International_Airport + do + local VillagePrefix = "Abu_Dhabi_International_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Al_Bateen_Airport + do + local VillagePrefix = "Al_Bateen_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Kish_International_Airport + do + local VillagePrefix = "Kish_International_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Al_Ain_International_Airport + do + local VillagePrefix = "Al_Ain_International_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Lavan_Island_Airport + do + local VillagePrefix = "Lavan_Island_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Jiroft_Airport + do + local VillagePrefix = "Jiroft_Airport" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + + -- Bandar_Abbas_Intl + do + local VillagePrefix = "Bandar_Abbas_Intl" + local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" ) + local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush() + local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" ) + local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush() + end + + --]] + + return self +end + +--- Start SCHEDULER for ATC_GROUND_PERSIANGULF object. +-- @param #ATC_GROUND_PERSIANGULF self +-- @param RepeatScanSeconds Time in second for defining occurency of alerts. +-- @return nothing +function ATC_GROUND_PERSIANGULF:Start( RepeatScanSeconds ) + RepeatScanSeconds = RepeatScanSeconds or 0.05 + self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, RepeatScanSeconds ) +end + + + + diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua index 8509928fb..f5233e3b0 100644 --- a/Moose Development/Moose/Functional/Artillery.lua +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -1,15 +1,15 @@ --- **Functional** - Control artillery units. --- +-- -- === --- +-- -- The ARTY class can be used to easily assign and manage targets for artillery units using an advanced queueing system. --- +-- -- ## Features: --- +-- -- * Multiple targets can be assigned. No restriction on number of targets. -- * Targets can be given a priority. Engagement of targets is executed a according to their priority. -- * Engagements can be scheduled, i.e. will be executed at a certain time of the day. --- * Multiple relocations of the group can be assigned and scheduled via queueing system. +-- * Multiple relocations of the group can be assigned and scheduled via queueing system. -- * Special weapon types can be selected for each attack, e.g. cruise missiles for Naval units. -- * Automatic rearming once the artillery is out of ammo (optional). -- * Automatic relocation after each firing engagement to prevent counter strikes (optional). @@ -18,17 +18,17 @@ -- * New targets can be added during the mission, e.g. when they are detected by recon units. -- * Targets and relocations can be assigned by placing markers on the F10 map. -- * Finite state machine implementation. Mission designer can interact when certain events occur. --- +-- -- ==== --- +-- -- ## [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) --- +-- -- === --- +-- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** --- +-- -- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) --- +-- -- ==== -- @module Functional.Arty -- @image Artillery.JPG @@ -37,10 +37,11 @@ --- ARTY class -- @type ARTY -- @field #string ClassName Name of the class. +-- @field #string lid Log id for DCS.log file. -- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players. -- @field #table targets All targets assigned. -- @field #table moves All moves assigned. --- @field #table currentTarget Holds the current target, if there is one assigned. +-- @field #ARTY.Target currentTarget Holds the current target, if there is one assigned. -- @field #table currentMove Holds the current commanded move, if there is one assigned. -- @field #number Nammo0 Initial amount total ammunition (shells+rockets+missiles) of the whole group. -- @field #number Nshells0 Initial amount of shells of the whole group. @@ -67,7 +68,7 @@ -- @field #number RearmingDistance Safe distance in meters between ARTY group and rearming group or place at which rearming is possible. Default 100 m. -- @field Wrapper.Group#GROUP RearmingGroup Unit designated to rearm the ARTY group. -- @field #number RearmingGroupSpeed Speed in km/h the rearming unit moves at. Default is 50% of the max speed possible of the group. --- @field #boolean RearmingGroupOnRoad If true, rearming group will move to ARTY group or rearming place using mainly roads. Default false. +-- @field #boolean RearmingGroupOnRoad If true, rearming group will move to ARTY group or rearming place using mainly roads. Default false. -- @field Core.Point#COORDINATE RearmingGroupCoord Initial coordinates of the rearming unit. After rearming complete, the unit will return to this position. -- @field Core.Point#COORDINATE RearmingPlaceCoord Coordinates of the rearming place. If the place is more than 100 m away from the ARTY group, the group will go there. -- @field #boolean RearmingArtyOnRoad If true, ARTY group will move to rearming place using mainly roads. Default false. @@ -98,58 +99,61 @@ -- @field #boolean markreadonly Marks for targets are readonly and cannot be removed by players. Default is false. -- @field #boolean autorelocate ARTY group will automatically move to within the max/min firing range. -- @field #number autorelocatemaxdist Max distance [m] the ARTY group will travel to get within firing range. Default 50000 m = 50 km. --- @field #boolean autorelocateonroad ARTY group will use mainly road to automatically get within firing range. Default is false. +-- @field #boolean autorelocateonroad ARTY group will use mainly road to automatically get within firing range. Default is false. +-- @field #number coalition The coalition of the arty group. +-- @field #boolean respawnafterdeath Respawn arty group after all units are dead. +-- @field #number respawndelay Respawn delay in seconds. -- @extends Core.Fsm#FSM_CONTROLLABLE --- Enables mission designers easily to assign targets for artillery units. Since the implementation is based on a Finite State Model (FSM), the mission designer can -- interact with the process at certain events or states. --- +-- -- A new ARTY object can be created with the @{#ARTY.New}(*group*) contructor. -- The parameter *group* has to be a MOOSE Group object and defines ARTY group. --- +-- -- The ARTY FSM process can be started by the @{#ARTY.Start}() command. -- -- ## The ARTY Process --- +-- -- ![Process](..\Presentations\ARTY\ARTY_Process.png) --- +-- -- ### Blue Branch -- After the FMS process is started the ARTY group will be in the state **CombatReady**. Once a target is assigned the **OpenFire** event will be triggered and the group starts -- firing. At this point the group in in the state **Firing**. -- When the defined number of shots has been fired on the current target the event **CeaseFire** is triggered. The group will stop firing and go back to the state **CombatReady**. -- If another target is defined (or multiple engagements of the same target), the cycle starts anew. --- +-- -- ### Violet Branch -- When the ARTY group runs out of ammunition, the event **Winchester** is triggered and the group enters the state **OutOfAmmo**. -- In this state, the group is unable to engage further targets. --- +-- -- ### Red Branch -- With the @{#ARTY.SetRearmingGroup}(*group*) command, a special group can be defined to rearm the ARTY group. If this unit has been assigned and the group has entered the state -- **OutOfAmmo** the event **Rearm** is triggered followed by a transition to the state **Rearming**. -- If the rearming group is less than 100 meters away from the ARTY group, the rearming process starts. If the rearming group is more than 100 meters away from the ARTY unit, the -- rearming group is routed to a point 20 to 100 m from the ARTY group. --- +-- -- Once the rearming is complete, the **Rearmed** event is triggered and the group enters the state **CombatReady**. At this point targeted can be engaged again. --- +-- -- ### Green Branch -- The ARTY group can be ordered to change its position via the @{#ARTY.AssignMoveCoord}() function as described below. When the group receives the command to move -- the event **Move** is triggered and the state changes to **Moving**. When the unit arrives to its destination the event **Arrived** is triggered and the group -- becomes **CombatReady** again. --- --- Note, that the ARTY group will not open fire while it is in state **Moving**. This property differentiates artillery from tanks. --- +-- +-- Note, that the ARTY group will not open fire while it is in state **Moving**. This property differentiates artillery from tanks. +-- -- ### Yellow Branch -- When a new target is assigned via the @{#ARTY.AssignTargetCoord}() function (see below), the **NewTarget** event is triggered. --- +-- -- ## Assigning Targets -- Assigning targets is a central point of the ARTY class. Multiple targets can be assigned simultanioulsly and are put into a queue. --- Of course, targets can be added at any time during the mission. For example, once they are detected by a reconnaissance unit. --- +-- Of course, targets can be added at any time during the mission. For example, once they are detected by a reconnaissance unit. +-- -- In order to add a target, the function @{#ARTY.AssignTargetCoord}(*coord*, *prio*, *radius*, *nshells*, *maxengage*, *time*, *weapontype*, *name*) has to be used. -- Only the first parameter *coord* is mandatory while all remaining parameters are all optional. --- +-- -- ### Parameters: --- +-- -- * *coord*: Coordinates of the target, given as @{Core.Point#COORDINATE} object. -- * *prio*: Priority of the target. This a number between 1 (high prio) and 100 (low prio). Targets with higher priority are engaged before targets with lower priority. -- * *radius*: Radius in meters which defines the area the ARTY group will attempt to be hitting. Default is 100 meters. @@ -165,64 +169,64 @@ -- For example, this is useful for naval units which carry a bigger arsenal (cannons and missiles). Default is Auto, i.e. DCS logic selects the appropriate weapon type. -- *name*: A special name can be defined for this target. Default name are the coordinates of the target in LL DMS format. If a name is already given for another target -- or the same target should be attacked two or more times with different parameters a suffix "#01", "#02", "#03" is automatically appended to the specified name. --- +-- -- ## Target Queue -- In case multiple targets have been defined, it is important to understand how the target queue works. --- +-- -- Here, the essential parameters are the priority *prio*, the number of engagements *maxengage* and the scheduled *time* as described above. --- +-- -- For example, we have assigned two targets one with *prio*=10 and the other with *prio*=50 and both targets should be engaged three times (*maxengage*=3). -- Let's first consider the case that none of the targets is scheduled to be executed at a certain time (*time*=nil). -- The ARTY group will first engage the target with higher priority (*prio*=10). After the engagement is finished, the target with lower priority is attacked. --- This is because the target with lower prio has been attacked one time less. After the attack on the lower priority task is finished and both targets +-- This is because the target with lower prio has been attacked one time less. After the attack on the lower priority task is finished and both targets -- have been engaged equally often, the target with the higher priority is engaged again. This coninues until a target has engaged three times. -- Once the maximum number of engagements is reached, the target is deleted from the queue. --- +-- -- In other words, the queue is first sorted with respect to the number of engagements and targets with the same number of engagements are sorted with -- respect to their priority. --- +-- -- ### Timed Engagements --- +-- -- As mentioned above, targets can be engaged at a specific time of the day via the *time* parameter. --- +-- -- If the *time* parameter is specified for a target, the first engagement of that target will happen at that time of the day and not before. -- This also applies when multiple engagements are requested via the *maxengage* parameter. The first attack will not happen before the specifed time. -- When that timed attack is finished, the *time* parameter is deleted and the remaining engagements are carried out in the same manner as for untimed targets (described above). --- +-- -- Of course, it can happen that a scheduled task should be executed at a time, when another target is already under attack. -- If the priority of the target is higher than the priority of the current target, then the current attack is cancelled and the engagement of the target with the higher -- priority is started. --- +-- -- By contrast, if the current target has a higher priority than the target scheduled at that time, the current attack is finished before the scheduled attack is started. --- +-- -- ## Determining the Amount of Ammo --- +-- -- In order to determin when a unit is out of ammo and possible initiate the rearming process it is necessary to know which types of weapons have to be counted. -- For most artillery unit types, this is simple because they only have one type of weapon and hence ammunition. --- +-- -- However, there are more complex scenarios. For example, naval units carry a big arsenal of different ammunition types ranging from various cannon shell types -- over surface-to-air missiles to cruise missiles. Obviously, not all of these ammo types can be employed for artillery tasks. --- +-- -- Unfortunately, there is no easy way to count only those ammo types useable as artillery. Therefore, to keep the implementation general the user -- can specify the names of the ammo types by the following functions: --- +-- -- * @{#ARTY.SetShellTypes}(*tableofnames*): Defines the ammo types for unguided cannons, e.g. *tableofnames*={"weapons.shells"}, i.e. **all** types of shells are counted. -- * @{#ARTY.SetRocketTypes}(*tableofnames*): Defines the ammo types of unguided rockets, e.g. *tableofnames*={"weapons.nurs"}, i.e. **all** types of rockets are counted. -- * @{#ARTY.SetMissileTypes}(*tableofnames*): Defines the ammo types of guided missiles, e.g. is *tableofnames*={"weapons.missiles"}, i.e. **all** types of missiles are counted. --- +-- -- **Note** that the default parameters "weapons.shells", "weapons.nurs", "weapons.missiles" **should in priciple** capture all the corresponding ammo types. -- However, the logic searches for the string "weapon.missies" in the ammo type. Especially for missiles, this string is often not contained in the ammo type descriptor. --- +-- -- One way to determin which types of ammo the unit carries, one can use the debug mode of the arty class via @{#ARTY.SetDebugON}(). --- In debug mode, the all ammo types of the group are printed to the monitor as message and can be found in the DCS.log file. --- --- ## Empoying Selected Weapons --- +-- In debug mode, the all ammo types of the group are printed to the monitor as message and can be found in the DCS.log file. +-- +-- ## Employing Selected Weapons +-- -- If an ARTY group carries multiple weapons, which can be used for artillery task, a certain weapon type can be selected to attack the target. -- This is done via the *weapontype* parameter of the @{#ARTY.AssignTargetCoord}(..., *weapontype*, ...) function. --- +-- -- The enumerator @{#ARTY.WeaponType} has been defined to select a certain weapon type. Supported values are: --- +-- -- * @{#ARTY.WeaponType}.Auto: Automatic weapon selection by the DCS logic. This is the default setting. -- * @{#ARTY.WeaponType}.Cannon: Only cannons are used during the attack. Corresponding ammo type are shells and can be defined by @{#ARTY.SetShellTypes}. -- * @{#ARTY.WeaponType}.Rockets: Only unguided are used during the attack. Corresponding ammo type are rockets/nurs and can be defined by @{#ARTY.SetRocketTypes}. @@ -230,96 +234,96 @@ -- * @{#ARTY.WeaponType}.TacticalNukes: Use tactical nuclear shells. This works only with units that have shells and is described below. -- * @{#ARTY.WeaponType}.IlluminationShells: Use illumination shells. This works only with units that have shells and is described below. -- * @{#ARTY.WeaponType}.SmokeShells: Use smoke shells. This works only with units that have shells and is described below. --- +-- -- ## Assigning Relocation Movements -- The ARTY group can be commanded to move. This is done by the @{#ARTY.AssignMoveCoord}(*coord*, *time*, *speed*, *onroad*, *cancel*, *name*) function. -- With this multiple timed moves of the group can be scheduled easily. By default, these moves will only be executed if the group is state **CombatReady**. --- +-- -- ### Parameters --- +-- -- * *coord*: Coordinates where the group should move to given as @{Core.Point#COORDINATE} object. -- * *time*: The time when the move should be executed. This has to be given as a string in the format "hh:mm:ss" (hh=hours, mm=minutes, ss=seconds). -- * *speed*: Speed of the group in km/h. -- * *onroad*: If this parameter is set to true, the group uses mainly roads to get to the commanded coordinates. -- * *cancel*: If set to true, any current engagement of targets is cancelled at the time the move should be executed. -- * *name*: Can be used to set a user defined name of the move. By default the name is created from the LL DMS coordinates. --- +-- -- ## Automatic Rearming --- +-- -- If an ARTY group runs out of ammunition, it can be rearmed automatically. --- +-- -- ### Rearming Group -- The first way to activate the automatic rearming is to define a rearming group with the function @{#ARTY.SetRearmingGroup}(*group*). For the blue side, this -- could be a M181 transport truck and for the red side an Ural-375 truck. --- +-- -- Once the ARTY group is out of ammo and the **Rearm** event is triggered, the defined rearming truck will drive to the ARTY group. -- So the rearming truck does not have to be placed nearby the artillery group. When the rearming is complete, the rearming truck will drive back to its original position. --- +-- -- ### Rearming Place -- The second alternative is to define a rearming place, e.g. a FRAP, airport or any other warehouse. This is done with the function @{#ARTY.SetRearmingPlace}(*coord*). -- The parameter *coord* specifies the coordinate of the rearming place which should not be further away then 100 meters from the warehouse. --- +-- -- When the **Rearm** event is triggered, the ARTY group will move to the rearming place. Of course, the group must be mobil. So for a mortar this rearming procedure would not work. --- +-- -- After the rearming is complete, the ARTY group will move back to its original position and resume normal operations. --- +-- -- ### Rearming Group **and** Rearming Place -- If both a rearming group *and* a rearming place are specified like described above, both the ARTY group and the rearming truck will move to the rearming place and meet there. --- +-- -- After the rearming is complete, both groups will move back to their original positions. --- +-- -- ## Simulated Weapons --- +-- -- In addtion to the standard weapons a group has available some special weapon types that are not possible to use in the native DCS environment are simulated. --- +-- -- ### Tactical Nukes --- +-- -- ARTY groups that can fire shells can also be used to fire tactical nukes. This is achieved by setting the weapon type to **ARTY.WeaponType.TacticalNukes** in the -- @{#ARTY.AssignTargetCoord}() function. -- -- By default, they group does not have any nukes available. To give the group the ability the function @{#ARTY.SetTacNukeShells}(*n*) can be used. -- This supplies the group with *n* nuclear shells, where *n* is restricted to the number of conventional shells the group can carry. --- Note that the group must always have convenctional shells left in order to fire a nuclear shell. --- +-- Note that the group must always have convenctional shells left in order to fire a nuclear shell. +-- -- The default explostion strength is 0.075 kilo tons TNT. The can be changed with the @{#ARTY.SetTacNukeWarhead}(*strength*), where *strength* is given in kilo tons TNT. --- +-- -- ### Illumination Shells --- --- ARTY groups that possess shells can fire shells with illumination bombs. First, the group needs to be equipped with this weapon. This is done by the +-- +-- ARTY groups that possess shells can fire shells with illumination bombs. First, the group needs to be equipped with this weapon. This is done by the -- function @{ARTY.SetIlluminationShells}(*n*, *power*), where *n* is the number of shells the group has available and *power* the illumination power in mega candela (mcd). --- +-- -- In order to execute an engagement with illumination shells one has to use the weapon type *ARTY.WeaponType.IlluminationShells* in the -- @{#ARTY.AssignTargetCoord}() function. --- +-- -- In the simulation, the explosive shell that is fired is destroyed once it gets close to the target point but before it can actually impact. -- At this position an illumination bomb is triggered at a random altitude between 500 and 1000 meters. This interval can be set by the function -- @{ARTY.SetIlluminationMinMaxAlt}(*minalt*, *maxalt*). --- +-- -- ### Smoke Shells --- +-- -- In a similar way to illumination shells, ARTY groups can also employ smoke shells. The numer of smoke shells the group has available is set by the function -- @{#ARTY.SetSmokeShells}(*n*, *color*), where *n* is the number of shells and *color* defines the smoke color. Default is SMOKECOLOR.Red. --- +-- -- The weapon type to be used in the @{#ARTY.AssignTargetCoord}() function is *ARTY.WeaponType.SmokeShells*. --- +-- -- The explosive shell the group fired is destroyed shortly before its impact on the ground and smoke of the speficied color is triggered at that position. -- --- +-- -- ## Assignments via Markers on F10 Map --- +-- -- Targets and relocations can be assigned by players via placing a mark on the F10 map. The marker text must contain certain keywords. --- +-- -- This feature can be turned on with the @{#ARTY.SetMarkAssignmentsOn}(*key*, *readonly*). The parameter *key* is optional. When set, it can be used as PIN, i.e. only -- players who know the correct key are able to assign and cancel targets or relocations. Default behavior is that all players belonging to the same coalition as the -- ARTY group are able to assign targets and moves without a key. --- +-- -- ### Target Assignments -- A new target can be assigned by writing **arty engage** in the marker text. -- This is followed by a **comma separated list** of (optional) keywords and parameters. -- First, it is important to address the ARTY group or groups that should engage. This can be done in numrous ways. The keywords are *battery*, *alias*, *cluster*. -- It is also possible to address all ARTY groups by the keyword *everyone* or *allbatteries*. These two can be used synonymously. -- **Note that**, if no battery is assigned nothing will happen. --- +-- -- * *everyone* or *allbatteries* The target is assigned to all batteries. -- * *battery* Name of the ARTY group that the target is assigned to. Note that **the name is case sensitive** and has to be given in quotation marks. Default is all ARTY groups of the right coalition. -- * *alias* Alias of the ARTY group that the target is assigned to. The alias is **case sensitive** and needs to be in quotation marks. @@ -334,7 +338,7 @@ -- * *lldms* Specify the coordinates in Lat/Long degrees, minutes and seconds format. The actual location of the marker is unimportant here. The group will engage the coordinates given in the lldms keyword. -- Format is DD:MM:SS[N,S] DD:MM:SS[W,E]. See example below. This can be useful when coordinates in this format are obtained from elsewhere. -- * *readonly* The marker is readonly and cannot be deleted by users. Hence, assignment cannot be cancelled by removing the marker. --- +-- -- Here are examples of valid marker texts: -- arty engage, battery "Blue Paladin Alpha" -- arty engage, everyone @@ -348,14 +352,14 @@ -- arty engage, battery "Blue MRLS 1", key 666 -- arty engage, battery "Paladin Alpha", weapon nukes, shots 1, time 20:15 -- arty engage, battery "Horwitzer 1", lldms 41:51:00N 41:47:58E --- +-- -- Note that the keywords and parameters are *case insensitve*. Only exception are the battery, alias and cluster names. -- These must be exactly the same as the names of the goups defined in the mission editor or the aliases and cluster names defined in the script. --- +-- -- ### Relocation Assignments --- +-- -- Markers can also be used to relocate the group with the keyphrase **arty move**. This is done in a similar way as assigning targets. Here, the (optional) keywords and parameters are: --- +-- -- * *time* Time for which which the relocation/move is schedules, e.g. 08:42. Default is as soon as possible. -- * *speed* The speed in km/h the group will drive at. Default is 70% of its max possible speed. -- * *on road* Group will use mainly roads. Default is off, i.e. it will go in a straight line from its current position to the assigned coordinate. @@ -365,9 +369,9 @@ -- * *cluster* The cluster of ARTY groups that is addessed. Clusters can be defined by the function @{#ARTY.AddToCluster}(*clusters*). Names are **case sensitive** and need to be in quotation marks. -- * *key* A number to authorize the target assignment. Only specifing the correct number will trigger an engagement. -- * *lldms* Specify the coordinates in Lat/Long degrees, minutes and seconds format. The actual location of the marker is unimportant. The group will move to the coordinates given in the lldms keyword. --- Format is DD:MM:SS[N,S] DD:MM:SS[W,E]. See example below. +-- Format is DD:MM:SS[N,S] DD:MM:SS[W,E]. See example below. -- * *readonly* Marker cannot be deleted by users any more. Hence, assignment cannot be cancelled by removing the marker. --- +-- -- Here are some examples: -- arty move, battery "Blue Paladin" -- arty move, battery "Blue MRLS", canceltarget, speed 10, on road @@ -377,55 +381,55 @@ -- arty move, cluster "Northern Batteries" "Southern Batteries" -- arty move, cluster "Northern Batteries", cluster "Southern Batteries" -- arty move, everyone --- +-- -- ### Requests --- +-- -- Marks can also be to send requests to the ARTY group. This is done by the keyword **arty request**, which can have the keywords --- +-- -- * *target* All assigned targets are reported. -- * *move* All assigned relocation moves are reported. -- * *ammo* Current ammunition status is reported. --- +-- -- For example -- arty request, everyone, ammo -- arty request, battery "Paladin Bravo", targets -- arty request, cluster "All Mortars", move --- +-- -- The actual location of the marker is irrelevant for these requests. --- +-- -- ### Cancel --- +-- -- Current actions can be cancelled by the keyword **arty cancel**. Actions that can be cancelled are current engagements, relocations and rearming assignments. --- +-- -- For example -- arty cancel, target, battery "Paladin Bravo" -- arty cancel, everyone, move -- arty cancel, rearming, battery "MRLS Charly" --- +-- -- ### Settings --- +-- -- A few options can be set by marks. The corresponding keyword is **arty set**. This can be used to define the rearming place and group for a battery. --- +-- -- To set the reamring place of a group at the marker position type -- arty set, battery "Paladin Alpha", rearming place --- +-- -- Setting the rearming group is independent of the position of the mark. Just create one anywhere on the map and type -- arty set, battery "Mortar Bravo", rearming group "Ammo Truck M818" -- Note that the name of the rearming group has to be given in quotation marks and spellt exactly as the group name defined in the mission editor. --- +-- -- ## Transporting --- --- ARTY groups can be transported to another location as @{Cargo.Cargo} by means of classes such as @{AI.AI_Cargo_APC}, @{AI.AI_Cargo_Dispatcher_APC}, +-- +-- ARTY groups can be transported to another location as @{Cargo.Cargo} by means of classes such as @{AI.AI_Cargo_APC}, @{AI.AI_Cargo_Dispatcher_APC}, -- @{AI.AI_Cargo_Helicopter}, @{AI.AI_Cargo_Dispatcher_Helicopter} or @{AI.AI_Cargo_Airplane}. --- +-- -- In order to do this, one needs to define an ARTY object via the @{#ARTY.NewFromCargoGroup}(*cargogroup*, *alias*) function. -- The first argument *cargogroup* has to be a @{Cargo.CargoGroup#CARGO_GROUP} object. The second argument *alias* is a string which can be freely chosen by the user. --- +-- -- ## Fine Tuning --- +-- -- The mission designer has a few options to tailor the ARTY object according to his needs. --- --- * @{#ARTY.SetAutoRelocateToFiringRange}(*maxdist*, *onroad*) lets the ARTY group automatically move to within firing range if a current target is outside the min/max firing range. The +-- +-- * @{#ARTY.SetAutoRelocateToFiringRange}(*maxdist*, *onroad*) lets the ARTY group automatically move to within firing range if a current target is outside the min/max firing range. The -- optional parameter *maxdist* is the maximum distance im km the group will move. If the distance is greater no relocation is performed. Default is 50 km. -- * @{#ARTY.SetAutoRelocateAfterEngagement}(*rmax*, *rmin*) will cause the ARTY group to change its position after each firing assignment. -- Optional parameters *rmax*, *rmin* define the max/min distance for relocation of the group. Default distance is randomly between 300 and 800 m. @@ -440,68 +444,68 @@ -- * @{#ARTY.SetWaitForShotTime}(*waittime*) sets the time after which a target is deleted from the queue if no shooting event occured after the target engagement started. -- Default is 300 seconds. Note that this can for example happen, when the assigned target is out of range. -- * @{#ARTY.SetDebugON}() and @{#ARTY.SetDebugOFF}() can be used to enable/disable the debug mode. --- +-- -- ## Examples --- +-- -- ### Assigning Multiple Targets -- This basic example illustrates how to assign multiple targets and defining a rearming group. -- -- Creat a new ARTY object from a Paladin group. -- paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) --- +-- -- -- Define a rearming group. This is a Transport M818 truck. -- paladin:SetRearmingGroup(GROUP:FindByName("Blue Ammo Truck")) --- +-- -- -- Set the max firing range. A Paladin unit has a range of 20 km. -- paladin:SetMaxFiringRange(20) --- +-- -- -- Low priorty (90) target, will be engage last. Target is engaged two times. At each engagement five shots are fired. -- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 3"):GetCoordinate(), 90, nil, 5, 2) -- -- Medium priorty (nil=50) target, will be engage second. Target is engaged two times. At each engagement ten shots are fired. -- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), nil, nil, 10, 2) -- -- High priorty (10) target, will be engage first. Target is engaged three times. At each engagement twenty shots are fired. -- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 2"):GetCoordinate(), 10, nil, 20, 3) --- +-- -- -- Start ARTY process. -- paladin:Start() -- **Note** --- +-- -- * If a parameter should be set to its default value, it has to be set to *nil* if other non-default parameters follow. Parameters at the end can simply be skiped. --- * In this example, the target coordinates are taken from groups placed in the mission edit using the COORDINATE:GetCoordinate() function. --- +-- * In this example, the target coordinates are taken from groups placed in the mission edit using the COORDINATE:GetCoordinate() function. +-- -- ### Scheduled Engagements -- -- Mission starts at 8 o'clock. -- -- Assign two scheduled targets. --- +-- -- -- Create ARTY object from Paladin group. -- paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) --- +-- -- -- Assign target coordinates. Priority=50 (medium), radius=100 m, use 5 shells per engagement, engage 1 time at two past 8 o'clock. -- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 50, 100, 5, 1, "08:02:00", ARTY.WeaponType.Auto, "Target 1") --- +-- -- -- Assign target coordinates. Priority=10 (high), radius=300 m, use 10 shells per engagement, engage 1 time at seven past 8 o'clock. -- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 2"):GetCoordinate(), 10, 300, 10, 1, "08:07:00", ARTY.WeaponType.Auto, "Target 2") --- +-- -- -- Start ARTY process. -- paladin:Start() --- +-- -- ### Specific Weapons -- This example demonstrates how to use specific weapons during an engagement. -- -- Define the Normandy as ARTY object. -- normandy=ARTY:New(GROUP:FindByName("Normandy")) --- +-- -- -- Add target: prio=50, radius=300 m, number of missiles=20, number of engagements=1, start time=08:05 hours, only use cruise missiles for this attack. -- normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 20, 300, 50, 1, "08:01:00", ARTY.WeaponType.CruiseMissile) --- +-- -- -- Add target: prio=50, radius=300 m, number of shells=100, number of engagements=1, start time=08:15 hours, only use cannons during this attack. -- normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 50, 300, 100, 1, "08:15:00", ARTY.WeaponType.Cannon) --- +-- -- -- Define shells that are counted to check whether the ship is out of ammo. -- -- Note that this is necessary because the Normandy has a lot of other shell type weapons which cannot be used to engage ground targets in an artillery style manner. -- normandy:SetShellTypes({"MK45_127"}) --- +-- -- -- Define missile types that are counted. -- normandy:SetMissileTypes({"BGM"}) --- +-- -- -- Start ARTY process. -- normandy:Start() -- @@ -509,13 +513,13 @@ -- This example demonstates how an ARTY group can be transported to another location as cargo. -- -- Define a group as CARGO_GROUP -- CargoGroupMortars=CARGO_GROUP:New(GROUP:FindByName("Mortars"), "Mortars", "Mortar Platoon Alpha", 100 , 10) --- +-- -- -- Define the mortar CARGO GROUP as ARTY object -- mortars=ARTY:NewFromCargoGroup(CargoGroupMortars, "Mortar Platoon Alpha") --- +-- -- -- Start ARTY process -- mortars:Start() --- +-- -- -- Setup AI cargo dispatcher for e.g. helos -- SetHeloCarriers = SET_GROUP:New():FilterPrefixes("CH-47D"):FilterStart() -- SetCargoMortars = SET_CARGO:New():FilterTypes("Mortars"):FilterStart() @@ -527,6 +531,7 @@ -- @field #ARTY ARTY={ ClassName="ARTY", + lid=nil, Debug=false, targets={}, moves={}, @@ -587,6 +592,9 @@ ARTY={ autorelocate=false, autorelocatemaxdist=50000, autorelocateonroad=false, + coalition=nil, + respawnafterdeath=false, + respawndelay=nil } --- Weapong type ID. See [here](http://wiki.hoggit.us/view/DCS_enum_weapon_flag). @@ -668,17 +676,30 @@ ARTY.db={ }, } ---- Some ID to identify who we are in output of the DCS.log file. --- @field #string id -ARTY.id="ARTY | " +--- Target. +-- @type ARTY.Target +-- @field #string name Name of target. +-- @field Core.Point#COORDINATE coord Target coordinates. +-- @field #number radius Shelling radius in meters. +-- @field #number nshells Number of shells (or other weapon types) fired upon target. +-- @field #number engaged Number of times this target was engaged. +-- @field #boolean underfire If true, target is currently under fire. +-- @field #number prio Priority of target. +-- @field #number maxengage Max number of times, the target will be engaged. +-- @field #number time Abs. mission time in seconds, when the target is scheduled to be attacked. +-- @field #number weapontype Type of weapon used for engagement. See #ARTY.WeaponType. +-- @field #number Tassigned Abs. mission time when target was assigned. +-- @field #boolean attackgroup If true, use task attack group rather than fire at point for engagement. --- Arty script version. -- @field #string version -ARTY.version="1.0.6" +ARTY.version="1.1.7" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list: +-- TODO: Add hit event and make the arty group relocate. +-- TODO: Handle rearming for ships. How? -- DONE: Delete targets from queue user function. -- DONE: Delete entire target queue user function. -- DONE: Add weapon types. Done but needs improvements. @@ -697,157 +718,141 @@ ARTY.version="1.0.6" -- DONE: Add command move to make arty group move. -- DONE: remove schedulers for status event. -- DONE: Improve handling of special weapons. When winchester if using selected weapons? --- TODO: Handle rearming for ships. How? -- DONE: Make coordinate after rearming general, i.e. also work after the group has moved to anonther location. -- DONE: Add set commands via markers. E.g. set rearming place. -- DONE: Test stationary types like mortas ==> rearming etc. --- TODO: Add hit event and make the arty group relocate. -- DONE: Add illumination and smoke. --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Creates a new ARTY object from a MOOSE CARGO_GROUP object. --- @param #ARTY self --- @param Cargo.CargoGroup#CARGO_GROUP cargogroup The CARGO GROUP object for which artillery tasks should be assigned. --- @param alias (Optional) Alias name the group will be calling itself when sending messages. Default is the group name. --- @return #ARTY ARTY object or nil if group does not exist or is not a ground or naval group. -function ARTY:NewFromCargoGroup(cargogroup, alias) - BASE:F2({cargogroup=cargogroup, alias=alias}) - - if cargogroup then - BASE:T(ARTY.id..string.format("ARTY script version %s. Added CARGO group %s.", ARTY.version, cargogroup:GetName())) - else - BASE:E(ARTY.id.."ERROR: Requested ARTY CARGO GROUP does not exist! (Has to be a MOOSE CARGO(!) group.)") - return nil - end - - -- Get group belonging to the cargo group. - local group=cargogroup:GetObject() - - -- Create ARTY object. - local arty=ARTY:New(group,alias) - - -- Set iscargo flag. - arty.iscargo=true - - -- Set cargo group object. - arty.cargogroup=cargogroup - - return arty -end - --- Creates a new ARTY object from a MOOSE group object. -- @param #ARTY self -- @param Wrapper.Group#GROUP group The GROUP object for which artillery tasks should be assigned. -- @param alias (Optional) Alias name the group will be calling itself when sending messages. Default is the group name. -- @return #ARTY ARTY object or nil if group does not exist or is not a ground or naval group. function ARTY:New(group, alias) - BASE:F2({group=group, alias=alias}) -- Inherits from FSM_CONTROLLABLE local self=BASE:Inherit(self, FSM_CONTROLLABLE:New()) -- #ARTY + -- If group name was given. + if type(group)=="string" then + self.groupname=group + group=GROUP:FindByName(group) + if not group then + self:E(string.format("ERROR: Requested ARTY group %s does not exist! (Has to be a MOOSE group.)", self.groupname)) + return nil + end + end + -- Check that group is present. if group then - self:T(ARTY.id..string.format("ARTY script version %s. Added group %s.", ARTY.version, group:GetName())) + self:T(string.format("ARTY script version %s. Added group %s.", ARTY.version, group:GetName())) else - self:E(ARTY.id.."ERROR: Requested ARTY group does not exist! (Has to be a MOOSE group.)") + self:E("ERROR: Requested ARTY group does not exist! (Has to be a MOOSE group.)") return nil end - + -- Check that we actually have a GROUND group. if not (group:IsGround() or group:IsShip()) then - self:E(ARTY.id..string.format("ERROR: ARTY group %s has to be a GROUND or SHIP group!", group:GetName())) + self:E(string.format("ERROR: ARTY group %s has to be a GROUND or SHIP group!", group:GetName())) return nil end - + -- Set the controllable for the FSM. self:SetControllable(group) - + -- Set the group name self.groupname=group:GetName() - + + -- Get coalition. + self.coalition=group:GetCoalition() + -- Set an alias name. if alias~=nil then self.alias=tostring(alias) else self.alias=self.groupname end - + + -- Log id. + self.lid=string.format("ARTY %s | ", self.alias) + -- Set the initial coordinates of the ARTY group. self.InitialCoord=group:GetCoordinate() - + -- Get DCS descriptors of group. local DCSgroup=Group.getByName(group:GetName()) local DCSunit=DCSgroup:getUnit(1) self.DCSdesc=DCSunit:getDesc() -- DCS descriptors. - self:T3(ARTY.id.."DCS descriptors for group "..group:GetName()) + self:T3(self.lid.."DCS descriptors for group "..group:GetName()) for id,desc in pairs(self.DCSdesc) do self:T3({id=id, desc=desc}) end - + -- Maximum speed in km/h. self.SpeedMax=group:GetSpeedMax() - + -- Group is mobile or not (e.g. mortars). if self.SpeedMax>1 then self.ismobile=true else self.ismobile=false end - + -- Set speed to 0.7 of maximum. self.Speed=self.SpeedMax * 0.7 - + -- Displayed name (similar to type name below) self.DisplayName=self.DCSdesc.displayName - + -- Is this infantry or not. self.IsArtillery=DCSunit:hasAttribute("Artillery") - + -- Type of group. self.Type=group:GetTypeName() - + -- Initial group strength. self.IniGroupStrength=#group:GetUnits() - --------------- + --------------- -- Transitions: --------------- -- Entry. self:AddTransition("*", "Start", "CombatReady") - + -- Blue branch. self:AddTransition("CombatReady", "OpenFire", "Firing") self:AddTransition("Firing", "CeaseFire", "CombatReady") - + -- Violett branch. self:AddTransition("CombatReady", "Winchester", "OutOfAmmo") - -- Red branch. + -- Red branch. self:AddTransition({"CombatReady", "OutOfAmmo"}, "Rearm", "Rearming") self:AddTransition("Rearming", "Rearmed", "Rearmed") - + -- Green branch. self:AddTransition("*", "Move", "Moving") self:AddTransition("Moving", "Arrived", "Arrived") - + -- Yellow branch. self:AddTransition("*", "NewTarget", "*") - + -- Not in diagram. self:AddTransition("*", "CombatReady", "CombatReady") self:AddTransition("*", "Status", "*") self:AddTransition("*", "NewMove", "*") self:AddTransition("*", "Dead", "*") - + self:AddTransition("*", "Respawn", "CombatReady") + -- Transport as cargo (not in diagram). self:AddTransition("*", "Loaded", "InTransit") self:AddTransition("InTransit", "UnLoaded", "CombatReady") - + -- Unknown transitons. To be checked if adding these causes problems. self:AddTransition("Rearming", "Arrived", "Rearming") self:AddTransition("Rearming", "Move", "Rearming") @@ -861,7 +866,7 @@ function ARTY:New(group, alias) -- @param #string Event Event. -- @param #string To To state. -- @param #table target Array holding the target info. - + --- User function for OnAfter "OpenFire" event. -- @function [parent=#ARTY] OnAfterOpenFire -- @param #ARTY self @@ -953,7 +958,15 @@ function ARTY:New(group, alias) -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. + -- @param #string Unitname Name of the dead unit. + --- User function for OnAfter "Respawn" event. + -- @function [parent=#ARTY] OnAfterRespawn + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. --- User function for OnEnter "CombatReady" state. -- @function [parent=#ARTY] OnEnterCombatReady @@ -1001,13 +1014,13 @@ function ARTY:New(group, alias) -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. - -- @param #string To To state. + -- @param #string To To state. --- Function to start the ARTY FSM process. -- @function [parent=#ARTY] Start -- @param #ARTY self - + --- Function to start the ARTY FSM process after a delay. -- @function [parent=#ARTY] __Start -- @param #ARTY self @@ -1016,7 +1029,7 @@ function ARTY:New(group, alias) --- Function to update the status of the ARTY group and tigger FSM events. Triggers the FSM event "Status". -- @function [parent=#ARTY] Status -- @param #ARTY self - + --- Function to update the status of the ARTY group and tigger FSM events after a delay. Triggers the FSM event "Status". -- @function [parent=#ARTY] __Status -- @param #ARTY self @@ -1025,11 +1038,13 @@ function ARTY:New(group, alias) --- Function called when a unit of the ARTY group died. Triggers the FSM event "Dead". -- @function [parent=#ARTY] Dead -- @param #ARTY self - + -- @param #string unitname Name of the unit that died. + --- Function called when a unit of the ARTY group died after a delay. Triggers the FSM event "Dead". -- @function [parent=#ARTY] __Dead -- @param #ARTY self -- @param #number Delay in seconds. + -- @param #string unitname Name of the unit that died. --- Add a new target for the ARTY group. Triggers the FSM event "NewTarget". -- @function [parent=#ARTY] NewTarget @@ -1094,7 +1109,7 @@ function ARTY:New(group, alias) -- @function [parent=#ARTY] __Arrived -- @param #ARTY self -- @param #number delay Delay in seconds. - + --- Tell ARTY group it is combat ready. Triggers the FSM event "CombatReady". -- @function [parent=#ARTY] CombatReady -- @param #ARTY self @@ -1102,7 +1117,7 @@ function ARTY:New(group, alias) --- Tell ARTY group it is combat ready after a delay. Triggers the FSM event "CombatReady". -- @function [parent=#ARTY] __CombatReady -- @param #ARTY self - -- @param #number delay Delay in seconds. + -- @param #number delay Delay in seconds. --- Tell ARTY group it is out of ammo. Triggers the FSM event "Winchester". -- @function [parent=#ARTY] Winchester @@ -1111,12 +1126,52 @@ function ARTY:New(group, alias) --- Tell ARTY group it is out of ammo after a delay. Triggers the FSM event "Winchester". -- @function [parent=#ARTY] __Winchester -- @param #ARTY self - -- @param #number delay Delay in seconds. + -- @param #number delay Delay in seconds. + + --- Respawn ARTY group. + -- @function [parent=#ARTY] Respawn + -- @param #ARTY self + + --- Respawn ARTY group after a delay. + -- @function [parent=#ARTY] __Respawn + -- @param #ARTY self + -- @param #number delay Delay in seconds. - return self end +--- Creates a new ARTY object from a MOOSE CARGO_GROUP object. +-- @param #ARTY self +-- @param Cargo.CargoGroup#CARGO_GROUP cargogroup The CARGO GROUP object for which artillery tasks should be assigned. +-- @param alias (Optional) Alias name the group will be calling itself when sending messages. Default is the group name. +-- @return #ARTY ARTY object or nil if group does not exist or is not a ground or naval group. +function ARTY:NewFromCargoGroup(cargogroup, alias) + BASE:F2({cargogroup=cargogroup, alias=alias}) + + if cargogroup then + BASE:T(self.lid..string.format("ARTY script version %s. Added CARGO group %s.", ARTY.version, cargogroup:GetName())) + else + BASE:E(self.lid.."ERROR: Requested ARTY CARGO GROUP does not exist! (Has to be a MOOSE CARGO(!) group.)") + return nil + end + + -- Get group belonging to the cargo group. + local group=cargogroup:GetObject() + + -- Create ARTY object. + local arty=ARTY:New(group,alias) + + -- Set iscargo flag. + arty.iscargo=true + + -- Set cargo group object. + arty.cargogroup=cargogroup + + return arty +end + + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- User Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1138,7 +1193,7 @@ end -- paladin:Start() function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, weapontype, name, unique) self:F({coord=coord, prio=prio, radius=radius, nshells=nshells, maxengage=maxengage, time=time, weapontype=weapontype, name=name, unique=unique}) - + -- Set default values. nshells=nshells or 5 radius=radius or 100 @@ -1167,26 +1222,26 @@ function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, w else text="ERROR: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter!" MESSAGE:New(text, 30):ToAll() - self:E(ARTY.id..text) + self:E(self.lid..text) return nil end if text~=nil then - self:E(ARTY.id..text) + self:E(self.lid..text) end - + -- Name of the target. - local _name=name or coord:ToStringLLDMS() + local _name=name or coord:ToStringLLDMS() local _unique=true - + -- Check if the name has already been used for another target. If so, the function returns a new unique name. _name,_unique=self:_CheckName(self.targets, _name, not unique) - + -- Target name should be unique and is not. if unique==true and _unique==false then - self:T(ARTY.id..string.format("%s: target %s should have a unique name but name was already given. Rejecting target!", self.groupname, _name)) + self:T(self.lid..string.format("%s: target %s should have a unique name but name was already given. Rejecting target!", self.groupname, _name)) return nil end - + -- Time in seconds. local _time if type(time)=="string" then @@ -1196,19 +1251,110 @@ function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, w else _time=timer.getAbsTime() end - + -- Prepare target array. local _target={name=_name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false, prio=prio, maxengage=maxengage, time=_time, weapontype=weapontype} - + -- Add to table. table.insert(self.targets, _target) - + -- Trigger new target event. self:__NewTarget(1, _target) - + return _name end +--- Assign a target group to the ARTY group. Note that this will use the Attack Group Task rather than the Fire At Point Task. +-- @param #ARTY self +-- @param Wrapper.Group#GROUP group Target group. +-- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50. +-- @param #number radius (Optional) Radius. Default is 100 m. +-- @param #number nshells (Optional) How many shells (or rockets) are fired on target per engagement. Default 5. +-- @param #number maxengage (Optional) How many times a target is engaged. Default 1. +-- @param #string time (Optional) Day time at which the target should be engaged. Passed as a string in format "08:13:45". Current task will be canceled. +-- @param #number weapontype (Optional) Type of weapon to be used to attack this target. Default ARTY.WeaponType.Auto, i.e. the DCS logic automatically determins the appropriate weapon. +-- @param #string name (Optional) Name of the target. Default is LL DMS coordinate of the target. If the name was already given, the numbering "#01", "#02",... is appended automatically. +-- @param #boolean unique (Optional) Target is unique. If the target name is already known, the target is rejected. Default false. +-- @return #string Name of the target. Can be used for further reference, e.g. deleting the target from the list. +-- @usage paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 10, 300, 10, 1, "08:02:00", ARTY.WeaponType.Auto, "Target 1") +-- paladin:Start() +function ARTY:AssignAttackGroup(group, prio, radius, nshells, maxengage, time, weapontype, name, unique) + + -- Set default values. + nshells=nshells or 5 + radius=radius or 100 + maxengage=maxengage or 1 + prio=prio or 50 + prio=math.max( 1, prio) + prio=math.min(100, prio) + if unique==nil then + unique=false + end + weapontype=weapontype or ARTY.WeaponType.Auto + + -- TODO Check if we have a group object. + if type(group)=="string" then + group=GROUP:FindByName(group) + end + + if group and group:IsAlive() then + + local coord=group:GetCoordinate() + + -- Name of the target. + local _name=group:GetName() + local _unique=true + + -- Check if the name has already been used for another target. If so, the function returns a new unique name. + _name,_unique=self:_CheckName(self.targets, _name, not unique) + + -- Target name should be unique and is not. + if unique==true and _unique==false then + self:T(self.lid..string.format("%s: target %s should have a unique name but name was already given. Rejecting target!", self.groupname, _name)) + return nil + end + + -- Time in seconds. + local _time + if type(time)=="string" then + _time=self:_ClockToSeconds(time) + elseif type(time)=="number" then + _time=timer.getAbsTime()+time + else + _time=timer.getAbsTime() + end + + -- Prepare target array. + local target={} --#ARTY.Target + target.attackgroup=true + target.name=_name + target.coord=coord + target.radius=radius + target.nshells=nshells + target.engaged=0 + target.underfire=false + target.prio=prio + target.time=_time + target.maxengage=maxengage + target.weapontype=weapontype + + -- Add to table. + table.insert(self.targets, target) + + -- Trigger new target event. + self:__NewTarget(1, target) + + return _name + else + self:E("ERROR: Group does not exist!") + end + + return nil +end + + + --- Assign coordinate to where the ARTY group should move. -- @param #ARTY self -- @param Core.Point#COORDINATE coord Coordinates of the new position. @@ -1221,31 +1367,31 @@ end -- @return #string Name of the move. Can be used for further reference, e.g. deleting the move from the list. function ARTY:AssignMoveCoord(coord, time, speed, onroad, cancel, name, unique) self:F({coord=coord, time=time, speed=speed, onroad=onroad, cancel=cancel, name=name, unique=unique}) - + -- Reject move if the group is immobile. if not self.ismobile then - self:T(ARTY.id..string.format("%s: group is immobile. Rejecting move request!", self.groupname)) - return nil + self:T(self.lid..string.format("%s: group is immobile. Rejecting move request!", self.groupname)) + return nil end - + -- Default if unique==nil then unique=false end - + -- Name of the target. local _name=name or coord:ToStringLLDMS() local _unique=true - + -- Check if the name has already been used for another target. If so, the function returns a new unique name. _name,_unique=self:_CheckName(self.moves, _name, not unique) - + -- Move name should be unique and is not. if unique==true and _unique==false then - self:T(ARTY.id..string.format("%s: move %s should have a unique name but name was already given. Rejecting move!", self.groupname, _name)) + self:T(self.lid..string.format("%s: move %s should have a unique name but name was already given. Rejecting move!", self.groupname, _name)) return nil end - + -- Set speed. if speed then -- Make sure, given speed is less than max physiaclly possible speed of group. @@ -1255,7 +1401,7 @@ function ARTY:AssignMoveCoord(coord, time, speed, onroad, cancel, name, unique) else speed=self.SpeedMax*0.7 end - + -- Default is off road. if onroad==nil then onroad=false @@ -1265,7 +1411,7 @@ function ARTY:AssignMoveCoord(coord, time, speed, onroad, cancel, name, unique) if cancel==nil then cancel=false end - + -- Time in seconds. local _time if type(time)=="string" then @@ -1275,138 +1421,164 @@ function ARTY:AssignMoveCoord(coord, time, speed, onroad, cancel, name, unique) else _time=timer.getAbsTime() end - + -- Prepare move array. local _move={name=_name, coord=coord, time=_time, speed=speed, onroad=onroad, cancel=cancel} - + -- Add to table. table.insert(self.moves, _move) - + return _name end --- Set alias, i.e. the name the group will use when sending messages. -- @param #ARTY self -- @param #string alias The alias for the group. +-- @return self function ARTY:SetAlias(alias) self:F({alias=alias}) self.alias=tostring(alias) + return self end --- Add ARTY group to one or more clusters. Enables addressing all ARTY groups within a cluster simultaniously via marker assignments. -- @param #ARTY self -- @param #table clusters Table of cluster names the group should belong to. +-- @return self function ARTY:AddToCluster(clusters) self:F({clusters=clusters}) - + -- Convert input to table. local names if type(clusters)=="table" then - names=clusters + names=clusters elseif type(clusters)=="string" then names={clusters} else -- error message - self:E(ARTY.id.."ERROR: Input parameter must be a string or a table in ARTY:AddToCluster()!") + self:E(self.lid.."ERROR: Input parameter must be a string or a table in ARTY:AddToCluster()!") return end -- Add names to cluster array. for _,cluster in pairs(names) do table.insert(self.clusters, cluster) - end + end + + return self end --- Set minimum firing range. Targets closer than this distance are not engaged. -- @param #ARTY self -- @param #number range Min range in kilometers. Default is 0.1 km. +-- @return self function ARTY:SetMinFiringRange(range) self:F({range=range}) self.minrange=range*1000 or 100 + return self end --- Set maximum firing range. Targets further away than this distance are not engaged. -- @param #ARTY self -- @param #number range Max range in kilometers. Default is 1000 km. +-- @return self function ARTY:SetMaxFiringRange(range) self:F({range=range}) self.maxrange=range*1000 or 1000*1000 + return self end --- Set time interval between status updates. During the status check, new events are triggered. -- @param #ARTY self -- @param #number interval Time interval in seconds. Default 10 seconds. +-- @return self function ARTY:SetStatusInterval(interval) self:F({interval=interval}) self.StatusInterval=interval or 10 + return self end --- Set time how it is waited a unit the first shot event happens. If no shot is fired after this time, the task to fire is aborted and the target removed. -- @param #ARTY self -- @param #number waittime Time in seconds. Default 300 seconds. +-- @return self function ARTY:SetWaitForShotTime(waittime) self:F({waittime=waittime}) self.WaitForShotTime=waittime or 300 + return self end --- Define the safe distance between ARTY group and rearming unit or rearming place at which rearming process is possible. -- @param #ARTY self --- @param #number distance Safe distance in meters. Default is 100 m. +-- @param #number distance Safe distance in meters. Default is 100 m. +-- @return self function ARTY:SetRearmingDistance(distance) self:F({distance=distance}) self.RearmingDistance=distance or 100 + return self end --- Assign a group, which is responsible for rearming the ARTY group. If the group is too far away from the ARTY group it will be guided towards the ARTY group. -- @param #ARTY self -- @param Wrapper.Group#GROUP group Group that is supposed to rearm the ARTY group. For the blue coalition, this is often a unarmed M818 transport whilst for red an unarmed Ural-375 transport can be used. +-- @return self function ARTY:SetRearmingGroup(group) self:F({group=group}) self.RearmingGroup=group + return self end --- Set the speed the rearming group moves at towards the ARTY group or the rearming place. -- @param #ARTY self -- @param #number speed Speed in km/h. +-- @return self function ARTY:SetRearmingGroupSpeed(speed) self:F({speed=speed}) self.RearmingGroupSpeed=speed + return self end ---- Define if rearming group uses mainly roads to drive to the ARTY group or rearming place. +--- Define if rearming group uses mainly roads to drive to the ARTY group or rearming place. -- @param #ARTY self -- @param #boolean onroad If true, rearming group uses mainly roads. If false, it drives directly to the ARTY group or rearming place. +-- @return self function ARTY:SetRearmingGroupOnRoad(onroad) self:F({onroad=onroad}) if onroad==nil then onroad=true end self.RearmingGroupOnRoad=onroad + return self end ---- Define if ARTY group uses mainly roads to drive to the rearming place. +--- Define if ARTY group uses mainly roads to drive to the rearming place. -- @param #ARTY self -- @param #boolean onroad If true, ARTY group uses mainly roads. If false, it drives directly to the rearming place. +-- @return self function ARTY:SetRearmingArtyOnRoad(onroad) self:F({onroad=onroad}) if onroad==nil then onroad=true end self.RearmingArtyOnRoad=onroad + return self end --- Defines the rearming place of the ARTY group. If the place is too far away from the ARTY group it will be routed to the place. -- @param #ARTY self -- @param Core.Point#COORDINATE coord Coordinates of the rearming place. +-- @return self function ARTY:SetRearmingPlace(coord) self:F({coord=coord}) self.RearmingPlaceCoord=coord + return self end --- Set automatic relocation of ARTY group if a target is assigned which is out of range. The unit will drive automatically towards or away from the target to be in max/min firing range. -- @param #ARTY self -- @param #number maxdistance (Optional) The maximum distance in km the group will travel to get within firing range. Default is 50 km. No automatic relocation is performed if targets are assigned which are further away. --- @param #boolean onroad (Optional) If true, ARTY group uses roads whenever possible. Default false, i.e. group will move in a straight line to the assigned coordinate. +-- @param #boolean onroad (Optional) If true, ARTY group uses roads whenever possible. Default false, i.e. group will move in a straight line to the assigned coordinate. +-- @return self function ARTY:SetAutoRelocateToFiringRange(maxdistance, onroad) self:F({distance=maxdistance, onroad=onroad}) self.autorelocate=true @@ -1416,50 +1588,74 @@ function ARTY:SetAutoRelocateToFiringRange(maxdistance, onroad) onroad=false end self.autorelocateonroad=onroad + return self end --- Set relocate after firing. Group will find a new location after each engagement. Default is off -- @param #ARTY self -- @param #number rmax (Optional) Max distance in meters, the group will move to relocate. Default is 800 m. -- @param #number rmin (Optional) Min distance in meters, the group will move to relocate. Default is 300 m. +-- @return self function ARTY:SetAutoRelocateAfterEngagement(rmax, rmin) self.relocateafterfire=true self.relocateRmax=rmax or 800 self.relocateRmin=rmin or 300 - + -- Ensure that Rmin<=Rmax self.relocateRmin=math.min(self.relocateRmin, self.relocateRmax) + + return self end --- Report messages of ARTY group turned on. This is the default. -- @param #ARTY self +-- @return self function ARTY:SetReportON() self.report=true + return self end --- Report messages of ARTY group turned off. Default is on. -- @param #ARTY self +-- @return self function ARTY:SetReportOFF() self.report=false + return self +end + +--- Respawn group once all units are dead. +-- @param #ARTY self +-- @param #number delay (Optional) Delay before respawn in seconds. +-- @return self +function ARTY:SetRespawnOnDeath(delay) + self.respawnafterdeath=true + self.respawndelay=delay + return self end --- Turn debug mode on. Information is printed to screen. -- @param #ARTY self +-- @return self function ARTY:SetDebugON() self.Debug=true + return self end --- Turn debug mode off. This is the default setting. -- @param #ARTY self +-- @return self function ARTY:SetDebugOFF() self.Debug=false + return self end --- Set default speed the group is moving at if not specified otherwise. -- @param #ARTY self -- @param #number speed Speed in km/h. +-- @return self function ARTY:SetSpeed(speed) self.Speed=speed + return self end --- Delete a target from target list. If the target is currently engaged, it is cancelled. @@ -1467,26 +1663,26 @@ end -- @param #string name Name of the target. function ARTY:RemoveTarget(name) self:F2(name) - + -- Get target ID from namd local id=self:_GetTargetIndexByName(name) - + if id then - + -- Remove target from table. - self:T(ARTY.id..string.format("Group %s: Removing target %s (id=%d).", self.groupname, name, id)) + self:T(self.lid..string.format("Group %s: Removing target %s (id=%d).", self.groupname, name, id)) table.remove(self.targets, id) - + -- Delete marker belonging to this engagement. if self.markallow then local batteryname,markTargetID, markMoveID=self:_GetMarkIDfromName(name) if batteryname==self.groupname and markTargetID~=nil then COORDINATE:RemoveMark(markTargetID) - end + end end - + end - self:T(ARTY.id..string.format("Group %s: Number of targets = %d.", self.groupname, #self.targets)) + self:T(self.lid..string.format("Group %s: Number of targets = %d.", self.groupname, #self.targets)) end --- Delete a move from move list. @@ -1494,16 +1690,16 @@ end -- @param #string name Name of the target. function ARTY:RemoveMove(name) self:F2(name) - + -- Get move ID from name. local id=self:_GetMoveIndexByName(name) - + if id then - + -- Remove move from table. - self:T(ARTY.id..string.format("Group %s: Removing move %s (id=%d).", self.groupname, name, id)) + self:T(self.lid..string.format("Group %s: Removing move %s (id=%d).", self.groupname, name, id)) table.remove(self.moves, id) - + -- Delete marker belonging to this relocation move. if self.markallow then local batteryname,markTargetID,markMoveID=self:_GetMarkIDfromName(name) @@ -1511,9 +1707,9 @@ function ARTY:RemoveMove(name) COORDINATE:RemoveMark(markMoveID) end end - + end - self:T(ARTY.id..string.format("Group %s: Number of moves = %d.", self.groupname, #self.moves)) + self:T(self.lid..string.format("Group %s: Number of moves = %d.", self.groupname, #self.moves)) end --- Delete ALL targets from current target list. @@ -1528,50 +1724,60 @@ end --- Define shell types that are counted to determine the ammo amount the ARTY group has. -- @param #ARTY self -- @param #table tableofnames Table of shell type names. +-- @return self function ARTY:SetShellTypes(tableofnames) self:F2(tableofnames) self.ammoshells={} for _,_type in pairs(tableofnames) do table.insert(self.ammoshells, _type) end + return self end --- Define rocket types that are counted to determine the ammo amount the ARTY group has. -- @param #ARTY self -- @param #table tableofnames Table of rocket type names. +-- @return self function ARTY:SetRocketTypes(tableofnames) self:F2(tableofnames) self.ammorockets={} for _,_type in pairs(tableofnames) do table.insert(self.ammorockets, _type) end + return self end --- Define missile types that are counted to determine the ammo amount the ARTY group has. -- @param #ARTY self -- @param #table tableofnames Table of rocket type names. +-- @return self function ARTY:SetMissileTypes(tableofnames) self:F2(tableofnames) self.ammomissiles={} for _,_type in pairs(tableofnames) do table.insert(self.ammomissiles, _type) end + return self end --- Set number of tactical nuclear warheads available to the group. -- Note that it can be max the number of normal shells. Also if all normal shells are empty, firing nuclear shells is also not possible any more until group gets rearmed. -- @param #ARTY self -- @param #number n Number of warheads for the whole group. +-- @return self function ARTY:SetTacNukeShells(n) self.Nukes=n + return self end --- Set nuclear warhead explosion strength. -- @param #ARTY self -- @param #number strength Explosion strength in kilo tons TNT. Default is 0.075 kt. +-- @return self function ARTY:SetTacNukeWarhead(strength) self.nukewarhead=strength or 0.075 self.nukewarhead=self.nukewarhead*1000*1000 -- convert to kg TNT. + return self end --- Set number of illumination shells available to the group. @@ -1579,24 +1785,28 @@ end -- @param #ARTY self -- @param #number n Number of illumination shells for the whole group. -- @param #number power (Optional) Power of illumination warhead in mega candela. Default 1.0 mcd. +-- @return self function ARTY:SetIlluminationShells(n, power) self.Nillu=n self.illuPower=power or 1.0 self.illuPower=self.illuPower * 1000000 + return self end --- Set minimum and maximum detotation altitude for illumination shells. A value between min/max is selected randomly. --- The illumination bomb will burn for 300 seconds (5 minutes). Assuming a descent rate of ~3 m/s the "optimal" altitude would be 900 m. +-- The illumination bomb will burn for 300 seconds (5 minutes). Assuming a descent rate of ~3 m/s the "optimal" altitude would be 900 m. -- @param #ARTY self -- @param #number minalt (Optional) Minium altitude in meters. Default 500 m. -- @param #number maxalt (Optional) Maximum altitude in meters. Default 1000 m. +-- @return self function ARTY:SetIlluminationMinMaxAlt(minalt, maxalt) self.illuMinalt=minalt or 500 self.illuMaxalt=maxalt or 1000 - + if self.illuMinalt>self.illuMaxalt then self.illuMinalt=self.illuMaxalt end + return self end --- Set number of smoke shells available to the group. @@ -1604,38 +1814,46 @@ end -- @param #ARTY self -- @param #number n Number of smoke shells for the whole group. -- @param Utilities.Utils#SMOKECOLOR color (Optional) Color of the smoke. Default SMOKECOLOR.Red. +-- @return self function ARTY:SetSmokeShells(n, color) self.Nsmoke=n self.smokeColor=color or SMOKECOLOR.Red + return self end --- Set nuclear fires and extra demolition explosions. -- @param #ARTY self -- @param #number nfires (Optional) Number of big smoke and fire objects created in the demolition zone. -- @param #number demolitionrange (Optional) Demolition range in meters. +-- @return self function ARTY:SetTacNukeFires(nfires, range) self.nukefire=true self.nukefires=nfires self.nukerange=range + return self end --- Enable assigning targets and moves by placing markers on the F10 map. -- @param #ARTY self -- @param #number key (Optional) Authorization key. Only players knowing this key can assign targets. Default is no authorization required. -- @param #boolean readonly (Optional) Marks are readonly and cannot be removed by players. This also means that targets cannot be cancelled by removing the mark. Default false. +-- @return self function ARTY:SetMarkAssignmentsOn(key, readonly) self.markkey=key self.markallow=true if readonly==nil then self.markreadonly=false end + return self end --- Disable assigning targets by placing markers on the F10 map. -- @param #ARTY self +-- @return self function ARTY:SetMarkTargetsOff() self.markallow=false self.markkey=nil + return self end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1650,15 +1868,15 @@ end -- @param #string To To state. function ARTY:onafterStart(Controllable, From, Event, To) self:_EventFromTo("onafterStart", Event, From, To) - + -- Debug output. local text=string.format("Started ARTY version %s for group %s.", ARTY.version, Controllable:GetName()) - self:E(ARTY.id..text) + self:I(self.lid..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - + -- Get Ammo. self.Nammo0, self.Nshells0, self.Nrockets0, self.Nmissiles0=self:GetAmmo(self.Debug) - + -- Init nuclear explosion parameters if they were not set by user. if self.nukerange==nil then self.nukerange=1500/75000*self.nukewarhead -- linear dependence @@ -1666,7 +1884,7 @@ function ARTY:onafterStart(Controllable, From, Event, To) if self.nukefires==nil then self.nukefires=20/1000/1000*self.nukerange*self.nukerange end - + -- Init nuclear shells. if self.Nukes~=nil then self.Nukes0=math.min(self.Nukes, self.Nshells0) @@ -1674,7 +1892,7 @@ function ARTY:onafterStart(Controllable, From, Event, To) self.Nukes=0 self.Nukes0=0 end - + -- Init illumination shells. if self.Nillu~=nil then self.Nillu0=math.min(self.Nillu, self.Nshells0) @@ -1682,15 +1900,15 @@ function ARTY:onafterStart(Controllable, From, Event, To) self.Nillu=0 self.Nillu0=0 end - + -- Init smoke shells. if self.Nsmoke~=nil then self.Nsmoke0=math.min(self.Nsmoke, self.Nshells0) else self.Nsmoke=0 self.Nsmoke0=0 - end - + end + -- Check if we have and arty type that is in the DB. local _dbproperties=self:_CheckDB(self.DisplayName) self:T({dbproperties=_dbproperties}) @@ -1700,7 +1918,7 @@ function ARTY:onafterStart(Controllable, From, Event, To) self[property]=value end end - + -- Some mobility consitency checks if group cannot move. if not self.ismobile then self.RearmingPlaceCoord=nil @@ -1708,35 +1926,35 @@ function ARTY:onafterStart(Controllable, From, Event, To) self.autorelocate=false --self.RearmingGroupSpeed=20 end - + -- Check that default speed is below max speed. self.Speed=math.min(self.Speed, self.SpeedMax) -- Set Rearming group speed if not specified by user if self.RearmingGroup then - + -- Get max speed of rearming group. local speedmax=self.RearmingGroup:GetSpeedMax() - self:T(ARTY.id..string.format("%s, rearming group %s max speed = %.1f km/h.", self.groupname, self.RearmingGroup:GetName(), speedmax)) - + self:T(self.lid..string.format("%s, rearming group %s max speed = %.1f km/h.", self.groupname, self.RearmingGroup:GetName(), speedmax)) + if self.RearmingGroupSpeed==nil then -- Set rearming group speed to 50% of max possible speed. self.RearmingGroupSpeed=speedmax*0.5 else - -- Ensure that speed is <= max speed. + -- Ensure that speed is <= max speed. self.RearmingGroupSpeed=math.min(self.RearmingGroupSpeed, self.RearmingGroup:GetSpeedMax()) end else -- Just to have a reasonable number for output format below. self.RearmingGroupSpeed=23 end - + local text=string.format("\n******************************************************\n") text=text..string.format("Arty group = %s\n", self.groupname) text=text..string.format("Arty alias = %s\n", self.alias) text=text..string.format("Artillery attribute = %s\n", tostring(self.IsArtillery)) text=text..string.format("Type = %s\n", self.Type) - text=text..string.format("Display Name = %s\n", self.DisplayName) + text=text..string.format("Display Name = %s\n", self.DisplayName) text=text..string.format("Number of units = %d\n", self.IniGroupStrength) text=text..string.format("Speed max = %d km/h\n", self.SpeedMax) text=text..string.format("Speed default = %d km/h\n", self.Speed) @@ -1755,9 +1973,9 @@ function ARTY:onafterStart(Controllable, From, Event, To) text=text..string.format("Number of illum. = %d\n", self.Nillu0) text=text..string.format("Illuminaton Power = %.3f mcd\n", self.illuPower/1000000) text=text..string.format("Illuminaton Minalt = %d m\n", self.illuMinalt) - text=text..string.format("Illuminaton Maxalt = %d m\n", self.illuMaxalt) + text=text..string.format("Illuminaton Maxalt = %d m\n", self.illuMaxalt) text=text..string.format("Number of smoke = %d\n", self.Nsmoke0) - text=text..string.format("Smoke color = %d\n", self.smokeColor) + text=text..string.format("Smoke color = %d\n", self.smokeColor) if self.RearmingGroup or self.RearmingPlaceCoord then text=text..string.format("Rearming safe dist. = %d m\n", self.RearmingDistance) end @@ -1790,7 +2008,7 @@ function ARTY:onafterStart(Controllable, From, Event, To) text=text..string.format("- %s\n", self:_TargetInfo(target)) local possible=self:_CheckWeaponTypePossible(target) if not possible then - self:E(ARTY.id..string.format("WARNING: Selected weapon type %s is not possible", self:_WeaponTypeName(target.weapontype))) + self:E(self.lid..string.format("WARNING: Selected weapon type %s is not possible", self:_WeaponTypeName(target.weapontype))) end if self.Debug then local zone=ZONE_RADIUS:New(target.name, target.coord:GetVec2(), target.radius) @@ -1813,27 +2031,27 @@ function ARTY:onafterStart(Controllable, From, Event, To) text=text..string.format("Missile types:\n") for _,_type in pairs(self.ammomissiles) do text=text..string.format("- %s\n", _type) - end + end text=text..string.format("******************************************************") if self.Debug then - self:E(ARTY.id..text) + self:I(self.lid..text) else - self:T(ARTY.id..text) + self:T(self.lid..text) end - + -- Set default ROE to weapon hold. self.Controllable:OptionROEHoldFire() - + -- Add event handler. - self:HandleEvent(EVENTS.Shot, self._OnEventShot) - self:HandleEvent(EVENTS.Dead, self._OnEventDead) + self:HandleEvent(EVENTS.Shot) --, self._OnEventShot) + self:HandleEvent(EVENTS.Dead) --, self._OnEventDead) --self:HandleEvent(EVENTS.MarkAdded, self._OnEventMarkAdded) -- Add DCS event handler - necessary for S_EVENT_MARK_* events. So we only start it, if this was requested. if self.markallow then world.addEventHandler(self) end - + -- Start checking status. self:__Status(self.StatusInterval) end @@ -1867,10 +2085,10 @@ function ARTY:_StatusReport(display) local Nnukes=self.Nukes local Nillu=self.Nillu local Nsmoke=self.Nsmoke - + local Tnow=timer.getTime() local Clock=self:_SecondsToClock(timer.getAbsTime()) - + local text=string.format("\n******************* STATUS ***************************\n") text=text..string.format("ARTY group = %s\n", self.groupname) text=text..string.format("Clock = %s\n", Clock) @@ -1881,7 +2099,7 @@ function ARTY:_StatusReport(display) text=text..string.format("Number of missiles = %d\n", Nmissiles) text=text..string.format("Number of nukes = %d\n", Nnukes) text=text..string.format("Number of illum. = %d\n", Nillu) - text=text..string.format("Number of smoke = %d\n", Nsmoke) + text=text..string.format("Number of smoke = %d\n", Nsmoke) if self.currentTarget then text=text..string.format("Current Target = %s\n", tostring(self.currentTarget.name)) text=text..string.format("Curr. Tgt assigned = %d\n", Tnow-self.currentTarget.Tassigned) @@ -1903,9 +2121,9 @@ function ARTY:_StatusReport(display) text=text..string.format("- %s\n", self:_MoveInfo(self.moves[i])) end text=text..string.format("******************************************************") - env.info(ARTY.id..text) - MESSAGE:New(text, 20):Clear():ToCoalitionIf(self.Controllable:GetCoalition(), display) - + env.info(self.lid..text) + MESSAGE:New(text, 20):Clear():ToCoalitionIf(self.coalition, display) + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1915,161 +2133,163 @@ end --- Eventhandler for shot event. -- @param #ARTY self -- @param Core.Event#EVENTDATA EventData -function ARTY:_OnEventShot(EventData) +function ARTY:OnEventShot(EventData) self:F(EventData) - + -- Weapon data. local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName local _weaponStrArray = self:_split(_weapon,"%.") local _weaponName = _weaponStrArray[#_weaponStrArray] - + -- Debug info. - self:T3(ARTY.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) - self:T3(ARTY.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) - self:T3(ARTY.id.."EVENT SHOT: Weapon type = ".._weapon) - self:T3(ARTY.id.."EVENT SHOT: Weapon name = ".._weaponName) - + self:T3(self.lid.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) + self:T3(self.lid.."EVENT SHOT: Ini group = "..EventData.IniGroupName) + self:T3(self.lid.."EVENT SHOT: Weapon type = ".._weapon) + self:T3(self.lid.."EVENT SHOT: Weapon name = ".._weaponName) + local group = EventData.IniGroup --Wrapper.Group#GROUP - + if group and group:IsAlive() then - + if EventData.IniGroupName == self.groupname then - + if self.currentTarget then - + -- Increase number of shots fired by this group on this target. self.Nshots=self.Nshots+1 - + -- Debug output. local text=string.format("%s, fired shot %d of %d with weapon %s on target %s.", self.alias, self.Nshots, self.currentTarget.nshells, _weaponName, self.currentTarget.name) - self:T(ARTY.id..text) + self:T(self.lid..text) MESSAGE:New(text, 5):Clear():ToAllIf(self.report or self.Debug) - + -- Last known position of the weapon fired. local _lastpos={x=0, y=0, z=0} - + --- Track the position of the weapon if it is supposed to model a tac nuke, illumination or smoke shell. -- @param #table _weapon local function _TrackWeapon(_data) - + -- When the pcall status returns false the weapon has hit. local _weaponalive,_currpos = pcall( function() return _data.weapon:getPoint() end) - + -- Debug - self:T3(ARTY.id..string.format("ARTY %s: Weapon still in air: %s", self.groupname, tostring(_weaponalive))) - + self:T3(self.lid..string.format("ARTY %s: Weapon still in air: %s", self.groupname, tostring(_weaponalive))) + -- Destroy weapon before impact. local _destroyweapon=false - + if _weaponalive then - + -- Update last position. _lastpos={x=_currpos.x, y=_currpos.y, z=_currpos.z} -- Coordinate and distance to target. - local _coord=COORDINATE:NewFromVec3(_lastpos) - local _dist=_coord:Get2DDistance(_data.target.coord) - + local _coord=COORDINATE:NewFromVec3(_lastpos) + local _dist=_coord:Get2DDistance(_data.target.coord) + -- Debug - self:T3(ARTY.id..string.format("ARTY %s weapon to target dist = %d m", self.groupname,_dist)) - + self:T3(self.lid..string.format("ARTY %s weapon to target dist = %d m", self.groupname,_dist)) + if _data.target.weapontype==ARTY.WeaponType.IlluminationShells then - + -- Check if within distace. if _dist<_data.target.radius then - + -- Get random coordinate within certain radius of the target. local _cr=_data.target.coord:GetRandomCoordinateInRadius(_data.target.radius) - + -- Get random altitude over target. local _alt=_cr:GetLandHeight()+math.random(self.illuMinalt, self.illuMaxalt) - + -- Adjust explosion height of coordinate. local _ci=COORDINATE:New(_cr.x,_alt,_cr.z) - + -- Create illumination flare. _ci:IlluminationBomb(self.illuPower) - - -- Destroy actual shell. - _destroyweapon=true - end - - elseif _data.target.weapontype==ARTY.WeaponType.SmokeShells then - - if _dist<_data.target.radius then - - -- Get random coordinate within a certain radius. - local _cr=_coord:GetRandomCoordinateInRadius(_data.target.radius) - - -- Fire smoke at this coordinate. - _cr:Smoke(self.smokeColor) - + -- Destroy actual shell. _destroyweapon=true - - end - + end + + elseif _data.target.weapontype==ARTY.WeaponType.SmokeShells then + + if _dist<_data.target.radius then + + -- Get random coordinate within a certain radius. + local _cr=_coord:GetRandomCoordinateInRadius(_data.target.radius) + + -- Fire smoke at this coordinate. + _cr:Smoke(self.smokeColor) + + -- Destroy actual shell. + _destroyweapon=true + + end + end - + if _destroyweapon then - - self:T2(ARTY.id..string.format("ARTY %s destroying shell, stopping timer.", self.groupname)) - + + self:T2(self.lid..string.format("ARTY %s destroying shell, stopping timer.", self.groupname)) + -- Destroy weapon and stop timer. _data.weapon:destroy() return nil - + else -- TODO: Make dt input parameter. local dt=0.02 - - self:T3(ARTY.id..string.format("ARTY %s tracking weapon again in %.3f seconds", self.groupname, dt)) - + + self:T3(self.lid..string.format("ARTY %s tracking weapon again in %.3f seconds", self.groupname, dt)) + -- Check again in 0.05 seconds. return timer.getTime() + dt - + end - + else - + -- Get impact coordinate. local _impactcoord=COORDINATE:NewFromVec3(_lastpos) - + + self:I(self.lid..string.format("ARTY %s weapon NOT ALIVE any more.", self.groupname)) + -- Create a "nuclear" explosion and blast at the impact point. - if _weapon.weapontype==ARTY.WeaponType.TacticalNukes then - self:T2(ARTY.id..string.format("ARTY %s triggering nuclear explosion in one second.", self.groupname)) + if _data.target.weapontype==ARTY.WeaponType.TacticalNukes then + self:T(self.lid..string.format("ARTY %s triggering nuclear explosion in one second.", self.groupname)) SCHEDULER:New(nil, ARTY._NuclearBlast, {self,_impactcoord}, 1.0) end - + -- Stop timer. return nil - + end - + end - + -- Start track the shell if we want to model a tactical nuke. local _tracknuke = self.currentTarget.weapontype==ARTY.WeaponType.TacticalNukes and self.Nukes>0 local _trackillu = self.currentTarget.weapontype==ARTY.WeaponType.IlluminationShells and self.Nillu>0 local _tracksmoke = self.currentTarget.weapontype==ARTY.WeaponType.SmokeShells and self.Nsmoke>0 if _tracknuke or _trackillu or _tracksmoke then - - self:T(ARTY.id..string.format("ARTY %s: Tracking of weapon starts in two seconds.", self.groupname)) - + + self:T(self.lid..string.format("ARTY %s: Tracking of weapon starts in two seconds.", self.groupname)) + local _peter={} _peter.weapon=EventData.weapon _peter.target=UTILS.DeepCopy(self.currentTarget) - + timer.scheduleFunction(_TrackWeapon, _peter, timer.getTime() + 2.0) end - + -- Get current ammo. local _nammo,_nshells,_nrockets,_nmissiles=self:GetAmmo() - + -- Decrease available nukes because we just fired one. if self.currentTarget.weapontype==ARTY.WeaponType.TacticalNukes then self.Nukes=self.Nukes-1 @@ -2084,60 +2304,60 @@ function ARTY:_OnEventShot(EventData) if self.currentTarget.weapontype==ARTY.WeaponType.SmokeShells then self.Nsmoke=self.Nsmoke-1 end - + -- Check if we are completely out of ammo. local _outofammo=false if _nammo==0 then - self:T(ARTY.id..string.format("Group %s completely out of ammo.", self.groupname)) + self:T(self.lid..string.format("Group %s completely out of ammo.", self.groupname)) _outofammo=true end - + -- Check if we are out of ammo of the weapon type used for this target. -- Note that should not happen because we only open fire with the available number of shots. local _partlyoutofammo=self:_CheckOutOfAmmo({self.currentTarget}) - + -- Weapon type name for current target. local _weapontype=self:_WeaponTypeName(self.currentTarget.weapontype) - self:T(ARTY.id..string.format("Group %s ammo: total=%d, shells=%d, rockets=%d, missiles=%d", self.groupname, _nammo, _nshells, _nrockets, _nmissiles)) - self:T(ARTY.id..string.format("Group %s uses weapontype %s for current target.", self.groupname, _weapontype)) - + self:T(self.lid..string.format("Group %s ammo: total=%d, shells=%d, rockets=%d, missiles=%d", self.groupname, _nammo, _nshells, _nrockets, _nmissiles)) + self:T(self.lid..string.format("Group %s uses weapontype %s for current target.", self.groupname, _weapontype)) + -- Default switches for cease fire and relocation. local _ceasefire=false local _relocate=false - + -- Check if number of shots reached max. if self.Nshots >= self.currentTarget.nshells then - + -- Debug message local text=string.format("Group %s stop firing on target %s.", self.groupname, self.currentTarget.name) - self:T(ARTY.id..text) + self:T(self.lid..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - + -- Cease fire. _ceasefire=true - + -- Relocate if enabled. _relocate=self.relocateafterfire end - + -- Check if we are (partly) out of ammo. if _outofammo or _partlyoutofammo then _ceasefire=true - end - + end + -- Relocate position. if _relocate then self:_Relocate() - end - + end + -- Cease fire on current target. if _ceasefire then self:CeaseFire(self.currentTarget) end - + else - self:E(ARTY.id..string.format("WARNING: No current target for group %s?!", self.groupname)) - end + self:E(self.lid..string.format("WARNING: No current target for group %s?!", self.groupname)) + end end end end @@ -2156,7 +2376,7 @@ function ARTY:onEvent(Event) -- Set battery and coalition. --local batteryname=self.groupname --local batterycoalition=self.Controllable:GetCoalition() - + self:T2(string.format("Event captured = %s", tostring(self.groupname))) self:T2(string.format("Event id = %s", tostring(Event.id))) self:T2(string.format("Event time = %s", tostring(Event.time))) @@ -2168,23 +2388,23 @@ function ARTY:onEvent(Event) local _unitname=Event.initiator:getName() self:T2(string.format("Event ini unit name = %s", tostring(_unitname))) end - + if Event.id==world.event.S_EVENT_MARK_ADDED then self:T2({event="S_EVENT_MARK_ADDED", battery=self.groupname, vec3=Event.pos}) - + elseif Event.id==world.event.S_EVENT_MARK_CHANGE then self:T({event="S_EVENT_MARK_CHANGE", battery=self.groupname, vec3=Event.pos}) - + -- Handle event. self:_OnEventMarkChange(Event) - + elseif Event.id==world.event.S_EVENT_MARK_REMOVED then self:T2({event="S_EVENT_MARK_REMOVED", battery=self.groupname, vec3=Event.pos}) - + -- Hande event. self:_OnEventMarkRemove(Event) end - + end --- Function called when a F10 map mark was removed. @@ -2193,17 +2413,17 @@ end function ARTY:_OnEventMarkRemove(Event) -- Get battery coalition and name. - local batterycoalition=self.Controllable:GetCoalition() + local batterycoalition=self.coalition --local batteryname=self.groupname - + if Event.text~=nil and Event.text:find("BATTERY") then - + -- Init defaults. local _cancelmove=false local _canceltarget=false local _name="" local _id=nil - + -- Check for key phrases of relocation or engagements in marker text. If not, return. if Event.text:find("Marked Relocation") then _cancelmove=true @@ -2216,22 +2436,22 @@ function ARTY:_OnEventMarkRemove(Event) else return end - + -- Check if there is a task which matches. if _id==nil then return end - + -- Check if the coalition is the same or an authorization key has been defined. if (batterycoalition==Event.coalition and self.markkey==nil) or self.markkey~=nil then - + -- Authentify key local _validkey=self:_MarkerKeyAuthentification(Event.text) - + -- Check if we have the right coalition. if _validkey then - - -- This should be the unique name of the target or move. + + -- This should be the unique name of the target or move. if _cancelmove then if self.currentMove and self.currentMove.name==_name then -- We do clear tasks here because in Arrived() it can cause a CTD if the group did actually arrive! @@ -2246,17 +2466,17 @@ function ARTY:_OnEventMarkRemove(Event) if self.currentTarget and self.currentTarget.name==_name then -- Cease fire. self:CeaseFire(self.currentTarget) - -- We still need to remove the target, because there might be more planned engagements (maxengage>1). + -- We still need to remove the target, because there might be more planned engagements (maxengage>1). self:RemoveTarget(_name) else -- Remove target from queue self:RemoveTarget(_name) end end - - end + + end end - end + end end --- Function called when a F10 map mark was changed. This happens when a user enters text. @@ -2266,59 +2486,60 @@ function ARTY:_OnEventMarkChange(Event) -- Check if marker has a text and the "arty" keyword. if Event.text~=nil and Event.text:lower():find("arty") then - + -- Convert (wrong x-->z, z-->x) vec3 - -- TODO: This needs to be "fixed", once DCS gives the correct numbers for x and z. - -- local vec3={y=Event.pos.y, x=Event.pos.x, z=Event.pos.z} - local vec3={y=Event.pos.y, x=Event.pos.z, z=Event.pos.x} - + -- DONE: This needs to be "fixed", once DCS gives the correct numbers for x and z. + -- Was fixed in DCS 2.5.5.34644! + local vec3={y=Event.pos.y, x=Event.pos.x, z=Event.pos.z} + --local vec3={y=Event.pos.y, x=Event.pos.z, z=Event.pos.x} + -- Get coordinate from vec3. local _coord=COORDINATE:NewFromVec3(vec3) - + -- Adjust y component to actual land height. When a coordinate is create it uses y=5 m! _coord.y=_coord:GetLandHeight() - + -- Get battery coalition and name. - local batterycoalition=self.Controllable:GetCoalition() + local batterycoalition=self.coalition local batteryname=self.groupname - + -- Check if the coalition is the same or an authorization key has been defined. if (batterycoalition==Event.coalition and self.markkey==nil) or self.markkey~=nil then - + -- Evaluate marker text and extract parameters. local _assign=self:_Markertext(Event.text) -- Check if ENGAGE or MOVE or REQUEST keywords were found. if _assign==nil or not (_assign.engage or _assign.move or _assign.request or _assign.cancel or _assign.set) then - self:T(ARTY.id..string.format("WARNING: %s, no keyword ENGAGE, MOVE, REQUEST, CANCEL or SET in mark text! Command will not be executed. Text:\n%s", self.groupname, Event.text)) + self:T(self.lid..string.format("WARNING: %s, no keyword ENGAGE, MOVE, REQUEST, CANCEL or SET in mark text! Command will not be executed. Text:\n%s", self.groupname, Event.text)) return end - - -- Check if job is assigned to this ARTY group. Default is for all ARTY groups. + + -- Check if job is assigned to this ARTY group. Default is for all ARTY groups. local _assigned=false - + -- If any array is filled something has been assigned. if _assign.everyone then - + -- Everyone was addressed. _assigned=true - + else --#_assign.battery>0 or #_assign.aliases>0 or #_assign.cluster>0 then - -- Loop over batteries. + -- Loop over batteries. for _,bat in pairs(_assign.battery) do if self.groupname==bat then _assigned=true end end - - -- Loop over aliases. + + -- Loop over aliases. for _,alias in pairs(_assign.aliases) do if self.alias==alias then _assigned=true end end - + -- Loop over clusters. for _,bat in pairs(_assign.cluster) do for _,cluster in pairs(self.clusters) do @@ -2329,21 +2550,28 @@ function ARTY:_OnEventMarkChange(Event) end end - + -- We were not addressed. if not _assigned then - self:T3(ARTY.id..string.format("INFO: ARTY group %s was not addressed! Mark text:\n%s", self.groupname, Event.text)) + self:T3(self.lid..string.format("INFO: ARTY group %s was not addressed! Mark text:\n%s", self.groupname, Event.text)) return + else + if self.Controllable and self.Controllable:IsAlive() then + + else + self:T3(self.lid..string.format("INFO: ARTY group %s was addressed but is NOT alive! Mark text:\n%s", self.groupname, Event.text)) + return + end end -- Coordinate was given in text, e.g. as lat, long. if _assign.coord then _coord=_assign.coord end - + -- Check if the authorization key is required and if it is valid. local _validkey=self:_MarkerKeyAuthentification(Event.text) - + -- Handle requests and return. if _assign.request and _validkey then if _assign.requestammo then @@ -2357,14 +2585,14 @@ function ARTY:_OnEventMarkChange(Event) end if _assign.requeststatus then self:_MarkRequestStatus() - end + end if _assign.requestrearming then self:Rearm() - end + end -- Requests Done ==> End of story! return end - + -- Cancel stuff and return. if _assign.cancel and _validkey then if _assign.cancelmove and self.currentMove then @@ -2390,7 +2618,7 @@ function ARTY:_OnEventMarkChange(Event) if _assign.setrearmingplace and self.ismobile then self:SetRearmingPlace(_coord) _coord:RemoveMark(Event.idx) - _coord:MarkToCoalition(string.format("Rearming place for battery %s", self.groupname), self.Controllable:GetCoalition(), false, string.format("New rearming place for battery %s defined.", self.groupname)) + _coord:MarkToCoalition(string.format("Rearming place for battery %s", self.groupname), self.coalition, false, string.format("New rearming place for battery %s defined.", self.groupname)) if self.Debug then _coord:SmokeOrange() end @@ -2398,7 +2626,7 @@ function ARTY:_OnEventMarkChange(Event) if _assign.setrearminggroup then _coord:RemoveMark(Event.idx) local rearminggroupcoord=_assign.setrearminggroup:GetCoordinate() - rearminggroupcoord:MarkToCoalition(string.format("Rearming group for battery %s", self.groupname), self.Controllable:GetCoalition(), false, string.format("New rearming group for battery %s defined.", self.groupname)) + rearminggroupcoord:MarkToCoalition(string.format("Rearming group for battery %s", self.groupname), self.coalition, false, string.format("New rearming group for battery %s defined.", self.groupname)) self:SetRearmingGroup(_assign.setrearminggroup) if self.Debug then rearminggroupcoord:SmokeOrange() @@ -2407,51 +2635,51 @@ function ARTY:_OnEventMarkChange(Event) -- Set stuff Done ==> End of story! return end - + -- Handle engagements and relocations. if _validkey then - + -- Remove old mark because it might contain confidential data such as the key. -- Also I don't know who can see the mark which was created. _coord:RemoveMark(Event.idx) - + -- Anticipate marker ID. -- WARNING: Make sure, no marks are set until the COORDINATE:MarkToCoalition() is called or the target/move name will be wrong and target cannot be removed by deleting its marker. local _id=UTILS._MarkID+1 - + if _assign.move then - + -- Create a new name. This determins the string we search when deleting a move! local _name=self:_MarkMoveName(_id) - + local text=string.format("%s, received new relocation assignment.", self.alias) text=text..string.format("\nCoordinates %s",_coord:ToStringLLDMS()) MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug) - + -- Assign a relocation of the arty group. local _movename=self:AssignMoveCoord(_coord, _assign.time, _assign.speed, _assign.onroad, _assign.movecanceltarget,_name, true) - + if _movename~=nil then local _mid=self:_GetMoveIndexByName(_movename) local _move=self.moves[_mid] - + -- Create new target name. local clock=tostring(self:_SecondsToClock(_move.time)) local _markertext=_movename..string.format(", Time=%s, Speed=%d km/h, Use Roads=%s.", clock, _move.speed, tostring(_move.onroad)) - + -- Create a new mark. This will trigger the mark added event. local _randomcoord=_coord:GetRandomCoordinateInRadius(100) _randomcoord:MarkToCoalition(_markertext, batterycoalition, self.markreadonly or _assign.readonly) else local text=string.format("%s, relocation not possible.", self.alias) MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug) - end - + end + else - + -- Create a new name. local _name=self:_MarkTargetName(_id) - + local text=string.format("%s, received new target assignment.", self.alias) text=text..string.format("\nCoordinates %s",_coord:ToStringLLDMS()) if _assign.time then @@ -2471,30 +2699,30 @@ function ARTY:_OnEventMarkChange(Event) end if _assign.weapontype then text=text..string.format("\nWeapon %s",self:_WeaponTypeName(_assign.weapontype)) - end + end MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug) - + -- Assign a new firing engagement. -- Note, we set unique=true so this target gets only added once. local _targetname=self:AssignTargetCoord(_coord,_assign.prio,_assign.radius,_assign.nshells,_assign.maxengage,_assign.time,_assign.weapontype, _name, true) - + if _targetname~=nil then local _tid=self:_GetTargetIndexByName(_targetname) local _target=self.targets[_tid] - + -- Create new target name. local clock=tostring(self:_SecondsToClock(_target.time)) local weapon=self:_WeaponTypeName(_target.weapontype) local _markertext=_targetname..string.format(", Priority=%d, Radius=%d m, Shots=%d, Engagements=%d, Weapon=%s, Time=%s", _target.prio, _target.radius, _target.nshells, _target.maxengage, weapon, clock) - + -- Create a new mark. This will trigger the mark added event. local _randomcoord=_coord:GetRandomCoordinateInRadius(250) _randomcoord:MarkToCoalition(_markertext, batterycoalition, self.markreadonly or _assign.readonly) - end + end end end - - end + + end end end @@ -2502,20 +2730,24 @@ end --- Event handler for event Dead. -- @param #ARTY self -- @param Core.Event#EVENTDATA EventData -function ARTY:_OnEventDead(EventData) +function ARTY:OnEventDead(EventData) self:F(EventData) -- Name of controllable. local _name=self.groupname -- Check for correct group. - if EventData.IniGroupName==_name then - + if EventData and EventData.IniGroupName and EventData.IniGroupName==_name then + + -- Name of the dead unit. + local unitname=tostring(EventData.IniUnitName) + -- Dead Unit. - self:T2(string.format("%s: Captured dead event for unit %s.", _name, EventData.IniUnitName)) - + self:T(self.lid..string.format("%s: Captured dead event for unit %s.", _name, unitname)) + -- FSM Dead event. We give one second for update of data base. - self:__Dead(1) + --self:__Dead(1, unitname) + self:Dead(unitname) end end @@ -2533,145 +2765,158 @@ end function ARTY:onafterStatus(Controllable, From, Event, To) self:_EventFromTo("onafterStatus", Event, From, To) - -- We have a cargo group ==> check if group was loaded into a carrier. - if self.cargogroup then - if self.cargogroup:IsLoaded() and not self:is("InTransit") then - -- Group is now InTransit state. Current target is canceled. - self:T(ARTY.id..string.format("Group %s has been loaded into a carrier and is now transported.", self.alias)) - self:Loaded() - elseif self.cargogroup:IsUnLoaded() then - -- Group has been unloaded and is combat ready again. - self:T(ARTY.id..string.format("Group %s has been unloaded from the carrier.", self.alias)) - self:UnLoaded() - end - end - - -- Debug current status info. - if self.Debug then - self:_StatusReport() - end - - -- Group is being transported as cargo ==> skip everything and check again in 5 seconds. - if self:is("InTransit") then - self:__Status(-5) - return - end - - -- Group on the move. - if self:is("Moving") then - self:T2(ARTY.id..string.format("%s: Moving", Controllable:GetName())) - end - - -- Group is rearming. - if self:is("Rearming") then - local _rearmed=self:_CheckRearmed() - if _rearmed then - self:T2(ARTY.id..string.format("%s: Rearming ==> Rearmed", Controllable:GetName())) - self:Rearmed() - end - end - - -- Group finished rearming. - if self:is("Rearmed") then - local distance=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) - self:T2(ARTY.id..string.format("%s: Rearmed. Distance ARTY to InitalCoord = %d m", Controllable:GetName(), distance)) - -- Check that ARTY group is back and set it to combat ready. - if distance <= self.RearmingDistance then - self:T2(ARTY.id..string.format("%s: Rearmed ==> CombatReady", Controllable:GetName())) - self:CombatReady() - end - end - - -- Group arrived at destination. - if self:is("Arrived") then - self:T2(ARTY.id..string.format("%s: Arrived ==> CombatReady", Controllable:GetName())) - self:CombatReady() - end - - -- Group is firing on target. - if self:is("Firing") then - -- Check that firing started after ~5 min. If not, target is removed. - self:_CheckShootingStarted() - end - - -- Check if targets are in range and update target.inrange value. - self:_CheckTargetsInRange() - - -- Check if selected weapon type for target is possible at all. E.g. request rockets for Paladin. - local notpossible={} - for i=1,#self.targets do - local _target=self.targets[i] - local possible=self:_CheckWeaponTypePossible(_target) - if not possible then - table.insert(notpossible, _target.name) - end - end - for _,targetname in pairs(notpossible) do - self:E(ARTY.id..string.format("%s: Removing target %s because requested weapon is not possible with this type of unit.", self.groupname, targetname)) - self:RemoveTarget(targetname) - end - - -- Get a valid timed target if it is due to be attacked. - local _timedTarget=self:_CheckTimedTargets() - - -- Get a valid normal target (one that is not timed). - local _normalTarget=self:_CheckNormalTargets() - - -- Get a commaned move to another location. - local _move=self:_CheckMoves() - - if _move then - - -- Command to move. - self:Move(_move) - - elseif _timedTarget then - - -- Cease fire on current target first. - if self.currentTarget then - self:CeaseFire(self.currentTarget) - end - - -- Open fire on timed target. - self:OpenFire(_timedTarget) - - elseif _normalTarget then - - -- Open fire on normal target. - self:OpenFire(_normalTarget) - - end - -- Get ammo. - local nammo, nshells, nrockets, nmissiles=self:GetAmmo() - - -- Check if we have a target in the queue for which weapons are still available. - local gotsome=false - if #self.targets>0 then - for i=1,#self.targets do - local _target=self.targets[i] - if self:_CheckWeaponTypeAvailable(_target)>0 then - gotsome=true + local ntot, nshells, nrockets, nmissiles=self:GetAmmo() + + -- FSM state. + local fsmstate=self:GetState() + self:I(self.lid..string.format("Status %s, Ammo total=%d: shells=%d [smoke=%d, illu=%d, nukes=%d*%.3f kT], rockets=%d, missiles=%d", fsmstate, ntot, nshells, self.Nsmoke, self.Nillu, self.Nukes, self.nukewarhead/1000000, nrockets, nmissiles)) + + if self.Controllable and self.Controllable:IsAlive() then + + -- We have a cargo group ==> check if group was loaded into a carrier. + if self.cargogroup then + if self.cargogroup:IsLoaded() and not self:is("InTransit") then + -- Group is now InTransit state. Current target is canceled. + self:T(self.lid..string.format("Group %s has been loaded into a carrier and is now transported.", self.alias)) + self:Loaded() + elseif self.cargogroup:IsUnLoaded() then + -- Group has been unloaded and is combat ready again. + self:T(self.lid..string.format("Group %s has been unloaded from the carrier.", self.alias)) + self:UnLoaded() end end + + -- Debug current status info. + if self.Debug then + self:_StatusReport() + end + + -- Group is being transported as cargo ==> skip everything and check again in 5 seconds. + if self:is("InTransit") then + self:__Status(-5) + return + end + + -- Group on the move. + if self:is("Moving") then + self:T2(self.lid..string.format("%s: Moving", Controllable:GetName())) + end + + -- Group is rearming. + if self:is("Rearming") then + local _rearmed=self:_CheckRearmed() + if _rearmed then + self:T2(self.lid..string.format("%s: Rearming ==> Rearmed", Controllable:GetName())) + self:Rearmed() + end + end + + -- Group finished rearming. + if self:is("Rearmed") then + local distance=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) + self:T2(self.lid..string.format("%s: Rearmed. Distance ARTY to InitalCoord = %d m", Controllable:GetName(), distance)) + -- Check that ARTY group is back and set it to combat ready. + if distance <= self.RearmingDistance then + self:T2(self.lid..string.format("%s: Rearmed ==> CombatReady", Controllable:GetName())) + self:CombatReady() + end + end + + -- Group arrived at destination. + if self:is("Arrived") then + self:T2(self.lid..string.format("%s: Arrived ==> CombatReady", Controllable:GetName())) + self:CombatReady() + end + + -- Group is firing on target. + if self:is("Firing") then + -- Check that firing started after ~5 min. If not, target is removed. + self:_CheckShootingStarted() + end + + -- Check if targets are in range and update target.inrange value. + self:_CheckTargetsInRange() + + -- Check if selected weapon type for target is possible at all. E.g. request rockets for Paladin. + local notpossible={} + for i=1,#self.targets do + local _target=self.targets[i] + local possible=self:_CheckWeaponTypePossible(_target) + if not possible then + table.insert(notpossible, _target.name) + end + end + for _,targetname in pairs(notpossible) do + self:E(self.lid..string.format("%s: Removing target %s because requested weapon is not possible with this type of unit.", self.groupname, targetname)) + self:RemoveTarget(targetname) + end + + -- Get a valid timed target if it is due to be attacked. + local _timedTarget=self:_CheckTimedTargets() + + -- Get a valid normal target (one that is not timed). + local _normalTarget=self:_CheckNormalTargets() + + -- Get a commaned move to another location. + local _move=self:_CheckMoves() + + if _move then + + -- Command to move. + self:Move(_move) + + elseif _timedTarget then + + -- Cease fire on current target first. + if self.currentTarget then + self:CeaseFire(self.currentTarget) + end + + -- Open fire on timed target. + self:OpenFire(_timedTarget) + + elseif _normalTarget then + + -- Open fire on normal target. + self:OpenFire(_normalTarget) + + end + + -- Get ammo. + local nammo, nshells, nrockets, nmissiles=self:GetAmmo() + + -- Check if we have a target in the queue for which weapons are still available. + local gotsome=false + if #self.targets>0 then + for i=1,#self.targets do + local _target=self.targets[i] + if self:_CheckWeaponTypeAvailable(_target)>0 then + gotsome=true + end + end + else + -- No targets in the queue. + gotsome=true + end + + -- No ammo available. Either completely blank or only queued targets for ammo which is out. + if (nammo==0 or not gotsome) and not (self:is("Moving") or self:is("Rearming") or self:is("OutOfAmmo")) then + self:Winchester() + end + + -- Group is out of ammo. + if self:is("OutOfAmmo") then + self:T2(self.lid..string.format("%s: OutOfAmmo ==> Rearm ==> Rearming", Controllable:GetName())) + self:Rearm() + end + + -- Call status again in ~10 sec. + self:__Status(self.StatusInterval) + else - -- No targets in the queue. - gotsome=true + self:E(self.lid..string.format("Arty group %s is not alive!", self.groupname)) end - - -- No ammo available. Either completely blank or only queued targets for ammo which is out. - if (nammo==0 or not gotsome) and not (self:is("Moving") or self:is("Rearming") or self:is("OutOfAmmo")) then - self:Winchester() - end - - -- Group is out of ammo. - if self:is("OutOfAmmo") then - self:T2(ARTY.id..string.format("%s: OutOfAmmo ==> Rearm ==> Rearming", Controllable:GetName())) - self:Rearm() - end - - -- Call status again in ~10 sec. - self:__Status(self.StatusInterval) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2687,7 +2932,7 @@ function ARTY:onbeforeLoaded(Controllable, From, Event, To) if self.currentTarget then self:CeaseFire(self.currentTarget) end - + return true end @@ -2713,7 +2958,7 @@ end function ARTY:onenterCombatReady(Controllable, From, Event, To) self:_EventFromTo("onenterCombatReady", Event, From, To) -- Debug info - self:T3(ARTY.id..string.format("onenterComabReady, from=%s, event=%s, to=%s", From, Event, To)) + self:T3(self.lid..string.format("onenterComabReady, from=%s, event=%s, to=%s", From, Event, To)) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2728,35 +2973,35 @@ end -- @return #boolean If true, proceed to onafterOpenfire. function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) self:_EventFromTo("onbeforeOpenFire", Event, From, To) - + -- Check that group has no current target already. if self.currentTarget then -- This should not happen. Some earlier check failed. - self:E(ARTY.id..string.format("ERROR: Group %s already has a target %s!", self.groupname, self.currentTarget.name)) + self:E(self.lid..string.format("ERROR: Group %s already has a target %s!", self.groupname, self.currentTarget.name)) -- Deny transition. return false end - + -- Check if target is in range. if not self:_TargetInRange(target) then -- This should not happen. Some earlier check failed. - self:E(ARTY.id..string.format("ERROR: Group %s, target %s is out of range!", self.groupname, self.currentTarget.name)) + self:E(self.lid..string.format("ERROR: Group %s, target %s is out of range!", self.groupname, self.currentTarget.name)) -- Deny transition. return false end -- Get the number of available shells, rockets or missiles requested for this target. local nfire=self:_CheckWeaponTypeAvailable(target) - + -- Adjust if less than requested ammo is left. target.nshells=math.min(target.nshells, nfire) - + -- No ammo left ==> deny transition. if target.nshells<1 then local text=string.format("%s, no ammo left to engage target %s with selected weapon type %s.") return false end - + return true end @@ -2766,13 +3011,13 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param #table target Array holding the target info. +-- @param #ARTY.Target target Array holding the target info. function ARTY:onafterOpenFire(Controllable, From, Event, To, target) self:_EventFromTo("onafterOpenFire", Event, From, To) - + -- Get target array index. local id=self:_GetTargetIndexByName(target.name) - + -- Target is now under fire and has been engaged once more. if id then -- Set under fire flag. @@ -2782,10 +3027,10 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) -- Set time the target was assigned. self.currentTarget.Tassigned=timer.getTime() end - + -- Distance to target local range=Controllable:GetCoordinate():Get2DDistance(target.coord) - + -- Get ammo. local Nammo, Nshells, Nrockets, Nmissiles=self:GetAmmo() local nfire=Nammo @@ -2812,24 +3057,28 @@ function ARTY:onafterOpenFire(Controllable, From, Event, To, target) nfire=Nmissiles _type="cruise missiles" end - + -- Adjust if less than requested ammo is left. target.nshells=math.min(target.nshells, nfire) - + -- Send message. local text=string.format("%s, opening fire on target %s with %d %s. Distance %.1f km.", Controllable:GetName(), target.name, target.nshells, _type, range/1000) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) - + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report) + --if self.Debug then -- local _coord=target.coord --Core.Point#COORDINATE -- local text=string.format("ARTY %s, Target %s, n=%d, weapon=%s", self.Controllable:GetName(), target.name, target.nshells, self:_WeaponTypeName(target.weapontype)) -- _coord:MarkToAll(text) --end - + -- Start firing. - self:_FireAtCoord(target.coord, target.radius, target.nshells, target.weapontype) - + if target.attackgroup then + self:_AttackGroup(target) + else + self:_FireAtCoord(target.coord, target.radius, target.nshells, target.weapontype) + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2843,17 +3092,17 @@ end -- @param #table target Array holding the target info. function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) self:_EventFromTo("onafterCeaseFire", Event, From, To) - + if target then - + -- Send message. local text=string.format("%s, ceasing fire on target %s.", Controllable:GetName(), target.name) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) - + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report) + -- Get target array index. local id=self:_GetTargetIndexByName(target.name) - + -- We have a target. if id then -- Target was actually engaged. (Could happen that engagement was aborted while group was still aiming.) @@ -2865,28 +3114,28 @@ function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) -- Target is not under fire any more. self.targets[id].underfire=false end - + -- If number of engagements has been reached, the target is removed. if target.engaged >= target.maxengage then self:RemoveTarget(target.name) end - + -- Set ROE to weapon hold. self.Controllable:OptionROEHoldFire() - + -- Clear tasks. self.Controllable:ClearTasks() - + else - self:E(ARTY.id.."ERROR: No target in cease fire for group %s.", self.groupname) + self:E(self.lid..string.format("ERROR: No target in cease fire for group %s.", self.groupname)) end - + -- Set number of shots to zero. self.Nshots=0 - + -- ARTY group has no current target any more. self.currentTarget=nil - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2899,12 +3148,12 @@ end -- @param #string To To state. function ARTY:onafterWinchester(Controllable, From, Event, To) self:_EventFromTo("onafterWinchester", Event, From, To) - + -- Send message. local text=string.format("%s, winchester!", Controllable:GetName()) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) - + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -2918,24 +3167,24 @@ end -- @return #boolean If true, proceed to onafterRearm. function ARTY:onbeforeRearm(Controllable, From, Event, To) self:_EventFromTo("onbeforeRearm", Event, From, To) - + local _rearmed=self:_CheckRearmed() if _rearmed then - self:T(ARTY.id..string.format("%s, group is already armed to the teeth. Rearming request denied!", self.groupname)) + self:T(self.lid..string.format("%s, group is already armed to the teeth. Rearming request denied!", self.groupname)) return false else - self:T(ARTY.id..string.format("%s, group might be rearmed.", self.groupname)) + self:T(self.lid..string.format("%s, group might be rearmed.", self.groupname)) end - + -- Check if a reaming unit or rearming place was specified. if self.RearmingGroup and self.RearmingGroup:IsAlive() then return true elseif self.RearmingPlaceCoord then - return true + return true else return false end - + end --- After "Rearm" event. Send message if reporting is on. Route rearming unit to ARTY group. @@ -2946,13 +3195,13 @@ end -- @param #string To To state. function ARTY:onafterRearm(Controllable, From, Event, To) self:_EventFromTo("onafterRearm", Event, From, To) - + -- Coordinate of ARTY unit. local coordARTY=self.Controllable:GetCoordinate() - + -- Remember current coordinates so that we find our way back home. self.InitialCoord=coordARTY - + -- Coordinate of rearming group. local coordRARM=nil if self.RearmingGroup then @@ -2961,71 +3210,71 @@ function ARTY:onafterRearm(Controllable, From, Event, To) -- Remember the coordinates of the rearming unit. After rearming it will go back to this position. self.RearmingGroupCoord=coordRARM end - + if self.RearmingGroup and self.RearmingPlaceCoord and self.ismobile then - + -- CASE 1: Rearming unit and ARTY group meet at rearming place. - + -- Send message. local text=string.format("%s, %s, request rearming at rearming place.", Controllable:GetName(), self.RearmingGroup:GetName()) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) - + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) + -- Distances. local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) local dR=coordRARM:Get2DDistance(self.RearmingPlaceCoord) - + -- Route ARTY group to rearming place. if dA > self.RearmingDistance then local _tocoord=self:_VicinityCoord(self.RearmingPlaceCoord, self.RearmingDistance/4, self.RearmingDistance/2) self:AssignMoveCoord(_tocoord, nil, nil, self.RearmingArtyOnRoad, false, "REARMING MOVE TO REARMING PLACE", true) end - + -- Route Rearming group to rearming place. if dR > self.RearmingDistance then local ToCoord=self:_VicinityCoord(self.RearmingPlaceCoord, self.RearmingDistance/4, self.RearmingDistance/2) self:_Move(self.RearmingGroup, ToCoord, self.RearmingGroupSpeed, self.RearmingGroupOnRoad) end - + elseif self.RearmingGroup then - + -- CASE 2: Rearming unit drives to ARTY group. - + -- Send message. local text=string.format("%s, %s, request rearming.", Controllable:GetName(), self.RearmingGroup:GetName()) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) - + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) + -- Distance between ARTY group and rearming unit. local distance=coordARTY:Get2DDistance(coordRARM) - + -- If distance is larger than ~100 m, the Rearming unit is routed to the ARTY group. if distance > self.RearmingDistance then - + -- Route rearming group to ARTY group. self:_Move(self.RearmingGroup, self:_VicinityCoord(coordARTY), self.RearmingGroupSpeed, self.RearmingGroupOnRoad) end - + elseif self.RearmingPlaceCoord then - + -- CASE 3: ARTY drives to rearming place. - + -- Send message. local text=string.format("%s, moving to rearming place.", Controllable:GetName()) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) - + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) + -- Distance. local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) - + -- Route ARTY group to rearming place. if dA > self.RearmingDistance then local _tocoord=self:_VicinityCoord(self.RearmingPlaceCoord) self:AssignMoveCoord(_tocoord, nil, nil, self.RearmingArtyOnRoad, false, "REARMING MOVE TO REARMING PLACE", true) - end - + end + end - + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3038,23 +3287,23 @@ end -- @param #string To To state. function ARTY:onafterRearmed(Controllable, From, Event, To) self:_EventFromTo("onafterRearmed", Event, From, To) - + -- Send message. local text=string.format("%s, rearming complete.", Controllable:GetName()) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) - + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) + -- "Rearm" tactical nukes as well. self.Nukes=self.Nukes0 self.Nillu=self.Nillu0 self.Nsmoke=self.Nsmoke0 - + -- Route ARTY group back to where it came from (if distance is > 100 m). local dist=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) if dist > self.RearmingDistance then self:AssignMoveCoord(self.InitialCoord, nil, nil, self.RearmingArtyOnRoad, false, "REARMING MOVE REARMING COMPLETE", true) end - + -- Route unit back to where it came from (if distance is > 100 m). if self.RearmingGroup and self.RearmingGroup:IsAlive() then local d=self.RearmingGroup:GetCoordinate():Get2DDistance(self.RearmingGroupCoord) @@ -3065,7 +3314,7 @@ function ARTY:onafterRearmed(Controllable, From, Event, To) self.RearmingGroup:ClearTasks() end end - + end --- Check if ARTY group is rearmed, i.e. has its full amount of ammo. @@ -3076,27 +3325,27 @@ function ARTY:_CheckRearmed() -- Get current ammo. local nammo,nshells,nrockets,nmissiles=self:GetAmmo() - + -- Number of units still alive. local units=self.Controllable:GetUnits() local nunits=0 if units then nunits=#units end - + -- Full Ammo count. local FullAmmo=self.Nammo0 * nunits / self.IniGroupStrength - + -- Rearming status in per cent. local _rearmpc=nammo/FullAmmo*100 - + -- Send message if rearming > 1% complete if _rearmpc>1 then local text=string.format("%s, rearming %d %% complete.", self.alias, _rearmpc) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) end - + -- Return if ammo is full. -- TODO: Strangely, I got the case that a Paladin got one more shell than it can max carry, i.e. 40 not 39 when rearming when it still had some ammo left. Need to report. if nammo>=FullAmmo then @@ -3117,16 +3366,16 @@ end -- @param #string To To state. -- @param #table move Table containing the move parameters. -- @param Core.Point#COORDINATE ToCoord Coordinate to which the ARTY group should move. --- @param #boolean OnRoad If true group should move on road mainly. +-- @param #boolean OnRoad If true group should move on road mainly. -- @return #boolean If true, proceed to onafterMove. function ARTY:onbeforeMove(Controllable, From, Event, To, move) self:_EventFromTo("onbeforeMove", Event, From, To) - + -- Check if group can actually move... if not self.ismobile then return false end - + -- Check if group is engaging. if self.currentTarget then if move.cancel then @@ -3137,7 +3386,7 @@ function ARTY:onbeforeMove(Controllable, From, Event, To, move) return false end end - + return true end @@ -3154,21 +3403,21 @@ function ARTY:onafterMove(Controllable, From, Event, To, move) -- Set alarm state to green and ROE to weapon hold. self.Controllable:OptionAlarmStateGreen() self.Controllable:OptionROEHoldFire() - + -- Take care of max speed. local _Speed=math.min(move.speed, self.SpeedMax) - + -- Smoke coordinate if self.Debug then move.coord:SmokeRed() end - + -- Set current move. self.currentMove=move -- Route group to coodinate. self:_Move(self.Controllable, move.coord, move.speed, move.onroad) - + end --- After "Arrived" event. Group has reached its destination. @@ -3182,15 +3431,15 @@ function ARTY:onafterArrived(Controllable, From, Event, To) -- Set alarm state to auto. self.Controllable:OptionAlarmStateAuto() - + -- WARNING: calling ClearTasks() here causes CTD of DCS when move is over. Dont know why? combotask? --self.Controllable:ClearTasks() - + -- Send message local text=string.format("%s, arrived at destination.", Controllable:GetName()) - self:T(ARTY.id..text) - MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) - + self:T(self.lid..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug) + -- Remove executed move from queue. if self.currentMove then self:RemoveMove(self.currentMove.name) @@ -3210,11 +3459,11 @@ end -- @param #table target Array holding the target parameters. function ARTY:onafterNewTarget(Controllable, From, Event, To, target) self:_EventFromTo("onafterNewTarget", Event, From, To) - + -- Debug message. local text=string.format("Adding new target %s.", target.name) MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:T(ARTY.id..text) + self:T(self.lid..text) end --- After "NewMove" event. @@ -3226,11 +3475,11 @@ end -- @param #table move Array holding the move parameters. function ARTY:onafterNewMove(Controllable, From, Event, To, move) self:_EventFromTo("onafterNewTarget", Event, From, To) - + -- Debug message. local text=string.format("Adding new move %s.", move.name) MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:T(ARTY.id..text) + self:T(self.lid..text) end @@ -3240,26 +3489,62 @@ end -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -function ARTY:onafterDead(Controllable, From, Event, To) +-- @param #string Unitname Name of the unit that died. +function ARTY:onafterDead(Controllable, From, Event, To, Unitname) self:_EventFromTo("onafterDead", Event, From, To) - - -- Number of units left in the group. - local units=self.Controllable:GetUnits() - local nunits=0 - if units~=nil then - nunits=#units - end - + + -- Number of units still alive. + --local nunits=self.Controllable and self.Controllable:CountAliveUnits() or 0 + local nunits=self.Controllable:CountAliveUnits() + -- Message. - local text=string.format("%s, one of our units just died! %d units left.", self.groupname, nunits) + local text=string.format("%s, our unit %s just died! %d units left.", self.groupname, Unitname, nunits) MESSAGE:New(text, 5):ToAllIf(self.Debug) - self:T(ARTY.id..text) - + self:I(self.lid..text) + -- Go to stop state. if nunits==0 then - self:Stop() + + -- Cease Fire on current target. + if self.currentTarget then + self:CeaseFire(self.currentTarget) + end + + if self.respawnafterdeath then + -- Respawn group. + if not self.respawning then + self.respawning=true + self:__Respawn(self.respawndelay or 1) + end + else + -- Stop FSM. + self:Stop() + end end - + +end + + +--- After "Dead" event, when a unit has died. When all units of a group are dead trigger "Stop" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterRespawn(Controllable, From, Event, To) + self:_EventFromTo("onafterRespawn", Event, From, To) + + env.info("FF Respawning arty group") + + local group=self.Controllable --Wrapper.Group#GROUP + + -- Respawn group. + self.Controllable=group:Respawn() + + self.respawning=false + + -- Call status again. + self:__Status(-1) end --- After "Stop" event. Unhandle events and cease fire on current target. @@ -3270,18 +3555,18 @@ end -- @param #string To To state. function ARTY:onafterStop(Controllable, From, Event, To) self:_EventFromTo("onafterStop", Event, From, To) - + -- Debug info. - self:T(ARTY.id..string.format("Stopping ARTY FSM for group %s.", Controllable:GetName())) - + self:I(self.lid..string.format("Stopping ARTY FSM for group %s.", tostring(Controllable:GetName()))) + -- Cease Fire on current target. if self.currentTarget then self:CeaseFire(self.currentTarget) end - + -- Remove all targets. --self:RemoveAllTargets() - + -- Unhandle event. self:UnHandleEvent(EVENTS.Shot) self:UnHandleEvent(EVENTS.Dead) @@ -3302,7 +3587,7 @@ function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) -- Controllable. local group=self.Controllable --Wrapper.Group#GROUP - + -- Tactical nukes are actually cannon shells. if weapontype==ARTY.WeaponType.TacticalNukes or weapontype==ARTY.WeaponType.IlluminationShells or weapontype==ARTY.WeaponType.SmokeShells then weapontype=ARTY.WeaponType.Cannon @@ -3310,17 +3595,47 @@ function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) -- Set ROE to weapon free. group:OptionROEOpenFire() - + -- Get Vec2 local vec2=coord:GetVec2() - + -- Get task. local fire=group:TaskFireAtPoint(vec2, radius, nshells, weapontype) - + -- Execute task. group:SetTask(fire) end +--- Set task for attacking a group. +-- @param #ARTY self +-- @param #ARTY.Target target Target data. +function ARTY:_AttackGroup(target) + + -- Controllable. + local group=self.Controllable --Wrapper.Group#GROUP + + local weapontype=target.weapontype + + -- Tactical nukes are actually cannon shells. + if weapontype==ARTY.WeaponType.TacticalNukes or weapontype==ARTY.WeaponType.IlluminationShells or weapontype==ARTY.WeaponType.SmokeShells then + weapontype=ARTY.WeaponType.Cannon + end + + -- Set ROE to weapon free. + group:OptionROEOpenFire() + + -- Target group. + local targetgroup=GROUP:FindByName(target.name) + + -- Get task. + local fire=group:TaskAttackGroup(targetgroup, weapontype, AI.Task.WeaponExpend.ONE, 1) + + -- Execute task. + group:SetTask(fire) +end + + + --- Model a nuclear blast/destruction by creating fires and destroy scenery. -- @param #ARTY self -- @param Core.Point#COORDINATE _coord Coordinate of the impact point (center of the blast). @@ -3328,78 +3643,78 @@ function ARTY:_NuclearBlast(_coord) local S0=self.nukewarhead local R0=self.nukerange - + -- Number of fires local N0=self.nukefires - + -- Create an explosion at the last known position. _coord:Explosion(S0) - + -- Huge fire at direct impact point. --if self.nukefire then _coord:BigSmokeAndFireHuge() --end - + -- Create a table of fire coordinates within the demolition zone. local _fires={} - for i=1,N0 do + for i=1,N0 do local _fire=_coord:GetRandomCoordinateInRadius(R0) local _dist=_fire:Get2DDistance(_coord) table.insert(_fires, {distance=_dist, coord=_fire}) end - + -- Sort scenery wrt to distance from impact point. local _sort = function(a,b) return a.distance < b.distance end table.sort(_fires,_sort) - + local function _explosion(R) -- At R=R0 ==> explosion strength is 1% of S0 at impact point. local alpha=math.log(100) local strength=S0*math.exp(-alpha*R/R0) - self:T2(ARTY.id..string.format("Nuclear explosion strength s(%.1f m) = %.5f (s/s0=%.1f %%), alpha=%.3f", R, strength, strength/S0*100, alpha)) + self:T2(self.lid..string.format("Nuclear explosion strength s(%.1f m) = %.5f (s/s0=%.1f %%), alpha=%.3f", R, strength, strength/S0*100, alpha)) return strength end - + local function ignite(_fires) for _,fire in pairs(_fires) do local _fire=fire.coord --Core.Point#COORDINATE - + -- Get distance to impact and calc exponential explosion strength. local R=_fire:Get2DDistance(_coord) local S=_explosion(R) - self:T2(ARTY.id..string.format("Explosion r=%.1f, s=%.3f", R, S)) - + self:T2(self.lid..string.format("Explosion r=%.1f, s=%.3f", R, S)) + -- Get a random Big Smoke and fire object. local _preset=math.random(0,7) local _density=S/S0 --math.random()+0.1 - + _fire:BigSmokeAndFire(_preset,_density) _fire:Explosion(S) - + end end - + if self.nukefire==true then ignite(_fires) end - ---[[ + +--[[ local ZoneNuke=ZONE_RADIUS:New("Nukezone", _coord:GetVec2(), 2000) -- Scan for Scenery objects. ZoneNuke:Scan(Object.Category.SCENERY) - + -- Array with all possible hideouts, i.e. scenery objects in the vicinity of the group. local scenery={} for SceneryTypeName, SceneryData in pairs(ZoneNuke:GetScannedScenery()) do for SceneryName, SceneryObject in pairs(SceneryData) do - + local SceneryObject = SceneryObject -- Wrapper.Scenery#SCENERY - + -- Position of the scenery object. local spos=SceneryObject:GetCoordinate() - + -- Distance from group to impact point. local distance= spos:Get2DDistance(_coord) @@ -3409,18 +3724,18 @@ function ARTY:_NuclearBlast(_coord) 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() + + --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() @@ -3438,30 +3753,30 @@ end -- @param #boolean OnRoad If true, use (mainly) roads. function ARTY:_Move(group, ToCoord, Speed, OnRoad) self:F2({group=group:GetName(), Speed=Speed, OnRoad=OnRoad}) - + -- Clear all tasks. group:ClearTasks() group:OptionAlarmStateGreen() group:OptionROEHoldFire() - + -- Set formation. local formation = "Off Road" - + -- Get max speed of group. local SpeedMax=group:GetSpeedMax() - + -- Set speed. Speed=Speed or SpeedMax*0.7 - + -- Make sure, we do not go above max speed possible. Speed=math.min(Speed, SpeedMax) - + -- Current coordinates of group. local cpini=group:GetCoordinate() -- Core.Point#COORDINATE - - -- Distance between current and final point. + + -- Distance between current and final point. local dist=cpini:Get2DDistance(ToCoord) - + -- Waypoint and task arrays. local path={} local task={} @@ -3472,13 +3787,13 @@ function ARTY:_Move(group, ToCoord, Speed, OnRoad) -- Route group on road if requested. if OnRoad then - + -- Get path on road. local _pathonroad=cpini:GetPathOnRoad(ToCoord) - + -- Check if we actually got a path. There are situations where nil is returned. In that case, we go directly. if _pathonroad then - + -- Just take the first and last point. local _first=_pathonroad[1] local _last=_pathonroad[#_pathonroad] @@ -3487,72 +3802,69 @@ function ARTY:_Move(group, ToCoord, Speed, OnRoad) _first:SmokeGreen() _last:SmokeGreen() end - + -- First point on road. path[#path+1]=_first:WaypointGround(Speed, "On Road") task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) - + -- Last point on road. path[#path+1]=_last:WaypointGround(Speed, "On Road") task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) end - + end - + -- Last waypoint at ToCoord. path[#path+1]=ToCoord:WaypointGround(Speed, formation) task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, true) - + --if self.Debug then -- cpini:SmokeBlue() -- ToCoord:SmokeBlue() --end - + -- Init waypoints of the group. local Waypoints={} - + -- New points are added to the default route. for i=1,#path do table.insert(Waypoints, i, path[i]) end - + -- Set task for all waypoints. for i=1,#Waypoints do group:SetTaskWaypoint(Waypoints[i], task[i]) end - + -- Submit task and route group along waypoints. group:Route(Waypoints) end --- Function called when group is passing a waypoint. --- @param Wrapper.Group#GROUP group Group for which waypoint passing should be monitored. +-- @param Wrapper.Group#GROUP group Group for which waypoint passing should be monitored. -- @param #ARTY arty ARTY object. -- @param #number i Waypoint number that has been reached. -- @param #boolean final True if it is the final waypoint. function ARTY._PassingWaypoint(group, arty, i, final) - -- Debug message. - local text=string.format("%s, passing waypoint %d.", group:GetName(), i) - if final then - text=string.format("%s, arrived at destination.", group:GetName()) - end - arty:T(ARTY.id..text) + if group and group:IsAlive() then - --[[ - if final then - MESSAGE:New(text, 10):ToCoalitionIf(group:GetCoalition(), arty.Debug or arty.report) - else - MESSAGE:New(text, 10):ToAllIf(arty.Debug) - end - ]] - - -- Arrived event. - if final and arty.groupname==group:GetName() then - arty:Arrived() - end + local groupname=tostring(group:GetName()) + -- Debug message. + local text=string.format("%s, passing waypoint %d.", groupname, i) + if final then + text=string.format("%s, arrived at destination.", groupname) + end + arty:T(arty.lid..text) + + -- Arrived event. + if final and arty.groupname==groupname then + arty:Arrived() + end + + end end --- Relocate to another position, e.g. after an engagement to avoid couter strikes. @@ -3561,7 +3873,7 @@ function ARTY:_Relocate() -- Current position. local _pos=self.Controllable:GetCoordinate() - + local _new=nil local _gotit=false local _n=0 @@ -3570,7 +3882,7 @@ function ARTY:_Relocate() -- Get a random coordinate. _new=_pos:GetRandomCoordinateInRadius(self.relocateRmax, self.relocateRmin) local _surface=_new:GetSurfaceType() - + -- Check that new coordinate is not water(-ish). if _surface~=land.SurfaceType.WATER and _surface~=land.SurfaceType.SHALLOW_WATER then _gotit=true @@ -3578,7 +3890,7 @@ function ARTY:_Relocate() -- Increase counter. _n=_n+1 until _gotit or _n>_nmax - + -- Assign relocation. if _gotit then self:AssignMoveCoord(_new, nil, nil, false, false, "RELOCATION MOVE AFTER FIRING") @@ -3594,70 +3906,70 @@ end -- @return #number Number of missiles the group has left. function ARTY:GetAmmo(display) self:F3({display=display}) - + -- Default is display false. if display==nil then display=false end - + -- Init counter. local nammo=0 local nshells=0 local nrockets=0 local nmissiles=0 - + -- Get all units. local units=self.Controllable:GetUnits() if units==nil then return nammo, nshells, nrockets, nmissiles end - + for _,unit in pairs(units) do - + if unit and unit:IsAlive() then - + -- Output. local text=string.format("ARTY group %s - unit %s:\n", self.groupname, unit:GetName()) - + -- Get ammo table. local ammotable=unit:GetAmmo() if ammotable ~= nil then - + local weapons=#ammotable - + -- Display ammo table if display then - self:E(ARTY.id..string.format("Number of weapons %d.", weapons)) - self:E({ammotable=ammotable}) - self:E(ARTY.id.."Ammotable:") + self:I(self.lid..string.format("Number of weapons %d.", weapons)) + self:I({ammotable=ammotable}) + self:I(self.lid.."Ammotable:") for id,bla in pairs(ammotable) do - self:E({id=id, ammo=bla}) + self:I({id=id, ammo=bla}) end end - + -- Loop over all weapons. for w=1,weapons do - + -- Number of current weapon. local Nammo=ammotable[w]["count"] - + -- Typename of current weapon local Tammo=ammotable[w]["desc"]["typeName"] - + local _weaponString = self:_split(Tammo,"%.") local _weaponName = _weaponString[#_weaponString] - + -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3 local Category=ammotable[w].desc.category - + -- Get missile category: Weapon.MissileCategory AAM=1, SAM=2, BM=3, ANTI_SHIP=4, CRUISE=5, OTHER=6 local MissileCategory=nil if Category==Weapon.Category.MISSILE then MissileCategory=ammotable[w].desc.missileCategory end - - + + -- Check for correct shell type. local _gotshell=false if #self.ammoshells>0 then @@ -3684,7 +3996,7 @@ function ARTY:GetAmmo(display) else if Category==Weapon.Category.ROCKET then _gotrocket=true - end + end end -- Check for correct missile type. @@ -3698,67 +4010,67 @@ function ARTY:GetAmmo(display) else if Category==Weapon.Category.MISSILE then _gotmissile=true - end + end end - + -- We are specifically looking for shells or rockets here. - if _gotshell then - + if _gotshell then + -- Add up all shells. nshells=nshells+Nammo - + -- Debug info. text=text..string.format("- %d shells of type %s\n", Nammo, _weaponName) - + elseif _gotrocket then - + -- Add up all rockets. nrockets=nrockets+Nammo - + -- Debug info. text=text..string.format("- %d rockets of type %s\n", Nammo, _weaponName) - + elseif _gotmissile then - + -- Add up all cruise missiles (category 5) if MissileCategory==Weapon.MissileCategory.CRUISE then nmissiles=nmissiles+Nammo end - + -- Debug info. text=text..string.format("- %d %s missiles of type %s\n", Nammo, self:_MissileCategoryName(MissileCategory), _weaponName) - + else - + -- Debug info. text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) - + end - + end end -- Debug text and send message. if display then - self:E(ARTY.id..text) + self:I(self.lid..text) else - self:T3(ARTY.id..text) + self:T3(self.lid..text) end MESSAGE:New(text, 10):ToAllIf(display) - + end end - + -- Total amount of ammunition. nammo=nshells+nrockets+nmissiles - + return nammo, nshells, nrockets, nmissiles end --- Returns a name of a missile category. -- @param #ARTY self -- @param #number categorynumber Number of missile category from weapon missile category enumerator. See https://wiki.hoggitworld.com/view/DCS_Class_Weapon --- @return #string Missile category name. +-- @return #string Missile category name. function ARTY:_MissileCategoryName(categorynumber) local cat="unknown" if categorynumber==Weapon.MissileCategory.AAM then @@ -3785,44 +4097,44 @@ end --- Extract engagement assignments and parameters from mark text. -- @param #ARTY self -- @param #string text Marker text. --- @return #boolean If true, authentification successful. +-- @return #boolean If true, authentification successful. function ARTY:_MarkerKeyAuthentification(text) -- Set battery and coalition. --local batteryname=self.groupname - local batterycoalition=self.Controllable:GetCoalition() + local batterycoalition=self.coalition -- Get assignment. local mykey=nil if self.markkey~=nil then - - -- keywords are split by "," + + -- keywords are split by "," local keywords=self:_split(text, ",") for _,key in pairs(keywords) do local s=self:_split(key, " ") local val=s[2] - if key:lower():find("key") then + if key:lower():find("key") then mykey=tonumber(val) - self:T(ARTY.id..string.format("Authorisation Key=%s.", val)) + self:T(self.lid..string.format("Authorisation Key=%s.", val)) end end - + end - + -- Check if the authorization key is required and if it is valid. local _validkey=true - + -- Check if group needs authorization. if self.markkey~=nil then -- Assume key is incorrect. _validkey=false - + -- If key was found, check if matches. if mykey~=nil then - _validkey=self.markkey==mykey - end - self:T2(ARTY.id..string.format("%s, authkey=%s == %s=playerkey ==> valid=%s", self.groupname, tostring(self.markkey), tostring(mykey), tostring(_validkey))) - + _validkey=self.markkey==mykey + end + self:T2(self.lid..string.format("%s, authkey=%s == %s=playerkey ==> valid=%s", self.groupname, tostring(self.markkey), tostring(mykey), tostring(_validkey))) + -- Send message local text="" if mykey==nil then @@ -3844,8 +4156,8 @@ end -- @return #table Table with assignment parameters, e.g. number of shots, radius, time etc. function ARTY:_Markertext(text) self:F(text) - - -- Assignment parameters. + + -- Assignment parameters. local assignment={} assignment.battery={} assignment.aliases={} @@ -3863,7 +4175,7 @@ function ARTY:_Markertext(text) assignment.cancelrearm=false assignment.setrearmingplace=false assignment.setrearminggroup=false - + -- Check for correct keywords. if text:lower():find("arty engage") or text:lower():find("arty attack") then assignment.engage=true @@ -3874,95 +4186,95 @@ function ARTY:_Markertext(text) elseif text:lower():find("arty cancel") then assignment.cancel=true elseif text:lower():find("arty set") then - assignment.set=true + assignment.set=true else - self:E(ARTY.id..'ERROR: Neither "ARTY ENGAGE" nor "ARTY MOVE" nor "ARTY RELOCATE" nor "ARTY REQUEST" nor "ARTY CANCEL" nor "ARTY SET" keyword specified!') + self:E(self.lid..'ERROR: Neither "ARTY ENGAGE" nor "ARTY MOVE" nor "ARTY RELOCATE" nor "ARTY REQUEST" nor "ARTY CANCEL" nor "ARTY SET" keyword specified!') return nil end - - -- keywords are split by "," + + -- keywords are split by "," local keywords=self:_split(text, ",") self:T({keywords=keywords}) for _,keyphrase in pairs(keywords) do - + -- Split keyphrase by space. First one is the key and second, ... the parameter(s) until the next comma. local str=self:_split(keyphrase, " ") local key=str[1] local val=str[2] - + -- Debug output. - self:T3(ARTY.id..string.format("%s, keyphrase = %s, key = %s, val = %s", self.groupname, tostring(keyphrase), tostring(key), tostring(val))) - + self:T3(self.lid..string.format("%s, keyphrase = %s, key = %s, val = %s", self.groupname, tostring(keyphrase), tostring(key), tostring(val))) + -- Battery name, i.e. which ARTY group should fire. if key:lower():find("battery") then - + local v=self:_split(keyphrase, '"') - - for i=2,#v,2 do + + for i=2,#v,2 do table.insert(assignment.battery, v[i]) - self:T2(ARTY.id..string.format("Key Battery=%s.", v[i])) + self:T2(self.lid..string.format("Key Battery=%s.", v[i])) end elseif key:lower():find("alias") then - + local v=self:_split(keyphrase, '"') - - for i=2,#v,2 do + + for i=2,#v,2 do table.insert(assignment.aliases, v[i]) - self:T2(ARTY.id..string.format("Key Aliases=%s.", v[i])) + self:T2(self.lid..string.format("Key Aliases=%s.", v[i])) end elseif key:lower():find("cluster") then - + local v=self:_split(keyphrase, '"') - - for i=2,#v,2 do + + for i=2,#v,2 do table.insert(assignment.cluster, v[i]) - self:T2(ARTY.id..string.format("Key Cluster=%s.", v[i])) + self:T2(self.lid..string.format("Key Cluster=%s.", v[i])) end - + elseif keyphrase:lower():find("everyone") or keyphrase:lower():find("all batteries") or keyphrase:lower():find("allbatteries") then - + assignment.everyone=true - self:T(ARTY.id..string.format("Key Everyone=true.")) - + self:T(self.lid..string.format("Key Everyone=true.")) + elseif keyphrase:lower():find("irrevocable") or keyphrase:lower():find("readonly") then - + assignment.readonly=true - self:T2(ARTY.id..string.format("Key Readonly=true.")) - + self:T2(self.lid..string.format("Key Readonly=true.")) + elseif (assignment.engage or assignment.move) and key:lower():find("time") then - + if val:lower():find("now") then assignment.time=self:_SecondsToClock(timer.getTime0()+2) else assignment.time=val - end - self:T2(ARTY.id..string.format("Key Time=%s.", val)) - + end + self:T2(self.lid..string.format("Key Time=%s.", val)) + elseif assignment.engage and key:lower():find("shot") then - + assignment.nshells=tonumber(val) - self:T(ARTY.id..string.format("Key Shot=%s.", val)) - + self:T(self.lid..string.format("Key Shot=%s.", val)) + elseif assignment.engage and key:lower():find("prio") then - + assignment.prio=tonumber(val) self:T2(string.format("Key Prio=%s.", val)) - + elseif assignment.engage and key:lower():find("maxengage") then - + assignment.maxengage=tonumber(val) - self:T2(ARTY.id..string.format("Key Maxengage=%s.", val)) - + self:T2(self.lid..string.format("Key Maxengage=%s.", val)) + elseif assignment.engage and key:lower():find("radius") then - + assignment.radius=tonumber(val) - self:T2(ARTY.id..string.format("Key Radius=%s.", val)) - + self:T2(self.lid..string.format("Key Radius=%s.", val)) + elseif assignment.engage and key:lower():find("weapon") then - + if val:lower():find("cannon") then assignment.weapontype=ARTY.WeaponType.Cannon elseif val:lower():find("rocket") then @@ -3974,107 +4286,107 @@ function ARTY:_Markertext(text) elseif val:lower():find("illu") then assignment.weapontype=ARTY.WeaponType.IlluminationShells elseif val:lower():find("smoke") then - assignment.weapontype=ARTY.WeaponType.SmokeShells + assignment.weapontype=ARTY.WeaponType.SmokeShells else assignment.weapontype=ARTY.WeaponType.Auto - end - self:T2(ARTY.id..string.format("Key Weapon=%s.", val)) - + end + self:T2(self.lid..string.format("Key Weapon=%s.", val)) + elseif (assignment.move or assignment.set) and key:lower():find("speed") then - + assignment.speed=tonumber(val) - self:T2(ARTY.id..string.format("Key Speed=%s.", val)) - + self:T2(self.lid..string.format("Key Speed=%s.", val)) + elseif (assignment.move or assignment.set) and (keyphrase:lower():find("on road") or keyphrase:lower():find("onroad") or keyphrase:lower():find("use road")) then - + assignment.onroad=true - self:T2(ARTY.id..string.format("Key Onroad=true.")) - + self:T2(self.lid..string.format("Key Onroad=true.")) + elseif assignment.move and (keyphrase:lower():find("cancel target") or keyphrase:lower():find("canceltarget")) then - + assignment.movecanceltarget=true - self:T2(ARTY.id..string.format("Key Cancel Target (before move)=true.")) - + self:T2(self.lid..string.format("Key Cancel Target (before move)=true.")) + elseif assignment.request and keyphrase:lower():find("rearm") then - + assignment.requestrearming=true - self:T2(ARTY.id..string.format("Key Request Rearming=true.")) - + self:T2(self.lid..string.format("Key Request Rearming=true.")) + elseif assignment.request and keyphrase:lower():find("ammo") then - + assignment.requestammo=true - self:T2(ARTY.id..string.format("Key Request Ammo=true.")) + self:T2(self.lid..string.format("Key Request Ammo=true.")) elseif assignment.request and keyphrase:lower():find("target") then - + assignment.requesttargets=true - self:T2(ARTY.id..string.format("Key Request Targets=true.")) + self:T2(self.lid..string.format("Key Request Targets=true.")) elseif assignment.request and keyphrase:lower():find("status") then - + assignment.requeststatus=true - self:T2(ARTY.id..string.format("Key Request Status=true.")) + self:T2(self.lid..string.format("Key Request Status=true.")) elseif assignment.request and (keyphrase:lower():find("move") or keyphrase:lower():find("relocation")) then - + assignment.requestmoves=true - self:T2(ARTY.id..string.format("Key Request Moves=true.")) - + self:T2(self.lid..string.format("Key Request Moves=true.")) + elseif assignment.cancel and (keyphrase:lower():find("engagement") or keyphrase:lower():find("attack") or keyphrase:lower():find("target")) then - + assignment.canceltarget=true - self:T2(ARTY.id..string.format("Key Cancel Target=true.")) - + self:T2(self.lid..string.format("Key Cancel Target=true.")) + elseif assignment.cancel and (keyphrase:lower():find("move") or keyphrase:lower():find("relocation")) then - + assignment.cancelmove=true - self:T2(ARTY.id..string.format("Key Cancel Move=true.")) + self:T2(self.lid..string.format("Key Cancel Move=true.")) elseif assignment.cancel and keyphrase:lower():find("rearm") then - + assignment.cancelrearm=true - self:T2(ARTY.id..string.format("Key Cancel Rearm=true.")) + self:T2(self.lid..string.format("Key Cancel Rearm=true.")) elseif assignment.set and keyphrase:lower():find("rearming place") then - + assignment.setrearmingplace=true - self:T(ARTY.id..string.format("Key Set Rearming Place=true.")) + self:T(self.lid..string.format("Key Set Rearming Place=true.")) elseif assignment.set and keyphrase:lower():find("rearming group") then local v=self:_split(keyphrase, '"') local groupname=v[2] - + local group=GROUP:FindByName(groupname) if group and group:IsAlive() then assignment.setrearminggroup=group end - - self:T2(ARTY.id..string.format("Key Set Rearming Group = %s.", tostring(groupname))) - + + self:T2(self.lid..string.format("Key Set Rearming Group = %s.", tostring(groupname))) + elseif key:lower():find("lldms") then - + local _flat = "%d+:%d+:%d+%s*[N,S]" local _flon = "%d+:%d+:%d+%s*[W,E]" local _lat=keyphrase:match(_flat) local _lon=keyphrase:match(_flon) - self:T2(ARTY.id..string.format("Key LLDMS: lat=%s, long=%s format=DMS", _lat,_lon)) - + self:T2(self.lid..string.format("Key LLDMS: lat=%s, long=%s format=DMS", _lat,_lon)) + if _lat and _lon then - + -- Convert DMS string to DD numbers format. local _latitude, _longitude=self:_LLDMS2DD(_lat, _lon) - self:T2(ARTY.id..string.format("Key LLDMS: lat=%.3f, long=%.3f format=DD", _latitude,_longitude)) - + self:T2(self.lid..string.format("Key LLDMS: lat=%.3f, long=%.3f format=DD", _latitude,_longitude)) + -- Convert LL to coordinate object. if _latitude and _longitude then assignment.coord=COORDINATE:NewFromLLDD(_latitude,_longitude) end - + end - end + end end - + return assignment end @@ -4105,7 +4417,7 @@ function ARTY:_MarkRequestMoves() else text=text..string.format("\n- no queued relocations") end - MESSAGE:New(text, 20):Clear():ToCoalition(self.Controllable:GetCoalition()) + MESSAGE:New(text, 20):Clear():ToCoalition(self.coalition) end --- Request Targets. @@ -4123,7 +4435,7 @@ function ARTY:_MarkRequestTargets() else text=text..string.format("\n- no queued targets") end - MESSAGE:New(text, 20):Clear():ToCoalition(self.Controllable:GetCoalition()) + MESSAGE:New(text, 20):Clear():ToCoalition(self.coalition) end --- Create a name for an engagement initiated by placing a marker. @@ -4150,19 +4462,19 @@ end -- @return #number ID of the marked relocation move or nil function ARTY:_GetMarkIDfromName(name) - -- keywords are split by "," + -- keywords are split by "," local keywords=self:_split(name, ",") local battery=nil local markTID=nil local markMID=nil - + for _,key in pairs(keywords) do local str=self:_split(key, "=") local par=str[1] local val=str[2] - + if par:find("BATTERY") then battery=val end @@ -4172,9 +4484,9 @@ function ARTY:_GetMarkIDfromName(name) if par:find("Marked Relocation ID") then markMID=tonumber(val) end - + end - + return battery, markTID, markMID end @@ -4187,22 +4499,22 @@ end -- @param #ARTY self function ARTY:_SortTargetQueuePrio() self:F2() - + -- Sort results table wrt times they have already been engaged. local function _sort(a, b) return (a.engaged < b.engaged) or (a.engaged==b.engaged and a.prio < b.prio) end table.sort(self.targets, _sort) - + -- Debug output. - self:T3(ARTY.id.."Sorted targets wrt prio and number of engagements:") + self:T3(self.lid.."Sorted targets wrt prio and number of engagements:") for i=1,#self.targets do local _target=self.targets[i] - self:T3(ARTY.id..string.format("Target %s", self:_TargetInfo(_target))) + self:T3(self.lid..string.format("Target %s", self:_TargetInfo(_target))) end end ---- Sort array with respect to time. Array elements must have a .time entry. +--- Sort array with respect to time. Array elements must have a .time entry. -- @param #ARTY self -- @param #table queue Array to sort. Should have elemnt .time. function ARTY:_SortQueueTime(queue) @@ -4224,12 +4536,12 @@ function ARTY:_SortQueueTime(queue) table.sort(queue, _sort) -- Debug output. - self:T3(ARTY.id.."Sorted queue wrt time:") + self:T3(self.lid.."Sorted queue wrt time:") for i=1,#queue do local _queue=queue[i] local _time=tostring(_queue.time) local _clock=tostring(self:_SecondsToClock(_queue.time)) - self:T3(ARTY.id..string.format("%s: time=%s, clock=%s", _queue.name, _time, _clock)) + self:T3(self.lid..string.format("%s: time=%s, clock=%s", _queue.name, _time, _clock)) end end @@ -4253,101 +4565,116 @@ end -- @param #ARTY self function ARTY:_CheckTargetsInRange() + local targets2delete={} + for i=1,#self.targets do local _target=self.targets[i] - - self:T3(ARTY.id..string.format("Before: Target %s - in range = %s", _target.name, tostring(_target.inrange))) - + + self:T3(self.lid..string.format("Before: Target %s - in range = %s", _target.name, tostring(_target.inrange))) + -- Check if target is in range. - local _inrange,_toofar,_tooclose=self:_TargetInRange(_target) - self:T3(ARTY.id..string.format("Inbetw: Target %s - in range = %s, toofar = %s, tooclose = %s", _target.name, tostring(_target.inrange), tostring(_toofar), tostring(_tooclose))) - - -- Init default for assigning moves into range. - local _movetowards=false - local _moveaway=false - - if _target.inrange==nil then - - -- First time the check is performed. We call the function again and send a message. - _target.inrange,_toofar,_tooclose=self:_TargetInRange(_target, self.report or self.Debug) - - -- Send group towards/away from target. - if _toofar then - _movetowards=true - elseif _tooclose then - _moveaway=true + local _inrange,_toofar,_tooclose,_remove=self:_TargetInRange(_target) + self:T3(self.lid..string.format("Inbetw: Target %s - in range = %s, toofar = %s, tooclose = %s", _target.name, tostring(_target.inrange), tostring(_toofar), tostring(_tooclose))) + + if _remove then + + -- The ARTY group is immobile and not cargo but the target is not in range! + table.insert(targets2delete, _target.name) + + else + + -- Init default for assigning moves into range. + local _movetowards=false + local _moveaway=false + + if _target.inrange==nil then + + -- First time the check is performed. We call the function again and send a message. + _target.inrange,_toofar,_tooclose=self:_TargetInRange(_target, self.report or self.Debug) + + -- Send group towards/away from target. + if _toofar then + _movetowards=true + elseif _tooclose then + _moveaway=true + end + + elseif _target.inrange==true then + + -- Target was in range at previous check... + + if _toofar then --...but is now too far away. + _movetowards=true + elseif _tooclose then --...but is now too close. + _moveaway=true + end + + elseif _target.inrange==false then + + -- Target was out of range at previous check. + + if _inrange then + -- Inform coalition that target is now in range. + local text=string.format("%s, target %s is now in range.", self.alias, _target.name) + self:T(self.lid..text) + MESSAGE:New(text,10):ToCoalitionIf(self.coalition, self.report or self.Debug) + end + end - - elseif _target.inrange==true then - - -- Target was in range at previous check... - - if _toofar then --...but is now too far away. - _movetowards=true - elseif _tooclose then --...but is now too close. - _moveaway=true - end - - elseif _target.inrange==false then - - -- Target was out of range at previous check. - - if _inrange then - -- Inform coalition that target is now in range. - local text=string.format("%s, target %s is now in range.", self.alias, _target.name) - self:T(ARTY.id..text) - MESSAGE:New(text,10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) - end - - end - - -- Assign a relocation command so that the unit will be in range of the requested target. - if self.autorelocate and (_movetowards or _moveaway) then - - -- Get current position. - local _from=self.Controllable:GetCoordinate() - local _dist=_from:Get2DDistance(_target.coord) - - if _dist<=self.autorelocatemaxdist then - - local _tocoord --Core.Point#COORDINATE - local _name="" - local _safetymargin=500 - - if _movetowards then - - -- Target was in range on previous check but now we are too far away. - local _waytogo=_dist-self.maxrange+_safetymargin - local _heading=self:_GetHeading(_from,_target.coord) - _tocoord=_from:Translate(_waytogo, _heading) - _name=string.format("%s, relocation to within max firing range of target %s", self.alias, _target.name) - - elseif _moveaway then - - -- Target was in range on previous check but now we are too far away. - local _waytogo=_dist-self.minrange+_safetymargin - local _heading=self:_GetHeading(_target.coord,_from) - _tocoord=_from:Translate(_waytogo, _heading) - _name=string.format("%s, relocation to within min firing range of target %s", self.alias, _target.name) + + -- Assign a relocation command so that the unit will be in range of the requested target. + if self.autorelocate and (_movetowards or _moveaway) then + + -- Get current position. + local _from=self.Controllable:GetCoordinate() + local _dist=_from:Get2DDistance(_target.coord) + + if _dist<=self.autorelocatemaxdist then + + local _tocoord --Core.Point#COORDINATE + local _name="" + local _safetymargin=500 + + if _movetowards then + + -- Target was in range on previous check but now we are too far away. + local _waytogo=_dist-self.maxrange+_safetymargin + local _heading=self:_GetHeading(_from,_target.coord) + _tocoord=_from:Translate(_waytogo, _heading) + _name=string.format("%s, relocation to within max firing range of target %s", self.alias, _target.name) + + elseif _moveaway then + + -- Target was in range on previous check but now we are too far away. + local _waytogo=_dist-self.minrange+_safetymargin + local _heading=self:_GetHeading(_target.coord,_from) + _tocoord=_from:Translate(_waytogo, _heading) + _name=string.format("%s, relocation to within min firing range of target %s", self.alias, _target.name) + + end + + -- Send info message. + MESSAGE:New(_name.." assigned.", 10):ToCoalitionIf(self.coalition, self.report or self.Debug) + + -- Assign relocation move. + self:AssignMoveCoord(_tocoord, nil, nil, self.autorelocateonroad, false, _name, true) end - - -- Send info message. - MESSAGE:New(_name.." assigned.", 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) - - -- Assign relocation move. - self:AssignMoveCoord(_tocoord, nil, nil, self.autorelocateonroad, false, _name, true) - + end - + + -- Update value. + _target.inrange=_inrange + + self:T3(self.lid..string.format("After: Target %s - in range = %s", _target.name, tostring(_target.inrange))) end - - -- Update value. - _target.inrange=_inrange - - self:T3(ARTY.id..string.format("After: Target %s - in range = %s", _target.name, tostring(_target.inrange))) - end + + -- Remove targets not in range. + for _,targetname in pairs(targets2delete) do + self:RemoveTarget(targetname) + end + end --- Check all normal (untimed) targets and return the target with the highest priority which has been engaged the fewest times. @@ -4355,75 +4682,75 @@ end -- @return #table Target which is due to be attacked now or nil if no target could be found. function ARTY:_CheckNormalTargets() self:F3() - + -- Sort targets w.r.t. prio and number times engaged already. self:_SortTargetQueuePrio() - + -- No target engagements if rearming! if self:is("Rearming") then return nil end - + -- Loop over all sorted targets. - for i=1,#self.targets do + for i=1,#self.targets do local _target=self.targets[i] - + -- Debug info. - self:T3(ARTY.id..string.format("Check NORMAL target %d: %s", i, self:_TargetInfo(_target))) - + self:T3(self.lid..string.format("Check NORMAL target %d: %s", i, self:_TargetInfo(_target))) + -- Check that target no time, is not under fire currently and in range. if _target.underfire==false and _target.time==nil and _target.maxengage > _target.engaged and self:_TargetInRange(_target) and self:_CheckWeaponTypeAvailable(_target)>0 then - + -- Debug info. - self:T2(ARTY.id..string.format("Found NORMAL target %s", self:_TargetInfo(_target))) - + self:T2(self.lid..string.format("Found NORMAL target %s", self:_TargetInfo(_target))) + return _target end end - + return nil end --- Check all timed targets and return the target which should be attacked next. -- @param #ARTY self --- @return #table Target which is due to be attacked now. +-- @return #table Target which is due to be attacked now. function ARTY:_CheckTimedTargets() self:F3() - + -- Current time. local Tnow=timer.getAbsTime() - + -- Sort Targets wrt time. self:_SortQueueTime(self.targets) - + -- No target engagements if rearming! if self:is("Rearming") then return nil end - + for i=1,#self.targets do local _target=self.targets[i] - + -- Debug info. - self:T3(ARTY.id..string.format("Check TIMED target %d: %s", i, self:_TargetInfo(_target))) - - -- Check if target has an attack time which has already passed. Also check that target is not under fire already and that it is in range. + self:T3(self.lid..string.format("Check TIMED target %d: %s", i, self:_TargetInfo(_target))) + + -- Check if target has an attack time which has already passed. Also check that target is not under fire already and that it is in range. if _target.time and Tnow>=_target.time and _target.underfire==false and self:_TargetInRange(_target) and self:_CheckWeaponTypeAvailable(_target)>0 then - + -- Check if group currently has a target and whether its priorty is lower than the timed target. if self.currentTarget then if self.currentTarget.prio > _target.prio then -- Current target under attack but has lower priority than this target. - self:T2(ARTY.id..string.format("Found TIMED HIGH PRIO target %s.", self:_TargetInfo(_target))) + self:T2(self.lid..string.format("Found TIMED HIGH PRIO target %s.", self:_TargetInfo(_target))) return _target end else -- No current target. - self:T2(ARTY.id..string.format("Found TIMED target %s.", self:_TargetInfo(_target))) + self:T2(self.lid..string.format("Found TIMED target %s.", self:_TargetInfo(_target))) return _target end end - + end return nil @@ -4431,37 +4758,37 @@ end --- Check all moves and return the one which should be executed next. -- @param #ARTY self --- @return #table Move which is due. +-- @return #table Move which is due. function ARTY:_CheckMoves() self:F3() - + -- Current time. local Tnow=timer.getAbsTime() - + -- Sort Targets wrt time. self:_SortQueueTime(self.moves) - + -- Check if we are currently firing. local firing=false if self.currentTarget then firing=true end - + -- Loop over all moves in queue. for i=1,#self.moves do - + -- Shortcut. local _move=self.moves[i] - + if string.find(_move.name, "REARMING MOVE") and ((self.currentMove and self.currentMove.name~=_move.name) or self.currentMove==nil) then - -- We got an rearming assignment which has priority. + -- We got an rearming assignment which has priority. return _move elseif (Tnow >= _move.time) and (firing==false or _move.cancel) and (not self.currentMove) and (not self:is("Rearming")) then -- Time for move is reached and maybe current target should be cancelled. return _move - end + end end - + return nil end @@ -4469,35 +4796,35 @@ end -- @param #ARTY self function ARTY:_CheckShootingStarted() self:F2() - + if self.currentTarget then - + -- Current time. local Tnow=timer.getTime() - + -- Get name and id of target. local name=self.currentTarget.name - + -- Time that passed after current target has been assigned. local dt=Tnow-self.currentTarget.Tassigned - + -- Debug info if self.Nshots==0 then - self:T(ARTY.id..string.format("%s, waiting for %d seconds for first shot on target %s.", self.groupname, dt, name)) + self:T(self.lid..string.format("%s, waiting for %d seconds for first shot on target %s.", self.groupname, dt, name)) end - + -- Check if we waited long enough and no shot was fired. if dt > self.WaitForShotTime and self.Nshots==0 then - + -- Debug info. - self:T(ARTY.id..string.format("%s, no shot event after %d seconds. Removing current target %s from list.", self.groupname, self.WaitForShotTime, name)) - + self:T(self.lid..string.format("%s, no shot event after %d seconds. Removing current target %s from list.", self.groupname, self.WaitForShotTime, name)) + -- CeaseFire. self:CeaseFire(self.currentTarget) - + -- Remove target from list. self:RemoveTarget(name) - + end end end @@ -4508,17 +4835,17 @@ end -- @return #number Arrayindex of target. function ARTY:_GetTargetIndexByName(name) self:F2(name) - + for i=1,#self.targets do local targetname=self.targets[i].name - self:T3(ARTY.id..string.format("Have target with name %s. Index = %d", targetname, i)) + self:T3(self.lid..string.format("Have target with name %s. Index = %d", targetname, i)) if targetname==name then - self:T2(ARTY.id..string.format("Found target with name %s. Index = %d", name, i)) + self:T2(self.lid..string.format("Found target with name %s. Index = %d", name, i)) return i end end - - self:T2(ARTY.id..string.format("WARNING: Target with name %s could not be found. (This can happen.)", name)) + + self:T2(self.lid..string.format("WARNING: Target with name %s could not be found. (This can happen.)", name)) return nil end @@ -4528,17 +4855,17 @@ end -- @return #number Arrayindex of move. function ARTY:_GetMoveIndexByName(name) self:F2(name) - + for i=1,#self.moves do local movename=self.moves[i].name - self:T3(ARTY.id..string.format("Have move with name %s. Index = %d", movename, i)) + self:T3(self.lid..string.format("Have move with name %s. Index = %d", movename, i)) if movename==name then - self:T2(ARTY.id..string.format("Found move with name %s. Index = %d", name, i)) + self:T2(self.lid..string.format("Found move with name %s. Index = %d", name, i)) return i end end - - self:T2(ARTY.id..string.format("WARNING: Move with name %s could not be found. (This can happen.)", name)) + + self:T2(self.lid..string.format("WARNING: Move with name %s could not be found. (This can happen.)", name)) return nil end @@ -4553,48 +4880,48 @@ function ARTY:_CheckOutOfAmmo(targets) -- Special weapon type requested ==> Check if corresponding ammo is empty. local _partlyoutofammo=false - + for _,Target in pairs(targets) do - + if Target.weapontype==ARTY.WeaponType.Auto and _nammo==0 then - self:T(ARTY.id..string.format("Group %s, auto weapon requested for target %s but all ammo is empty.", self.groupname, Target.name)) + self:T(self.lid..string.format("Group %s, auto weapon requested for target %s but all ammo is empty.", self.groupname, Target.name)) _partlyoutofammo=true - + elseif Target.weapontype==ARTY.WeaponType.Cannon and _nshells==0 then - - self:T(ARTY.id..string.format("Group %s, cannons requested for target %s but shells empty.", self.groupname, Target.name)) + + self:T(self.lid..string.format("Group %s, cannons requested for target %s but shells empty.", self.groupname, Target.name)) _partlyoutofammo=true - + elseif Target.weapontype==ARTY.WeaponType.TacticalNukes and self.Nukes<=0 then - - self:T(ARTY.id..string.format("Group %s, tactical nukes requested for target %s but nukes empty.", self.groupname, Target.name)) + + self:T(self.lid..string.format("Group %s, tactical nukes requested for target %s but nukes empty.", self.groupname, Target.name)) _partlyoutofammo=true elseif Target.weapontype==ARTY.WeaponType.IlluminationShells and self.Nillu<=0 then - - self:T(ARTY.id..string.format("Group %s, illumination shells requested for target %s but illumination shells empty.", self.groupname, Target.name)) + + self:T(self.lid..string.format("Group %s, illumination shells requested for target %s but illumination shells empty.", self.groupname, Target.name)) _partlyoutofammo=true elseif Target.weapontype==ARTY.WeaponType.SmokeShells and self.Nsmoke<=0 then - - self:T(ARTY.id..string.format("Group %s, smoke shells requested for target %s but smoke shells empty.", self.groupname, Target.name)) + + self:T(self.lid..string.format("Group %s, smoke shells requested for target %s but smoke shells empty.", self.groupname, Target.name)) _partlyoutofammo=true - + elseif Target.weapontype==ARTY.WeaponType.Rockets and _nrockets==0 then - - self:T(ARTY.id..string.format("Group %s, rockets requested for target %s but rockets empty.", self.groupname, Target.name)) + + self:T(self.lid..string.format("Group %s, rockets requested for target %s but rockets empty.", self.groupname, Target.name)) _partlyoutofammo=true elseif Target.weapontype==ARTY.WeaponType.CruiseMissile and _nmissiles==0 then - - self:T(ARTY.id..string.format("Group %s, cruise missiles requested for target %s but all missiles empty.", self.groupname, Target.name)) + + self:T(self.lid..string.format("Group %s, cruise missiles requested for target %s but all missiles empty.", self.groupname, Target.name)) _partlyoutofammo=true - + end - + end - + return _partlyoutofammo end @@ -4606,7 +4933,7 @@ function ARTY:_CheckWeaponTypeAvailable(target) -- Get current ammo of group. local Nammo, Nshells, Nrockets, Nmissiles=self:GetAmmo() - + -- Check if enough ammo is there for the selected weapon type. local nfire=Nammo if target.weapontype==ARTY.WeaponType.Auto then @@ -4624,7 +4951,7 @@ function ARTY:_CheckWeaponTypeAvailable(target) elseif target.weapontype==ARTY.WeaponType.CruiseMissile then nfire=Nmissiles end - + return nfire end --- Check if a selected weapon type is in principle possible for this group. The current amount of ammo might be zero but the group still can be rearmed at a later point in time. @@ -4650,7 +4977,7 @@ function ARTY:_CheckWeaponTypePossible(target) elseif target.weapontype==ARTY.WeaponType.CruiseMissile then possible=self.Nmissiles0>0 end - + return possible end @@ -4661,7 +4988,7 @@ end -- @param #boolean makeunique If true, a new unique name is returned by appending the running index. -- @return #string Unique name, which is not already given for another target. function ARTY:_CheckName(givennames, name, makeunique) - self:F2({givennames=givennames, name=name}) + self:F2({givennames=givennames, name=name}) local newname=name local counter=1 @@ -4670,54 +4997,54 @@ function ARTY:_CheckName(givennames, name, makeunique) if makeunique==nil then makeunique=true end - + repeat -- until a unique name is found. - + -- We assume the name is unique. local _unique=true - + -- Loop over all targets already defined. for _,_target in pairs(givennames) do - + -- Target name. local _givenname=_target.name - + -- Name is already used by another target. if _givenname==newname then - + -- Name is already used for another target ==> try again with new name. _unique=false - + end - + -- Debug info. - self:T3(ARTY.id..string.format("%d: givenname = %s, newname=%s, unique = %s, makeunique = %s", n, tostring(_givenname), newname, tostring(_unique), tostring(makeunique))) + self:T3(self.lid..string.format("%d: givenname = %s, newname=%s, unique = %s, makeunique = %s", n, tostring(_givenname), newname, tostring(_unique), tostring(makeunique))) end - + -- Create a new name if requested and try again. if _unique==false and makeunique==true then - + -- Define newname = "name #01" newname=string.format("%s #%02d", name, counter) - + -- Increase counter. counter=counter+1 end - + -- Name is not unique and we don't want to make it unique. if _unique==false and makeunique==false then - self:T3(ARTY.id..string.format("Name %s is not unique. Return false.", tostring(newname))) - + self:T3(self.lid..string.format("Name %s is not unique. Return false.", tostring(newname))) + -- Return return name, false end - + -- Increase loop counter. We try max 100 times. n=n+1 until (_unique or n==nmax) - + -- Debug output and return new name. - self:T3(ARTY.id..string.format("Original name %s, new name = %s", name, newname)) + self:T3(self.lid..string.format("Original name %s, new name = %s", name, newname)) return newname, true end @@ -4728,24 +5055,25 @@ end -- @return #boolean True if target is in range, false otherwise. -- @return #boolean True if ARTY group is too far away from the target, i.e. distance > max firing range. -- @return #boolean True if ARTY group is too close to the target, i.e. distance < min finring range. +-- @return #boolean True if target should be removed since ARTY group is immobile and not cargo. function ARTY:_TargetInRange(target, message) self:F3(target) - + -- Default is no message. if message==nil then message=false end -- Distance between ARTY group and target. - self:E({controllable=self.Controllable, targetcoord=target.coord}) + self:T3({controllable=self.Controllable, targetcoord=target.coord}) local _dist=self.Controllable:GetCoordinate():Get2DDistance(target.coord) - + -- Assume we are in range. local _inrange=true local _tooclose=false local _toofar=false local text="" - + if _dist < self.minrange then _inrange=false _tooclose=true @@ -4755,19 +5083,21 @@ function ARTY:_TargetInRange(target, message) _toofar=true text=string.format("%s, target is out of range. Distance of %.1f km is greater than max range of %.1f km.", self.alias, _dist/1000, self.maxrange/1000) end - + -- Debug output. if not _inrange then - self:T(ARTY.id..text) - MESSAGE:New(text, 5):ToCoalitionIf(self.Controllable:GetCoalition(), (self.report and message) or (self.Debug and message)) - end - - -- Remove target if ARTY group cannot move, e.g. Mortas. No chance to be ever in range - unless they are cargo. - if not (self.ismobile or self.iscargo) and _inrange==false then - self:RemoveTarget(target.name) + self:T(self.lid..text) + MESSAGE:New(text, 5):ToCoalitionIf(self.coalition, (self.report and message) or (self.Debug and message)) end - return _inrange,_toofar,_tooclose + -- Remove target if ARTY group cannot move, e.g. Mortas. No chance to be ever in range - unless they are cargo. + local _remove=false + if not (self.ismobile or self.iscargo) and _inrange==false then + --self:RemoveTarget(target.name) + _remove=true + end + + return _inrange,_toofar,_tooclose,_remove end --- Get the weapon type name, which should be used to attack the target. @@ -4790,12 +5120,12 @@ function ARTY:_WeaponTypeName(tnumber) elseif tnumber==ARTY.WeaponType.IlluminationShells then name="Illumination Shells" elseif tnumber==ARTY.WeaponType.SmokeShells then - name="Smoke Shells" + name="Smoke Shells" end return name end ---- Find a random coordinate in the vicinity of another coordinate. +--- Find a random coordinate in the vicinity of another coordinate. -- @param #ARTY self -- @param Core.Point#COORDINATE coord Center coordinate. -- @param #number rmin (Optional) Minimum distance in meters from center coordinate. Default 20 m. @@ -4810,11 +5140,11 @@ function ARTY:_VicinityCoord(coord, rmin, rmax) local vec2=coord:GetRandomVec2InRadius(rmax, rmin) local pops=COORDINATE:NewFromVec2(vec2) -- Debug info. - self:T3(ARTY.id..string.format("Vicinity distance = %d (rmin=%d, rmax=%d)", pops:Get2DDistance(coord), rmin, rmax)) + self:T3(self.lid..string.format("Vicinity distance = %d (rmin=%d, rmax=%d)", pops:Get2DDistance(coord), rmin, rmax)) return pops end ---- Print event-from-to string to DCS log file. +--- Print event-from-to string to DCS log file. -- @param #ARTY self -- @param #string BA Before/after info. -- @param #string Event Event. @@ -4822,7 +5152,7 @@ end -- @param #string To To state. function ARTY:_EventFromTo(BA, Event, From, To) local text=string.format("%s: %s EVENT %s: %s --> %s", BA, self.groupname, Event, From, To) - self:T3(ARTY.id..text) + self:T3(self.lid..text) end --- Split string. C.f. http://stackoverflow.com/questions/1426954/split-string-in-lua @@ -4831,7 +5161,7 @@ end -- @param #string sep Speparator for split. -- @return #table Split text. function ARTY:_split(str, sep) - self:F3({str=str, sep=sep}) + self:F3({str=str, sep=sep}) local result = {} local regex = ("([^%s]+)"):format(sep) for each in str:gmatch(regex) do @@ -4842,13 +5172,14 @@ end --- Returns the target parameters as formatted string. -- @param #ARTY self +-- @param #ARTY.Target target The target data. -- @return #string name, prio, radius, nshells, engaged, maxengage, time, weapontype function ARTY:_TargetInfo(target) local clock=tostring(self:_SecondsToClock(target.time)) local weapon=self:_WeaponTypeName(target.weapontype) local _underfire=tostring(target.underfire) - return string.format("%s: prio=%d, radius=%d, nshells=%d, engaged=%d/%d, weapontype=%s, time=%s, underfire=%s", - target.name, target.prio, target.radius, target.nshells, target.engaged, target.maxengage, weapon, clock,_underfire) + return string.format("%s: prio=%d, radius=%d, nshells=%d, engaged=%d/%d, weapontype=%s, time=%s, underfire=%s, attackgroup=%s", + target.name, target.prio, target.radius, target.nshells, target.engaged, target.maxengage, weapon, clock,_underfire, tostring(target.attackgroup)) end --- Returns a formatted string with information about all move parameters. @@ -4872,29 +5203,29 @@ function ARTY:_LLDMS2DD(l1,l2) -- Make an array of lat and long. local _latlong={l1,l2} - + local _latitude=nil local _longitude=nil - + for _,ll in pairs(_latlong) do - + -- Format is expected as "DD:MM:SS" or "D:M:S". - local _format = "%d+:%d+:%d+" + local _format = "%d+:%d+:%d+" local _ldms=ll:match(_format) - + if _ldms then - + -- Split DMS to degrees, minutes and seconds. local _dms=self:_split(_ldms, ":") local _deg=tonumber(_dms[1]) local _min=tonumber(_dms[2]) local _sec=tonumber(_dms[3]) - + -- Convert DMS to DD. local function DMS2DD(d,m,s) return d+m/60+s/3600 end - + -- Detect with hemisphere is meant. if ll:match("N") then _latitude=DMS2DD(_deg,_min,_sec) @@ -4905,19 +5236,19 @@ function ARTY:_LLDMS2DD(l1,l2) elseif ll:match("E") then _longitude=DMS2DD(_deg,_min,_sec) end - + -- Debug text. local text=string.format("DMS %02d Deg %02d min %02d sec",_deg,_min,_sec) - self:T2(ARTY.id..text) + self:T2(self.lid..text) - end + end end - + -- Debug text. local text=string.format("\nLatitude %s", tostring(_latitude)) text=text..string.format("\nLongitude %s", tostring(_longitude)) - self:T2(ARTY.id..text) - + self:T2(self.lid..text) + return _latitude,_longitude end @@ -4927,14 +5258,14 @@ end -- @return #string Time in format Hours:minutes:seconds. function ARTY:_SecondsToClock(seconds) self:F3({seconds=seconds}) - + if seconds==nil then return nil end - + -- Seconds local seconds = tonumber(seconds) - + -- Seconds of this day. local _seconds=seconds%(60*60*24) @@ -4954,23 +5285,23 @@ end -- @param #string clock String of clock time. E.g., "06:12:35". function ARTY:_ClockToSeconds(clock) self:F3({clock=clock}) - + if clock==nil then return nil end - + -- Seconds init. local seconds=0 - + -- Split additional days. local dsplit=self:_split(clock, "+") - + -- Convert days to seconds. if #dsplit>1 then seconds=seconds+tonumber(dsplit[2])*60*60*24 end - -- Split hours, minutes, seconds + -- Split hours, minutes, seconds local tsplit=self:_split(dsplit[1], ":") -- Get time in seconds @@ -4988,13 +5319,11 @@ function ARTY:_ClockToSeconds(clock) end i=i+1 end - - self:T3(ARTY.id..string.format("Clock %s = %d seconds", clock, seconds)) + + self:T3(self.lid..string.format("Clock %s = %d seconds", clock, seconds)) return seconds end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - \ No newline at end of file diff --git a/Moose Development/Moose/Functional/CleanUp.lua b/Moose Development/Moose/Functional/CleanUp.lua index daed34b02..a2183c097 100644 --- a/Moose Development/Moose/Functional/CleanUp.lua +++ b/Moose Development/Moose/Functional/CleanUp.lua @@ -1,54 +1,54 @@ --- **Functional** -- Keep airbases clean of crashing or colliding airplanes, and kill missiles when being fired at airbases. --- +-- -- === --- +-- -- ## Features: --- --- +-- +-- -- * Try to keep the airbase clean and operational. -- * Prevent airplanes from crashing. -- * Clean up obstructing airplanes from the runway that are standing still for a period of time. -- * Prevent airplanes firing missiles within the airbase zone. --- +-- -- === --- +-- -- ## Missions: --- +-- -- [CLA - CleanUp Airbase](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/CLA%20-%20CleanUp%20Airbase) --- +-- -- === --- +-- -- Specific airbases need to be provided that need to be guarded. Each airbase registered, will be guarded within a zone of 8 km around the airbase. -- Any unit that fires a missile, or shoots within the zone of an airbase, will be monitored by CLEANUP_AIRBASE. --- Within the 8km zone, units cannot fire any missile, which prevents the airbase runway to receive missile or bomb hits. +-- Within the 8km zone, units cannot fire any missile, which prevents the airbase runway to receive missile or bomb hits. -- Any airborne or ground unit that is on the runway below 30 meters (default value) will be automatically removed if it is damaged. --- +-- -- This is not a full 100% secure implementation. It is still possible that CLEANUP_AIRBASE cannot prevent (in-time) to keep the airbase clean. -- The following situations may happen that will still stop the runway of an airbase: --- +-- -- * A damaged unit is not removed on time when above the runway, and crashes on the runway. -- * A bomb or missile is still able to dropped on the runway. -- * Units collide on the airbase, and could not be removed on time. --- +-- -- When a unit is within the airbase zone and needs to be monitored, -- its status will be checked every 0.25 seconds! This is required to ensure that the airbase is kept clean. -- But as a result, there is more CPU overload. --- +-- -- So as an advise, I suggest you use the CLEANUP_AIRBASE class with care: --- +-- -- * Only monitor airbases that really need to be monitored! -- * Try not to monitor airbases that are likely to be invaded by enemy troops. -- For these airbases, there is little use to keep them clean, as they will be invaded anyway... --- +-- -- By following the above guidelines, you can add airbase cleanup with acceptable CPU overhead. --- +-- -- === --- +-- -- ### Author: **FlightControl** --- ### Contributions: --- +-- ### Contributions: +-- -- === --- +-- -- @module Functional.CleanUp -- @image CleanUp_Airbases.JPG @@ -60,29 +60,29 @@ -- @extends #CLEANUP_AIRBASE.__ --- Keeps airbases clean, and tries to guarantee continuous airbase operations, even under combat. --- +-- -- # 1. CLEANUP_AIRBASE Constructor --- +-- -- Creates the main object which is preventing the airbase to get polluted with debris on the runway, which halts the airbase. --- +-- -- -- Clean these Zones. --- CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi ) --- +-- CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi } ) +-- -- -- or -- CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi ) -- CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi ) --- +-- -- # 2. Add or Remove airbases --- +-- -- The method @{#CLEANUP_AIRBASE.AddAirbase}() to add an airbase to the cleanup validation process. -- The method @{#CLEANUP_AIRBASE.RemoveAirbase}() removes an airbase from the cleanup validation process. --- +-- -- # 3. Clean missiles and bombs within the airbase zone. --- +-- -- When missiles or bombs hit the runway, the airbase operations stop. -- Use the method @{#CLEANUP_AIRBASE.SetCleanMissiles}() to control the cleaning of missiles, which will prevent airbases to stop. -- Note that this method will not allow anymore airbases to be attacked, so there is a trade-off here to do. --- +-- -- @field #CLEANUP_AIRBASE CLEANUP_AIRBASE = { ClassName = "CLEANUP_AIRBASE", @@ -106,11 +106,11 @@ CLEANUP_AIRBASE.__.Airbases = {} -- or -- CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi ) -- CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi ) -function CLEANUP_AIRBASE:New( AirbaseNames ) +function CLEANUP_AIRBASE:New( AirbaseNames ) local self = BASE:Inherit( self, BASE:New() ) -- #CLEANUP_AIRBASE self:F( { AirbaseNames } ) - + if type( AirbaseNames ) == 'table' then for AirbaseID, AirbaseName in pairs( AirbaseNames ) do self:AddAirbase( AirbaseName ) @@ -119,9 +119,9 @@ function CLEANUP_AIRBASE:New( AirbaseNames ) local AirbaseName = AirbaseNames self:AddAirbase( AirbaseName ) end - + self:HandleEvent( EVENTS.Birth, self.__.OnEventBirth ) - + self.__.CleanUpScheduler = SCHEDULER:New( self, self.__.CleanUpSchedule, {}, 1, self.TimeInterval ) self:HandleEvent( EVENTS.EngineShutdown , self.__.EventAddForCleanUp ) @@ -130,7 +130,7 @@ function CLEANUP_AIRBASE:New( AirbaseNames ) self:HandleEvent( EVENTS.PilotDead, self.__.OnEventCrash ) self:HandleEvent( EVENTS.Dead, self.__.OnEventCrash ) self:HandleEvent( EVENTS.Crash, self.__.OnEventCrash ) - + for UnitName, Unit in pairs( _DATABASE.UNITS ) do local Unit = Unit -- Wrapper.Unit#UNIT if Unit:IsAlive() ~= nil then @@ -144,7 +144,7 @@ function CLEANUP_AIRBASE:New( AirbaseNames ) end end end - + return self end @@ -155,7 +155,7 @@ end function CLEANUP_AIRBASE:AddAirbase( AirbaseName ) self.__.Airbases[AirbaseName] = AIRBASE:FindByName( AirbaseName ) self:F({"Airbase:", AirbaseName, self.__.Airbases[AirbaseName]:GetDesc()}) - + return self end @@ -197,7 +197,7 @@ function CLEANUP_AIRBASE.__:IsInAirbase( Vec2 ) break; end end - + return InAirbase end @@ -233,7 +233,7 @@ end -- @param DCS#Weapon MissileObject function CLEANUP_AIRBASE.__:DestroyMissile( MissileObject ) self:F( { MissileObject } ) - + if MissileObject and MissileObject:isExist() then MissileObject:destroy() self:T( "MissileObject Destroyed") @@ -245,7 +245,7 @@ end function CLEANUP_AIRBASE.__:OnEventBirth( EventData ) self:F( { EventData } ) - if EventData.IniUnit:IsAlive() ~= nil then + if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() ~= nil then if self:IsInAirbase( EventData.IniUnit:GetVec2() ) then self.CleanUpList[EventData.IniDCSUnitName] = {} self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnit = EventData.IniUnit @@ -267,7 +267,7 @@ function CLEANUP_AIRBASE.__:OnEventCrash( Event ) --TODO: DCS BUG - This stuff is not working due to a DCS bug. Burning units cannot be destroyed. -- self:T("before getGroup") - -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired + -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired -- self:T("after getGroup") -- _grp:destroy() -- self:T("after deactivateGroup") @@ -280,7 +280,7 @@ function CLEANUP_AIRBASE.__:OnEventCrash( Event ) self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName end - + end --- Detects if a unit shoots a missile. @@ -334,16 +334,16 @@ function CLEANUP_AIRBASE.__:AddForCleanUp( CleanUpUnit, CleanUpUnitName ) self.CleanUpList[CleanUpUnitName] = {} self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName - + local CleanUpGroup = CleanUpUnit:GetGroup() - + self.CleanUpList[CleanUpUnitName].CleanUpGroup = CleanUpGroup self.CleanUpList[CleanUpUnitName].CleanUpGroupName = CleanUpGroup:GetName() self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime() self.CleanUpList[CleanUpUnitName].CleanUpMoved = false self:T( { "CleanUp: Add to CleanUpList: ", CleanUpGroup:GetName(), CleanUpUnitName } ) - + end --- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given AirbaseNames. If this is the case, add the Group to the CLEANUP_AIRBASE List. @@ -369,7 +369,7 @@ function CLEANUP_AIRBASE.__:EventAddForCleanUp( Event ) end end end - + end @@ -380,26 +380,26 @@ function CLEANUP_AIRBASE.__:CleanUpSchedule() local CleanUpCount = 0 for CleanUpUnitName, CleanUpListData in pairs( self.CleanUpList ) do CleanUpCount = CleanUpCount + 1 - + local CleanUpUnit = CleanUpListData.CleanUpUnit -- Wrapper.Unit#UNIT local CleanUpGroupName = CleanUpListData.CleanUpGroupName if CleanUpUnit:IsAlive() ~= nil then - + if self:IsInAirbase( CleanUpUnit:GetVec2() ) then if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then - + local CleanUpCoordinate = CleanUpUnit:GetCoordinate() - + self:T( { "CleanUp Scheduler", CleanUpUnitName } ) if CleanUpUnit:GetLife() <= CleanUpUnit:GetLife0() * 0.95 then if CleanUpUnit:IsAboveRunway() then if CleanUpUnit:InAir() then - + local CleanUpLandHeight = CleanUpCoordinate:GetLandHeight() local CleanUpUnitHeight = CleanUpCoordinate.y - CleanUpLandHeight - + if CleanUpUnitHeight < 100 then self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because below safe height and damaged." } ) self:DestroyUnit( CleanUpUnit ) @@ -439,7 +439,6 @@ function CLEANUP_AIRBASE.__:CleanUpSchedule() end end self:T(CleanUpCount) - + return true end - diff --git a/Moose Development/Moose/Functional/Designate.lua b/Moose Development/Moose/Functional/Designate.lua index b8b1c24bb..a11aa21b8 100644 --- a/Moose Development/Moose/Functional/Designate.lua +++ b/Moose Development/Moose/Functional/Designate.lua @@ -469,7 +469,7 @@ do -- DESIGNATE self.CC = CC self.Detection = Detection self.AttackSet = AttackSet - self.RecceSet = Detection:GetDetectionSetGroup() + self.RecceSet = Detection:GetDetectionSet() self.Recces = {} self.Designating = {} self:SetDesignateName() @@ -1182,7 +1182,7 @@ do -- DESIGNATE local DetectedItem = self.Detection:GetDetectedItemByIndex( Index ) - local TargetSetUnit = self.Detection:GetDetectedSet( DetectedItem ) + local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem ) local MarkingCount = 0 local MarkedTypes = {} @@ -1352,7 +1352,7 @@ do -- DESIGNATE end local DetectedItem = self.Detection:GetDetectedItemByIndex( Index ) - local TargetSetUnit = self.Detection:GetDetectedSet( DetectedItem ) + local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem ) local Recces = self.Recces @@ -1377,7 +1377,7 @@ do -- DESIGNATE function DESIGNATE:onafterSmoke( From, Event, To, Index, Color ) local DetectedItem = self.Detection:GetDetectedItemByIndex( Index ) - local TargetSetUnit = self.Detection:GetDetectedSet( DetectedItem ) + local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem ) local TargetSetUnitCount = TargetSetUnit:Count() local MarkedCount = 0 @@ -1393,7 +1393,7 @@ do -- DESIGNATE self:F( "Smoking ..." ) local RecceGroup = self.RecceSet:FindNearestGroupFromPointVec2(SmokeUnit:GetPointVec2()) - local RecceUnit = RecceGroup:GetUnit( 1 ) + local RecceUnit = RecceGroup:GetUnit( 1 ) -- Wrapper.Unit#UNIT if RecceUnit then @@ -1422,7 +1422,7 @@ do -- DESIGNATE function DESIGNATE:onafterIlluminate( From, Event, To, Index ) local DetectedItem = self.Detection:GetDetectedItemByIndex( Index ) - local TargetSetUnit = self.Detection:GetDetectedSet( DetectedItem ) + local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem ) local TargetUnit = TargetSetUnit:GetFirst() if TargetUnit then diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 7402729ab..eb9b2e982 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -291,23 +291,40 @@ do -- DETECTION_BASE --- @type DETECTION_BASE.DetectedItems -- @list <#DETECTION_BASE.DetectedItem> - --- @type DETECTION_BASE.DetectedItem + --- Detected item data structrue. + -- @type DETECTION_BASE.DetectedItem -- @field #boolean IsDetected Indicates if the DetectedItem has been detected or not. - -- @field Core.Set#SET_UNIT Set - -- @field Core.Set#SET_UNIT Set -- The Set of Units in the detected area. - -- @field Core.Zone#ZONE_UNIT Zone -- The Zone of the detected area. - -- @field #boolean Changed Documents if the detected area has changes. + -- @field Core.Set#SET_UNIT Set The Set of Units in the detected area. + -- @field Core.Zone#ZONE_UNIT Zone The Zone of the detected area. + -- @field #boolean Changed Documents if the detected area has changed. -- @field #table Changes A list of the changes reported on the detected area. (It is up to the user of the detected area to consume those changes). - -- @field #number ID -- The identifier of the detected area. + -- @field #number ID The identifier of the detected area. -- @field #boolean FriendliesNearBy Indicates if there are friendlies within the detected area. -- @field Wrapper.Unit#UNIT NearestFAC The nearest FAC near the Area. -- @field Core.Point#COORDINATE Coordinate The last known coordinate of the DetectedItem. + -- @field Core.Point#COORDINATE InterceptCoord Intercept coordiante. + -- @field #number DistanceRecce Distance in meters of the Recce. + -- @field #number Index Detected item key. Could also be a string. + -- @field #string ItemID ItemPrefix .. "." .. self.DetectedItemMax. + -- @field #boolean Locked Lock detected item. + -- @field #table PlayersNearBy Table of nearby players. + -- @field #table FriendliesDistance Table of distances to friendly units. + -- @field #string TypeName Type name of the detected unit. + -- @field #string CategoryName Catetory name of the detected unit. + -- @field #string Name Name of the detected object. + -- @field #boolean IsVisible If true, detected object is visible. + -- @field #number LastTime Last time the detected item was seen. + -- @field DCS#Vec3 LastPos Last known position of the detected item. + -- @field DCS#Vec3 LastVelocity Last recorded 3D velocity vector of the detected item. + -- @field #boolean KnowType Type of detected item is known. + -- @field #boolean KnowDistance Distance to the detected item is known. + -- @field #number Distance Distance to the detected item. --- DETECTION constructor. -- @param #DETECTION_BASE self - -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. + -- @param Core.Set#SET_GROUP DetectionSet The @{Set} of @{Group}s that is used to detect the units. -- @return #DETECTION_BASE self - function DETECTION_BASE:New( DetectionSetGroup ) + function DETECTION_BASE:New( DetectionSet ) -- Inherits from BASE local self = BASE:Inherit( self, FSM:New() ) -- #DETECTION_BASE @@ -316,7 +333,7 @@ do -- DETECTION_BASE self.DetectedItemMax = 0 self.DetectedItems = {} - self.DetectionSetGroup = DetectionSetGroup + self.DetectionSet = DetectionSet self.RefreshTimeInterval = 30 @@ -398,7 +415,7 @@ do -- DETECTION_BASE -- @param #string To The To State string. self:AddTransition( "Detecting", "Detect", "Detecting" ) - self:AddTransition( "Detecting", "DetectionGroup", "Detecting" ) + self:AddTransition( "Detecting", "Detection", "Detecting" ) --- OnBefore Transition Handler for Event Detect. -- @function [parent=#DETECTION_BASE] OnBeforeDetect @@ -441,15 +458,18 @@ do -- DETECTION_BASE -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. + -- @param #table Units Table of detected units. --- Synchronous Event Trigger for Event Detected. -- @function [parent=#DETECTION_BASE] Detected -- @param #DETECTION_BASE self + -- @param #table Units Table of detected units. --- Asynchronous Event Trigger for Event Detected. -- @function [parent=#DETECTION_BASE] __Detected -- @param #DETECTION_BASE self -- @param #number Delay The delay in seconds. + -- @param #table Units Table of detected units. self:AddTransition( "Detecting", "DetectedItem", "Detecting" ) @@ -540,14 +560,40 @@ do -- DETECTION_BASE self.DetectedObjects[DetectionObjectName].Distance = 10000000 end - for DetectionGroupID, DetectionGroupData in pairs( self.DetectionSetGroup:GetSet() ) do - --self:F( { DetectionGroupData } ) - self:F( { DetectionGroup = DetectionGroupData:GetName() } ) - self:__DetectionGroup( DetectDelay, DetectionGroupData, DetectionTimeStamp ) -- Process each detection asynchronously. - self.DetectionCount = self.DetectionCount + 1 - DetectDelay = DetectDelay + 1 - end + + -- Count alive(!) groups only. Solves issue #1173 https://github.com/FlightControl-Master/MOOSE/issues/1173 + self.DetectionCount = self:CountAliveRecce() + + local DetectionInterval = self.DetectionCount / ( self.RefreshTimeInterval - 1 ) + + self:ForEachAliveRecce( + function( DetectionGroup ) + self:__Detection( DetectDelay, DetectionGroup, DetectionTimeStamp ) -- Process each detection asynchronously. + DetectDelay = DetectDelay + DetectionInterval + end + ) + + self:__Detect( -self.RefreshTimeInterval ) + end + + --- @param #DETECTION_BASE self + -- @param #number The amount of alive recce. + function DETECTION_BASE:CountAliveRecce() + + return self.DetectionSet:CountAlive() + + end + + --- @param #DETECTION_BASE self + function DETECTION_BASE:ForEachAliveRecce( IteratorFunction, ... ) + self:F2( arg ) + + self.DetectionSet:ForEachGroupAlive( IteratorFunction, arg ) + + return self + end + --- @param #DETECTION_BASE self -- @param #string From The From State string. @@ -555,7 +601,7 @@ do -- DETECTION_BASE -- @param #string To The To State string. -- @param Wrapper.Group#GROUP DetectionGroup The Group detecting. -- @param #number DetectionTimeStamp Time stamp of detection event. - function DETECTION_BASE:onafterDetectionGroup( From, Event, To, DetectionGroup, DetectionTimeStamp ) + function DETECTION_BASE:onafterDetection( From, Event, To, Detection, DetectionTimeStamp ) --self:F( { DetectedObjects = self.DetectedObjects } ) @@ -563,16 +609,16 @@ do -- DETECTION_BASE local HasDetectedObjects = false - if DetectionGroup:IsAlive() then + if Detection and Detection:IsAlive() then --self:T( { "DetectionGroup is Alive", DetectionGroup:GetName() } ) - local DetectionGroupName = DetectionGroup:GetName() - local DetectionUnit = DetectionGroup:GetUnit(1) + local DetectionGroupName = Detection:GetName() + local DetectionUnit = Detection:GetUnit(1) local DetectedUnits = {} - local DetectedTargets = DetectionGroup:GetDetectedTargets( + local DetectedTargets = Detection:GetDetectedTargets( self.DetectVisual, self.DetectOptical, self.DetectRadar, @@ -624,7 +670,7 @@ do -- DETECTION_BASE local DetectedObjectVec3 = DetectedObject:getPoint() local DetectedObjectVec2 = { x = DetectedObjectVec3.x, y = DetectedObjectVec3.z } - local DetectionGroupVec3 = DetectionGroup:GetVec3() + local DetectionGroupVec3 = Detection:GetVec3() local DetectionGroupVec2 = { x = DetectionGroupVec3.x, y = DetectionGroupVec3.z } local Distance = ( ( DetectedObjectVec3.x - DetectionGroupVec3.x )^2 + @@ -788,23 +834,27 @@ do -- DETECTION_BASE -- IsDetected = false! -- This is used in A2A_TASK_DISPATCHER to initiate fighter sweeping! The TASK_A2A_INTERCEPT tasks will be replaced with TASK_A2A_SWEEP tasks. for DetectedObjectName, DetectedObject in pairs( self.DetectedObjects ) do - if self.DetectedObjects[DetectedObjectName].IsDetected == true and self.DetectedObjects[DetectedObjectName].DetectionTimeStamp + 60 <= DetectionTimeStamp then + if self.DetectedObjects[DetectedObjectName].IsDetected == true and self.DetectedObjects[DetectedObjectName].DetectionTimeStamp + 300 <= DetectionTimeStamp then self.DetectedObjects[DetectedObjectName].IsDetected = false end end self:CreateDetectionItems() -- Polymorphic call to Create/Update the DetectionItems list for the DETECTION_ class grouping method. + for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + self:UpdateDetectedItemDetection( DetectedItem ) + self:CleanDetectionItem( DetectedItem, DetectedItemID ) -- Any DetectionItem that has a Set with zero elements in it, must be removed from the DetectionItems list. + if DetectedItem then self:__DetectedItem( 0.1, DetectedItem ) end + end - - self:__Detect( self.RefreshTimeInterval ) end + end @@ -812,7 +862,7 @@ do -- DETECTION_BASE do -- DetectionItems Creation - -- Clean the DetectedItem table. + --- Clean the DetectedItem table. -- @param #DETECTION_BASE self -- @return #DETECTION_BASE function DETECTION_BASE:CleanDetectionItem( DetectedItem, DetectedItemID ) @@ -838,7 +888,7 @@ do -- DETECTION_BASE local DetectedItems = self:GetDetectedItems() for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do - local DetectedSet = self:GetDetectedSet( DetectedItem ) + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) if DetectedSet then DetectedSet:RemoveUnitsByName( UnitName ) end @@ -1012,7 +1062,7 @@ do -- DETECTION_BASE --- Set the parameters to calculate to optimal intercept point. -- @param #DETECTION_BASE self -- @param #boolean Intercept Intercept is true if an intercept point is calculated. Intercept is false if it is disabled. The default Intercept is false. - -- @param #number IntereptDelay If Intercept is true, then InterceptDelay is the average time it takes to get airplanes airborne. + -- @param #number InterceptDelay If Intercept is true, then InterceptDelay is the average time it takes to get airplanes airborne. -- @return #DETECTION_BASE self function DETECTION_BASE:SetIntercept( Intercept, InterceptDelay ) self:F2() @@ -1229,17 +1279,17 @@ do -- DETECTION_BASE --- Returns if there are friendlies nearby the FAC units ... -- @param #DETECTION_BASE self - -- @param DetectedItem + -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @param DCS#Unit.Category Category The category of the unit. -- @return #boolean true if there are friendlies nearby function DETECTION_BASE:IsFriendliesNearBy( DetectedItem, Category ) - --self:F( { "FriendliesNearBy Test", DetectedItem.FriendliesNearBy } ) +-- self:F( { "FriendliesNearBy Test", DetectedItem.FriendliesNearBy } ) return ( DetectedItem.FriendliesNearBy and DetectedItem.FriendliesNearBy[Category] ~= nil ) or false end --- Returns friendly units nearby the FAC units ... -- @param #DETECTION_BASE self - -- @param DetectedItem + -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @param DCS#Unit.Category Category The category of the unit. -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. function DETECTION_BASE:GetFriendliesNearBy( DetectedItem, Category ) @@ -1249,6 +1299,7 @@ do -- DETECTION_BASE --- Returns if there are friendlies nearby the intercept ... -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return #boolean trhe if there are friendlies near the intercept. function DETECTION_BASE:IsFriendliesNearIntercept( DetectedItem ) @@ -1257,6 +1308,7 @@ do -- DETECTION_BASE --- Returns friendly units nearby the intercept point ... -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The detected item. -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. function DETECTION_BASE:GetFriendliesNearIntercept( DetectedItem ) @@ -1265,7 +1317,8 @@ do -- DETECTION_BASE --- Returns the distance used to identify friendlies near the deteted item ... -- @param #DETECTION_BASE self - -- @return #number The distance. + -- @param #DETECTION_BASE.DetectedItem DetectedItem The detected item. + -- @return #table A table of distances to friendlies. function DETECTION_BASE:GetFriendliesDistance( DetectedItem ) return DetectedItem.FriendliesDistance @@ -1273,6 +1326,7 @@ do -- DETECTION_BASE --- Returns if there are friendlies nearby the FAC units ... -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return #boolean trhe if there are friendlies nearby function DETECTION_BASE:IsPlayersNearBy( DetectedItem ) @@ -1281,6 +1335,7 @@ do -- DETECTION_BASE --- Returns friendly units nearby the FAC units ... -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The detected item. -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. function DETECTION_BASE:GetPlayersNearBy( DetectedItem ) @@ -1289,10 +1344,11 @@ do -- DETECTION_BASE --- Background worker function to determine if there are friendlies nearby ... -- @param #DETECTION_BASE self + -- @param #table TargetData function DETECTION_BASE:ReportFriendliesNearBy( TargetData ) --self:F( { "Search Friendlies", DetectedItem = TargetData.DetectedItem } ) - local DetectedItem = TargetData.DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedItem = TargetData.DetectedItem --#DETECTION_BASE.DetectedItem local DetectedSet = TargetData.DetectedItem.Set local DetectedUnit = DetectedSet:GetFirst() -- Wrapper.Unit#UNIT @@ -1376,33 +1432,37 @@ do -- DETECTION_BASE world.searchObjects( Object.Category.UNIT, SphereSearch, FindNearByFriendlies, TargetData ) DetectedItem.PlayersNearBy = nil - local DetectionZone = ZONE_UNIT:New( "DetectionPlayers", DetectedUnit, self.FriendliesRange ) _DATABASE:ForEachPlayer( --- @param Wrapper.Unit#UNIT PlayerUnit function( PlayerUnitName ) local PlayerUnit = UNIT:FindByName( PlayerUnitName ) - if PlayerUnit and PlayerUnit:IsInZone(DetectionZone) then + -- Fix for issue https://github.com/FlightControl-Master/MOOSE/issues/1225 + if PlayerUnit and PlayerUnit:IsAlive() then + local coord=PlayerUnit:GetCoordinate() + + if coord and coord:IsInRadius( DetectedUnitCoord, self.FriendliesRange ) then - local PlayerUnitCategory = PlayerUnit:GetDesc().category - - if ( not self.FriendliesCategory ) or ( self.FriendliesCategory and ( self.FriendliesCategory == PlayerUnitCategory ) ) then - - local PlayerUnitName = PlayerUnit:GetName() + local PlayerUnitCategory = PlayerUnit:GetDesc().category - DetectedItem.PlayersNearBy = DetectedItem.PlayersNearBy or {} - DetectedItem.PlayersNearBy[PlayerUnitName] = PlayerUnit - - -- Friendlies are sorted per unit category. - DetectedItem.FriendliesNearBy = DetectedItem.FriendliesNearBy or {} - DetectedItem.FriendliesNearBy[PlayerUnitCategory] = DetectedItem.FriendliesNearBy[PlayerUnitCategory] or {} - DetectedItem.FriendliesNearBy[PlayerUnitCategory][PlayerUnitName] = PlayerUnit - - local Distance = DetectedUnitCoord:Get2DDistance( PlayerUnit:GetCoordinate() ) - DetectedItem.FriendliesDistance = DetectedItem.FriendliesDistance or {} - DetectedItem.FriendliesDistance[Distance] = PlayerUnit - + if ( not self.FriendliesCategory ) or ( self.FriendliesCategory and ( self.FriendliesCategory == PlayerUnitCategory ) ) then + + local PlayerUnitName = PlayerUnit:GetName() + + DetectedItem.PlayersNearBy = DetectedItem.PlayersNearBy or {} + DetectedItem.PlayersNearBy[PlayerUnitName] = PlayerUnit + + -- Friendlies are sorted per unit category. + DetectedItem.FriendliesNearBy = DetectedItem.FriendliesNearBy or {} + DetectedItem.FriendliesNearBy[PlayerUnitCategory] = DetectedItem.FriendliesNearBy[PlayerUnitCategory] or {} + DetectedItem.FriendliesNearBy[PlayerUnitCategory][PlayerUnitName] = PlayerUnit + + local Distance = DetectedUnitCoord:Get2DDistance( PlayerUnit:GetCoordinate() ) + DetectedItem.FriendliesDistance = DetectedItem.FriendliesDistance or {} + DetectedItem.FriendliesDistance[Distance] = PlayerUnit + + end end end end @@ -1514,31 +1574,31 @@ do -- DETECTION_BASE --- Adds a new DetectedItem to the DetectedItems list. -- The DetectedItem is a table and contains a SET_UNIT in the field Set. -- @param #DETECTION_BASE self - -- @param ItemPrefix - -- @param DetectedItemKey The key of the DetectedItem. + -- @param #string ItemPrefix Prefix of detected item. + -- @param #number DetectedItemKey The key of the DetectedItem. Default self.DetectedItemMax. Could also be a string in principle. -- @param Core.Set#SET_UNIT Set (optional) The Set of Units to be added. -- @return #DETECTION_BASE.DetectedItem function DETECTION_BASE:AddDetectedItem( ItemPrefix, DetectedItemKey, Set ) - local DetectedItem = {} + local DetectedItem = {} --#DETECTION_BASE.DetectedItem self.DetectedItemCount = self.DetectedItemCount + 1 self.DetectedItemMax = self.DetectedItemMax + 1 - if DetectedItemKey then - self.DetectedItems[DetectedItemKey] = DetectedItem - else - self.DetectedItems[self.DetectedItemMax] = DetectedItem - end - - self.DetectedItemsByIndex[self.DetectedItemMax] = DetectedItem + DetectedItemKey = DetectedItemKey or self.DetectedItemMax + self.DetectedItems[DetectedItemKey] = DetectedItem + self.DetectedItemsByIndex[DetectedItemKey] = DetectedItem + DetectedItem.Index = DetectedItemKey DetectedItem.Set = Set or SET_UNIT:New():FilterDeads():FilterCrashes() - DetectedItem.Index = DetectedItemKey or self.DetectedItemMax DetectedItem.ItemID = ItemPrefix .. "." .. self.DetectedItemMax DetectedItem.ID = self.DetectedItemMax DetectedItem.Removed = false + if self.Locking then + self:LockDetectedItem( DetectedItem ) + end + return DetectedItem end @@ -1549,9 +1609,11 @@ do -- DETECTION_BASE -- @param Core.Set#SET_UNIT Set (optional) The Set of Units to be added. -- @param Core.Zone#ZONE_UNIT Zone (optional) The Zone to be added where the Units are located. -- @return #DETECTION_BASE.DetectedItem - function DETECTION_BASE:AddDetectedItemZone( DetectedItemKey, Set, Zone ) + function DETECTION_BASE:AddDetectedItemZone( ItemPrefix, DetectedItemKey, Set, Zone ) - local DetectedItem = self:AddDetectedItem( "AREA", DetectedItemKey, Set ) + self:F( { ItemPrefix, DetectedItemKey, Set, Zone } ) + + local DetectedItem = self:AddDetectedItem( ItemPrefix, DetectedItemKey, Set ) DetectedItem.Zone = Zone @@ -1624,7 +1686,7 @@ do -- DETECTION_BASE -- @return #DETECTION_BASE.DetectedItem function DETECTION_BASE:GetDetectedItemByIndex( Index ) - self:F( { DetectedItemsByIndex = self.DetectedItemsByIndex } ) + self:F( { self.DetectedItemsByIndex } ) local DetectedItem = self.DetectedItemsByIndex[Index] if DetectedItem then @@ -1661,7 +1723,7 @@ do -- DETECTION_BASE -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return Core.Set#SET_UNIT DetectedSet - function DETECTION_BASE:GetDetectedSet( DetectedItem ) + function DETECTION_BASE:GetDetectedItemSet( DetectedItem ) local DetectedSetUnit = DetectedItem and DetectedItem.Set if DetectedSetUnit then @@ -1697,6 +1759,7 @@ do -- DETECTION_BASE --- Checks if there is at least one UNIT detected in the Set of the the DetectedItem. -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return #boolean true if at least one UNIT is detected from the DetectedSet, false if no UNIT was detected from the DetectedSet. function DETECTION_BASE:IsDetectedItemDetected( DetectedItem ) @@ -1724,6 +1787,68 @@ do -- DETECTION_BASE end + --- Lock the detected items when created and lock all existing detected items. + -- @param #DETECTION_BASE self + -- @return #DETECTION_BASE + function DETECTION_BASE:LockDetectedItems() + + for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + self:LockDetectedItem( DetectedItem ) + end + self.Locking = true + + return self + end + + + --- Unlock the detected items when created and unlock all existing detected items. + -- @param #DETECTION_BASE self + -- @return #DETECTION_BASE + function DETECTION_BASE:UnlockDetectedItems() + + for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + self:UnlockDetectedItem( DetectedItem ) + end + self.Locking = nil + + return self + end + + --- Validate if the detected item is locked. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @return #boolean + function DETECTION_BASE:IsDetectedItemLocked( DetectedItem ) + + return self.Locking and DetectedItem.Locked == true + + end + + + --- Lock a detected item. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @return #DETECTION_BASE + function DETECTION_BASE:LockDetectedItem( DetectedItem ) + + DetectedItem.Locked = true + + return self + end + + --- Unlock a detected item. + -- @param #DETECTION_BASE self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @return #DETECTION_BASE + function DETECTION_BASE:UnlockDetectedItem( DetectedItem ) + + DetectedItem.Locked = nil + + return self + end + + + --- Set the detected item coordinate. -- @param #DETECTION_BASE self @@ -1759,6 +1884,20 @@ do -- DETECTION_BASE return nil end + --- Get a list of the detected item coordinates. + -- @param #DETECTION_BASE self + -- @return #table A table of Core.Point#COORDINATE + function DETECTION_BASE:GetDetectedItemCoordinates() + + local Coordinates = {} + + for DetectedItemID, DetectedItem in pairs( self:GetDetectedItems() ) do + Coordinates[DetectedItem] = self:GetDetectedItemCoordinate( DetectedItem ) + end + + return Coordinates + end + --- Set the detected item threatlevel. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem The DetectedItem to calculate the threatlevel for. @@ -1810,13 +1949,13 @@ do -- DETECTION_BASE return nil end - --- Get the detection Groups. + --- Get the Detection Set. -- @param #DETECTION_BASE self - -- @return Core.Set#SET_GROUP - function DETECTION_BASE:GetDetectionSetGroup() + -- @return #DETECTION_BASE self + function DETECTION_BASE:GetDetectionSet() - local DetectionSetGroup = self.DetectionSetGroup - return DetectionSetGroup + local DetectionSet = self.DetectionSet + return DetectionSet end --- Find the nearest Recce of the DetectedItem. @@ -1828,7 +1967,7 @@ do -- DETECTION_BASE local NearestRecce = nil local DistanceRecce = 1000000000 -- Units are not further than 1000000 km away from an area :-) - for RecceGroupName, RecceGroup in pairs( self.DetectionSetGroup:GetSet() ) do + for RecceGroupName, RecceGroup in pairs( self.DetectionSet:GetSet() ) do if RecceGroup and RecceGroup:IsAlive() then for RecceUnit, RecceUnit in pairs( RecceGroup:GetUnits() ) do if RecceUnit:IsActive() then @@ -1947,7 +2086,8 @@ do -- DETECTION_UNITS function DETECTION_UNITS:CreateDetectionItems() -- Loop the current detected items, and check if each object still exists and is detected. - for DetectedItemKey, DetectedItem in pairs( self.DetectedItems ) do + for DetectedItemKey, _DetectedItem in pairs( self.DetectedItems ) do + local DetectedItem=_DetectedItem --#DETECTION_BASE.DetectedItem local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT @@ -2033,7 +2173,7 @@ do -- DETECTION_UNITS local DetectedFirstUnitCoord = DetectedFirstUnit:GetCoordinate() self:SetDetectedItemCoordinate( DetectedItem, DetectedFirstUnitCoord, DetectedFirstUnit ) - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table + self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSet } ) -- Fill the Friendlies table self:SetDetectedItemThreatLevel( DetectedItem ) self:NearestRecce( DetectedItem ) @@ -2268,7 +2408,7 @@ do -- DETECTION_TYPES local DetectedUnitCoord = DetectedFirstUnit:GetCoordinate() self:SetDetectedItemCoordinate( DetectedItem, DetectedUnitCoord, DetectedFirstUnit ) - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table + self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSet } ) -- Fill the Friendlies table self:SetDetectedItemThreatLevel( DetectedItem ) self:NearestRecce( DetectedItem ) end @@ -2286,7 +2426,7 @@ do -- DETECTION_TYPES function DETECTION_TYPES:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) self:F( { DetectedItem = DetectedItem } ) - local DetectedSet = self:GetDetectedSet( DetectedItem ) + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) local DetectedItemID = self:GetDetectedItemID( DetectedItem ) self:T( DetectedItem ) @@ -2402,28 +2542,67 @@ do -- DETECTION_AREAS -- @param Wrapper.Group#GROUP AttackGroup The group to get the settings for. -- @param Core.Settings#SETTINGS Settings (Optional) Message formatting settings to use. -- @return Core.Report#REPORT The report of the detection items. - function DETECTION_AREAS:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) + function DETECTION_AREAS:DetectedItemReportMenu( DetectedItem, AttackGroup, Settings ) self:F( { DetectedItem = DetectedItem } ) local DetectedItemID = self:GetDetectedItemID( DetectedItem ) if DetectedItem then - local DetectedSet = self:GetDetectedSet( DetectedItem ) + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) local ReportSummaryItem local DetectedZone = self:GetDetectedItemZone( DetectedItem ) local DetectedItemCoordinate = DetectedZone:GetCoordinate() local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) + local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) + + local Report = REPORT:New() + Report:Add( DetectedItemID ) + Report:Add( string.format( "Threat: [%s%s]", string.rep( "â– ", ThreatLevelA2G ), string.rep( "â–¡", 10-ThreatLevelA2G ) ) ) + + return Report + end + + return nil + end + + --- Report summary of a detected item using a given numeric index. + -- @param #DETECTION_AREAS self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @param Wrapper.Group#GROUP AttackGroup The group to get the settings for. + -- @param Core.Settings#SETTINGS Settings (Optional) Message formatting settings to use. + -- @return Core.Report#REPORT The report of the detection items. + function DETECTION_AREAS:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) + self:F( { DetectedItem = DetectedItem } ) + + local DetectedItemID = self:GetDetectedItemID( DetectedItem ) + + if DetectedItem then + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) + local ReportSummaryItem + + --local DetectedZone = self:GetDetectedItemZone( DetectedItem ) + local DetectedItemCoordinate = self:GetDetectedItemCoordinate( DetectedItem ) + local DetectedAir = DetectedSet:HasAirUnits() + local DetectedAltitude = self:GetDetectedItemCoordinate( DetectedItem ) + local DetectedItemCoordText = "" + if DetectedAir > 0 then + DetectedItemCoordText = DetectedItemCoordinate:ToStringA2A( AttackGroup, Settings ) + else + DetectedItemCoordText = DetectedItemCoordinate:ToStringA2G( AttackGroup, Settings ) + end + + local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) local DetectedItemsCount = DetectedSet:Count() local DetectedItemsTypes = DetectedSet:GetTypeNames() local Report = REPORT:New() Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) - Report:Add( string.format( "Threat: [%s]", string.rep( "â– ", ThreatLevelA2G ), string.rep( "â–¡", 10-ThreatLevelA2G ) ) ) + Report:Add( string.format( "Threat: [%s%s]", string.rep( "â– ", ThreatLevelA2G ), string.rep( "â–¡", 10-ThreatLevelA2G ) ) ) Report:Add( string.format("Type: %2d of %s", DetectedItemsCount, DetectedItemsTypes ) ) - Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) + --Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) return Report end @@ -2741,7 +2920,7 @@ do -- DETECTION_AREAS if AddedToDetectionArea == false then -- New detection area - local DetectedItem = self:AddDetectedItemZone( nil, + local DetectedItem = self:AddDetectedItemZone( "AREA", nil, SET_UNIT:New():FilterDeads():FilterCrashes(), ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange ) ) @@ -2773,7 +2952,7 @@ do -- DETECTION_AREAS -- If there were friendlies nearby, and now there aren't any friendlies nearby, we flag the area as "changed". -- This is for the A2G dispatcher to detect if there is a change in the tactical situation. local OldFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) - self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table + self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSet } ) -- Fill the Friendlies table local NewFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) if OldFriendliesNearbyGround ~= NewFriendliesNearbyGround then DetectedItem.Changed = true @@ -2819,3 +2998,5 @@ do -- DETECTION_AREAS end end + + diff --git a/Moose Development/Moose/Functional/DetectionZones.lua b/Moose Development/Moose/Functional/DetectionZones.lua new file mode 100644 index 000000000..a64710a15 --- /dev/null +++ b/Moose Development/Moose/Functional/DetectionZones.lua @@ -0,0 +1,416 @@ +do -- DETECTION_ZONES + + --- @type DETECTION_ZONES + -- @field DCS#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. + -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Wrapper.Unit}s, @{Zone}s, the center @{Wrapper.Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. + -- @extends Functional.Detection#DETECTION_BASE + + --- (old, to be revised ) Detect units within the battle zone for a list of @{Core.Zone}s detecting targets following (a) detection method(s), + -- and will build a list (table) of @{Core.Set#SET_UNIT}s containing the @{Wrapper.Unit#UNIT}s detected. + -- The class is group the detected units within zones given a DetectedZoneRange parameter. + -- A set with multiple detected zones will be created as there are groups of units detected. + -- + -- ## 4.1) Retrieve the Detected Unit Sets and Detected Zones + -- + -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Functional.Detection#DECTECTION_BASE} and + -- the methods to manage the DetectedItems[].Zone(s) is implemented in @{Functional.Detection#DETECTION_ZONES}. + -- + -- Retrieve the DetectedItems[].Set with the method @{Functional.Detection#DETECTION_BASE.GetDetectedSet}(). A @{Core.Set#SET_UNIT} object will be returned. + -- + -- Retrieve the formed @{Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZones}(). + -- To understand the amount of zones created, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZoneCount}(). + -- If you want to obtain a specific zone from the DetectedZones, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZone}() with a given index. + -- + -- ## 4.4) Flare or Smoke detected units + -- + -- Use the methods @{Functional.Detection#DETECTION_ZONES.FlareDetectedUnits}() or @{Functional.Detection#DETECTION_ZONES.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. + -- + -- ## 4.5) Flare or Smoke or Bound detected zones + -- + -- Use the methods: + -- + -- * @{Functional.Detection#DETECTION_ZONES.FlareDetectedZones}() to flare in a color + -- * @{Functional.Detection#DETECTION_ZONES.SmokeDetectedZones}() to smoke in a color + -- * @{Functional.Detection#DETECTION_ZONES.SmokeDetectedZones}() to bound with a tire with a white flag + -- + -- the detected zones when a new detection has taken place. + -- + -- @field #DETECTION_ZONES + DETECTION_ZONES = { + ClassName = "DETECTION_ZONES", + DetectionZoneRange = nil, + } + + + --- DETECTION_ZONES constructor. + -- @param #DETECTION_ZONES self + -- @param Core.Set#SET_ZONE DetectionSetZone The @{Set} of ZONE_RADIUS. + -- @param DCS#Coalition.side DetectionCoalition The coalition of the detection. + -- @return #DETECTION_ZONES + function DETECTION_ZONES:New( DetectionSetZone, DetectionCoalition ) + + -- Inherits from DETECTION_BASE + local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetZone ) ) -- #DETECTION_ZONES + + self.DetectionSetZone = DetectionSetZone -- Core.Set#SET_ZONE + self.DetectionCoalition = DetectionCoalition + + self._SmokeDetectedUnits = false + self._FlareDetectedUnits = false + self._SmokeDetectedZones = false + self._FlareDetectedZones = false + self._BoundDetectedZones = false + + return self + end + + --- @param #DETECTION_ZONES self + -- @param #number The amount of alive recce. + function DETECTION_ZONES:CountAliveRecce() + + return self.DetectionSetZone:Count() + + end + + --- @param #DETECTION_ZONES self + function DETECTION_ZONES:ForEachAliveRecce( IteratorFunction, ... ) + self:F2( arg ) + + self.DetectionSetZone:ForEachZone( IteratorFunction, arg ) + + return self + end + + --- Report summary of a detected item using a given numeric index. + -- @param #DETECTION_ZONES self + -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. + -- @param Wrapper.Group#GROUP AttackGroup The group to get the settings for. + -- @param Core.Settings#SETTINGS Settings (Optional) Message formatting settings to use. + -- @return Core.Report#REPORT The report of the detection items. + function DETECTION_ZONES:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings ) + self:F( { DetectedItem = DetectedItem } ) + + local DetectedItemID = self:GetDetectedItemID( DetectedItem ) + + if DetectedItem then + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) + local ReportSummaryItem + + local DetectedZone = self:GetDetectedItemZone( DetectedItem ) + local DetectedItemCoordinate = DetectedZone:GetCoordinate() + local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings ) + + local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem ) + local DetectedItemsCount = DetectedSet:Count() + local DetectedItemsTypes = DetectedSet:GetTypeNames() + + local Report = REPORT:New() + Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) + Report:Add( string.format( "Threat: [%s]", string.rep( "â– ", ThreatLevelA2G ), string.rep( "â–¡", 10-ThreatLevelA2G ) ) ) + Report:Add( string.format("Type: %2d of %s", DetectedItemsCount, DetectedItemsTypes ) ) + Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) + + return Report + end + + return nil + end + + --- Report detailed of a detection result. + -- @param #DETECTION_ZONES self + -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for. + -- @return #string + function DETECTION_ZONES:DetectedReportDetailed( AttackGroup ) --R2.1 Fixed missing report + self:F() + + local Report = REPORT:New() + for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do + local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem + local ReportSummary = self:DetectedItemReportSummary( DetectedItem, AttackGroup ) + Report:SetTitle( "Detected areas:" ) + Report:Add( ReportSummary:Text() ) + end + + local ReportText = Report:Text() + + return ReportText + end + + + --- Calculate the optimal intercept point of the DetectedItem. + -- @param #DETECTION_ZONES self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + function DETECTION_ZONES:CalculateIntercept( DetectedItem ) + + local DetectedCoord = DetectedItem.Coordinate +-- local DetectedSpeed = DetectedCoord:GetVelocity() +-- local DetectedHeading = DetectedCoord:GetHeading() +-- +-- if self.Intercept then +-- local DetectedSet = DetectedItem.Set +-- -- todo: speed +-- +-- local TranslateDistance = DetectedSpeed * self.InterceptDelay +-- +-- local InterceptCoord = DetectedCoord:Translate( TranslateDistance, DetectedHeading ) +-- +-- DetectedItem.InterceptCoord = InterceptCoord +-- else +-- DetectedItem.InterceptCoord = DetectedCoord +-- end + DetectedItem.InterceptCoord = DetectedCoord + end + + + + --- Smoke the detected units + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:SmokeDetectedUnits() + self:F2() + + self._SmokeDetectedUnits = true + return self + end + + --- Flare the detected units + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:FlareDetectedUnits() + self:F2() + + self._FlareDetectedUnits = true + return self + end + + --- Smoke the detected zones + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:SmokeDetectedZones() + self:F2() + + self._SmokeDetectedZones = true + return self + end + + --- Flare the detected zones + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:FlareDetectedZones() + self:F2() + + self._FlareDetectedZones = true + return self + end + + --- Bound the detected zones + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:BoundDetectedZones() + self:F2() + + self._BoundDetectedZones = true + return self + end + + --- Make text documenting the changes of the detected zone. + -- @param #DETECTION_ZONES self + -- @param #DETECTION_BASE.DetectedItem DetectedItem + -- @return #string The Changes text + function DETECTION_ZONES:GetChangeText( DetectedItem ) + self:F( DetectedItem ) + + local MT = {} + + for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do + + if ChangeCode == "AA" then + MT[#MT+1] = "Detected new area " .. ChangeData.ID .. ". The center target is a " .. ChangeData.ItemUnitType .. "." + end + + if ChangeCode == "RAU" then + MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". Removed the center target." + end + + if ChangeCode == "AAU" then + MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". The new center target is a " .. ChangeData.ItemUnitType .. "." + end + + if ChangeCode == "RA" then + MT[#MT+1] = "Removed old area " .. ChangeData.ID .. ". No more targets in this area." + end + + if ChangeCode == "AU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "ID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = "Detected for area " .. ChangeData.ID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "." + end + + if ChangeCode == "RU" then + local MTUT = {} + for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do + if ChangeUnitType ~= "ID" then + MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType + end + end + MT[#MT+1] = "Removed for area " .. ChangeData.ID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "." + end + + end + + return table.concat( MT, "\n" ) + + end + + + --- Make a DetectionSet table. This function will be overridden in the derived clsses. + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES self + function DETECTION_ZONES:CreateDetectionItems() + + + self:F( "Checking Detected Items for new Detected Units ..." ) + + local DetectedUnits = SET_UNIT:New() + + -- First go through all zones, and check if there are new Zones. + -- New Zones become a new DetectedItem. + for ZoneName, DetectionZone in pairs( self.DetectionSetZone:GetSet() ) do + + local DetectedItem = self:GetDetectedItemByKey( ZoneName ) + + if DetectedItem == nil then + DetectedItem = self:AddDetectedItemZone( "ZONE", ZoneName, nil, DetectionZone ) + end + + local DetectedItemSetUnit = self:GetDetectedItemSet( DetectedItem ) + + -- Scan the zone + DetectionZone:Scan( { Object.Category.UNIT }, { Unit.Category.GROUND_UNIT } ) + + -- For all the units in the zone, + -- check if they are of the same coalition to be included. + local ZoneUnits = DetectionZone:GetScannedUnits() + for DCSUnitID, DCSUnit in pairs( ZoneUnits ) do + local UnitName = DCSUnit:getName() + local ZoneUnit = UNIT:FindByName( UnitName ) + local ZoneUnitCoalition = ZoneUnit:GetCoalition() + if ZoneUnitCoalition == self.DetectionCoalition then + if DetectedItemSetUnit:FindUnit( UnitName ) == nil and DetectedUnits:FindUnit( UnitName ) == nil then + self:F( "Adding " .. UnitName ) + DetectedItemSetUnit:AddUnit( ZoneUnit ) + DetectedUnits:AddUnit( ZoneUnit ) + end + end + end + end + + + -- Now all the tests should have been build, now make some smoke and flares... + -- We also report here the friendlies within the detected areas. + + for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do + + local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem + local DetectedSet = self:GetDetectedItemSet( DetectedItem ) + local DetectedFirstUnit = DetectedSet:GetFirst() + local DetectedZone = self:GetDetectedItemZone( DetectedItem ) + + -- Set the last known coordinate to the detection item. + local DetectedZoneCoord = DetectedZone:GetCoordinate() + self:SetDetectedItemCoordinate( DetectedItem, DetectedZoneCoord, DetectedFirstUnit ) + + self:CalculateIntercept( DetectedItem ) + + -- We search for friendlies nearby. + -- If there weren't any friendlies nearby, and now there are friendlies nearby, we flag the area as "changed". + -- If there were friendlies nearby, and now there aren't any friendlies nearby, we flag the area as "changed". + -- This is for the A2G dispatcher to detect if there is a change in the tactical situation. + local OldFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table + local NewFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) + if OldFriendliesNearbyGround ~= NewFriendliesNearbyGround then + DetectedItem.Changed = true + end + + self:SetDetectedItemThreatLevel( DetectedItem ) -- Calculate A2G threat level + --self:NearestRecce( DetectedItem ) + + + if DETECTION_ZONES._SmokeDetectedUnits or self._SmokeDetectedUnits then + DetectedZone:SmokeZone( SMOKECOLOR.Red, 30 ) + end + + --DetectedSet:Flush( self ) + + DetectedSet:ForEachUnit( + --- @param Wrapper.Unit#UNIT DetectedUnit + function( DetectedUnit ) + if DetectedUnit:IsAlive() then + --self:T( "Detected Set #" .. DetectedItem.ID .. ":" .. DetectedUnit:GetName() ) + if DETECTION_ZONES._FlareDetectedUnits or self._FlareDetectedUnits then + DetectedUnit:FlareGreen() + end + if DETECTION_ZONES._SmokeDetectedUnits or self._SmokeDetectedUnits then + DetectedUnit:SmokeGreen() + end + end + end + ) + if DETECTION_ZONES._FlareDetectedZones or self._FlareDetectedZones then + DetectedZone:FlareZone( SMOKECOLOR.White, 30, math.random( 0,90 ) ) + end + if DETECTION_ZONES._SmokeDetectedZones or self._SmokeDetectedZones then + DetectedZone:SmokeZone( SMOKECOLOR.White, 30 ) + end + + if DETECTION_ZONES._BoundDetectedZones or self._BoundDetectedZones then + self.CountryID = DetectedSet:GetFirst():GetCountry() + DetectedZone:BoundZone( 12, self.CountryID ) + end + end + + end + + --- @param #DETECTION_ZONES self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Detection The element on which the detection is based. + -- @param #number DetectionTimeStamp Time stamp of detection event. + function DETECTION_ZONES:onafterDetection( From, Event, To, Detection, DetectionTimeStamp ) + + self.DetectionRun = self.DetectionRun + 1 + if self.DetectionCount > 0 and self.DetectionRun == self.DetectionCount then + self:CreateDetectionItems() -- Polymorphic call to Create/Update the DetectionItems list for the DETECTION_ class grouping method. + + for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + self:UpdateDetectedItemDetection( DetectedItem ) + self:CleanDetectionItem( DetectedItem, DetectedItemID ) -- Any DetectionItem that has a Set with zero elements in it, must be removed from the DetectionItems list. + if DetectedItem then + self:__DetectedItem( 0.1, DetectedItem ) + end + end + self:__Detect( -self.RefreshTimeInterval ) + end + end + + + --- Set IsDetected flag for the DetectedItem, which can have more units. + -- @param #DETECTION_ZONES self + -- @return #DETECTION_ZONES.DetectedItem DetectedItem + -- @return #boolean true if at least one UNIT is detected from the DetectedSet, false if no UNIT was detected from the DetectedSet. + function DETECTION_ZONES:UpdateDetectedItemDetection( DetectedItem ) + + local IsDetected = true + + DetectedItem.IsDetected = true + + return IsDetected + end + +end \ No newline at end of file diff --git a/Moose Development/Moose/Functional/Escort.lua b/Moose Development/Moose/Functional/Escort.lua index 89d77425c..43686ba06 100644 --- a/Moose Development/Moose/Functional/Escort.lua +++ b/Moose Development/Moose/Functional/Escort.lua @@ -872,7 +872,7 @@ function ESCORT:_AttackTarget( DetectedItem ) EscortGroup:OptionROTPassiveDefense() EscortGroup:SetState( EscortGroup, "Escort", self ) - local DetectedSet = self.Detection:GetDetectedSet( DetectedItem ) + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) local Tasks = {} @@ -895,7 +895,7 @@ function ESCORT:_AttackTarget( DetectedItem ) else - local DetectedSet = self.Detection:GetDetectedSet( DetectedItem ) + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) local Tasks = {} @@ -934,7 +934,7 @@ function ESCORT:_AssistTarget( EscortGroupAttack, DetectedItem ) EscortGroupAttack:OptionROEOpenFire() EscortGroupAttack:OptionROTVertical() - local DetectedSet = self.Detection:GetDetectedSet( DetectedItem ) + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) local Tasks = {} @@ -956,7 +956,7 @@ function ESCORT:_AssistTarget( EscortGroupAttack, DetectedItem ) ) else - local DetectedSet = self.Detection:GetDetectedSet( DetectedItem ) + local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem ) local Tasks = {} diff --git a/Moose Development/Moose/Functional/Fox.lua b/Moose Development/Moose/Functional/Fox.lua new file mode 100644 index 000000000..5c5ec9f20 --- /dev/null +++ b/Moose Development/Moose/Functional/Fox.lua @@ -0,0 +1,1816 @@ +--- **Functional** - (R2.5) - Yet Another Missile Trainer. +-- +-- +-- Practice to evade missiles without being destroyed. +-- +-- +-- ## Main Features: +-- +-- * Handles air-to-air and surface-to-air missiles. +-- * Define your own training zones on the map. Players in this zone will be protected. +-- * Define launch zones. Only missiles launched in these zones are tracked. +-- * Define protected AI groups. +-- * F10 radio menu to adjust settings for each player. +-- * Alert on missile launch (optional). +-- * Marker of missile launch position (optional). +-- * Adaptive update of missile-to-player distance. +-- * Finite State Machine (FSM) implementation. +-- * Easy to use. See examples below. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Functional.FOX +-- @image Functional_FOX.png + + +--- FOX class. +-- @type FOX +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table menuadded Table of groups the menu was added for. +-- @field #boolean menudisabled If true, F10 menu for players is disabled. +-- @field #boolean destroy Default player setting for destroying missiles. +-- @field #boolean launchalert Default player setting for launch alerts. +-- @field #boolean marklaunch Default player setting for mark launch coordinates. +-- @field #table players Table of players. +-- @field #table missiles Table of tracked missiles. +-- @field #table safezones Table of practice zones. +-- @field #table launchzones Table of launch zones. +-- @field Core.Set#SET_GROUP protectedset Set of protected groups. +-- @field #number explosionpower Power of explostion when destroying the missile in kg TNT. Default 5 kg TNT. +-- @field #number explosiondist Missile player distance in meters for destroying smaller missiles. Default 200 m. +-- @field #number explosiondist2 Missile player distance in meters for destroying big missiles. Default 500 m. +-- @field #number bigmissilemass Explosion power of big missiles. Default 50 kg TNT. Big missiles will be destroyed earlier. +-- @field #number dt50 Time step [sec] for missile position updates if distance to target > 50 km. Default 5 sec. +-- @field #number dt10 Time step [sec] for missile position updates if distance to target > 10 km and < 50 km. Default 1 sec. +-- @field #number dt05 Time step [sec] for missile position updates if distance to target > 5 km and < 10 km. Default 0.5 sec. +-- @field #number dt01 Time step [sec] for missile position updates if distance to target > 1 km and < 5 km. Default 0.1 sec. +-- @field #number dt00 Time step [sec] for missile position updates if distance to target < 1 km. Default 0.01 sec. +-- @field #boolean +-- @extends Core.Fsm#FSM + +--- Fox 3! +-- +-- === +-- +-- ![Banner Image](..\Presentations\FOX\FOX_Main.png) +-- +-- # The FOX Concept +-- +-- As you probably know [Fox](https://en.wikipedia.org/wiki/Fox_(code_word)) is a NATO brevity code for launching air-to-air munition. Therefore, the class name is not 100% accurate as this +-- script handles air-to-air but also surface-to-air missiles. +-- +-- # Basic Script +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Training Zones +-- +-- Players are only protected if they are inside one of the training zones. +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Add training zones. +-- fox:AddSafeZone(ZONE:New("Training Zone Alpha")) +-- fox:AddSafeZone(ZONE:New("Training Zone Bravo")) +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Launch Zones +-- +-- Missile launches are only monitored if the shooter is inside the defined launch zone. +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Add training zones. +-- fox:AddLaunchZone(ZONE:New("Launch Zone SA-10 Krim")) +-- fox:AddLaunchZone(ZONE:New("Training Zone Bravo")) +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Protected AI Groups +-- +-- Define AI protected groups. These groups cannot be harmed by missiles. +-- +-- ## Add Individual Groups +-- +-- -- Create a new missile trainer object. +-- fox=FOX:New() +-- +-- -- Add single protected group(s). +-- fox:AddProtectedGroup(GROUP:FindByName("A-10 Protected")) +-- fox:AddProtectedGroup(GROUP:FindByName("Yak-40")) +-- +-- -- Start missile trainer. +-- fox:Start() +-- +-- # Fine Tuning +-- +-- Todo! +-- +-- # Special Events +-- +-- Todo! +-- +-- +-- @field #FOX +FOX = { + ClassName = "FOX", + Debug = false, + lid = nil, + menuadded = {}, + menudisabled = nil, + destroy = nil, + launchalert = nil, + marklaunch = nil, + missiles = {}, + players = {}, + safezones = {}, + launchzones = {}, + protectedset = nil, + explosionpower = 0.1, + explosiondist = 200, + explosiondist2 = 500, + bigmissilemass = 50, + destroy = nil, + dt50 = 5, + dt10 = 1, + dt05 = 0.5, + dt01 = 0.1, + dt00 = 0.01, +} + + +--- Player data table holding all important parameters of each player. +-- @type FOX.PlayerData +-- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field #string unitname Name of the unit. +-- @field Wrapper.Client#CLIENT client Client object of player. +-- @field #string callsign Callsign of player. +-- @field Wrapper.Group#GROUP group Aircraft group of player. +-- @field #string groupname Name of the the player aircraft group. +-- @field #string name Player name. +-- @field #number coalition Coalition number of player. +-- @field #boolean destroy Destroy missile. +-- @field #boolean launchalert Alert player on detected missile launch. +-- @field #boolean marklaunch Mark position of launched missile on F10 map. +-- @field #number defeated Number of missiles defeated. +-- @field #number dead Number of missiles not defeated. +-- @field #boolean inzone Player is inside a protected zone. + +--- Missile data table. +-- @type FOX.MissileData +-- @field Wrapper.Unit#UNIT weapon Missile weapon unit. +-- @field #boolean active If true the missile is active. +-- @field #string missileType Type of missile. +-- @field #string missileName Name of missile. +-- @field #number missileRange Range of missile in meters. +-- @field #number fuseDist Fuse distance in meters. +-- @field #number explosive Explosive mass in kg TNT. +-- @field Wrapper.Unit#UNIT shooterUnit Unit that shot the missile. +-- @field Wrapper.Group#GROUP shooterGroup Group that shot the missile. +-- @field #number shooterCoalition Coalition side of the shooter. +-- @field #string shooterName Name of the shooter unit. +-- @field #number shotTime Abs. mission time in seconds the missile was fired. +-- @field Core.Point#COORDINATE shotCoord Coordinate where the missile was fired. +-- @field Wrapper.Unit#UNIT targetUnit Unit that was targeted. +-- @field #string targetName Name of the target unit or "unknown". +-- @field #string targetOrig Name of the "original" target, i.e. the one right after launched. +-- @field #FOX.PlayerData targetPlayer Player that was targeted or nil. + +--- Main radio menu on group level. +-- @field #table MenuF10 Root menu table on group level. +FOX.MenuF10={} + +--- Main radio menu on mission level. +-- @field #table MenuF10Root Root menu on mission level. +FOX.MenuF10Root=nil + +--- FOX class version. +-- @field #string version +FOX.version="0.6.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ToDo list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO list: +-- DONE: safe zones +-- DONE: mark shooter on F10 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new FOX class object. +-- @param #FOX self +-- @return #FOX self. +function FOX:New() + + self.lid="FOX | " + + -- Inherit everthing from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #FOX + + -- Defaults: + self:SetDefaultMissileDestruction(true) + self:SetDefaultLaunchAlerts(true) + self:SetDefaultLaunchMarks(true) + + -- Explosion/destruction defaults. + self:SetExplosionDistance() + self:SetExplosionDistanceBigMissiles() + self:SetExplosionPower() + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FOX script. + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("*", "MissileLaunch", "*") -- Missile was launched. + self:AddTransition("*", "MissileDestroyed", "*") -- Missile was destroyed before impact. + self:AddTransition("*", "EnterSafeZone", "*") -- Player enters a safe zone. + self:AddTransition("*", "ExitSafeZone", "*") -- Player exists a safe zone. + self:AddTransition("Running", "Stop", "Stopped") -- Stop FOX script. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the FOX. Initializes parameters and starts event handlers. + -- @function [parent=#FOX] Start + -- @param #FOX self + + --- Triggers the FSM event "Start" after a delay. Starts the FOX. Initializes parameters and starts event handlers. + -- @function [parent=#FOX] __Start + -- @param #FOX self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the FOX and all its event handlers. + -- @param #FOX self + + --- Triggers the FSM event "Stop" after a delay. Stops the FOX and all its event handlers. + -- @function [parent=#FOX] __Stop + -- @param #FOX self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#FOX] Status + -- @param #FOX self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#FOX] __Status + -- @param #FOX self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "MissileLaunch". + -- @function [parent=#FOX] MissileLaunch + -- @param #FOX self + -- @param #FOX.MissileData missile Data of the fired missile. + + --- Triggers the FSM delayed event "MissileLaunch". + -- @function [parent=#FOX] __MissileLaunch + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.MissileData missile Data of the fired missile. + + --- On after "MissileLaunch" event user function. Called when a missile was launched. + -- @function [parent=#FOX] OnAfterMissileLaunch + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.MissileData missile Data of the fired missile. + + --- Triggers the FSM event "MissileDestroyed". + -- @function [parent=#FOX] MissileDestroyed + -- @param #FOX self + -- @param #FOX.MissileData missile Data of the destroyed missile. + + --- Triggers the FSM delayed event "MissileDestroyed". + -- @function [parent=#FOX] __MissileDestroyed + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.MissileData missile Data of the destroyed missile. + + --- On after "MissileDestroyed" event user function. Called when a missile was destroyed. + -- @function [parent=#FOX] OnAfterMissileDestroyed + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.MissileData missile Data of the destroyed missile. + + + --- Triggers the FSM event "EnterSafeZone". + -- @function [parent=#FOX] EnterSafeZone + -- @param #FOX self + -- @param #FOX.PlayerData player Player data. + + --- Triggers the FSM delayed event "EnterSafeZone". + -- @function [parent=#FOX] __EnterSafeZone + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.PlayerData player Player data. + + --- On after "EnterSafeZone" event user function. Called when a player enters a safe zone. + -- @function [parent=#FOX] OnAfterEnterSafeZone + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.PlayerData player Player data. + + + --- Triggers the FSM event "ExitSafeZone". + -- @function [parent=#FOX] ExitSafeZone + -- @param #FOX self + -- @param #FOX.PlayerData player Player data. + + --- Triggers the FSM delayed event "ExitSafeZone". + -- @function [parent=#FOX] __ExitSafeZone + -- @param #FOX self + -- @param #number delay Delay in seconds before the function is called. + -- @param #FOX.PlayerData player Player data. + + --- On after "ExitSafeZone" event user function. Called when a player exists a safe zone. + -- @function [parent=#FOX] OnAfterExitSafeZone + -- @param #FOX self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #FOX.PlayerData player Player data. + + + return self +end + +--- On after Start event. Starts the missile trainer and adds event handlers. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FOX:onafterStart(From, Event, To) + + -- Short info. + local text=string.format("Starting FOX Missile Trainer %s", FOX.version) + env.info(text) + + -- Handle events: + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Shot) + + if self.Debug then + self:HandleEvent(EVENTS.Hit) + end + + if self.Debug then + self:TraceClass(self.ClassName) + self:TraceLevel(2) + end + + self:__Status(-20) +end + +--- On after Stop event. Stops the missile trainer and unhandles events. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FOX:onafterStop(From, Event, To) + + -- Short info. + local text=string.format("Stopping FOX Missile Trainer %s", FOX.version) + env.info(text) + + -- Handle events: + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Shot) + + if self.Debug then + self:UnhandleEvent(EVENTS.Hit) + end + +end + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add a training zone. Players in the zone are safe. +-- @param #FOX self +-- @param Core.Zone#ZONE zone Training zone. +-- @return #FOX self +function FOX:AddSafeZone(zone) + + table.insert(self.safezones, zone) + + return self +end + +--- Add a launch zone. Only missiles launched within these zones will be tracked. +-- @param #FOX self +-- @param Core.Zone#ZONE zone Training zone. +-- @return #FOX self +function FOX:AddLaunchZone(zone) + + table.insert(self.launchzones, zone) + + return self +end + +--- Add a protected set of groups. +-- @param #FOX self +-- @param Core.Set#SET_GROUP groupset The set of groups. +-- @return #FOX self +function FOX:SetProtectedGroupSet(groupset) + self.protectedset=groupset + return self +end + +--- Add a group to the protected set. +-- @param #FOX self +-- @param Wrapper.Group#GROUP group Protected group. +-- @return #FOX self +function FOX:AddProtectedGroup(group) + + if not self.protectedset then + self.protectedset=SET_GROUP:New() + end + + self.protectedset:AddGroup(group) + + return self +end + +--- Set explosion power. This is an "artificial" explosion generated when the missile is destroyed. Just for the visual effect. +-- Don't set the explosion power too big or it will harm the aircraft in the vicinity. +-- @param #FOX self +-- @param #number power Explosion power in kg TNT. Default 0.1 kg. +-- @return #FOX self +function FOX:SetExplosionPower(power) + + self.explosionpower=power or 0.1 + + return self +end + +--- Set missile-player distance when missile is destroyed. +-- @param #FOX self +-- @param #number distance Distance in meters. Default 200 m. +-- @return #FOX self +function FOX:SetExplosionDistance(distance) + + self.explosiondist=distance or 200 + + return self +end + +--- Set missile-player distance when BIG missiles are destroyed. +-- @param #FOX self +-- @param #number distance Distance in meters. Default 500 m. +-- @param #number explosivemass Explosive mass of missile threshold in kg TNT. Default 50 kg. +-- @return #FOX self +function FOX:SetExplosionDistanceBigMissiles(distance, explosivemass) + + self.explosiondist2=distance or 500 + + self.bigmissilemass=explosivemass or 50 + + return self +end + +--- Disable F10 menu for all players. +-- @param #FOX self +-- @param #boolean switch If true debug mode on. If false/nil debug mode off +-- @return #FOX self +function FOX:SetDisableF10Menu() + + self.menudisabled=true + + return self +end + +--- Set default player setting for missile destruction. +-- @param #FOX self +-- @param #boolean switch If true missiles are destroyed. If false/nil missiles are not destroyed. +-- @return #FOX self +function FOX:SetDefaultMissileDestruction(switch) + + if switch==nil then + self.destroy=false + else + self.destroy=switch + end + + return self +end + +--- Set default player setting for launch alerts. +-- @param #FOX self +-- @param #boolean switch If true launch alerts to players are active. If false/nil no launch alerts are given. +-- @return #FOX self +function FOX:SetDefaultLaunchAlerts(switch) + + if switch==nil then + self.launchalert=false + else + self.launchalert=switch + end + + return self +end + +--- Set default player setting for marking missile launch coordinates +-- @param #FOX self +-- @param #boolean switch If true missile launches are marked. If false/nil marks are disabled. +-- @return #FOX self +function FOX:SetDefaultLaunchMarks(switch) + + if switch==nil then + self.marklaunch=false + else + self.marklaunch=switch + end + + return self +end + + +--- Set debug mode on/off. +-- @param #FOX self +-- @param #boolean switch If true debug mode on. If false/nil debug mode off. +-- @return #FOX self +function FOX:SetDebugOnOff(switch) + + if switch==nil then + self.Debug=false + else + self.Debug=switch + end + + return self +end + +--- Set debug mode on. +-- @param #FOX self +-- @return #FOX self +function FOX:SetDebugOn() + self:SetDebugOnOff(true) + return self +end + +--- Set debug mode off. +-- @param #FOX self +-- @return #FOX self +function FOX:SetDebugOff() + self:SetDebugOff(false) + return self +end + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check spawn queue and spawn aircraft if necessary. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function FOX:onafterStatus(From, Event, To) + + -- Get FSM state. + local fsmstate=self:GetState() + + local time=timer.getAbsTime() + local clock=UTILS.SecondsToClock(time) + + -- Status. + self:I(self.lid..string.format("Missile trainer status %s: %s", clock, fsmstate)) + + -- Check missile status. + self:_CheckMissileStatus() + + -- Check player status. + self:_CheckPlayers() + + if fsmstate=="Running" then + self:__Status(-10) + end +end + +--- Check status of players. +-- @param #FOX self +function FOX:_CheckPlayers() + + for playername,_playersettings in pairs(self.players) do + local playersettings=_playersettings --#FOX.PlayerData + + local unitname=playersettings.unitname + local unit=UNIT:FindByName(unitname) + + if unit and unit:IsAlive() then + + local coord=unit:GetCoordinate() + + local issafe=self:_CheckCoordSafe(coord) + + + if issafe then + + ----------------------------- + -- Player INSIDE Safe Zone -- + ----------------------------- + + if not playersettings.inzone then + self:EnterSafeZone(playersettings) + playersettings.inzone=true + end + + else + + ------------------------------ + -- Player OUTSIDE Safe Zone -- + ------------------------------ + + if playersettings.inzone==true then + self:ExitSafeZone(playersettings) + playersettings.inzone=false + end + + end + end + end + +end + +--- Remove missile. +-- @param #FOX self +-- @param #FOX.MissileData missile Missile data. +function FOX:_RemoveMissile(missile) + + if missile then + for i,_missile in pairs(self.missiles) do + local m=_missile --#FOX.MissileData + if missile.missileName==m.missileName then + table.remove(self.missiles, i) + return + end + end + end + +end + +--- Missile status. +-- @param #FOX self +function FOX:_CheckMissileStatus() + + local text="Missiles:" + local inactive={} + for i,_missile in pairs(self.missiles) do + local missile=_missile --#FOX.MissileData + + local targetname="unkown" + if missile.targetUnit then + targetname=missile.targetUnit:GetName() + end + local playername="none" + if missile.targetPlayer then + playername=missile.targetPlayer.name + end + local active=tostring(missile.active) + local mtype=missile.missileType + local dtype=missile.missileType + local range=UTILS.MetersToNM(missile.missileRange) + + if not active then + table.insert(inactive,i) + end + local heading=self:_GetWeapongHeading(missile.weapon) + + text=text..string.format("\n[%d] %s: active=%s, range=%.1f NM, heading=%03d, target=%s, player=%s, missilename=%s", i, mtype, active, range, heading, targetname, playername, missile.missileName) + + end + if #self.missiles==0 then + text=text.." none" + end + self:I(self.lid..text) + + -- Remove inactive missiles. + for i=#self.missiles,1,-1 do + local missile=self.missiles[i] --#FOX.MissileData + if missile and not missile.active then + table.remove(self.missiles, i) + end + end + +end + +--- Check if missile target is protected. +-- @param #FOX self +-- @param Wrapper.Unit#UNIT targetunit Target unit. +-- @return #boolean If true, unit is protected. +function FOX:_IsProtected(targetunit) + + if not self.protectedset then + return false + end + + if targetunit and targetunit:IsAlive() then + + -- Get Group. + local targetgroup=targetunit:GetGroup() + + if targetgroup then + local targetname=targetgroup:GetName() + + for _,_group in pairs(self.protectedset:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + + if group then + local groupname=group:GetName() + + -- Target belongs to a protected set. + if targetname==groupname then + return true + end + end + + end + end + end + + return false +end + +--- Missle launch event. +-- @param #FOX self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #FOX.MissileData missile Fired missile +function FOX:onafterMissileLaunch(From, Event, To, missile) + + -- Tracking info and init of last bomb position. + local text=string.format("FOX: Tracking missile %s(%s) - target %s - shooter %s", missile.missileType, missile.missileName, tostring(missile.targetName), missile.shooterName) + self:I(FOX.lid..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + + -- Loop over players. + for _,_player in pairs(self.players) do + local player=_player --#FOX.PlayerData + + -- Player position. + local playerUnit=player.unit + + -- Check that player is alive and of the opposite coalition. + if playerUnit and playerUnit:IsAlive() and player.coalition~=missile.shooterCoalition then + + -- Player missile distance. + local distance=playerUnit:GetCoordinate():Get3DDistance(missile.shotCoord) + + -- Player bearing to missile. + local bearing=playerUnit:GetCoordinate():HeadingTo(missile.shotCoord) + + -- Alert that missile has been launched. + if player.launchalert then + + -- Alert directly targeted players or players that are within missile max range. + if (missile.targetPlayer and player.unitname==missile.targetPlayer.unitname) or (distance=self.bigmissilemass + end + + -- If missile is 150 m from target ==> destroy missile if in safe zone. + if destroymissile and self:_CheckCoordSafe(targetCoord) then + + -- Destroy missile. + self:I(self.lid..string.format("Destroying missile %s(%s) fired by %s aimed at %s [player=%s] at distance %.1f m", + missile.missileType, missile.missileName, missile.shooterName, target:GetName(), tostring(missile.targetPlayer~=nil), distance)) + _ordnance:destroy() + + -- Missile is not active any more. + missile.active=false + + -- Debug smoke. + if self.Debug then + missileCoord:SmokeRed() + targetCoord:SmokeGreen() + end + + -- Create event. + self:MissileDestroyed(missile) + + -- Little explosion for the visual effect. + if self.explosionpower>0 and distance>50 and (distShooter==nil or (distShooter and distShooter>50)) then + missileCoord:Explosion(self.explosionpower) + end + + -- Target was a player. + if missile.targetPlayer then + + -- Message to target. + local text=string.format("Destroying missile. %s", self:_DeadText()) + MESSAGE:New(text, 10):ToGroup(target:GetGroup()) + + -- Increase dead counter. + missile.targetPlayer.dead=missile.targetPlayer.dead+1 + end + + -- Terminate timer. + return nil + + else + + -- Time step. + local dt=1.0 + if distance>50000 then + -- > 50 km + dt=self.dt50 --=5.0 + elseif distance>10000 then + -- 10-50 km + dt=self.dt10 --=1.0 + elseif distance>5000 then + -- 5-10 km + dt=self.dt05 --0.5 + elseif distance>1000 then + -- 1-5 km + dt=self.dt01 --0.1 + else + -- < 1 km + dt=self.dt00 --0.01 + end + + -- Check again in dt seconds. + return timer.getTime()+dt + end + + else + + -- Destroy missile. + self:T(self.lid..string.format("Missile %s(%s) fired by %s has no current target. Checking back in 0.1 sec.", missile.missileType, missile.missileName, missile.shooterName)) + return timer.getTime()+0.1 + + -- No target ==> terminate timer. + --return nil + end + + else + + ------------------------------------- + -- Missile does not exist any more -- + ------------------------------------- + + if target then + + -- Get human player. + local player=self:_GetPlayerFromUnit(target) + + -- Check for player and distance < 10 km. + if player and player.unit:IsAlive() then -- and missileCoord and player.unit:GetCoordinate():Get3DDistance(missileCoord)<10*1000 then + local text=string.format("Missile defeated. Well done, %s!", player.name) + MESSAGE:New(text, 10):ToClient(player.client) + + -- Increase defeated counter. + player.defeated=player.defeated+1 + end + + end + + -- Missile is not active any more. + missile.active=false + + --Terminate the timer. + self:T(FOX.lid..string.format("Terminating missile track timer.")) + return nil + + end -- _status check + + end -- end function trackBomb + + -- Weapon is not yet "alife" just yet. Start timer with a little delay. + self:T(FOX.lid..string.format("Tracking of missile starts in 0.0001 seconds.")) + timer.scheduleFunction(trackMissile, missile.weapon, timer.getTime()+0.0001) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- FOX event handler for event birth. +-- @param #FOX self +-- @param Core.Event#EVENTDATA EventData +function FOX:OnEventBirth(EventData) + self:F3({eventbirth = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event BIRTH!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!") + self:E(EventData) + return + end + + -- Player unit and name. + local _unitName=EventData.IniUnitName + local playerunit, playername=self:_GetPlayerUnitAndName(_unitName) + + -- Debug info. + self:T(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T(self.lid.."BIRTH: player = "..tostring(playername)) + + -- Check if player entered. + if playerunit and playername then + + local _uid=playerunit:GetID() + local _group=playerunit:GetGroup() + local _callsign=playerunit:GetCallsign() + + -- Debug output. + local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.", playername, _callsign, _unitName, _group:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + -- Add F10 radio menu for player. + if not self.menudisabled then + SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1) + end + + -- Player data. + local playerData={} --#FOX.PlayerData + + -- Player unit, client and callsign. + playerData.unit = playerunit + playerData.unitname = _unitName + playerData.group = _group + playerData.groupname = _group:GetName() + playerData.name = playername + playerData.callsign = playerData.unit:GetCallsign() + playerData.client = CLIENT:FindByName(_unitName, nil, true) + playerData.coalition = _group:GetCoalition() + + playerData.destroy=playerData.destroy or self.destroy + playerData.launchalert=playerData.launchalert or self.launchalert + playerData.marklaunch=playerData.marklaunch or self.marklaunch + + playerData.defeated=playerData.defeated or 0 + playerData.dead=playerData.dead or 0 + + -- Init player data. + self.players[playername]=playerData + + end +end + +--- Get missile target. +-- @param #FOX self +-- @param #FOX.MissileData missile The missile data table. +function FOX:GetMissileTarget(missile) + + local target=nil + local targetName="unknown" + local targetUnit=nil --Wrapper.Unit#UNIT + + if missile.weapon and missile.weapon:isExist() then + + -- Get target of missile. + target=missile.weapon:getTarget() + + -- Get the target unit. Note if if _target is not nil, the unit can sometimes not be found! + if target then + self:T2({missiletarget=target}) + + -- Get target unit. + targetUnit=UNIT:Find(target) + + if targetUnit then + targetName=targetUnit:GetName() + + missile.targetUnit=targetUnit + missile.targetPlayer=self:_GetPlayerFromUnit(missile.targetUnit) + end + + end + end + + -- Missile got new target. + if missile.targetName and missile.targetName~=targetName then + self:I(self.lid..string.format("Missile %s(%s) changed target to %s. Previous target was %s.", missile.missileType, missile.missileName, targetName, missile.targetName)) + end + + -- Set target name. + missile.targetName=targetName + +end + +--- FOX event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). +-- @param #FOX self +-- @param Core.Event#EVENTDATA EventData +function FOX:OnEventShot(EventData) + self:T2({eventshot=EventData}) + + if EventData.Weapon==nil then + return + end + if EventData.IniDCSUnit==nil then + return + end + + -- Weapon data. + local _weapon = EventData.WeaponName + local _target = EventData.Weapon:getTarget() + local _targetName = "unknown" + local _targetUnit = nil --Wrapper.Unit#UNIT + + -- Weapon descriptor. + local desc=EventData.Weapon:getDesc() + self:T2({desc=desc}) + + -- Weapon category: 0=Shell, 1=Missile, 2=Rocket, 3=BOMB + local weaponcategory=desc.category + + -- Missile category: 1=AAM, 2=SAM, 6=OTHER + local missilecategory=desc.missileCategory + + local missilerange=nil + if missilecategory then + missilerange=desc.rangeMaxAltMax + end + + -- Debug info. + self:T2(FOX.lid.."EVENT SHOT: FOX") + self:T2(FOX.lid..string.format("EVENT SHOT: Ini unit = %s", tostring(EventData.IniUnitName))) + self:T2(FOX.lid..string.format("EVENT SHOT: Ini group = %s", tostring(EventData.IniGroupName))) + self:T2(FOX.lid..string.format("EVENT SHOT: Weapon type = %s", tostring(_weapon))) + self:T2(FOX.lid..string.format("EVENT SHOT: Weapon categ = %s", tostring(weaponcategory))) + self:T2(FOX.lid..string.format("EVENT SHOT: Missil categ = %s", tostring(missilecategory))) + self:T2(FOX.lid..string.format("EVENT SHOT: Missil range = %s", tostring(missilerange))) + + + -- Check if fired in launch zone. + if not self:_CheckCoordLaunch(EventData.IniUnit:GetCoordinate()) then + self:T(self.lid.."Missile was not fired in launch zone. No tracking!") + return + end + + -- Track missiles of type AAM=1, SAM=2 or OTHER=6 + local _track = weaponcategory==1 and missilecategory and (missilecategory==1 or missilecategory==2 or missilecategory==6) + + -- Only track missiles + if _track then + + local missile={} --#FOX.MissileData + + missile.active=true + missile.weapon=EventData.weapon + missile.missileType=_weapon + missile.missileRange=missilerange + missile.missileName=EventData.weapon:getName() + missile.shooterUnit=EventData.IniUnit + missile.shooterGroup=EventData.IniGroup + missile.shooterCoalition=EventData.IniUnit:GetCoalition() + missile.shooterName=EventData.IniUnitName + missile.shotTime=timer.getAbsTime() + missile.shotCoord=EventData.IniUnit:GetCoordinate() + missile.fuseDist=desc.fuseDist + missile.explosive=desc.warhead.explosiveMass or desc.warhead.shapedExplosiveMass + missile.targetOrig=missile.targetName + + -- Set missile target name, unit and player. + self:GetMissileTarget(missile) + + self:I(FOX.lid..string.format("EVENT SHOT: Shooter=%s %s(%s) ==> Target=%s, fuse dist=%s, explosive=%s", + tostring(missile.shooterName), tostring(missile.missileType), tostring(missile.missileName), tostring(missile.targetName), tostring(missile.fuseDist), tostring(missile.explosive))) + + -- Only track if target was a player or target is protected. Saw the 9M311 missiles have no target! + if missile.targetPlayer or self:_IsProtected(missile.targetUnit) or missile.targetName=="unknown" then + + -- Add missile table. + table.insert(self.missiles, missile) + + -- Trigger MissileLaunch event. + self:__MissileLaunch(0.1, missile) + + end + + end --if _track + +end + +--- FOX event handler for event hit. +-- @param #FOX self +-- @param Core.Event#EVENTDATA EventData +function FOX:OnEventHit(EventData) + self:T({eventhit = EventData}) + + -- Nil checks. + if EventData.Weapon==nil then + return + end + if EventData.IniUnit==nil then + return + end + if EventData.TgtUnit==nil then + return + end + + local weapon=EventData.Weapon + local weaponname=weapon:getName() + + for i,_missile in pairs(self.missiles) do + local missile=_missile --#FOX.MissileData + if missile.missileName==weaponname then + self:I(self.lid..string.format("WARNING: Missile %s (%s) hit target %s. Missile trainer target was %s.", missile.missileType, missile.missileName, EventData.TgtUnitName, missile.targetName)) + self:I({missile=missile}) + return + end + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MENU Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add menu commands for player. +-- @param #FOX self +-- @param #string _unitName Name of player unit. +function FOX:_AddF10Commands(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check for player unit. + if _unit and playername then + + -- Get group and ID. + local group=_unit:GetGroup() + local gid=group:GetID() + + if group and gid then + + if not self.menuadded[gid] then + + -- Enable switch so we don't do this twice. + self.menuadded[gid]=true + + -- Set menu root path. + local _rootPath=nil + if FOX.MenuF10Root then + ------------------------ + -- MISSON LEVEL MENUE -- + ------------------------ + + -- F10/FOX/... + _rootPath=FOX.MenuF10Root + + else + ------------------------ + -- GROUP LEVEL MENUES -- + ------------------------ + + -- Main F10 menu: F10/FOX/ + if FOX.MenuF10[gid]==nil then + FOX.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "FOX") + end + + -- F10/FOX/... + _rootPath=FOX.MenuF10[gid] + + end + + + -------------------------------- + -- F10/F FOX/F1 Help + -------------------------------- + --local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) + -- F10/FOX/F1 Help/ + --missionCommands.addCommandForGroup(gid, "Subtitles On/Off", _helpPath, self._SubtitlesOnOff, self, _unitName) -- F7 + --missionCommands.addCommandForGroup(gid, "Trapsheet On/Off", _helpPath, self._TrapsheetOnOff, self, _unitName) -- F8 + + ------------------------- + -- F10/F FOX/ + ------------------------- + + missionCommands.addCommandForGroup(gid, "Destroy Missiles On/Off", _rootPath, self._ToggleDestroyMissiles, self, _unitName) -- F1 + missionCommands.addCommandForGroup(gid, "Launch Alerts On/Off", _rootPath, self._ToggleLaunchAlert, self, _unitName) -- F2 + missionCommands.addCommandForGroup(gid, "Mark Launch On/Off", _rootPath, self._ToggleLaunchMark, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "My Status", _rootPath, self._MyStatus, self, _unitName) -- F4 + + end + else + self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName)) + end + else + self:E(self.lid..string.format("ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.", _unitName)) + end + +end + + +--- Turn player's launch alert on/off. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_MyStatus(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#FOX.PlayerData + + if playerData then + + local m,mtext=self:_GetTargetMissiles(playerData.name) + + local text=string.format("Status of player %s:\n", playerData.name) + local safe=self:_CheckCoordSafe(playerData.unit:GetCoordinate()) + + text=text..string.format("Destroy missiles? %s\n", tostring(playerData.destroy)) + text=text..string.format("Launch alert? %s\n", tostring(playerData.launchalert)) + text=text..string.format("Launch marks? %s\n", tostring(playerData.marklaunch)) + text=text..string.format("Am I safe? %s\n", tostring(safe)) + text=text..string.format("Missiles defeated: %d\n", playerData.defeated) + text=text..string.format("Missiles destroyed: %d\n", playerData.dead) + text=text..string.format("Me target: %d\n%s", m, mtext) + + MESSAGE:New(text, 10, nil, true):ToClient(playerData.client) + + end + end +end + +--- Turn player's launch alert on/off. +-- @param #FOX self +-- @param #string playername Name of the player. +-- @return #number Number of missiles targeting the player. +-- @return #string Missile info. +function FOX:_GetTargetMissiles(playername) + + local text="" + local n=0 + for _,_missile in pairs(self.missiles) do + local missile=_missile --#FOX.MissileData + + if missile.targetPlayer and missile.targetPlayer.name==playername then + n=n+1 + text=text..string.format("Type %s: active %s\n", missile.missileType, tostring(missile.active)) + end + + end + + return n,text +end + +--- Turn player's launch alert on/off. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_ToggleLaunchAlert(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#FOX.PlayerData + + if playerData then + + -- Invert state. + playerData.launchalert=not playerData.launchalert + + -- Inform player. + local text="" + if playerData.launchalert==true then + text=string.format("%s, missile launch alerts are now ENABLED.", playerData.name) + else + text=string.format("%s, missile launch alerts are now DISABLED.", playerData.name) + end + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + end +end + +--- Turn player's launch marks on/off. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_ToggleLaunchMark(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#FOX.PlayerData + + if playerData then + + -- Invert state. + playerData.marklaunch=not playerData.marklaunch + + -- Inform player. + local text="" + if playerData.marklaunch==true then + text=string.format("%s, missile launch marks are now ENABLED.", playerData.name) + else + text=string.format("%s, missile launch marks are now DISABLED.", playerData.name) + end + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + end +end + + +--- Turn destruction of missiles on/off for player. +-- @param #FOX self +-- @param #string _unitname Name of the player unit. +function FOX:_ToggleDestroyMissiles(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#FOX.PlayerData + + if playerData then + + -- Invert state. + playerData.destroy=not playerData.destroy + + -- Inform player. + local text="" + if playerData.destroy==true then + text=string.format("%s, incoming missiles will be DESTROYED.", playerData.name) + else + text=string.format("%s, incoming missiles will NOT be DESTROYED.", playerData.name) + end + MESSAGE:New(text, 5):ToClient(playerData.client) + + end + end +end + + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get a random text message in case you die. +-- @param #FOX self +-- @return #string Text in case you die. +function FOX:_DeadText() + + local texts={} + texts[1]="You're dead!" + texts[2]="Meet your maker!" + texts[3]="Time to meet your maker!" + texts[4]="Well, I guess that was it!" + texts[5]="Bye, bye!" + texts[6]="Cheers buddy, was nice knowing you!" + + local r=math.random(#texts) + + return texts[r] +end + + +--- Check if a coordinate lies within a safe training zone. +-- @param #FOX self +-- @param Core.Point#COORDINATE coord Coordinate to check. +-- @return #boolean True if safe. +function FOX:_CheckCoordSafe(coord) + + -- No safe zones defined ==> Everything is safe. + if #self.safezones==0 then + return true + end + + -- Loop over all zones. + for _,_zone in pairs(self.safezones) do + local zone=_zone --Core.Zone#ZONE + local inzone=zone:IsCoordinateInZone(coord) + if inzone then + return true + end + end + + return false +end + +--- Check if a coordinate lies within a launch zone. +-- @param #FOX self +-- @param Core.Point#COORDINATE coord Coordinate to check. +-- @return #boolean True if in launch zone. +function FOX:_CheckCoordLaunch(coord) + + -- No safe zones defined ==> Everything is safe. + if #self.launchzones==0 then + return true + end + + -- Loop over all zones. + for _,_zone in pairs(self.launchzones) do + local zone=_zone --Core.Zone#ZONE + local inzone=zone:IsCoordinateInZone(coord) + if inzone then + return true + end + end + + return false +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FOX self +-- @param DCS#Weapon weapon The weapon. +-- @return #number Heading of weapon in degrees or -1. +function FOX:_GetWeapongHeading(weapon) + + if weapon and weapon:isExist() then + + local wp=weapon:getPosition() + + local wph = math.atan2(wp.x.z, wp.x.x) + + if wph < 0 then + wph=wph+2*math.pi + end + + wph=math.deg(wph) + + return wph + end + + return -1 +end + +--- Tell player notching headings. +-- @param #FOX self +-- @param #FOX.PlayerData playerData Player data. +-- @param DCS#Weapon weapon The weapon. +function FOX:_SayNotchingHeadings(playerData, weapon) + + if playerData and playerData.unit and playerData.unit:IsAlive() then + + local nr, nl=self:_GetNotchingHeadings(weapon) + + if nr and nl then + local text=string.format("Notching heading %03d° or %03d°", nr, nl) + MESSAGE:New(text, 5, "FOX"):ToClient(playerData.client) + end + + end + +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FOX self +-- @param DCS#Weapon weapon The weapon. +-- @return #number Notching heading right, i.e. missile heading +90� +-- @return #number Notching heading left, i.e. missile heading -90�. +function FOX:_GetNotchingHeadings(weapon) + + if weapon then + + local hdg=self:_GetWeapongHeading(weapon) + + local hdg1=hdg+90 + if hdg1>360 then + hdg1=hdg1-360 + end + + local hdg2=hdg-90 + if hdg2<0 then + hdg2=hdg2+360 + end + + return hdg1, hdg2 + end + + return nil, nil +end + +--- Returns the player data from a unit name. +-- @param #FOX self +-- @param #string unitName Name of the unit. +-- @return #FOX.PlayerData Player data. +function FOX:_GetPlayerFromUnitname(unitName) + + for _,_player in pairs(self.players) do + local player=_player --#FOX.PlayerData + + if player.unitname==unitName then + return player + end + end + + return nil +end + +--- Retruns the player data from a unit. +-- @param #FOX self +-- @param Wrapper.Unit#UNIT unit +-- @return #FOX.PlayerData Player data. +function FOX:_GetPlayerFromUnit(unit) + + if unit and unit:IsAlive() then + + -- Name of the unit + local unitname=unit:GetName() + + for _,_player in pairs(self.players) do + local player=_player --#FOX.PlayerData + + if player.unitname==unitname then + return player + end + end + + end + + return nil +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #FOX self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @return #string Name of the player or nil. +function FOX:_GetPlayerUnitAndName(_unitName) + self:F2(_unitName) + + if _unitName ~= nil then + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + + if DCSunit then + + -- Get player name if any. + local playername=DCSunit:getPlayerName() + + -- Unit object. + local unit=UNIT:Find(DCSunit) + + -- Debug. + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + + -- Check if enverything is there. + if DCSunit and unit and playername then + self:T(self.lid..string.format("Found DCS unit %s with player %s.", tostring(_unitName), tostring(playername))) + return unit, playername + end + + end + + end + + -- Return nil if we could not find a player. + return nil,nil +end + + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/PseudoATC.lua b/Moose Development/Moose/Functional/PseudoATC.lua index bfb458078..1143b00e8 100644 --- a/Moose Development/Moose/Functional/PseudoATC.lua +++ b/Moose Development/Moose/Functional/PseudoATC.lua @@ -81,7 +81,7 @@ -- @field #PSEUDOATC PSEUDOATC={ ClassName = "PSEUDOATC", - player={}, + group={}, Debug=false, mdur=30, mrefresh=120, @@ -98,7 +98,7 @@ PSEUDOATC.id="PseudoATC | " --- PSEUDOATC version. -- @field #number version -PSEUDOATC.version="0.9.1" +PSEUDOATC.version="0.9.2" ----------------------------------------------------------------------------------------------------------------------------------------- @@ -383,16 +383,23 @@ function PSEUDOATC:PlayerEntered(unit) local PlayerName=unit:GetPlayerName() local UnitName=unit:GetName() local CallSign=unit:GetCallsign() + local UID=unit:GetDCSObject():getID() + + if not self.group[GID] then + self.group[GID]={} + self.group[GID].player={} + end + -- Init player table. - self.player[GID]={} - self.player[GID].group=group - self.player[GID].unit=unit - self.player[GID].groupname=GroupName - self.player[GID].unitname=UnitName - self.player[GID].playername=PlayerName - self.player[GID].callsign=CallSign - self.player[GID].waypoints=group:GetTaskRoute() + self.group[GID].player[UID]={} + self.group[GID].player[UID].group=group + self.group[GID].player[UID].unit=unit + self.group[GID].player[UID].groupname=GroupName + self.group[GID].player[UID].unitname=UnitName + self.group[GID].player[UID].playername=PlayerName + self.group[GID].player[UID].callsign=CallSign + self.group[GID].player[UID].waypoints=group:GetTaskRoute() -- Info message. local text=string.format("Player %s entered unit %s of group %s (id=%d).", PlayerName, UnitName, GroupName, GID) @@ -400,19 +407,26 @@ function PSEUDOATC:PlayerEntered(unit) MESSAGE:New(text, 30):ToAllIf(self.Debug) -- Create main F10 menu, i.e. "F10/Pseudo ATC" - self.player[GID].menu_main=missionCommands.addSubMenuForGroup(GID, "Pseudo ATC") + local countPlayerInGroup = 0 + for _ in pairs(self.group[GID].player) do countPlayerInGroup = countPlayerInGroup + 1 end + if countPlayerInGroup <= 1 then + self.group[GID].menu_main=missionCommands.addSubMenuForGroup(GID, "Pseudo ATC") + end + + -- Create/update custom menu for player + self:MenuCreatePlayer(GID,UID) -- Create/update list of nearby airports. - self:LocalAirports(GID) + self:LocalAirports(GID,UID) -- Create submenu of local airports. - self:MenuAirports(GID) + self:MenuAirports(GID,UID) -- Create submenu Waypoints. - self:MenuWaypoints(GID) + self:MenuWaypoints(GID,UID) -- Start scheduler to refresh the F10 menues. - self.player[GID].scheduler, self.player[GID].schedulerid=SCHEDULER:New(nil, self.MenuRefresh, {self, GID}, self.mrefresh, self.mrefresh) + self.group[GID].player[UID].scheduler, self.group[GID].player[UID].schedulerid=SCHEDULER:New(nil, self.MenuRefresh, {self, GID, UID}, self.mrefresh, self.mrefresh) end @@ -425,24 +439,23 @@ function PSEUDOATC:PlayerLanded(unit, place) -- Gather some information. local group=unit:GetGroup() - local id=group:GetID() - local PlayerName=self.player[id].playername - local Callsign=self.player[id].callsign - local UnitName=self.player[id].unitname - local GroupName=self.player[id].groupname - local CallSign=self.player[id].callsign + local GID=group:GetID() + local UID=unit:GetDCSObject():getID() + local PlayerName=self.group[GID].player[UID].playername + local UnitName=self.group[GID].player[UID].unitname + local GroupName=self.group[GID].player[UID].groupname -- Debug message. - local text=string.format("Player %s in unit %s of group %s (id=%d) landed at %s.", PlayerName, UnitName, GroupName, id, place) + local text=string.format("Player %s in unit %s of group %s (id=%d) landed at %s.", PlayerName, UnitName, GroupName, GID, place) self:T(PSEUDOATC.id..text) MESSAGE:New(text, 30):ToAllIf(self.Debug) -- Stop altitude reporting timer if its activated. - self:AltitudeTimerStop(id) + self:AltitudeTimerStop(GID,UID) -- Welcome message. if place and self.chatty then - local text=string.format("Touchdown! Welcome to %s. Have a nice day!", place) + local text=string.format("Touchdown! Welcome to %s pilot %s. Have a nice day!", place,PlayerName) MESSAGE:New(text, self.mdur):ToGroup(group) end @@ -457,15 +470,15 @@ function PSEUDOATC:PlayerTakeOff(unit, place) -- Gather some information. local group=unit:GetGroup() - local id=group:GetID() - local PlayerName=self.player[id].playername - local Callsign=self.player[id].callsign - local UnitName=self.player[id].unitname - local GroupName=self.player[id].groupname - local CallSign=self.player[id].callsign + local GID=group:GetID() + local UID=unit:GetDCSObject():getID() + local PlayerName=self.group[GID].player[UID].playername + local CallSign=self.group[GID].player[UID].callsign + local UnitName=self.group[GID].player[UID].unitname + local GroupName=self.group[GID].player[UID].groupname -- Debug message. - local text=string.format("Player %s in unit %s of group %s (id=%d) took off at %s.", PlayerName, UnitName, GroupName, id, place) + local text=string.format("Player %s in unit %s of group %s (id=%d) took off at %s.", PlayerName, UnitName, GroupName, GID, place) self:T(PSEUDOATC.id..text) MESSAGE:New(text, 30):ToAllIf(self.Debug) @@ -485,30 +498,44 @@ function PSEUDOATC:PlayerLeft(unit) -- Get id. local group=unit:GetGroup() - local id=group:GetID() + local GID=group:GetID() + local UID=unit:GetDCSObject():getID() - if self.player[id] then + if self.group[GID].player[UID] then + local PlayerName=self.group[GID].player[UID].playername + local CallSign=self.group[GID].player[UID].callsign + local UnitName=self.group[GID].player[UID].unitname + local GroupName=self.group[GID].player[UID].groupname -- Debug message. - local text=string.format("Player %s (callsign %s) of group %s just left unit %s.", self.player[id].playername, self.player[id].callsign, self.player[id].groupname, self.player[id].unitname) + local text=string.format("Player %s (callsign %s) of group %s just left unit %s.", PlayerName, CallSign, GroupName, UnitName) self:T(PSEUDOATC.id..text) MESSAGE:New(text, 30):ToAllIf(self.Debug) -- Stop scheduler for menu updates - if self.player[id].schedulerid then - self.player[id].scheduler:Stop(self.player[id].schedulerid) + if self.group[GID].player[UID].schedulerid then + self.group[GID].player[UID].scheduler:Stop(self.group[GID].player[UID].schedulerid) end -- Stop scheduler for reporting alt if it runs. - self:AltitudeTimerStop(id) + self:AltitudeTimerStop(GID,UID) + -- Remove own menu. + if self.group[GID].player[UID].menu_own then + missionCommands.removeItemForGroup(GID,self.group[GID].player[UID].menu_own) + end -- Remove main menu. - if self.player[id].menu_main then - missionCommands.removeItem(self.player[id].menu_main) + -- WARNING: Remove only if last human element of group + + local countPlayerInGroup = 0 + for _ in pairs(self.group[GID].player) do countPlayerInGroup = countPlayerInGroup + 1 end + + if self.group[GID].menu_main and countPlayerInGroup==1 then + missionCommands.removeItemForGroup(GID,self.group[GID].menu_main) end -- Remove player array. - self.player[id]=nil + self.group[GID].player[UID]=nil end end @@ -518,80 +545,94 @@ end --- Refreshes all player menues. -- @param #PSEUDOATC self. --- @param #number id Group id of player unit. -function PSEUDOATC:MenuRefresh(id) - self:F({id=id}) - +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:MenuRefresh(GID,UID) + self:F({GID=GID,UID=UID}) -- Debug message. - local text=string.format("Refreshing menues for player %s in group %s.", self.player[id].playername, self.player[id].groupname) + local text=string.format("Refreshing menues for player %s in group %s.", self.group[GID].player[UID].playername, self.group[GID].player[UID].groupname) self:T(PSEUDOATC.id..text) MESSAGE:New(text,30):ToAllIf(self.Debug) -- Clear menu. - self:MenuClear(id) + self:MenuClear(GID,UID) -- Create list of nearby airports. - self:LocalAirports(id) + self:LocalAirports(GID,UID) -- Create submenu Local Airports. - self:MenuAirports(id) + self:MenuAirports(GID,UID) -- Create submenu Waypoints etc. - self:MenuWaypoints(id) + self:MenuWaypoints(GID,UID) end +--- Create player menus. +-- @param #PSEUDOATC self. +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:MenuCreatePlayer(GID,UID) + self:F({GID=GID,UID=UID}) + -- Table for menu entries. + local PlayerName=self.group[GID].player[UID].playername + self.group[GID].player[UID].menu_own=missionCommands.addSubMenuForGroup(GID, PlayerName, self.group[GID].menu_main) +end + + --- Clear player menus. -- @param #PSEUDOATC self. --- @param #number id Group id of player unit. -function PSEUDOATC:MenuClear(id) - self:F(id) +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:MenuClear(GID,UID) + self:F({GID=GID,UID=UID}) -- Debug message. - local text=string.format("Clearing menus for player %s in group %s.", self.player[id].playername, self.player[id].groupname) + local text=string.format("Clearing menus for player %s in group %s.", self.group[GID].player[UID].playername, self.group[GID].player[UID].groupname) self:T(PSEUDOATC.id..text) MESSAGE:New(text,30):ToAllIf(self.Debug) -- Delete Airports menu. - if self.player[id].menu_airports then - missionCommands.removeItemForGroup(id, self.player[id].menu_airports) - self.player[id].menu_airports=nil + if self.group[GID].player[UID].menu_airports then + missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_airports) + self.group[GID].player[UID].menu_airports=nil else self:T2(PSEUDOATC.id.."No airports to clear menus.") end -- Delete waypoints menu. - if self.player[id].menu_waypoints then - missionCommands.removeItemForGroup(id, self.player[id].menu_waypoints) - self.player[id].menu_waypoints=nil + if self.group[GID].player[UID].menu_waypoints then + missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_waypoints) + self.group[GID].player[UID].menu_waypoints=nil end -- Delete report alt until touchdown menu command. - if self.player[id].menu_reportalt then - missionCommands.removeItemForGroup(id, self.player[id].menu_reportalt) - self.player[id].menu_reportalt=nil + if self.group[GID].player[UID].menu_reportalt then + missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_reportalt) + self.group[GID].player[UID].menu_reportalt=nil end -- Delete request current alt menu command. - if self.player[id].menu_requestalt then - missionCommands.removeItemForGroup(id, self.player[id].menu_requestalt) - self.player[id].menu_requestalt=nil + if self.group[GID].player[UID].menu_requestalt then + missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_requestalt) + self.group[GID].player[UID].menu_requestalt=nil end end --- Create "F10/Pseudo ATC/Local Airports/Airport Name/" menu items each containing weather report and BR request. -- @param #PSEUDOATC self --- @param #number id Group id of player unit for which menues are created. -function PSEUDOATC:MenuAirports(id) - self:F(id) +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:MenuAirports(GID,UID) + self:F({GID=GID,UID=UID}) -- Table for menu entries. - self.player[id].menu_airports=missionCommands.addSubMenuForGroup(id, "Local Airports", self.player[id].menu_main) + self.group[GID].player[UID].menu_airports=missionCommands.addSubMenuForGroup(GID, "Local Airports", self.group[GID].player[UID].menu_own) local i=0 - for _,airport in pairs(self.player[id].airports) do + for _,airport in pairs(self.group[GID].player[UID].airports) do i=i+1 if i > 10 then @@ -603,37 +644,38 @@ function PSEUDOATC:MenuAirports(id) local pos=AIRBASE:FindByName(name):GetCoordinate() --F10menu_ATC_airports[ID][name] = missionCommands.addSubMenuForGroup(ID, name, F10menu_ATC) - local submenu=missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_airports) + local submenu=missionCommands.addSubMenuForGroup(GID, name, self.group[GID].player[UID].menu_airports) -- Create menu reporting commands - missionCommands.addCommandForGroup(id, "Weather Report", submenu, self.ReportWeather, self, id, pos, name) - missionCommands.addCommandForGroup(id, "Request BR", submenu, self.ReportBR, self, id, pos, name) + missionCommands.addCommandForGroup(GID, "Weather Report", submenu, self.ReportWeather, self, GID, UID, pos, name) + missionCommands.addCommandForGroup(GID, "Request BR", submenu, self.ReportBR, self, GID, UID, pos, name) -- Debug message. - self:T(string.format(PSEUDOATC.id.."Creating airport menu item %s for ID %d", name, id)) + self:T(string.format(PSEUDOATC.id.."Creating airport menu item %s for ID %d", name, GID)) end end --- Create "F10/Pseudo ATC/Waypoints/ menu items. -- @param #PSEUDOATC self --- @param #number id Group id of player unit for which menues are created. -function PSEUDOATC:MenuWaypoints(id) - self:F(id) +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:MenuWaypoints(GID, UID) + self:F({GID=GID, UID=UID}) -- Player unit and callsign. - local unit=self.player[id].unit --Wrapper.Unit#UNIT - local callsign=self.player[id].callsign +-- local unit=self.group[GID].player[UID].unit --Wrapper.Unit#UNIT + local callsign=self.group[GID].player[UID].callsign -- Debug info. - self:T(PSEUDOATC.id..string.format("Creating waypoint menu for %s (ID %d).", callsign, id)) + self:T(PSEUDOATC.id..string.format("Creating waypoint menu for %s (ID %d).", callsign, GID)) - if #self.player[id].waypoints>0 then + if #self.group[GID].player[UID].waypoints>0 then -- F10/PseudoATC/Waypoints - self.player[id].menu_waypoints=missionCommands.addSubMenuForGroup(id, "Waypoints", self.player[id].menu_main) + self.group[GID].player[UID].menu_waypoints=missionCommands.addSubMenuForGroup(GID, "Waypoints", self.group[GID].player[UID].menu_own) local j=0 - for i, wp in pairs(self.player[id].waypoints) do + for i, wp in pairs(self.group[GID].player[UID].waypoints) do -- Increase counter j=j+1 @@ -647,16 +689,16 @@ function PSEUDOATC:MenuWaypoints(id) local name=string.format("Waypoint %d", i-1) -- "F10/PseudoATC/Waypoints/Waypoint X" - local submenu=missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_waypoints) + local submenu=missionCommands.addSubMenuForGroup(GID, name, self.group[GID].player[UID].menu_waypoints) -- Menu commands for each waypoint "F10/PseudoATC/My Aircraft (callsign)/Waypoints/Waypoint X/" - missionCommands.addCommandForGroup(id, "Weather Report", submenu, self.ReportWeather, self, id, pos, name) - missionCommands.addCommandForGroup(id, "Request BR", submenu, self.ReportBR, self, id, pos, name) + missionCommands.addCommandForGroup(GID, "Weather Report", submenu, self.ReportWeather, self, GID, UID, pos, name) + missionCommands.addCommandForGroup(GID, "Request BR", submenu, self.ReportBR, self, GID, UID, pos, name) end end - self.player[id].menu_reportalt = missionCommands.addCommandForGroup(id, "Talk me down", self.player[id].menu_main, self.AltidudeTimerToggle, self, id) - self.player[id].menu_requestalt = missionCommands.addCommandForGroup(id, "Request altitude", self.player[id].menu_main, self.ReportHeight, self, id) + self.group[GID].player[UID].menu_reportalt = missionCommands.addCommandForGroup(GID, "Talk me down", self.group[GID].player[UID].menu_own, self.AltidudeTimerToggle, self, GID, UID) + self.group[GID].player[UID].menu_requestalt = missionCommands.addCommandForGroup(GID, "Request altitude", self.group[GID].player[UID].menu_own, self.ReportHeight, self, GID, UID) end ----------------------------------------------------------------------------------------------------------------------------------------- @@ -664,14 +706,15 @@ end --- Weather Report. Report pressure QFE/QNH, temperature, wind at certain location. -- @param #PSEUDOATC self --- @param #number id Group id to which the report is delivered. +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. -- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured. -- @param #string location Name of the location at which the pressure is measured. -function PSEUDOATC:ReportWeather(id, position, location) - self:F({id=id, position=position, location=location}) +function PSEUDOATC:ReportWeather(GID, UID, position, location) + self:F({GID=GID, UID=UID, position=position, location=location}) -- Player unit system settings. - local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername) or _SETTINGS --Core.Settings#SETTINGS local text=string.format("Local weather at %s:\n", location) @@ -723,23 +766,24 @@ function PSEUDOATC:ReportWeather(id, position, location) end -- Message text. - local text=text..string.format("Wind from %s at %s (%s).", Ds, Vs, Bd) + local text=text..string.format("%s, Wind from %s at %s (%s).", self.group[GID].player[UID].playername, Ds, Vs, Bd) -- Send message - self:_DisplayMessageToGroup(self.player[id].unit, text, self.mdur, true) + self:_DisplayMessageToGroup(self.group[GID].player[UID].unit, text, self.mdur, true) end --- Report absolute bearing and range form player unit to airport. -- @param #PSEUDOATC self --- @param #number id Group id to the report is delivered. +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. -- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured. -- @param #string location Name of the location at which the pressure is measured. -function PSEUDOATC:ReportBR(id, position, location) - self:F({id=id, position=position, location=location}) +function PSEUDOATC:ReportBR(GID, UID, position, location) + self:F({GID=GID, UID=UID, position=position, location=location}) -- Current coordinates. - local unit=self.player[id].unit --Wrapper.Unit#UNIT + local unit=self.group[GID].player[UID].unit --Wrapper.Unit#UNIT local coord=unit:GetCoordinate() -- Direction vector from current position (coord) to target (position). @@ -752,7 +796,7 @@ function PSEUDOATC:ReportBR(id, position, location) local Bs=string.format('%03d°', angle) -- Settings. - local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername) or _SETTINGS --Core.Settings#SETTINGS local Rs=string.format("%.1f NM", UTILS.MetersToNM(range)) @@ -763,18 +807,20 @@ function PSEUDOATC:ReportBR(id, position, location) -- Message text. local text=string.format("%s: Bearing %s, Range %s.", location, Bs, Rs) - -- Send message to player group. - MESSAGE:New(text, self.mdur):ToGroup(self.player[id].group) + -- Send message + self:_DisplayMessageToGroup(self.group[GID].player[UID].unit, text, self.mdur, true) + end --- Report altitude above ground level of player unit. -- @param #PSEUDOATC self --- @param #number id Group id to the report is delivered. +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. -- @param #number dt (Optional) Duration the message is displayed. -- @param #boolean _clear (Optional) Clear previouse messages. -- @return #number Altitude above ground. -function PSEUDOATC:ReportHeight(id, dt, _clear) - self:F({id=id, dt=dt}) +function PSEUDOATC:ReportHeight(GID, UID, dt, _clear) + self:F({GID=GID, UID=UID, dt=dt}) local dt = dt or self.mdur if _clear==nil then @@ -791,7 +837,7 @@ function PSEUDOATC:ReportHeight(id, dt, _clear) end -- Get height AGL. - local unit=self.player[id].unit --Wrapper.Unit#UNIT + local unit=self.group[GID].player[UID].unit --Wrapper.Unit#UNIT if unit and unit:IsAlive() then @@ -800,7 +846,7 @@ function PSEUDOATC:ReportHeight(id, dt, _clear) local callsign=unit:GetCallsign() -- Settings. - local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername) or _SETTINGS --Core.Settings#SETTINGS -- Height string. local Hs=string.format("%d ft", UTILS.MetersToFeet(height)) @@ -817,7 +863,7 @@ function PSEUDOATC:ReportHeight(id, dt, _clear) end -- Send message to player group. - self:_DisplayMessageToGroup(self.player[id].unit,_text, dt,_clear) + self:_DisplayMessageToGroup(self.group[GID].player[UID].unit,_text, dt,_clear) -- Return height return height @@ -830,47 +876,50 @@ end --- Toggle report altitude reporting on/off. -- @param #PSEUDOATC self. --- @param #number id Group id of player unit. -function PSEUDOATC:AltidudeTimerToggle(id) - self:F(id) +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:AltidudeTimerToggle(GID,UID) + self:F({GID=GID, UID=UID}) - if self.player[id].altimerid then + if self.group[GID].player[UID].altimerid then -- If the timer is on, we turn it off. - self:AltitudeTimerStop(id) + self:AltitudeTimerStop(GID, UID) else -- If the timer is off, we turn it on. - self:AltitudeTimeStart(id) + self:AltitudeTimeStart(GID, UID) end end --- Start altitude reporting scheduler. -- @param #PSEUDOATC self. --- @param #number id Group id of player unit. -function PSEUDOATC:AltitudeTimeStart(id) - self:F(id) +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:AltitudeTimeStart(GID, UID) + self:F({GID=GID, UID=UID}) -- Debug info. - self:T(PSEUDOATC.id..string.format("Starting altitude report timer for player ID %d.", id)) + self:T(PSEUDOATC.id..string.format("Starting altitude report timer for player ID %d.", UID)) -- Start timer. Altitude is reported every ~3 seconds. - self.player[id].altimer, self.player[id].altimerid=SCHEDULER:New(nil, self.ReportHeight, {self, id, 0.1, true}, 1, 3) + self.group[GID].player[UID].altimer, self.group[GID].player[UID].altimerid=SCHEDULER:New(nil, self.ReportHeight, {self, GID, UID, 0.1, true}, 1, 3) end --- Stop/destroy DCS scheduler function for reporting altitude. -- @param #PSEUDOATC self. --- @param #number id Group id of player unit. -function PSEUDOATC:AltitudeTimerStop(id) - +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:AltitudeTimerStop(GID, UID) + self:F({GID=GID,UID=UID}) -- Debug info. - self:T(PSEUDOATC.id..string.format("Stopping altitude report timer for player ID %d.", id)) + self:T(PSEUDOATC.id..string.format("Stopping altitude report timer for player ID %d.", UID)) -- Stop timer. - if self.player[id].altimerid then - self.player[id].altimer:Stop(self.player[id].altimerid) + if self.group[GID].player[UID].altimerid then + self.group[GID].player[UID].altimer:Stop(self.group[GID].player[UID].altimerid) end - self.player[id].altimer=nil - self.player[id].altimerid=nil + self.group[GID].player[UID].altimer=nil + self.group[GID].player[UID].altimerid=nil end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -878,16 +927,17 @@ end --- Create list of nearby airports sorted by distance to player unit. -- @param #PSEUDOATC self --- @param #number id Group id of player unit. -function PSEUDOATC:LocalAirports(id) - self:F(id) +-- @param #number GID Group id of player unit. +-- @param #number UID Unit id of player. +function PSEUDOATC:LocalAirports(GID, UID) + self:F({GID=GID, UID=UID}) -- Airports table. - self.player[id].airports=nil - self.player[id].airports={} + self.group[GID].player[UID].airports=nil + self.group[GID].player[UID].airports={} -- Current player position. - local pos=self.player[id].unit:GetCoordinate() + local pos=self.group[GID].player[UID].unit:GetCoordinate() -- Loop over coalitions. for i=0,2 do @@ -903,7 +953,7 @@ function PSEUDOATC:LocalAirports(id) local d=q:Get2DDistance(pos) -- Add to table. - table.insert(self.player[id].airports, {distance=d, name=name}) + table.insert(self.group[GID].player[UID].airports, {distance=d, name=name}) end end @@ -914,7 +964,7 @@ function PSEUDOATC:LocalAirports(id) end -- Sort airports table w.r.t. distance to player. - table.sort(self.player[id].airports, compare) + table.sort(self.group[GID].player[UID].airports, compare) end @@ -992,3 +1042,5 @@ function PSEUDOATC:_myname(unitname) return string.format("%s (%s)", csign, pname) end + + diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 5b1616c81..327e2e518 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -150,6 +150,8 @@ -- @field #number parkingscanradius Radius in meters until which parking spots are scanned for obstacles like other units, statics or scenery. -- @field #boolean parkingscanscenery If true, area around parking spots is scanned for scenery objects. Default is false. -- @field #boolean parkingverysafe If true, parking spots are considered as non-free until a possible aircraft has left and taken off. Default false. +-- @field #boolean despawnair If true, aircraft are despawned when they reach their destination zone. Default. +-- @field #boolean eplrs If true, turn on EPLSR datalink for the RAT group. -- @extends Core.Spawn#SPAWN --- Implements an easy to use way to randomly fill your map with AI aircraft. @@ -240,7 +242,7 @@ -- * AIRBASE.TerminalType.OpenMed: Open/Shelter air airplane only. -- * AIRBASE.TerminalType.OpenBig: Open air spawn points. Generally larger but does not guarantee large aircraft are capable of spawning there. -- * AIRBASE.TerminalType.OpenMedOrBig: Combines OpenMed and OpenBig spots. --- * AIRBASE.TerminalType.HelicopterUnsable: Combines HelicopterOnly, OpenMed and OpenBig. +-- * AIRBASE.TerminalType.HelicopterUsable: Combines HelicopterOnly, OpenMed and OpenBig. -- * AIRBASE.TerminalType.FighterAircraft: Combines Shelter, OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. -- -- So for example @@ -428,6 +430,8 @@ RAT={ parkingscanradius=40, -- Scan radius. parkingscanscenery=false, -- Scan parking spots for scenery obstacles. parkingverysafe=false, -- Very safe option. + despawnair=true, + eplrs=false, } ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -546,7 +550,7 @@ RAT.id="RAT | " --- RAT version. -- @list version RAT.version={ - version = "2.3.4", + version = "2.3.9", print = true, } @@ -717,6 +721,11 @@ function RAT:Spawn(naircraft) self.FLcruise=005*RAT.unit.FL2m end end + + -- Enable helos to go to destinations 100 meters away. + if self.category==RAT.cat.heli then + self.mindist=50 + end -- Run consistency checks. self:_CheckConsistency() @@ -1093,6 +1102,14 @@ function RAT:SetParkingSpotSafeOFF() return self end +--- Aircraft that reach their destination zone are not despawned. They will probably go the the nearest airbase and try to land. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetDespawnAirOFF() + self.despawnair=false + return self +end + --- Set takeoff type. Starting cold at airport, starting hot at airport, starting at runway, starting in the air. -- Default is "takeoff-coldorhot". So there is a 50% chance that the aircraft starts with cold engines and 50% that it starts with hot engines. -- @param #RAT self @@ -1627,6 +1644,19 @@ function RAT:Invisible() return self end +--- Turn EPLRS datalink on/off. +-- @param #RAT self +-- @param #boolean switch If true (or nil), turn EPLRS on. +-- @return #RAT RAT self object. +function RAT:SetEPLRS(switch) + if switch==nil or switch==true then + self.eplrs=true + else + self.eplrs=false + end + return self +end + --- Aircraft are immortal. -- @param #RAT self -- @return #RAT RAT self object. @@ -1812,14 +1842,14 @@ function RAT:ATC_Delay(time) end --- Set minimum distance between departure and destination. Default is 5 km. --- Minimum distance should not be smaller than maybe ~500 meters to ensure that departure and destination are different. +-- Minimum distance should not be smaller than maybe ~100 meters to ensure that departure and destination are different. -- @param #RAT self -- @param #number dist Distance in km. -- @return #RAT RAT self object. function RAT:SetMinDistance(dist) self:F2(dist) -- Distance in meters. Absolute minimum is 500 m. - self.mindist=math.max(500, dist*1000) + self.mindist=math.max(100, dist*1000) return self end @@ -2149,6 +2179,11 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live self:_CommandImmortal(group, true) end + -- Set group to be immortal. + if self.eplrs then + group:CommandEPLRS(true, 1) + end + -- Set ROE, default is "weapon hold". self:_SetROE(group, self.roe) @@ -2446,7 +2481,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) local VxCruiseMax if self.Vcruisemax then -- User input. - VxCruiseMax = min(self.Vcruisemax, self.aircraft.Vmax) + VxCruiseMax = math.min(self.Vcruisemax, self.aircraft.Vmax) else -- Max cruise speed 90% of Vmax or 900 km/h whichever is lower. VxCruiseMax = math.min(self.aircraft.Vmax*0.90, 250) @@ -3357,11 +3392,19 @@ function RAT:_GetAirportsOfMap() local _name=airbase:getName() local _myab=AIRBASE:FindByName(_name) - -- Add airport to table. - table.insert(self.airports_map, _myab) + if _myab then - local text="MOOSE: Airport ID = ".._myab:GetID().." and Name = ".._myab:GetName()..", Category = ".._myab:GetCategory()..", TypeName = ".._myab:GetTypeName() - self:T(RAT.id..text) + -- Add airport to table. + table.insert(self.airports_map, _myab) + + local text="MOOSE: Airport ID = ".._myab:GetID().." and Name = ".._myab:GetName()..", Category = ".._myab:GetCategory()..", TypeName = ".._myab:GetTypeName() + self:T(RAT.id..text) + + else + + self:E(RAT.id..string.format("WARNING: Airbase %s does not exsist as MOOSE object!", tostring(_name))) + + end end end @@ -3373,7 +3416,7 @@ function RAT:_GetAirportsOfCoalition() for _,coalition in pairs(self.ctable) do for _,_airport in pairs(self.airports_map) do local airport=_airport --Wrapper.Airbase#AIRBASE - local category=airport:GetDesc().category + local category=airport:GetAirbaseCategory() if airport:GetCoalition()==coalition then -- Planes cannot land on FARPs. --local condition1=self.category==RAT.cat.plane and airport:GetTypeName()=="FARP" @@ -3599,13 +3642,18 @@ function RAT:Status(message, forID) local text=string.format("Flight %s will be despawned NOW!", self.alias) self:T(RAT.id..text) - -- Despawn old group. + + -- Respawn group if (not self.norespawn) and (not self.respawn_after_takeoff) then local idx=self:GetSpawnIndexFromGroup(group) local coord=group:GetCoordinate() self:_Respawn(idx, coord, 0) end - self:_Despawn(group, 0) + + -- Despawn old group. + if self.despawnair then + self:_Despawn(group, 0) + end end @@ -3799,7 +3847,7 @@ function RAT:_OnBirth(EventData) -- Check if any unit of the group was spawned on top of another unit in the MOOSE data base. local ontop=false - if self.checkontop and (_airbase and _airbase:GetDesc().category==Airbase.Category.AIRDROME) then + if self.checkontop and (_airbase and _airbase:GetAirbaseCategory()==Airbase.Category.AIRDROME) then ontop=self:_CheckOnTop(SpawnGroup, self.ontopradius) end @@ -4409,7 +4457,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport if (Airport~=nil) and (Type~=RAT.wp.air) then local AirbaseID = Airport:GetID() - local AirbaseCategory = Airport:GetDesc().category + local AirbaseCategory = Airport:GetAirbaseCategory() if AirbaseCategory == Airbase.Category.SHIP then RoutePoint.linkUnit = AirbaseID RoutePoint.helipadId = AirbaseID @@ -5093,7 +5141,7 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take local spawnonrunway=false local spawnonairport=false if spawnonground then - local AirbaseCategory = departure:GetDesc().category + local AirbaseCategory = departure:GetAirbaseCategory() if AirbaseCategory == Airbase.Category.SHIP then spawnonship=true elseif AirbaseCategory == Airbase.Category.HELIPAD then @@ -5125,6 +5173,7 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, take if self.uncontrolled then -- This is used in the SPAWN:SpawnWithIndex() function. Some values are overwritten there! self.SpawnUnControlled=true + SpawnTemplate.uncontrolled=true end -- Number of units in the group. With grouping this can actually differ from the template group size! @@ -5435,7 +5484,7 @@ function RAT:_ATCInit(airports_map) if not RAT.ATC.init then local text text="Starting RAT ATC.\nSimultanious = "..RAT.ATC.Nclearance.."\n".."Delay = "..RAT.ATC.delay - self:T(RAT.id..text) + BASE:T(RAT.id..text) RAT.ATC.init=true for _,ap in pairs(airports_map) do local name=ap:GetName() @@ -5458,7 +5507,7 @@ end -- @param #string name Group name of the flight. -- @param #string dest Name of the destination airport. function RAT:_ATCAddFlight(name, dest) - self:T(string.format("%sATC %s: Adding flight %s with destination %s.", RAT.id, dest, name, dest)) + BASE:T(string.format("%sATC %s: Adding flight %s with destination %s.", RAT.id, dest, name, dest)) RAT.ATC.flight[name]={} RAT.ATC.flight[name].destination=dest RAT.ATC.flight[name].Tarrive=-1 @@ -5483,7 +5532,7 @@ end -- @param #string name Group name of the flight. -- @param #number time Time the fight first registered. function RAT:_ATCRegisterFlight(name, time) - self:T(RAT.id.."Flight ".. name.." registered at ATC for landing clearance.") + BASE:T(RAT.id.."Flight ".. name.." registered at ATC for landing clearance.") RAT.ATC.flight[name].Tarrive=time RAT.ATC.flight[name].holding=0 end @@ -5514,7 +5563,7 @@ 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) - self:T(RAT.id..text) + BASE:T(RAT.id..text) elseif hold==RAT.ATC.onfinal then @@ -5522,7 +5571,7 @@ function RAT:_ATCStatus() local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60) - self:T(RAT.id..text) + BASE:T(RAT.id..text) elseif hold==RAT.ATC.unregistered then @@ -5530,7 +5579,7 @@ function RAT:_ATCStatus() --self:T(string.format("ATC %s: Flight %s is not registered yet (hold %d).", dest, name, hold)) else - self:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") + BASE:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().") end end @@ -5572,12 +5621,12 @@ 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.", name, flight,qID, nqueue, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) - self:T(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.", name, flight, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60) - self:T(RAT.id..text) + BASE:T(RAT.id..text) -- Clear flight for landing. RAT:_ATCClearForLanding(name, flight) @@ -5705,12 +5754,7 @@ function RAT:_ATCQueue() for k,v in ipairs(_queue) do table.insert(RAT.ATC.airport[airport].queue, v[1]) end - - --fvh - --for k,v in ipairs(RAT.ATC.airport[airport].queue) do - --print(string.format("queue #%02i flight \"%s\" holding %d seconds",k, v, RAT.ATC.flight[v].holding)) - --end - + end end diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 95b800521..6b292f2fd 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -1,39 +1,49 @@ --- **Functional** - Range Practice. --- +-- -- === --- +-- -- The RANGE class enables easy set up of bombing and strafing ranges within DCS World. --- +-- -- Implementation is based on the [Simple Range Script](https://forums.eagle.ru/showthread.php?t=157991) by [Ciribob](https://forums.eagle.ru/member.php?u=112175), which itself was motivated -- by a script by SNAFU [see here](https://forums.eagle.ru/showthread.php?t=109174). --- --- [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is highly recommended for this class. --- --- ## Features: -- --- * Impact points of bombs, rockets and missils are recorded and distance to closest range target is measured and reported to the player. --- * Number of hits on strafing passes are counted and reported. Also the percentage of hits w.r.t fired shots is evaluated. --- * Results of all bombing and strafing runs are stored and top 10 results can be displayed. +-- [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is highly recommended for this class. +-- +-- **Main Features:** +-- +-- * Impact points of bombs, rockets and missiles are recorded and distance to closest range target is measured and reported to the player. +-- * Number of hits on strafing passes are counted and reported. Also the percentage of hits w.r.t fired shots is evaluated. +-- * Results of all bombing and strafing runs are stored and top 10 results can be displayed. -- * Range targets can be marked by smoke. --- * Range can be illuminated by illumination bombs for night practices. +-- * Range can be illuminated by illumination bombs for night missions. -- * Bomb, rocket and missile impact points can be marked by smoke. -- * Direct hits on targets can trigger flares. -- * Smoke and flare colors can be adjusted for each player via radio menu. -- * Range information and weather report at the range can be reported via radio menu. --- --- More information and examples can be found below. +-- * Persistence: Bombing range results can be saved to disk and loaded the next time the mission is started. +-- * Range control voice overs (>40) for hit assessment. +-- +-- === +-- +-- ## Youtube Videos: + -- +-- * [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- * [MOOSE - On the Range - Demonstration Video](https://www.youtube.com/watch?v=kIXcxNB9_3M) -- -- === --- --- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) --- ### [MOOSE - On the Range - Demonstration Video](https://www.youtube.com/watch?v=kIXcxNB9_3M) +-- +-- ## Missions: Example missions will be added later. -- -- === --- +-- +-- ## Sound files: Check out the pinned messages in the Moose discord *#func-range* channel. +-- +-- === +-- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** --- +-- -- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536), [Ciribob](https://forums.eagle.ru/member.php?u=112175) --- +-- -- === -- @module Functional.Range -- @image Range.JPG @@ -43,30 +53,32 @@ -- @type RANGE -- @field #string ClassName Name of the Class. -- @field #boolean Debug If true, debug info is send as messages on the screen. +-- @field #string id String id of range for output in DCS log. -- @field #string rangename Name of the range. -- @field Core.Point#COORDINATE location Coordinate of the range location. -- @field #number rangeradius Radius of range defining its total size for e.g. smoking bomb impact points and sending radio messages. Default 5 km. --- @field Core.Zone#ZONE rangezone MOOSE zone object of the range. For example, no bomb impacts are smoked if bombs fall outside of the range zone. +-- @field Core.Zone#ZONE rangezone MOOSE zone object of the range. For example, no bomb impacts are smoked if bombs fall outside of the range zone. -- @field #table strafeTargets Table of strafing targets. -- @field #table bombingTargets Table of targets to bomb. -- @field #number nbombtargets Number of bombing targets. -- @field #number nstrafetargets Number of strafing targets. +-- @field #boolean messages Globally enable/disable all messages to players. -- @field #table MenuAddedTo Table for monitoring which players already got an F10 menu. -- @field #table planes Table for administration. -- @field #table strafeStatus Table containing the current strafing target a player as assigned to. -- @field #table strafePlayerResults Table containing the strafing results of each player. -- @field #table bombPlayerResults Table containing the bombing results of each player. --- @field #table PlayerSettings Indiviual player settings. +-- @field #table PlayerSettings Individual player settings. -- @field #number dtBombtrack Time step [sec] used for tracking released bomb/rocket positions. Default 0.005 seconds. --- @field #number BombtrackThreshold Bombs/rockets/missiles are only tracked if player-range distance is smaller than this threashold [m]. Default 25000 m. +-- @field #number BombtrackThreshold Bombs/rockets/missiles are only tracked if player-range distance is smaller than this threshold [m]. Default 25000 m. -- @field #number Tmsg Time [sec] messages to players are displayed. Default 30 sec. -- @field #string examinergroupname Name of the examiner group which should get all messages. -- @field #boolean examinerexclusive If true, only the examiner gets messages. If false, clients and examiner get messages. --- @field #number strafemaxalt Maximum altitude above ground for registering for a strafe run. Default is 914 m = 3000 ft. +-- @field #number strafemaxalt Maximum altitude above ground for registering for a strafe run. Default is 914 m = 3000 ft. -- @field #number ndisplayresult Number of (player) results that a displayed. Default is 10. -- @field Utilities.Utils#SMOKECOLOR BombSmokeColor Color id used for smoking bomb targets. -- @field Utilities.Utils#SMOKECOLOR StrafeSmokeColor Color id used to smoke strafe targets. --- @field Utilities.Utils#SMOKECOLOR StrafePitSmokeColor Color id used to smoke strafe pit approach boxes. +-- @field Utilities.Utils#SMOKECOLOR StrafePitSmokeColor Color id used to smoke strafe pit approach boxes. -- @field #number illuminationminalt Minimum altitude AGL in meters at which illumination bombs are fired. Default is 500 m. -- @field #number illuminationmaxalt Maximum altitude AGL in meters at which illumination bombs are fired. Default is 1000 m. -- @field #number scorebombdistance Distance from closest target up to which bomb hits are counted. Default 1000 m. @@ -75,174 +87,247 @@ -- @field #boolean trackbombs If true (default), all bomb types are tracked and impact point to closest bombing target is evaluated. -- @field #boolean trackrockets If true (default), all rocket types are tracked and impact point to closest bombing target is evaluated. -- @field #boolean trackmissiles If true (default), all missile types are tracked and impact point to closest bombing target is evaluated. --- @extends Core.Base#BASE +-- @field #boolean defaultsmokebomb If true, initialize player settings to smoke bomb. +-- @field #boolean autosave If true, automatically save results every X seconds. +-- @field #number instructorfreq Frequency on which the range control transmitts. +-- @field Core.RadioQueue#RADIOQUEUE instructor Instructor radio queue. +-- @field #number rangecontrolfreq Frequency on which the range control transmitts. +-- @field Core.RadioQueue#RADIOQUEUE rangecontrol Range control radio queue. +-- @field #string soundpath Path inside miz file where the sound files are located. Default is "Range Soundfiles/". +-- @extends Core.Fsm#FSM ---- Enables a mission designer to easily set up practice ranges in DCS. A new RANGE object can be created with the @{#RANGE.New}(rangename) contructor. --- The parameter "rangename" defindes the name of the range. It has to be unique since this is also the name displayed in the radio menu. +--- *Don't only practice your art, but force your way into its secrets; art deserves that, for it and knowledge can raise man to the Divine.* - Ludwig van Beethoven -- +-- === +-- +-- ![Banner Image](..\Presentations\RANGE\RANGE_Main.png) +-- +-- # The Range Concept +-- +-- The RANGE class enables a mission designer to easily set up practice ranges in DCS. A new RANGE object can be created with the @{#RANGE.New}(*rangename*) contructor. +-- The parameter *rangename* defines the name of the range. It has to be unique since this is also the name displayed in the radio menu. +-- -- Generally, a range consists of strafe pits and bombing targets. For strafe pits the number of hits for each pass is counted and tabulated. -- For bombing targets, the distance from the impact point of the bomb, rocket or missile to the closest range target is measured and tabulated. -- Each player can display his best results via a function in the radio menu or see the best best results from all players. --- +-- -- When all targets have been defined in the script, the range is started by the @{#RANGE.Start}() command. --- +-- -- **IMPORTANT** --- +-- -- Due to a DCS bug, it is not possible to directly monitor when a player enters a plane. So in a mission with client slots, it is vital that --- a player first enters as spector and **after that** jumps into the slot of his aircraft! +-- a player first enters as spectator or hits ESC twice and **after that** jumps into the slot of his aircraft! -- If that is not done, the script is not started correctly. This can be checked by looking at the radio menues. If the mission was entered correctly, --- there should be an "On the Range" menu items in the "F10. Other..." menu. --- --- ## Strafe Pits --- Each strafe pit can consist of multiple targets. Often one findes two or three strafe targets next to each other. +-- there should be an "On the Range" menu items in the "F10. Other..." menu. +-- +-- # Strafe Pits -- +-- Each strafe pit can consist of multiple targets. Often one finds two or three strafe targets next to each other. +-- -- A strafe pit can be added to the range by the @{#RANGE.AddStrafePit}(*targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*) function. --- --- * The first parameter *targetnames* defines the target or targets. This has to be given as a lua table which contains the names of @{Wrapper.Unit} or @{Static} objects defined in the mission editor. +-- +-- * The first parameter *targetnames* defines the target or targets. This has to be given as a lua table which contains the names of @{Wrapper.Unit} or @{Static} objects defined in the mission editor. -- * In order to perform a valid pass on the strafe pit, the pilot has to begin his run from the correct direction. Therefore, an "approach box" is defined in front -- of the strafe targets. The parameters *boxlength* and *boxwidth* define the size of the box while the parameter *heading* defines its direction. -- If the parameter *heading* is passed as **nil**, the heading is automatically taken from the heading of the first target unit as defined in the ME. -- The parameter *inverseheading* turns the heading around by 180 degrees. This is sometimes useful, since the default heading of strafe target units point in the -- wrong/opposite direction. --- * The parameter *goodpass* defines the number of hits a pilot has to achive during a run to be judged as a "good" pass. +-- * The parameter *goodpass* defines the number of hits a pilot has to achieve during a run to be judged as a "good" pass. -- * The last parameter *foulline* sets the distance from the pit targets to the foul line. Hit from closer than this line are not counted! --- +-- -- Another function to add a strafe pit is @{#RANGE.AddStrafePitGroup}(*group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*). Here, -- the first parameter *group* is a MOOSE @{Wrapper.Group} object and **all** units in this group define **one** strafe pit. --- +-- -- Finally, a valid approach has to be performed below a certain maximum altitude. The default is 914 meters (3000 ft) AGL. This is a parameter valid for all -- strafing pits of the range and can be adjusted by the @{#RANGE.SetMaxStrafeAlt}(maxalt) function. +-- +-- # Bombing targets -- --- ## Bombing targets -- One ore multiple bombing targets can be added to the range by the @{#RANGE.AddBombingTargets}(targetnames, goodhitrange, randommove) function. --- +-- -- * The first parameter *targetnames* has to be a lua table, which contains the names of @{Wrapper.Unit} and/or @{Static} objects defined in the mission editor. -- Note that the @{Range} logic **automatically** determines, if a name belongs to a @{Wrapper.Unit} or @{Static} object now. -- * The (optional) parameter *goodhitrange* specifies the radius around the target. If a bomb or rocket falls at a distance smaller than this number, the hit is considered to be "good". -- * If final (optional) parameter "*randommove*" can be enabled to create moving targets. If this parameter is set to true, the units of this bombing target will randomly move within the range zone. --- Note that there might be quirks since DCS units can get stuck in buildings etc. So it might be safer to manually define a route for the units in the mission editor if moving targets are desired. --- +-- Note that there might be quirks since DCS units can get stuck in buildings etc. So it might be safer to manually define a route for the units in the mission editor if moving targets are desired. +-- +-- ## Adding Groups +-- -- Another possibility to add bombing targets is the @{#RANGE.AddBombingTargetGroup}(*group, goodhitrange, randommove*) function. Here the parameter *group* is a MOOSE @{Wrapper.Group} object -- and **all** units in this group are defined as bombing targets. -- --- ## Fine Tuning --- Many range parameters have good default values. However, the mission designer can change these settings easily with the supplied user functions: +-- ## Specifying Coordinates -- +-- It is also possible to specify coordinates rather than unit or static objects as bombing target locations. This has the advantage, that even when the unit/static object is dead, the specified +-- coordinate will still be a valid impact point. This can be done via the @{#RANGE.AddBombingTargetCoordinate}(*coord*, *name*, *goodhitrange*) function. +-- +-- # Fine Tuning +-- +-- Many range parameters have good default values. However, the mission designer can change these settings easily with the supplied user functions: +-- -- * @{#RANGE.SetMaxStrafeAlt}() sets the max altitude for valid strafing runs. -- * @{#RANGE.SetMessageTimeDuration}() sets the duration how long (most) messages are displayed. -- * @{#RANGE.SetDisplayedMaxPlayerResults}() sets the number of results displayed. --- * @{#RANGE.SetRangeRadius}() defines the total range area. --- * @{#RANGE.SetBombTargetSmokeColor}() sets the color used to smoke bombing targets. +-- * @{#RANGE.SetRangeRadius}() defines the total range area. +-- * @{#RANGE.SetBombTargetSmokeColor}() sets the color used to smoke bombing targets. -- * @{#RANGE.SetStrafeTargetSmokeColor}() sets the color used to smoke strafe targets. -- * @{#RANGE.SetStrafePitSmokeColor}() sets the color used to smoke strafe pit approach boxes. -- * @{#RANGE.SetSmokeTimeDelay}() sets the time delay between smoking bomb/rocket impact points after impact. -- * @{#RANGE.TrackBombsON}() or @{#RANGE.TrackBombsOFF}() can be used to enable/disable tracking and evaluating of all bomb types a player fires. -- * @{#RANGE.TrackRocketsON}() or @{#RANGE.TrackRocketsOFF}() can be used to enable/disable tracking and evaluating of all rocket types a player fires. -- * @{#RANGE.TrackMissilesON}() or @{#RANGE.TrackMissilesOFF}() can be used to enable/disable tracking and evaluating of all missile types a player fires. +-- +-- # Radio Menu -- --- ## Radio Menu -- Each range gets a radio menu with various submenus where each player can adjust his individual settings or request information about the range or his scores. --- --- The main range menu can be found at "F10. Other..." --> "Fxx. On the Range..." --> "F1. Your Range Name...". +-- +-- The main range menu can be found at "F10. Other..." --> "F*X*. On the Range..." --> "F1. ...". -- -- The range menu contains the following submenues: -- --- * "F1. Mark Targets": Various ways to mark targets. --- * "F2. My Settings": Player specific settings. --- * "F3. Stats" Player: statistics and scores. --- * "Range Information": Information about the range, such as bearing and range. Also range and player specific settings are displayed. --- * "Weather Report": Temperatur, wind and QFE pressure information is provided. +-- ![Banner Image](..\Presentations\RANGE\Menu_Main.png) +-- +-- * "F1. Statistics...": Range results of all players and personal stats. +-- * "F2. Mark Targets": Mark range targets by smoke or flares. +-- * "F3. My Settings" Personal settings. +-- * "F4. Range Info": Information about the range, such as bearing and range. -- --- ## Examples +-- ## F1 Statistics +-- +-- ![Banner Image](..\Presentations\RANGE\Menu_Stats.png) +-- +-- ## F2 Mark Targets +-- +-- ![Banner Image](..\Presentations\RANGE\Menu_Stats.png) +-- +-- ## F3 My Settings +-- +-- ![Banner Image](..\Presentations\RANGE\Menu_MySettings.png) +-- +-- ## F4 Range Info +-- +-- ![Banner Image](..\Presentations\RANGE\Menu_RangeInfo.png) +-- +-- # Voice Overs +-- +-- Voice over sound files can be downloaded from the Moose Discord. Check the pinned messages in the *#func-range* channel. +-- +-- Instructor radio will inform players when they enter or exit the range zone and provide the radio frequency of the range control for hit assessment. +-- This can be enabled via the @{#RANGE.SetInstructorRadio}(*frequency*) functions, where *frequency* is the AM frequency in MHz. +-- +-- The range control can be enabled via the @{#RANGE.SetRangeControl}(*frequency*) functions, where *frequency* is the AM frequency in MHz. +-- +-- By default, the sound files are placed in the "Range Soundfiles/" folder inside the mission (.miz) file. Another folder can be specified via the @{#RANGE.SetSoundfilesPath}(*path*) function. +-- +-- # Persistence +-- +-- To automatically save bombing results to disk, use the @{#RANGE.SetAutosave}() function. Bombing results will be saved as csv file in your "Saved Games\DCS.openbeta\Logs" directory. +-- Each range has a separate file, which is named "RANGE-<*RangeName*>_BombingResults.csv". +-- +-- The next time you start the mission, these results are also automatically loaded. +-- +-- Strafing results are currently **not** saved. +-- +-- # Examples +-- +-- ## Goldwater Range -- --- ### Goldwater Range -- This example shows hot to set up the [Barry M. Goldwater range](https://en.wikipedia.org/wiki/Barry_M._Goldwater_Air_Force_Range). -- It consists of two strafe pits each has two targets plus three bombing targets. --- +-- -- -- Strafe pits. Each pit can consist of multiple targets. Here we have two pits and each of the pits has two targets. -- -- These are names of the corresponding units defined in the ME. -- local strafepit_left={"GWR Strafe Pit Left 1", "GWR Strafe Pit Left 2"} -- local strafepit_right={"GWR Strafe Pit Right 1", "GWR Strafe Pit Right 2"} --- +-- -- -- Table of bombing target names. Again these are the names of the corresponding units as defined in the ME. -- local bombtargets={"GWR Bomb Target Circle Left", "GWR Bomb Target Circle Right", "GWR Bomb Target Hard"} --- +-- -- -- Create a range object. -- GoldwaterRange=RANGE:New("Goldwater Range") --- +-- -- -- Distance between strafe target and foul line. You have to specify the names of the unit or static objects. -- -- Note that this could also be done manually by simply measuring the distance between the target and the foul line in the ME. -- GoldwaterRange:GetFoullineDistance("GWR Strafe Pit Left 1", "GWR Foul Line Left") --- +-- -- -- Add strafe pits. Each pit (left and right) consists of two targets. -- GoldwaterRange:AddStrafePit(strafepit_left, 3000, 300, nil, true, 20, fouldist) -- GoldwaterRange:AddStrafePit(strafepit_right, nil, nil, nil, true, nil, fouldist) --- +-- -- -- Add bombing targets. A good hit is if the bomb falls less then 50 m from the target. -- GoldwaterRange:AddBombingTargets(bombtargets, 50) --- +-- -- -- Start range. -- GoldwaterRange:Start() --- --- The [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is (implicitly) used in this example. --- --- ## Debugging --- +-- +-- The [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is (implicitly) used in this example. +-- +-- # Debugging +-- -- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in -- C:\Users\\Saved Games\DCS\Logs\dcs.log -- All output concerning the RANGE class should have the string "RANGE" in the corresponding line. --- +-- -- The verbosity of the output can be increased by adding the following lines to your script: --- +-- -- BASE:TraceOnOff(true) -- BASE:TraceLevel(1) -- BASE:TraceClass("RANGE") --- +-- -- To get even more output you can increase the trace level to 2 or even 3, c.f. @{BASE} for more details. --- +-- -- The function @{#RANGE.DebugON}() can be used to send messages on screen. It also smokes all defined strafe and bombing targets, the strafe pit approach boxes and the range zone. --- +-- -- Note that it can happen that the RANGE radio menu is not shown. Check that the range object is defined as a **global** variable rather than a local one. --- The could avoid the lua garbage collection to accidentally/falsely deallocate the RANGE objects. --- --- --- +-- The could avoid the lua garbage collection to accidentally/falsely deallocate the RANGE objects. +-- +-- +-- -- @field #RANGE RANGE={ - ClassName = "RANGE", - Debug=false, - rangename=nil, - location=nil, - rangeradius=5000, - rangezone=nil, - strafeTargets={}, - bombingTargets={}, - nbombtargets=0, - nstrafetargets=0, - MenuAddedTo = {}, - planes = {}, - strafeStatus = {}, + ClassName = "RANGE", + Debug = false, + id = nil, + rangename = nil, + location = nil, + messages = true, + rangeradius = 5000, + rangezone = nil, + strafeTargets = {}, + bombingTargets = {}, + nbombtargets = 0, + nstrafetargets = 0, + MenuAddedTo = {}, + planes = {}, + strafeStatus = {}, strafePlayerResults = {}, - bombPlayerResults = {}, - PlayerSettings = {}, - dtBombtrack=0.005, - BombtrackThreshold=25000, - Tmsg=30, - examinergroupname=nil, - examinerexclusive=nil, - strafemaxalt=914, - ndisplayresult=10, - BombSmokeColor=SMOKECOLOR.Red, - StrafeSmokeColor=SMOKECOLOR.Green, - StrafePitSmokeColor=SMOKECOLOR.White, - illuminationminalt=500, - illuminationmaxalt=1000, - scorebombdistance=1000, - TdelaySmoke=3.0, - eventmoose=true, - trackbombs=true, - trackrockets=true, - trackmissiles=true, + bombPlayerResults = {}, + PlayerSettings = {}, + dtBombtrack = 0.005, + BombtrackThreshold = 25000, + Tmsg = 30, + examinergroupname = nil, + examinerexclusive = nil, + strafemaxalt = 914, + ndisplayresult = 10, + BombSmokeColor = SMOKECOLOR.Red, + StrafeSmokeColor = SMOKECOLOR.Green, + StrafePitSmokeColor = SMOKECOLOR.White, + illuminationminalt = 500, + illuminationmaxalt = 1000, + scorebombdistance = 1000, + TdelaySmoke = 3.0, + eventmoose = true, + trackbombs = true, + trackrockets = true, + trackmissiles = true, + defaultsmokebomb = true, + autosave = false, + instructorfreq = nil, + instructor = nil, + rangecontrolfreq = nil, + rangecontrol = nil, + soundpath = "Range Soundfiles/" } --- Default range parameters. @@ -262,23 +347,180 @@ RANGE.Defaults={ foulline=610, } +--- Target type, i.e. unit, static, or coordinate. +-- @type RANGE.TargetType +-- @field #string UNIT Target is a unit. +-- @field #string STATIC Target is a static. +-- @field #string COORD Target is a coordinate. +RANGE.TargetType={ + UNIT="Unit", + STATIC="Static", + COORD="Coordinate", +} + +--- Player settings. +-- @type RANGE.PlayerData +-- @field #boolean smokebombimpact Smoke bomb impact points. +-- @field #boolean flaredirecthits Flare when player directly hits a target. +-- @field #number smokecolor Color of smoke. +-- @field #number flarecolor Color of flares. +-- @field #boolean messages Display info messages. +-- @field Wrapper.Client#CLIENT client Client object of player. +-- @field #string unitname Name of player aircraft unit. +-- @field #string playername Name of player. +-- @field #string airframe Aircraft type name. +-- @field #boolean inzone If true, player is inside the range zone. + +--- Bomb target data. +-- @type RANGE.BombTarget +-- @field #string name Name of unit. +-- @field Wrapper.Unit#UNIT target Target unit. +-- @field Core.Point#COORDINATE coordinate Coordinate of the target. +-- @field #number goodhitrange Range in meters for a good hit. +-- @field #boolean move If true, unit move randomly. +-- @field #number speed Speed of unit. +-- @field #RANGE.TargetType type Type of target. + +--- Strafe target data. +-- @type RANGE.StrafeTarget +-- @field #string name Name of the unit. +-- @field Core.Zone#ZONE_POLYGON polygon Polygon zone. +-- @field Core.Point#COORDINATE coordinate Center coordinate of the pit. +-- @field #number goodPass Number of hits for a good pass. +-- @field #table targets Table of target units. +-- @field #number foulline Foul line +-- @field #number smokepoints Number of smoke points. +-- @field #number heading Heading of pit. + +--- Bomb target result. +-- @type RANGE.BombResult +-- @field #string name Name of closest target. +-- @field #number distance Distance in meters. +-- @field #number radial Radial in degrees. +-- @field #string weapon Name of the weapon. +-- @field #string quality Hit quality. +-- @field #string player Player name. +-- @field #string airframe Aircraft type of player. +-- @field #number time Time via timer.getAbsTime() in seconds of impact. +-- @field #string date OS date. + +--- Sound file data. +-- @type RANGE.Soundfile +-- @field #string filename Name of the file +-- @field #number duration Duration in seconds. + +--- Sound files. +-- @type RANGE.Sound +-- @field #RANGE.Soundfile RC0 +-- @field #RANGE.Soundfile RC1 +-- @field #RANGE.Soundfile RC2 +-- @field #RANGE.Soundfile RC3 +-- @field #RANGE.Soundfile RC4 +-- @field #RANGE.Soundfile RC5 +-- @field #RANGE.Soundfile RC6 +-- @field #RANGE.Soundfile RC7 +-- @field #RANGE.Soundfile RC8 +-- @field #RANGE.Soundfile RC9 +-- @field #RANGE.Soundfile RCAccuracy +-- @field #RANGE.Soundfile RCDegrees +-- @field #RANGE.Soundfile RCExcellentHit +-- @field #RANGE.Soundfile RCExcellentPass +-- @field #RANGE.Soundfile RCFeet +-- @field #RANGE.Soundfile RCFor +-- @field #RANGE.Soundfile RCGoodHit +-- @field #RANGE.Soundfile RCGoodPass +-- @field #RANGE.Soundfile RCHitsOnTarget +-- @field #RANGE.Soundfile RCImpact +-- @field #RANGE.Soundfile RCIneffectiveHit +-- @field #RANGE.Soundfile RCIneffectivePass +-- @field #RANGE.Soundfile RCInvalidHit +-- @field #RANGE.Soundfile RCLeftStrafePitTooQuickly +-- @field #RANGE.Soundfile RCPercent +-- @field #RANGE.Soundfile RCPoorHit +-- @field #RANGE.Soundfile RCPoorPass +-- @field #RANGE.Soundfile RCRollingInOnStrafeTarget +-- @field #RANGE.Soundfile RCTotalRoundsFired +-- @field #RANGE.Soundfile RCWeaponImpactedTooFar +-- @field #RANGE.Soundfile IR0 +-- @field #RANGE.Soundfile IR1 +-- @field #RANGE.Soundfile IR2 +-- @field #RANGE.Soundfile IR3 +-- @field #RANGE.Soundfile IR4 +-- @field #RANGE.Soundfile IR5 +-- @field #RANGE.Soundfile IR6 +-- @field #RANGE.Soundfile IR7 +-- @field #RANGE.Soundfile IR8 +-- @field #RANGE.Soundfile IR9 +-- @field #RANGE.Soundfile IRDecimal +-- @field #RANGE.Soundfile IRMegaHertz +-- @field #RANGE.Soundfile IREnterRange +-- @field #RANGE.Soundfile IRExitRange +RANGE.Sound = { + RC0={filename="RC-0.ogg", duration=0.60}, + RC1={filename="RC-1.ogg", duration=0.47}, + RC2={filename="RC-2.ogg", duration=0.43}, + RC3={filename="RC-3.ogg", duration=0.50}, + RC4={filename="RC-4.ogg", duration=0.58}, + RC5={filename="RC-5.ogg", duration=0.54}, + RC6={filename="RC-6.ogg", duration=0.61}, + RC7={filename="RC-7.ogg", duration=0.53}, + RC8={filename="RC-8.ogg", duration=0.34}, + RC9={filename="RC-9.ogg", duration=0.54}, + RCAccuracy={filename="RC-Accuracy.ogg", duration=0.67}, + RCDegrees={filename="RC-Degrees.ogg", duration=0.59}, + RCExcellentHit={filename="RC-ExcellentHit.ogg", duration=0.76}, + RCExcellentPass={filename="RC-ExcellentPass.ogg", duration=0.89}, + RCFeet={filename="RC-Feet.ogg", duration=0.49}, + RCFor={filename="RC-For.ogg", duration=0.64}, + RCGoodHit={filename="RC-GoodHit.ogg", duration=0.52}, + RCGoodPass={filename="RC-GoodPass.ogg", duration=0.62}, + RCHitsOnTarget={filename="RC-HitsOnTarget.ogg", duration=0.88}, + RCImpact={filename="RC-Impact.ogg", duration=0.61}, + RCIneffectiveHit={filename="RC-IneffectiveHit.ogg", duration=0.86}, + RCIneffectivePass={filename="RC-IneffectivePass.ogg", duration=0.99}, + RCInvalidHit={filename="RC-InvalidHit.ogg", duration=2.97}, + RCLeftStrafePitTooQuickly={filename="RC-LeftStrafePitTooQuickly.ogg", duration=3.09}, + RCPercent={filename="RC-Percent.ogg", duration=0.56}, + RCPoorHit={filename="RC-PoorHit.ogg", duration=0.54}, + RCPoorPass={filename="RC-PoorPass.ogg", duration=0.68}, + RCRollingInOnStrafeTarget={filename="RC-RollingInOnStrafeTarget.ogg", duration=1.38}, + RCTotalRoundsFired={filename="RC-TotalRoundsFired.ogg", duration=1.22}, + RCWeaponImpactedTooFar={filename="RC-WeaponImpactedTooFar.ogg", duration=3.73}, + IR0={filename="IR-0.ogg", duration=0.55}, + IR1={filename="IR-1.ogg", duration=0.41}, + IR2={filename="IR-2.ogg", duration=0.37}, + IR3={filename="IR-3.ogg", duration=0.41}, + IR4={filename="IR-4.ogg", duration=0.37}, + IR5={filename="IR-5.ogg", duration=0.43}, + IR6={filename="IR-6.ogg", duration=0.55}, + IR7={filename="IR-7.ogg", duration=0.43}, + IR8={filename="IR-8.ogg", duration=0.38}, + IR9={filename="IR-9.ogg", duration=0.55}, + IRDecimal={filename="IR-Decimal.ogg", duration=0.54}, + IRMegaHertz={filename="IR-MegaHertz.ogg", duration=0.87}, + IREnterRange={filename="IR-EnterRange.ogg", duration=4.83}, + IRExitRange={filename="IR-ExitRange.ogg", duration=3.10}, +} + --- Global list of all defined range names. -- @field #table Names RANGE.Names={} ---- Main radio menu. --- @field #table MenuF10 +--- Main radio menu on group level. +-- @field #table MenuF10 Root menu table on group level. RANGE.MenuF10={} ---- Some ID to identify who we are in output of the DCS.log file. --- @field #string id -RANGE.id="RANGE | " +--- Main radio menu on mission level. +-- @field #table MenuF10Root Root menu on mission level. +RANGE.MenuF10Root=nil --- Range script version. -- @field #string version -RANGE.version="1.2.1" +RANGE.version="2.2.2" --TODO list: +--TODO: Verbosity level for messages. +--TODO: Add option for default settings such as smoke off. --TODO: Add custom weapons, which can be specified by the user. --TODO: Check if units are still alive. --DONE: Add statics for strafe pits. @@ -300,46 +542,158 @@ function RANGE:New(rangename) BASE:F({rangename=rangename}) -- Inherit BASE. - local self=BASE:Inherit(self, BASE:New()) -- #RANGE - + local self=BASE:Inherit(self, FSM:New()) -- #RANGE + -- Get range name. --TODO: make sure that the range name is not given twice. This would lead to problems in the F10 radio menu. self.rangename=rangename or "Practice Range" - + + -- Log id. + self.id=string.format("RANGE %s | ", self.rangename) + -- Debug info. - local text=string.format("RANGE script version %s - creating new RANGE object of name: %s.", RANGE.version, self.rangename) - self:E(RANGE.id..text) + local text=string.format("Script version %s - creating new RANGE object %s.", RANGE.version, self.rangename) + self:I(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) - + + -- Defaults + self:SetDefaultPlayerSmokeBomb() + + -- Start State. + self:SetStartState("Stopped") + + --- + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start RANGE script. + self:AddTransition("*", "Status", "*") -- Status of RANGE script. + self:AddTransition("*", "Impact", "*") -- Impact of bomb/rocket/missile. + self:AddTransition("*", "EnterRange", "*") -- Player enters the range. + self:AddTransition("*", "ExitRange", "*") -- Player leaves the range. + self:AddTransition("*", "Save", "*") -- Save player results. + self:AddTransition("*", "Load", "*") -- Load player results. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the RANGE. Initializes parameters and starts event handlers. + -- @function [parent=#RANGE] Start + -- @param #RANGE self + + --- Triggers the FSM event "Start" after a delay. Starts the RANGE. Initializes parameters and starts event handlers. + -- @function [parent=#RANGE] __Start + -- @param #RANGE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the RANGE and all its event handlers. + -- @param #RANGE self + + --- Triggers the FSM event "Stop" after a delay. Stops the RANGE and all its event handlers. + -- @function [parent=#RANGE] __Stop + -- @param #RANGE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Status". + -- @function [parent=#RANGE] Status + -- @param #RANGE self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#RANGE] __Status + -- @param #RANGE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Impact". + -- @function [parent=#RANGE] Impact + -- @param #RANGE self + -- @param #RANGE.BombResult result Data of bombing run. + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- Triggers the FSM delayed event "Impact". + -- @function [parent=#RANGE] __Impact + -- @param #RANGE self + -- @param #number delay Delay in seconds before the function is called. + -- @param #RANGE.BombResult result Data of the bombing run. + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- On after "Impact" event user function. Called when a bomb/rocket/missile impacted. + -- @function [parent=#RANGE] OnAfterImpact + -- @param #RANGE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #RANGE.BombResult result Data of the bombing run. + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- Triggers the FSM event "EnterRange". + -- @function [parent=#RANGE] EnterRange + -- @param #RANGE self + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- Triggers the FSM delayed event "EnterRange". + -- @function [parent=#RANGE] __EnterRange + -- @param #RANGE self + -- @param #number delay Delay in seconds before the function is called. + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- On after "EnterRange" event user function. Called when a player enters the range zone. + -- @function [parent=#RANGE] OnAfterEnterRange + -- @param #RANGE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- Triggers the FSM event "ExitRange". + -- @function [parent=#RANGE] ExitRange + -- @param #RANGE self + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- Triggers the FSM delayed event "ExitRange". + -- @function [parent=#RANGE] __ExitRange + -- @param #RANGE self + -- @param #number delay Delay in seconds before the function is called. + -- @param #RANGE.PlayerData player Data of player settings etc. + + --- On after "ExitRange" event user function. Called when a player leaves the range zone. + -- @function [parent=#RANGE] OnAfterExitRange + -- @param #RANGE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #RANGE.PlayerData player Data of player settings etc. + -- Return object. return self end --- Initializes number of targets and location of the range. Starts the event handlers. -- @param #RANGE self -function RANGE:Start() - self:F() +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterStart() -- Location/coordinate of range. local _location=nil - + -- Count bomb targets. local _count=0 for _,_target in pairs(self.bombingTargets) do _count=_count+1 - + -- Get range location. if _location==nil then - _location=_target.target:GetCoordinate() --Core.Point#COORDINATE + _location=self:_GetBombTargetCoordinate(_target) end end self.nbombtargets=_count - + -- Count strafing targets. _count=0 for _,_target in pairs(self.strafeTargets) do _count=_count+1 - + for _,_unit in pairs(_target.targets) do if _location==nil then _location=_unit:GetCoordinate() @@ -347,54 +701,112 @@ function RANGE:Start() end end self.nstrafetargets=_count - + -- Location of the range. We simply take the first unit/target we find if it was not explicitly specified by the user. if self.location==nil then self.location=_location end - + if self.location==nil then - local text=string.format("ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) - self:E(RANGE.id..text) + local text=string.format("ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.nstrafetargets, self.nbombtargets) + self:E(self.id..text) return end - + -- Define a MOOSE zone of the range. if self.rangezone==nil then self.rangezone=ZONE_RADIUS:New(self.rangename, {x=self.location.x, y=self.location.z}, self.rangeradius) end - + -- Starting range. local text=string.format("Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets) - self:E(RANGE.id..text) + self:I(self.id..text) MESSAGE:New(text,10):ToAllIf(self.Debug) - + -- Event handling. if self.eventmoose then -- Events are handled my MOOSE. - self:T(RANGE.id.."Events are handled by MOOSE.") + self:T(self.id.."Events are handled by MOOSE.") self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Hit) self:HandleEvent(EVENTS.Shot) else -- Events are handled directly by DCS. - self:T(RANGE.id.."Events are handled directly by DCS.") + self:T(self.id.."Events are handled directly by DCS.") world.addEventHandler(self) end - + -- Make bomb target move randomly within the range zone. for _,_target in pairs(self.bombingTargets) do -- Check if it is a static object. - local _static=self:_CheckStatic(_target.target:GetName()) - + --local _static=self:_CheckStatic(_target.target:GetName()) + local _static=_target.type==RANGE.TargetType.STATIC + if _target.move and _static==false and _target.speed>1 then local unit=_target.target --Wrapper.Unit#UNIT _target.target:PatrolZones({self.rangezone}, _target.speed*0.75, "Off road") end + + end + + -- Init range control. + if self.rangecontrolfreq then + + -- Radio queue. + self.rangecontrol=RADIOQUEUE:New(self.rangecontrolfreq, nil, self.rangename) + + -- Init numbers. + self.rangecontrol:SetDigit(0, RANGE.Sound.RC0.filename, RANGE.Sound.RC0.duration, self.soundpath) + self.rangecontrol:SetDigit(1, RANGE.Sound.RC1.filename, RANGE.Sound.RC1.duration, self.soundpath) + self.rangecontrol:SetDigit(2, RANGE.Sound.RC2.filename, RANGE.Sound.RC2.duration, self.soundpath) + self.rangecontrol:SetDigit(3, RANGE.Sound.RC3.filename, RANGE.Sound.RC3.duration, self.soundpath) + self.rangecontrol:SetDigit(4, RANGE.Sound.RC4.filename, RANGE.Sound.RC4.duration, self.soundpath) + self.rangecontrol:SetDigit(5, RANGE.Sound.RC5.filename, RANGE.Sound.RC5.duration, self.soundpath) + self.rangecontrol:SetDigit(6, RANGE.Sound.RC6.filename, RANGE.Sound.RC6.duration, self.soundpath) + self.rangecontrol:SetDigit(7, RANGE.Sound.RC7.filename, RANGE.Sound.RC7.duration, self.soundpath) + self.rangecontrol:SetDigit(8, RANGE.Sound.RC8.filename, RANGE.Sound.RC8.duration, self.soundpath) + self.rangecontrol:SetDigit(9, RANGE.Sound.RC9.filename, RANGE.Sound.RC9.duration, self.soundpath) + + -- Set location where the messages are transmitted from. + self.rangecontrol:SetSenderCoordinate(self.location) + + -- Start range control radio queue. + self.rangecontrol:Start(1, 0.1) + + -- Init range control. + if self.instructorfreq then + + -- Radio queue. + self.instructor=RADIOQUEUE:New(self.instructorfreq, nil, self.rangename) + + -- Init numbers. + self.instructor:SetDigit(0, RANGE.Sound.IR0.filename, RANGE.Sound.IR0.duration, self.soundpath) + self.instructor:SetDigit(1, RANGE.Sound.IR1.filename, RANGE.Sound.IR1.duration, self.soundpath) + self.instructor:SetDigit(2, RANGE.Sound.IR2.filename, RANGE.Sound.IR2.duration, self.soundpath) + self.instructor:SetDigit(3, RANGE.Sound.IR3.filename, RANGE.Sound.IR3.duration, self.soundpath) + self.instructor:SetDigit(4, RANGE.Sound.IR4.filename, RANGE.Sound.IR4.duration, self.soundpath) + self.instructor:SetDigit(5, RANGE.Sound.IR5.filename, RANGE.Sound.IR5.duration, self.soundpath) + self.instructor:SetDigit(6, RANGE.Sound.IR6.filename, RANGE.Sound.IR6.duration, self.soundpath) + self.instructor:SetDigit(7, RANGE.Sound.IR7.filename, RANGE.Sound.IR7.duration, self.soundpath) + self.instructor:SetDigit(8, RANGE.Sound.IR8.filename, RANGE.Sound.IR8.duration, self.soundpath) + self.instructor:SetDigit(9, RANGE.Sound.IR9.filename, RANGE.Sound.IR9.duration, self.soundpath) + + -- Set location where the messages are transmitted from. + self.instructor:SetSenderCoordinate(self.location) + + -- Start instructor radio queue. + self.instructor:Start(1, 0.1) + + end end + -- Load prev results. + if self.autosave then + self:Load() + end + -- Debug mode: smoke all targets and range zone. if self.Debug then self:_MarkTargetsOnMap() @@ -403,155 +815,283 @@ function RANGE:Start() self:_SmokeStrafeTargetBoxes() self.rangezone:SmokeZone(SMOKECOLOR.White) end - + + self:__Status(-60) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Set maximal strafing altitude. Player entering a strafe pit above that altitude are not registered for a valid pass. -- @param #RANGE self -- @param #number maxalt Maximum altitude AGL in meters. Default is 914 m= 3000 ft. +-- @return #RANGE self function RANGE:SetMaxStrafeAlt(maxalt) self.strafemaxalt=maxalt or RANGE.Defaults.strafemaxalt + return self end --- Set time interval for tracking bombs. A smaller time step increases accuracy but needs more CPU time. -- @param #RANGE self -- @param #number dt Time interval in seconds. Default is 0.005 s. +-- @return #RANGE self function RANGE:SetBombtrackTimestep(dt) self.dtBombtrack=dt or RANGE.Defaults.dtBombtrack + return self end --- Set time how long (most) messages are displayed. -- @param #RANGE self -- @param #number time Time in seconds. Default is 30 s. +-- @return #RANGE self function RANGE:SetMessageTimeDuration(time) self.Tmsg=time or RANGE.Defaults.Tmsg + return self +end + +--- Automatically save player results to disc. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetAutosaveOn() + self.autosave=true + return self +end + +--- Switch off auto save player results. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetAutosaveOff() + self.autosave=false + return self end --- Set messages to examiner. The examiner will receive messages from all clients. -- @param #RANGE self -- @param #string examinergroupname Name of the group of the examiner. -- @param #boolean exclusively If true, messages are send exclusively to the examiner, i.e. not to the clients. +-- @return #RANGE self function RANGE:SetMessageToExaminer(examinergroupname, exclusively) self.examinergroupname=examinergroupname self.examinerexclusive=exclusively + return self end --- Set max number of player results that are displayed. -- @param #RANGE self -- @param #number nmax Number of results. Default is 10. +-- @return #RANGE self function RANGE:SetDisplayedMaxPlayerResults(nmax) self.ndisplayresult=nmax or RANGE.Defaults.ndisplayresult + return self end --- Set range radius. Defines the area in which e.g. bomb impacts are smoked. -- @param #RANGE self -- @param #number radius Radius in km. Default 5 km. +-- @return #RANGE self function RANGE:SetRangeRadius(radius) self.rangeradius=radius*1000 or RANGE.Defaults.rangeradius + return self +end + +--- Set player setting whether bomb impact points are smoked or not. +-- @param #RANGE self +-- @param #boolean switch If true nor nil default is to smoke impact points of bombs. +-- @return #RANGE self +function RANGE:SetDefaultPlayerSmokeBomb(switch) + if switch==true or switch==nil then + self.defaultsmokebomb=true + else + self.defaultsmokebomb=false + end + return self end --- Set bomb track threshold distance. Bombs/rockets/missiles are only tracked if player-range distance is less than this distance. Default 25 km. -- @param #RANGE self -- @param #number distance Threshold distance in km. Default 25 km. +-- @return #RANGE self function RANGE:SetBombtrackThreshold(distance) - self.BombtrackThreshold=distance*1000 or 25*1000 + self.BombtrackThreshold=(distance or 25)*1000 + return self end ---- Set range location. If this is not done, one (random) unit position of the range is used to determine the center of the range. +--- Set range location. If this is not done, one (random) unit position of the range is used to determine the location of the range. +-- The range location determines the position at which the weather data is evaluated. -- @param #RANGE self --- @param Core.Point#COORDINATE coordinate Coordinate of the center of the range. +-- @param Core.Point#COORDINATE coordinate Coordinate of the range. +-- @return #RANGE self function RANGE:SetRangeLocation(coordinate) self.location=coordinate + return self end --- Set range zone. For example, no bomb impact points are smoked if a bomb falls outside of this zone. -- If a zone is not explicitly specified, the range zone is determined by its location and radius. -- @param #RANGE self -- @param Core.Zone#ZONE zone MOOSE zone defining the range perimeters. -function RANGE:SetRangeLocation(zone) +-- @return #RANGE self +function RANGE:SetRangeZone(zone) self.rangezone=zone + return self end --- Set smoke color for marking bomb targets. By default bomb targets are marked by red smoke. -- @param #RANGE self -- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Red. +-- @return #RANGE self function RANGE:SetBombTargetSmokeColor(colorid) self.BombSmokeColor=colorid or SMOKECOLOR.Red + return self +end + +--- Set score bomb distance. +-- @param #RANGE self +-- @param #number distance Distance in meters. Default 1000 m. +-- @return #RANGE self +function RANGE:SetScoreBombDistance(distance) + self.scorebombdistance=distance or 1000 + return self end --- Set smoke color for marking strafe targets. By default strafe targets are marked by green smoke. -- @param #RANGE self -- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.Green. +-- @return #RANGE self function RANGE:SetStrafeTargetSmokeColor(colorid) self.StrafeSmokeColor=colorid or SMOKECOLOR.Green + return self end --- Set smoke color for marking strafe pit approach boxes. By default strafe pit boxes are marked by white smoke. -- @param #RANGE self -- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default SMOKECOLOR.White. +-- @return #RANGE self function RANGE:SetStrafePitSmokeColor(colorid) self.StrafePitSmokeColor=colorid or SMOKECOLOR.White + return self end --- Set time delay between bomb impact and starting to smoke the impact point. -- @param #RANGE self -- @param #number delay Time delay in seconds. Default is 3 seconds. +-- @return #RANGE self function RANGE:SetSmokeTimeDelay(delay) self.TdelaySmoke=delay or RANGE.Defaults.TdelaySmoke + return self end --- Enable debug modus. -- @param #RANGE self +-- @return #RANGE self function RANGE:DebugON() self.Debug=true + return self end --- Disable debug modus. -- @param #RANGE self +-- @return #RANGE self function RANGE:DebugOFF() self.Debug=false + return self end +--- Disable ALL messages to players. +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetMessagesOFF() + self.messages=false + return self +end + +--- Enable messages to players. This is the default +-- @param #RANGE self +-- @return #RANGE self +function RANGE:SetMessagesON() + self.messages=true + return self +end + + --- Enables tracking of all bomb types. Note that this is the default setting. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackBombsON() self.trackbombs=true + return self end --- Disables tracking of all bomb types. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackBombsOFF() self.trackbombs=false + return self end --- Enables tracking of all rocket types. Note that this is the default setting. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackRocketsON() self.trackrockets=true + return self end --- Disables tracking of all rocket types. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackRocketsOFF() self.trackrockets=false + return self end --- Enables tracking of all missile types. Note that this is the default setting. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackMissilesON() self.trackmissiles=true + return self end --- Disables tracking of all missile types. -- @param #RANGE self +-- @return #RANGE self function RANGE:TrackMissilesOFF() self.trackmissiles=false + return self end +--- Enable range control and set frequency. +-- @param #RANGE self +-- @param #number frequency Frequency in MHz. Default 256 MHz. +-- @return #RANGE self +function RANGE:SetRangeControl(frequency) + self.rangecontrolfreq=frequency or 256 + return self +end + +--- Enable instructor radio and set frequency. +-- @param #RANGE self +-- @param #number frequency Frequency in MHz. Default 305 MHz. +-- @return #RANGE self +function RANGE:SetInstructorRadio(frequency) + self.instructorfreq=frequency or 305 + return self +end + +--- Set sound files folder within miz file. +-- @param #RANGE self +-- @param #string path Path for sound files. Default "ATIS Soundfiles/". Mind the slash "/" at the end! +-- @return #RANGE self +function RANGE:SetSoundfilesPath(path) + self.soundpath=tostring(path or "Range Soundfiles/") + self:I(self.id..string.format("Setting sound files path to %s", self.soundpath)) + return self +end + --- Add new strafe pit. For a strafe pit, hits from guns are counted. One pit can consist of several units. -- Note, an approach is only valid, if the player enters via a zone in front of the pit, which defined by boxlength and boxheading. -- Furthermore, the player must not be too high and fly in the direction of the pit to make a valid target apporoach. @@ -563,47 +1103,48 @@ end -- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false. -- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. -- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. +-- @return #RANGE self function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) self:F({targetnames=targetnames, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) - -- Create table if necessary. + -- Create table if necessary. if type(targetnames) ~= "table" then targetnames={targetnames} end - + -- Make targets local _targets={} local center=nil --Wrapper.Unit#UNIT local ntargets=0 - + for _i,_name in ipairs(targetnames) do - + -- Check if we have a static or unit object. local _isstatic=self:_CheckStatic(_name) - local unit=nil + local unit=nil if _isstatic==true then - + -- Add static object. - self:T(RANGE.id..string.format("Adding STATIC object %s as strafe target #%d.", _name, _i)) + self:T(self.id..string.format("Adding STATIC object %s as strafe target #%d.", _name, _i)) unit=STATIC:FindByName(_name, false) - + elseif _isstatic==false then - + -- Add unit object. - self:T(RANGE.id..string.format("Adding UNIT object %s as strafe target #%d.", _name, _i)) + self:T(self.id..string.format("Adding UNIT object %s as strafe target #%d.", _name, _i)) unit=UNIT:FindByName(_name) - + else - + -- Neither unit nor static object with this name could be found. local text=string.format("ERROR! Could not find ANY strafe target object with name %s.", _name) - self:E(RANGE.id..text) + self:E(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) - + end - - -- Add object to targets. + + -- Add object to targets. if unit then table.insert(_targets, unit) -- Define center as the first unit we find @@ -612,24 +1153,24 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe end ntargets=ntargets+1 end - + end - + -- Check if at least one target could be found. if ntargets==0 then local text=string.format("ERROR! No strafe target could be found when calling RANGE:AddStrafePit() for range %s", self.rangename) - self:E(RANGE.id..text) + self:E(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) - return + return end -- Approach box dimensions. local l=boxlength or RANGE.Defaults.boxlength local w=(boxwidth or RANGE.Defaults.boxwidth)/2 - + -- Heading: either manually entered or automatically taken from unit heading. local heading=heading or center:GetHeading() - + -- Invert the heading since some units point in the "wrong" direction. In particular the strafe pit from 476th range objects. if inverseheading ~= nil then if inverseheading then @@ -642,44 +1183,56 @@ function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inversehe if heading>360 then heading=heading-360 end - + -- Number of hits called a "good" pass. goodpass=goodpass or RANGE.Defaults.goodpass - + -- Foule line distance. foulline=foulline or RANGE.Defaults.foulline - + -- Coordinate of the range. local Ccenter=center:GetCoordinate() - + -- Name of the target defined as its unit name. local _name=center:GetName() - -- Points defining the approach area. + -- Points defining the approach area. local p={} p[#p+1]=Ccenter:Translate( w, heading+90) p[#p+1]= p[#p]:Translate( l, heading) p[#p+1]= p[#p]:Translate(2*w, heading-90) p[#p+1]= p[#p]:Translate( -l, heading) - + local pv2={} for i,p in ipairs(p) do pv2[i]={x=p.x, y=p.z} end - + -- Create polygon zone. local _polygon=ZONE_POLYGON_BASE:New(_name, pv2) - + -- Create tires --_polygon:BoundZone() - + + local st={} --#RANGE.StrafeTarget + st.name=_name + st.polygon=_polygon + st.coordinate=Ccenter + st.goodPass=goodpass + st.targets=_targets + st.foulline=foulline + st.smokepoints=p + st.heading=heading + -- Add zone to table. - table.insert(self.strafeTargets, {name=_name, polygon=_polygon, coordinate= Ccenter, goodPass=goodpass, targets=_targets, foulline=foulline, smokepoints=p, heading=heading}) - + table.insert(self.strafeTargets, st) + -- Debug info - local text=string.format("Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, l, w, goodpass, foulline) - self:T(RANGE.id..text) + local text=string.format("Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, l, w, goodpass, foulline) + self:T(self.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) + + return self end @@ -695,31 +1248,33 @@ end -- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false. -- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. -- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. +-- @return #RANGE self function RANGE:AddStrafePitGroup(group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) self:F({group=group, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) if group and group:IsAlive() then - + -- Get units of group. local _units=group:GetUnits() - + -- Make table of unit names. local _names={} for _,_unit in ipairs(_units) do - + local _unit=_unit --Wrapper.Unit#UNIT - + if _unit and _unit:IsAlive() then local _name=_unit:GetName() table.insert(_names,_name) end - + end - + -- Add strafe pit. - self:AddStrafePit(_names, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) + self:AddStrafePit(_names, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) end + return self end --- Add bombing target(s) to range. @@ -727,6 +1282,7 @@ end -- @param #table targetnames Table containing names of unit or static objects serving as bomb targets. -- @param #number goodhitrange (Optional) Max distance from target unit (in meters) which is considered as a good hit. Default is 25 m. -- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +-- @return #RANGE self function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) self:F({targetnames=targetnames, goodhitrange=goodhitrange, randommove=randommove}) @@ -734,28 +1290,30 @@ function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) if type(targetnames) ~= "table" then targetnames={targetnames} end - + -- Default range is 25 m. goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange - + for _,name in pairs(targetnames) do - + -- Check if we have a static or unit object. local _isstatic=self:_CheckStatic(name) - + if _isstatic==true then local _static=STATIC:FindByName(name) - self:T2(RANGE.id..string.format("Adding static bombing target %s with hit range %d.", name, goodhitrange, false)) + self:T2(self.id..string.format("Adding static bombing target %s with hit range %d.", name, goodhitrange, false)) self:AddBombingTargetUnit(_static, goodhitrange) elseif _isstatic==false then local _unit=UNIT:FindByName(name) - self:T2(RANGE.id..string.format("Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove)) + self:T2(self.id..string.format("Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove)) self:AddBombingTargetUnit(_unit, goodhitrange) else - self:E(RANGE.id..string.format("ERROR! Could not find bombing target %s.", name)) + self:E(self.id..string.format("ERROR! Could not find bombing target %s.", name)) end - + end + + return self end --- Add a unit or static object as bombing target. @@ -763,40 +1321,80 @@ end -- @param Wrapper.Positionable#POSITIONABLE unit Positionable (unit or static) of the strafe target. -- @param #number goodhitrange Max distance from unit which is considered as a good hit. -- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +-- @return #RANGE self function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) self:F({unit=unit, goodhitrange=goodhitrange, randommove=randommove}) - - -- Get name of positionable. + + -- Get name of positionable. local name=unit:GetName() - + -- Check if we have a static or unit object. local _isstatic=self:_CheckStatic(name) - + -- Default range is 25 m. goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange -- Set randommove to false if it was not specified. if randommove==nil or _isstatic==true then randommove=false - end - + end + -- Debug or error output. if _isstatic==true then - self:T(RANGE.id..string.format("Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) + self:I(self.id..string.format("Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) elseif _isstatic==false then - self:T(RANGE.id..string.format("Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) + self:I(self.id..string.format("Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove))) else - self:E(RANGE.id..string.format("ERROR! No bombing target with name %s could be found. Carefully check all UNIT and STATIC names defined in the mission editor!", name)) + self:E(self.id..string.format("ERROR! No bombing target with name %s could be found. Carefully check all UNIT and STATIC names defined in the mission editor!", name)) end - + -- Get max speed of unit in km/h. local speed=0 if _isstatic==false then speed=self:_GetSpeed(unit) end - + + local target={} --#RANGE.BombTarget + target.name=name + target.target=unit + target.goodhitrange=goodhitrange + target.move=randommove + target.speed=speed + target.coordinate=unit:GetCoordinate() + if _isstatic then + target.type=RANGE.TargetType.STATIC + else + target.type=RANGE.TargetType.UNIT + end + -- Insert target to table. - table.insert(self.bombingTargets, {name=name, target=unit, goodhitrange=goodhitrange, move=randommove, speed=speed}) + table.insert(self.bombingTargets, target) + + return self +end + + +--- Add a coordinate of a bombing target. This +-- @param #RANGE self +-- @param Core.Point#COORDINATE coord The coordinate. +-- @param #string name Name of target. +-- @param #number goodhitrange Max distance from unit which is considered as a good hit. +-- @return #RANGE self +function RANGE:AddBombingTargetCoordinate(coord, name, goodhitrange) + + local target={} --#RANGE.BombTarget + target.name=name or "Bomb Target" + target.target=nil + target.goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange + target.move=false + target.speed=0 + target.coordinate=coord + target.type=RANGE.TargetType.COORD + + -- Insert target to table. + table.insert(self.bombingTargets, target) + + return self end --- Add all units of a group as bombing targets. @@ -804,20 +1402,22 @@ end -- @param Wrapper.Group#GROUP group Group of bombing targets. -- @param #number goodhitrange Max distance from unit which is considered as a good hit. -- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +-- @return #RANGE self function RANGE:AddBombingTargetGroup(group, goodhitrange, randommove) self:F({group=group, goodhitrange=goodhitrange, randommove=randommove}) - + if group then - + local _units=group:GetUnits() - + for _,_unit in pairs(_units) do if _unit and _unit:IsAlive() then self:AddBombingTargetUnit(_unit, goodhitrange, randommove) end end end - + + return self end --- Measures the foule line distance between two unit or static objects. @@ -828,10 +1428,10 @@ end function RANGE:GetFoullineDistance(namepit, namefoulline) self:F({namepit=namepit, namefoulline=namefoulline}) - -- Check if we have units or statics. + -- Check if we have units or statics. local _staticpit=self:_CheckStatic(namepit) local _staticfoul=self:_CheckStatic(namefoulline) - + -- Get the unit or static pit object. local pit=nil if _staticpit==true then @@ -839,9 +1439,9 @@ function RANGE:GetFoullineDistance(namepit, namefoulline) elseif _staticpit==false then pit=UNIT:FindByName(namepit) else - self:E(RANGE.id..string.format("ERROR! Pit object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namepit)) + self:E(self.id..string.format("ERROR! Pit object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namepit)) end - + -- Get the unit or static foul line object. local foul=nil if _staticfoul==true then @@ -849,23 +1449,24 @@ function RANGE:GetFoullineDistance(namepit, namefoulline) elseif _staticfoul==false then foul=UNIT:FindByName(namefoulline) else - self:E(RANGE.id..string.format("ERROR! Foul line object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namefoulline)) + self:E(self.id..string.format("ERROR! Foul line object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namefoulline)) end - + -- Get the distance between the two objects. local fouldist=0 if pit~=nil and foul~=nil then fouldist=pit:GetCoordinate():Get2DDistance(foul:GetCoordinate()) else - self:E(RANGE.id..string.format("ERROR! Foul line distance could not be determined. Check pit object name %s and foul line object name %s in the ME.", namepit, namefoulline)) + self:E(self.id..string.format("ERROR! Foul line distance could not be determined. Check pit object name %s and foul line object name %s in the ME.", namepit, namefoulline)) end - self:T(RANGE.id..string.format("Foul line distance = %.1f m.", fouldist)) + self:T(self.id..string.format("Foul line distance = %.1f m.", fouldist)) return fouldist end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event Handling +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- General event handler. -- @param #RANGE self @@ -889,49 +1490,49 @@ function RANGE:onEvent(Event) local EventData={} local _playerunit=nil local _playername=nil - + if Event.initiator then EventData.IniUnitName = Event.initiator:getName() EventData.IniDCSGroup = Event.initiator:getGroup() EventData.IniGroupName = Event.initiator:getGroup():getName() - -- Get player unit and name. This returns nil,nil if the event was not fired by a player unit. And these are the only events we are interested in. - _playerunit, _playername = self:_GetPlayerUnitAndName(EventData.IniUnitName) + -- Get player unit and name. This returns nil,nil if the event was not fired by a player unit. And these are the only events we are interested in. + _playerunit, _playername = self:_GetPlayerUnitAndName(EventData.IniUnitName) end - if Event.target then + if Event.target then EventData.TgtUnitName = Event.target:getName() EventData.TgtUnit = UNIT:FindByName(EventData.TgtUnitName) end - + if Event.weapon then EventData.Weapon = Event.weapon EventData.weapon = Event.weapon EventData.WeaponTypeName = Event.weapon:getTypeName() - end - + end + -- Event info. - self:T3(RANGE.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) - self:T3(RANGE.id..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) - self:T3(RANGE.id..string.format("EVENT: Ini group = %s" , tostring(EventData.IniGroupName))) - self:T3(RANGE.id..string.format("EVENT: Ini player = %s" , tostring(_playername))) - self:T3(RANGE.id..string.format("EVENT: Tgt unit = %s" , tostring(EventData.TgtUnitName))) - self:T3(RANGE.id..string.format("EVENT: Wpn type = %s" , tostring(EventData.WeaponTypeName))) - + self:T3(self.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) + self:T3(self.id..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) + self:T3(self.id..string.format("EVENT: Ini group = %s" , tostring(EventData.IniGroupName))) + self:T3(self.id..string.format("EVENT: Ini player = %s" , tostring(_playername))) + self:T3(self.id..string.format("EVENT: Tgt unit = %s" , tostring(EventData.TgtUnitName))) + self:T3(self.id..string.format("EVENT: Wpn type = %s" , tostring(EventData.WeaponTypeName))) + -- Call event Birth function. if Event.id==world.event.S_EVENT_BIRTH and _playername then self:OnEventBirth(EventData) end - + -- Call event Shot function. if Event.id==world.event.S_EVENT_SHOT and _playername and Event.weapon then self:OnEventShot(EventData) end - + -- Call event Hit function. if Event.id==world.event.S_EVENT_HIT and _playername and DCStgtunit then self:OnEventHit(EventData) end - + end @@ -940,49 +1541,53 @@ end -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventBirth(EventData) self:F({eventbirth = EventData}) - - local _unitName=EventData.IniUnitName + + local _unitName=EventData.IniUnitName local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) - - self:T3(RANGE.id.."BIRTH: unit = "..tostring(EventData.IniUnitName)) - self:T3(RANGE.id.."BIRTH: group = "..tostring(EventData.IniGroupName)) - self:T3(RANGE.id.."BIRTH: player = "..tostring(_playername)) - + + self:T3(self.id.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.id.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T3(self.id.."BIRTH: player = "..tostring(_playername)) + if _unit and _playername then - + local _uid=_unit:GetID() local _group=_unit:GetGroup() local _gid=_group:GetID() local _callsign=_unit:GetCallsign() - + -- Debug output. local text=string.format("Player %s, callsign %s entered unit %s (UID %d) of group %s (GID %d)", _playername, _callsign, _unitName, _uid, _group:GetName(), _gid) - self:T(RANGE.id..text) + self:T(self.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) - - self:_GetAmmo(_unitName) - + -- Reset current strafe status. self.strafeStatus[_uid] = nil - - -- Add Menu commands. - self:_AddF10Commands(_unitName) - + + -- Add Menu commands after a delay of 0.1 seconds. + SCHEDULER:New(nil, self._AddF10Commands, {self,_unitName}, 0.1) + -- By default, some bomb impact points and do not flare each hit on target. - self.PlayerSettings[_playername]={} - self.PlayerSettings[_playername].smokebombimpact=true + self.PlayerSettings[_playername]={} --#RANGE.PlayerData + self.PlayerSettings[_playername].smokebombimpact=self.defaultsmokebomb self.PlayerSettings[_playername].flaredirecthits=false self.PlayerSettings[_playername].smokecolor=SMOKECOLOR.Blue self.PlayerSettings[_playername].flarecolor=FLARECOLOR.Red self.PlayerSettings[_playername].delaysmoke=true - + self.PlayerSettings[_playername].messages=true + self.PlayerSettings[_playername].client=CLIENT:FindByName(_unitName, nil, true) + self.PlayerSettings[_playername].unitname=_unitName + self.PlayerSettings[_playername].playername=_playername + self.PlayerSettings[_playername].airframe=EventData.IniUnit:GetTypeName() + self.PlayerSettings[_playername].inzone=false + -- Start check in zone timer. if self.planes[_uid] ~= true then SCHEDULER:New(nil, self._CheckInZone, {self, EventData.IniUnitName}, 1, 1) self.planes[_uid] = true end - - end + + end end --- Range event handler for event hit. @@ -990,11 +1595,11 @@ end -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventHit(EventData) self:F({eventhit = EventData}) - + -- Debug info. - self:T3(RANGE.id.."HIT: Ini unit = "..tostring(EventData.IniUnitName)) - self:T3(RANGE.id.."HIT: Ini group = "..tostring(EventData.IniGroupName)) - self:T3(RANGE.id.."HIT: Tgt target = "..tostring(EventData.TgtUnitName)) + self:T3(self.id.."HIT: Ini unit = "..tostring(EventData.IniUnitName)) + self:T3(self.id.."HIT: Ini group = "..tostring(EventData.IniGroupName)) + self:T3(self.id.."HIT: Tgt target = "..tostring(EventData.TgtUnitName)) -- Player info local _unitName = EventData.IniUnitName @@ -1002,135 +1607,153 @@ function RANGE:OnEventHit(EventData) if _unit==nil or _playername==nil then return end - + -- Unit ID local _unitID = _unit:GetID() -- Target local target = EventData.TgtUnit local targetname = EventData.TgtUnitName - + -- Current strafe target of player. local _currentTarget = self.strafeStatus[_unitID] -- Player has rolled in on a strafing target. if _currentTarget and target:IsAlive() then - + local playerPos = _unit:GetCoordinate() local targetPos = target:GetCoordinate() -- Loop over valid targets for this run. for _,_target in pairs(_currentTarget.zone.targets) do - + -- Check the the target is the same that was actually hit. if _target and _target:IsAlive() and _target:GetName() == targetname then - + -- Get distance between player and target. local dist=playerPos:Get2DDistance(targetPos) - - if dist > _currentTarget.zone.foulline then + + if dist > _currentTarget.zone.foulline then -- Increase hit counter of this run. _currentTarget.hits = _currentTarget.hits + 1 - + -- Flare target. if _unit and _playername and self.PlayerSettings[_playername].flaredirecthits then targetPos:Flare(self.PlayerSettings[_playername].flarecolor) end else -- Too close to the target. - if _currentTarget.pastfoulline==false and _unit and _playername then + if _currentTarget.pastfoulline==false and _unit and _playername then local _d=_currentTarget.zone.foulline local text=string.format("%s, Invalid hit!\nYou already passed foul line distance of %d m for target %s.", self:_myname(_unitName), _d, targetname) - self:_DisplayMessageToGroup(_unit, text, 10) - self:T2(RANGE.id..text) + self:_DisplayMessageToGroup(_unit, text) + self:T2(self.id..text) _currentTarget.pastfoulline=true end end - + end end end - + -- Bombing Targets for _,_bombtarget in pairs(self.bombingTargets) do - + local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE - + -- Check if one of the bomb targets was hit. if _target and _target:IsAlive() and _bombtarget.name == targetname then - + if _unit and _playername then - + -- Position of target. local targetPos = _target:GetCoordinate() - + -- Message to player. --local text=string.format("%s, direct hit on target %s.", self:_myname(_unitName), targetname) --self:DisplayMessageToGroup(_unit, text, 10, true) - + -- Flare target. if self.PlayerSettings[_playername].flaredirecthits then targetPos:Flare(self.PlayerSettings[_playername].flarecolor) end - + end end end end ---- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). +--- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun). -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventShot(EventData) self:F({eventshot = EventData}) - + + -- Nil checks. + if EventData.Weapon==nil then + return + end + if EventData.IniDCSUnit==nil then + return + end + -- Weapon data. local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName - local _weaponStrArray = self:_split(_weapon,"%.") + local _weaponStrArray = UTILS.Split(_weapon,"%.") local _weaponName = _weaponStrArray[#_weaponStrArray] - + + -- Weapon descriptor. + local desc=EventData.Weapon:getDesc() + + -- Weapon category: 0=SHELL, 1=MISSILE, 2=ROCKET, 3=BOMB (Weapon.Category.X) + local weaponcategory=desc.category + -- Debug info. - self:T(RANGE.id.."EVENT SHOT: Range "..self.rangename) - self:T(RANGE.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) - self:T(RANGE.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) - self:T(RANGE.id.."EVENT SHOT: Weapon type = ".._weapon) - self:T(RANGE.id.."EVENT SHOT: Weapon name = ".._weaponName) - + self:T(self.id.."EVENT SHOT: Range "..self.rangename) + self:T(self.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) + self:T(self.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) + self:T(self.id.."EVENT SHOT: Weapon type = ".._weapon) + self:T(self.id.."EVENT SHOT: Weapon name = ".._weaponName) + self:T(self.id.."EVENT SHOT: Weapon cate = "..weaponcategory) + -- Special cases: - local _viggen=string.match(_weapon, "ROBOT") or string.match(_weapon, "RB75") or string.match(_weapon, "BK90") or string.match(_weapon, "RB15") or string.match(_weapon, "RB04") - + --local _viggen=string.match(_weapon, "ROBOT") or string.match(_weapon, "RB75") or string.match(_weapon, "BK90") or string.match(_weapon, "RB15") or string.match(_weapon, "RB04") + -- Tracking conditions for bombs, rockets and missiles. - local _bombs=string.match(_weapon, "weapons.bombs") - local _rockets=string.match(_weapon, "weapons.nurs") - local _missiles=string.match(_weapon, "weapons.missiles") or _viggen - + local _bombs = weaponcategory==Weapon.Category.BOMB --string.match(_weapon, "weapons.bombs") + local _rockets = weaponcategory==Weapon.Category.ROCKET --string.match(_weapon, "weapons.nurs") + local _missiles = weaponcategory==Weapon.Category.MISSILE --string.match(_weapon, "weapons.missiles") or _viggen + -- Check if any condition applies here. local _track = (_bombs and self.trackbombs) or (_rockets and self.trackrockets) or (_missiles and self.trackmissiles) - + -- Get unit name. local _unitName = EventData.IniUnitName - + -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) -- Set this to larger value than the threshold. local dPR=self.BombtrackThreshold*2 - - -- Distance player to range. + + -- Distance player to range. if _unit and _playername then dPR=_unit:GetCoordinate():Get2DDistance(self.location) - self:T(RANGE.id..string.format("Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR/1000)) + self:T(self.id..string.format("Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR/1000)) end - -- Only track if distance player to range is < 25 km. - if _track and dPR<=self.BombtrackThreshold then + -- Only track if distance player to range is < 25 km. Also check that a player shot. No need to track AI weapons. + if _track and dPR<=self.BombtrackThreshold and _unit and _playername then + + -- Player data. + local playerData=self.PlayerSettings[_playername] --#RANGE.PlayerData -- Tracking info and init of last bomb position. - self:T(RANGE.id..string.format("RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName())) - + self:T(self.id..string.format("RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName())) + -- Init bomb position. local _lastBombPos = {x=0,y=0,z=0} - + -- Function monitoring the position of a bomb until impact. local function trackBomb(_ordnance) @@ -1140,55 +1763,70 @@ function RANGE:OnEventShot(EventData) return _ordnance:getPoint() end) - self:T3(RANGE.id..string.format("Range %s: Bomb still in air: %s", self.rangename, tostring(_status))) + self:T2(self.id..string.format("Range %s: Bomb still in air: %s", self.rangename, tostring(_status))) if _status then - - -- Still in the air. Remember this position. + + ---------------------------- + -- Weapon is still in air -- + ---------------------------- + + -- Remember this position. _lastBombPos = {x = _bombPos.x, y = _bombPos.y, z= _bombPos.z } - -- Check again in 0.005 seconds. + -- Check again in ~0.005 seconds ==> 200 checks per second. return timer.getTime() + self.dtBombtrack - + else - - -- Bomb did hit the ground. + + ----------------------------- + -- Bomb did hit the ground -- + ----------------------------- + -- Get closet target to last position. - local _closetTarget = nil - local _distance = nil - local _hitquality = "POOR" - + local _closetTarget=nil --#RANGE.BombTarget + local _distance=nil + local _closeCoord=nil + local _hitquality="POOR" + -- Get callsign. local _callsign=self:_myname(_unitName) - + -- Coordinate of impact point. local impactcoord=COORDINATE:NewFromVec3(_lastBombPos) - - -- Distance from range. We dont want to smoke targets outside of the range. - local impactdist=impactcoord:Get2DDistance(self.location) - + + -- Check if impact happened in range zone. + local insidezone=self.rangezone:IsCoordinateInZone(impactcoord) + + -- Impact point of bomb. + if self.Debug then + impactcoord:MarkToAll("Bomb impact point") + end + -- Smoke impact point of bomb. - if self.PlayerSettings[_playername].smokebombimpact and impactdist%.1f km). No score!", _callsign, self.scorebombdistance/1000) + self:_DisplayMessageToGroup(_unit, _message, nil, false) + + if self.rangecontrol then + self.rangecontrol:NewTransmission(RANGE.Sound.RCWeaponImpactedTooFar.filename, RANGE.Sound.RCWeaponImpactedTooFar.duration, self.soundpath, nil, nil, _message, self.subduration) + end + + else + self:T(self.id.."Weapon impacted outside range zone.") end - + --Terminate the timer - self:T(RANGE.id..string.format("Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername)) + self:T(self.id..string.format("Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername)) return nil end -- _status check - + end -- end function trackBomb -- Weapon is not yet "alife" just yet. Start timer in one second. - self:T(RANGE.id..string.format("Range %s, player %s: Tracking of weapon starts in one second.", self.rangename, _playername)) - timer.scheduleFunction(trackBomb, EventData.weapon, timer.getTime() + 1.0) - + self:T(self.id..string.format("Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername)) + timer.scheduleFunction(trackBomb, EventData.weapon, timer.getTime()+0.1) + end --if _track (string.match) and player-range distance < threshold. + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check spawn queue and spawn aircraft if necessary. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterStatus(From, Event, To) + + -- Check range status. + self:I(self.id..string.format("Range status: %s", self:GetState())) + + -- Check player status. + self:_CheckPlayers() + + -- Check back in ~10 seconds. + self:__Status(-10) +end + +--- Function called after player enters the range zone. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #RANGE.PlayerData player Player data. +function RANGE:onafterEnterRange(From, Event, To, player) + + if self.instructor and self.rangecontrol then + + -- Range control radio frequency split. + local RF=UTILS.Split(string.format("%.3f", self.rangecontrolfreq), ".") + + -- Radio message that player entered the range + self.instructor:NewTransmission(RANGE.Sound.IREnterRange.filename, RANGE.Sound.IREnterRange.duration, self.soundpath) + self.instructor:Number2Transmission(RF[1]) + if tonumber(RF[2])>0 then + self.instructor:NewTransmission(RANGE.Sound.IRDecimal.filename, RANGE.Sound.IRDecimal.duration, self.soundpath) + self.instructor:Number2Transmission(RF[2]) + end + self.instructor:NewTransmission(RANGE.Sound.IRMegaHertz.filename, RANGE.Sound.IRMegaHertz.duration, self.soundpath) + end end +--- Function called after player leaves the range zone. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #RANGE.PlayerData player Player data. +function RANGE:onafterExitRange(From, Event, To, player) + + if self.instructor then + self.instructor:NewTransmission(RANGE.Sound.IRExitRange.filename, RANGE.Sound.IRExitRange.duration, self.soundpath) + end + +end + + +--- Function called after bomb impact on range. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #RANGE.BombResult result Result of bomb impact. +-- @param #RANGE.PlayerData player Player data table. +function RANGE:onafterImpact(From, Event, To, result, player) + + -- Only display target name if there is more than one bomb target. + local targetname=nil + if #self.bombingTargets>1 then + local targetname=result.name + end + + -- Send message to player. + local text=string.format("%s, impact %03d° for %d ft", player.playername, result.radial, UTILS.MetersToFeet(result.distance)) + if targetname then + text=text..string.format(" from bulls of target %s.") + else + text=text.."." + end + text=text..string.format(" %s hit.", result.quality) + + if self.rangecontrol then + self.rangecontrol:NewTransmission(RANGE.Sound.RCImpact.filename, RANGE.Sound.RCImpact.duration, self.soundpath, nil, nil, text, self.subduration) + self.rangecontrol:Number2Transmission(string.format("%03d", result.radial), nil, 0.1) + self.rangecontrol:NewTransmission(RANGE.Sound.RCDegrees.filename, RANGE.Sound.RCDegrees.duration, self.soundpath) + self.rangecontrol:NewTransmission(RANGE.Sound.RCFor.filename, RANGE.Sound.RCFor.duration, self.soundpath) + self.rangecontrol:Number2Transmission(string.format("%d", UTILS.MetersToFeet(result.distance))) + self.rangecontrol:NewTransmission(RANGE.Sound.RCFeet.filename, RANGE.Sound.RCFeet.duration, self.soundpath) + if result.quality=="POOR" then + self.rangecontrol:NewTransmission(RANGE.Sound.RCPoorHit.filename, RANGE.Sound.RCPoorHit.duration, self.soundpath, nil, 0.5) + elseif result.quality=="INEFFECTIVE" then + self.rangecontrol:NewTransmission(RANGE.Sound.RCIneffectiveHit.filename, RANGE.Sound.RCIneffectiveHit.duration, self.soundpath, nil, 0.5) + elseif result.quality=="GOOD" then + self.rangecontrol:NewTransmission(RANGE.Sound.RCGoodHit.filename, RANGE.Sound.RCGoodHit.duration, self.soundpath, nil, 0.5) + elseif result.quality=="EXCELLENT" then + self.rangecontrol:NewTransmission(RANGE.Sound.RCExcellentHit.filename, RANGE.Sound.RCExcellentHit.duration, self.soundpath, nil, 0.5) + end + + end + + -- Unit. + local unit=UNIT:FindByName(player.unitname) + + -- Send message. + self:_DisplayMessageToGroup(unit, text, nil, true) + self:T(self.id..text) + + -- Save results. + if self.autosave then + self:Save() + end + +end + +--- Function called before save event. Checks that io and lfs are desanitized. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onbeforeSave(From, Event, To) + if io and lfs then + return true + else + self:E(self.id..string.format("WARNING: io and/or lfs not desanitized. Cannot save player results.")) + return false + end +end + +--- Function called after save. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterSave(From, Event, To) + + local function _savefile(filename, data) + local f=io.open(filename, "wb") + if f then + f:write(data) + f:close() + self:I(self.id..string.format("Saving player results to file %s", tostring(filename))) + else + self:E(self.id..string.format("ERROR: Could not save results to file %s", tostring(filename))) + end + end + + -- Path. + local path=lfs.writedir()..[[Logs\]] + + -- Set file name. + local filename=path..string.format("RANGE-%s_BombingResults.csv", self.rangename) + + -- Header line. + local scores="Name,Pass,Target,Distance,Radial,Quality,Weapon,Airframe,Mission Time" + + -- Loop over all players. + for playername,results in pairs(self.bombPlayerResults) do + + -- Loop over player grades table. + for i,_result in pairs(results) do + local result=_result --#RANGE.BombResult + local distance=result.distance + local weapon=result.weapon + local target=result.name + local radial=result.radial + local quality=result.quality + local time=UTILS.SecondsToClock(result.time) + local airframe=result.airframe + local date="n/a" + if os then + date=os.date() + end + scores=scores..string.format("\n%s,%d,%s,%.2f,%03d,%s,%s,%s,%s,%s", playername, i, target, distance, radial, quality, weapon, airframe, time, date) + end + end + + _savefile(filename, scores) +end + +--- Function called before save event. Checks that io and lfs are desanitized. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onbeforeLoad(From, Event, To) + if io and lfs then + return true + else + self:E(self.id..string.format("WARNING: io and/or lfs not desanitized. Cannot load player results.")) + return false + end +end + +--- On after "Load" event. Loads results of all players from file. +-- @param #RANGE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RANGE:onafterLoad(From, Event, To) + + --- Function that load data from a file. + local function _loadfile(filename) + local f=io.open(filename, "rb") + if f then + --self:I(self.id..string.format("Loading player results from file %s", tostring(filename))) + local data=f:read("*all") + f:close() + return data + else + self:E(self.id..string.format("WARNING: Could not load player results from file %s. File might not exist just yet.", tostring(filename))) + return nil + end + end + + -- Path in DCS log file. + local path=lfs.writedir()..[[Logs\]] + + -- Set file name. + local filename=path..string.format("RANGE-%s_BombingResults.csv", self.rangename) + + -- Info message. + local text=string.format("Loading player bomb results from file %s", filename) + self:I(self.id..text) + + -- Load asset data from file. + local data=_loadfile(filename) + + if data then + + -- Split by line break. + local results=UTILS.Split(data,"\n") + + -- Remove first header line. + table.remove(results, 1) + + -- Init player scores table. + self.bombPlayerResults={} + + -- Loop over all lines. + for _,_result in pairs(results) do + + -- Parameters are separated by commata. + local resultdata=UTILS.Split(_result, ",") + + -- Grade table + local result={} --#RANGE.BombResult + + -- Player name. + local playername=resultdata[1] + result.player=playername + + -- Results data. + result.name=tostring(resultdata[3]) + result.distance=tonumber(resultdata[4]) + result.radial=tonumber(resultdata[5]) + result.quality=tostring(resultdata[6]) + result.weapon=tostring(resultdata[7]) + result.airframe=tostring(resultdata[8]) + result.time=UTILS.ClockToSeconds(resultdata[9] or "00:00:00") + result.date=resultdata[10] or "n/a" + + -- Create player array if necessary. + self.bombPlayerResults[playername]=self.bombPlayerResults[playername] or {} + + -- Add result to table. + table.insert(self.bombPlayerResults[playername], result) + end + end +end + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Display Messages +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Start smoking a coordinate with a delay. -- @param #table _args Argements passed. @@ -1258,58 +2186,58 @@ end -- @param #string _unitName Name of the player unit. function RANGE:_DisplayMyStrafePitResults(_unitName) self:F(_unitName) - + -- Get player unit and name local _unit,_playername = self:_GetPlayerUnitAndName(_unitName) - + if _unit and _playername then - + -- Message header. local _message = string.format("My Top %d Strafe Pit Results:\n", self.ndisplayresult) - + -- Get player results. local _results = self.strafePlayerResults[_playername] - + -- Create message. if _results == nil then -- No score yet. _message = string.format("%s: No Score yet.", _playername) else - + -- Sort results table wrt number of hits. local _sort = function( a,b ) return a.hits > b.hits end table.sort(_results,_sort) - + -- Prepare message of best results. local _bestMsg = "" local _count = 1 - + -- Loop over results for _,_result in pairs(_results) do - + -- Message text. _message = _message..string.format("\n[%d] Hits %d - %s - %s", _count, _result.hits, _result.zone.name, _result.text) - + -- Best result. - if _bestMsg == "" then + if _bestMsg == "" then _bestMsg = string.format("Hits %d - %s - %s", _result.hits, _result.zone.name, _result.text) end - + -- 10 runs if _count == self.ndisplayresult then break end - + -- Increase counter _count = _count+1 end - + -- Message text. _message = _message .."\n\nBEST: ".._bestMsg end - -- Send message to group. - self:_DisplayMessageToGroup(_unit, _message, nil, true) + -- Send message to group. + self:_DisplayMessageToGroup(_unit, _message, nil, true, true) end end @@ -1318,54 +2246,54 @@ end -- @param #string _unitName Name fo the player unit. function RANGE:_DisplayStrafePitResults(_unitName) self:F(_unitName) - + -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - + -- Check if we have a unit which is a player. if _unit and _playername then - + -- Results table. local _playerResults = {} - + -- Message text. local _message = string.format("Strafe Pit Results - Top %d Players:\n", self.ndisplayresult) - + -- Loop over player results. for _playerName,_results in pairs(self.strafePlayerResults) do - + -- Get the best result of the player. local _best = nil - for _,_result in pairs(_results) do + for _,_result in pairs(_results) do if _best == nil or _result.hits > _best.hits then _best = _result end end - - -- Add best result to table. + + -- Add best result to table. if _best ~= nil then local text=string.format("%s: Hits %i - %s - %s", _playerName, _best.hits, _best.zone.name, _best.text) table.insert(_playerResults,{msg = text, hits = _best.hits}) end - + end - + --Sort list! local _sort = function( a,b ) return a.hits > b.hits end table.sort(_playerResults,_sort) - + -- Add top 10 results. for _i = 1, math.min(#_playerResults, self.ndisplayresult) do _message = _message..string.format("\n[%d] %s", _i, _playerResults[_i].msg) end - + -- In case there are no scores yet. if #_playerResults<1 then _message = _message.."No player scored yet." end - + -- Send message. - self:_DisplayMessageToGroup(_unit, _message, nil, true) + self:_DisplayMessageToGroup(_unit, _message, nil, true, true) end end @@ -1375,54 +2303,52 @@ end function RANGE:_DisplayMyBombingResults(_unitName) self:F(_unitName) - -- Get player unit and name. + -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - + if _unit and _playername then - + -- Init message. local _message = string.format("My Top %d Bombing Results:\n", self.ndisplayresult) - + -- Results from player. local _results = self.bombPlayerResults[_playername] - + -- No score so far. if _results == nil then _message = _playername..": No Score yet." else - + -- Sort results wrt to distance. local _sort = function( a,b ) return a.distance < b.distance end table.sort(_results,_sort) - + -- Loop over results. local _bestMsg = "" - local _count = 1 - for _,_result in pairs(_results) do - + for i,_result in pairs(_results) do + local result=_result --#RANGE.BombResult + -- Message with name, weapon and distance. - _message = _message.."\n"..string.format("[%d] %d m - %s - %s - %s hit", _count, _result.distance, _result.name, _result.weapon, _result.quality) - + _message = _message.."\n"..string.format("[%d] %d m %03d° - %s - %s - %s hit", i, result.distance, result.radial, result.name, result.weapon, result.quality) + -- Store best/first result. if _bestMsg == "" then - _bestMsg = string.format("%d m - %s - %s - %s hit",_result.distance,_result.name,_result.weapon, _result.quality) + _bestMsg = string.format("%d m %03d° - %s - %s - %s hit", result.distance, result.radial, result.name, result.weapon, result.quality) end - + -- Best 10 runs only. - if _count == self.ndisplayresult then + if i==self.ndisplayresult then break end - - -- Increase counter. - _count = _count+1 + end - + -- Message. _message = _message .."\n\nBEST: ".._bestMsg end - + -- Send message. - self:_DisplayMessageToGroup(_unit, _message, nil, true) + self:_DisplayMessageToGroup(_unit, _message, nil, true, true) end end @@ -1431,22 +2357,22 @@ end -- @param #string _unitName Name of player unit. function RANGE:_DisplayBombingResults(_unitName) self:F(_unitName) - + -- Results table. local _playerResults = {} - + -- Get player unit and name. local _unit, _player = self:_GetPlayerUnitAndName(_unitName) - + -- Check if we have a unit with a player. if _unit and _player then - + -- Message header. local _message = string.format("Bombing Results - Top %d Players:\n", self.ndisplayresult) - + -- Loop over players. for _playerName,_results in pairs(self.bombPlayerResults) do - + -- Find best result of player. local _best = nil for _,_result in pairs(_results) do @@ -1454,31 +2380,31 @@ function RANGE:_DisplayBombingResults(_unitName) _best = _result end end - + -- Put best result of player into table. if _best ~= nil then local bestres=string.format("%s: %d m - %s - %s - %s hit", _playerName, _best.distance, _best.name, _best.weapon, _best.quality) table.insert(_playerResults, {msg = bestres, distance = _best.distance}) end - + end - + -- Sort list of player results. local _sort = function( a,b ) return a.distance < b.distance end table.sort(_playerResults,_sort) - + -- Loop over player results. - for _i = 1, math.min(#_playerResults, self.ndisplayresult) do + for _i = 1, math.min(#_playerResults, self.ndisplayresult) do _message = _message..string.format("\n[%d] %s", _i, _playerResults[_i].msg) end - + -- In case there are no scores yet. if #_playerResults<1 then _message = _message.."No player scored yet." end - + -- Send message. - self:_DisplayMessageToGroup(_unit, _message, nil, true) + self:_DisplayMessageToGroup(_unit, _message, nil, true, true) end end @@ -1490,28 +2416,33 @@ function RANGE:_DisplayRangeInfo(_unitname) -- Get player unit and player name. local unit, playername = self:_GetPlayerUnitAndName(_unitname) - + -- Check if we have a player. if unit and playername then - + -- Message text. local text="" - + -- Current coordinates. local coord=unit:GetCoordinate() - + if self.location then - + + local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS + -- Direction vector from current position (coord) to target (position). local position=self.location --Core.Point#COORDINATE + local bulls=position:ToStringBULLS(unit:GetCoalition(), settings) + local lldms=position:ToStringLLDMS(settings) + local llddm=position:ToStringLLDDM(settings) local rangealt=position:GetLandHeight() local vec3=coord:GetDirectionVec3(position) local angle=coord:GetAngleDegrees(vec3) local range=coord:Get2DDistance(position) - + -- Bearing string. local Bs=string.format('%03d°', angle) - + local texthit if self.PlayerSettings[playername].flaredirecthits then texthit=string.format("Flare direct hits: ON (flare color %s)\n", self:_flarecolor2text(self.PlayerSettings[playername].flarecolor)) @@ -1530,9 +2461,8 @@ function RANGE:_DisplayRangeInfo(_unitname) else textdelay=string.format("Smoke bomb delay: OFF") end - + -- Player unit settings. - local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS local trange=string.format("%.1f km", range/1000) local trangealt=string.format("%d m", rangealt) local tstrafemaxalt=string.format("%d m", self.strafemaxalt) @@ -1541,11 +2471,14 @@ function RANGE:_DisplayRangeInfo(_unitname) trangealt=string.format("%d feet", UTILS.MetersToFeet(rangealt)) tstrafemaxalt=string.format("%d feet", UTILS.MetersToFeet(self.strafemaxalt)) end - + -- Message. text=text..string.format("Information on %s:\n", self.rangename) text=text..string.format("-------------------------------------------------------\n") text=text..string.format("Bearing %s, Range %s\n", Bs, trange) + text=text..string.format("%s\n", bulls) + text=text..string.format("%s\n", lldms) + text=text..string.format("%s\n", llddm) text=text..string.format("Altitude ASL: %s\n", trangealt) text=text..string.format("Max strafing alt AGL: %s\n", tstrafemaxalt) text=text..string.format("# of strafe targets: %d\n", self.nstrafetargets) @@ -1553,12 +2486,12 @@ function RANGE:_DisplayRangeInfo(_unitname) text=text..texthit text=text..textbomb text=text..textdelay - + -- Send message to player group. - self:_DisplayMessageToGroup(unit, text, nil, true) - + self:_DisplayMessageToGroup(unit, text, nil, true, true) + -- Debug output. - self:T2(RANGE.id..text) + self:T2(self.id..text) end end end @@ -1571,28 +2504,38 @@ function RANGE:_DisplayBombTargets(_unitname) -- Get player unit and player name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitname) - + -- Check if we have a player. if _unit and _playername then - + -- Player settings. local _settings=_DATABASE:GetPlayerSettings(_playername) or _SETTINGS --Core.Settings#SETTINGS - + -- Message text. local _text="Bomb Target Locations:" - + for _,_bombtarget in pairs(self.bombingTargets) do - local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE - if _target and _target:IsAlive() then + local bombtarget=_bombtarget --#RANGE.BombTarget + + -- Coordinate of bombtarget. + local coord=self:_GetBombTargetCoordinate(bombtarget) + + if coord then - -- Core.Point#COORDINATE - local coord=_target:GetCoordinate() --Core.Point#COORDINATE - local mycoord=coord:ToStringA2G(_unit, _settings) - _text=_text..string.format("\n- %s: %s",_bombtarget.name, mycoord) + -- Get elevation + local elevation=coord:GetLandHeight() + local eltxt=string.format("%d m", elevation) + if _settings:IsImperial() then + elevation=UTILS.MetersToFeet(elevation) + eltxt=string.format("%d ft", elevation) + end + + local ca2g=coord:ToStringA2G(_unit,_settings) + _text=_text..string.format("\n- %s:\n%s @ %s", bombtarget.name or "unknown", ca2g, eltxt) end end - - self:_DisplayMessageToGroup(_unit,_text, nil, true) + + self:_DisplayMessageToGroup(_unit,_text, 60, true, true) end end @@ -1604,23 +2547,23 @@ function RANGE:_DisplayStrafePits(_unitname) -- Get player unit and player name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitname) - + -- Check if we have a player. if _unit and _playername then - + -- Player settings. local _settings=_DATABASE:GetPlayerSettings(_playername) or _SETTINGS --Core.Settings#SETTINGS - + -- Message text. local _text="Strafe Target Locations:" - + for _,_strafepit in pairs(self.strafeTargets) do local _target=_strafepit --Wrapper.Positionable#POSITIONABLE - + -- Pit parameters. local coord=_strafepit.coordinate --Core.Point#COORDINATE local heading=_strafepit.heading - + -- Turn heading around ==> approach heading. if heading>180 then heading=heading-180 @@ -1629,10 +2572,10 @@ function RANGE:_DisplayStrafePits(_unitname) end local mycoord=coord:ToStringA2G(_unit, _settings) - _text=_text..string.format("\n- %s: %s - heading %03d",_strafepit.name, mycoord, heading) + _text=_text..string.format("\n- %s: heading %03d°\n%s",_strafepit.name, heading, mycoord) end - - self:_DisplayMessageToGroup(_unit,_text, nil, true) + + self:_DisplayMessageToGroup(_unit,_text, nil, true, true) end end @@ -1645,44 +2588,44 @@ function RANGE:_DisplayRangeWeather(_unitname) -- Get player unit and player name. local unit, playername = self:_GetPlayerUnitAndName(_unitname) - + -- Check if we have a player. if unit and playername then - + -- Message text. local text="" - + -- Current coordinates. local coord=unit:GetCoordinate() - + if self.location then - + -- Get atmospheric data at range location. local position=self.location --Core.Point#COORDINATE local T=position:GetTemperature() local P=position:GetPressure() local Wd,Ws=position:GetWind() - + -- Get Beaufort wind scale. - local Bn,Bd=UTILS.BeaufortScale(Ws) - + local Bn,Bd=UTILS.BeaufortScale(Ws) + local WD=string.format('%03d°', Wd) local Ts=string.format("%d°C",T) - + local hPa2inHg=0.0295299830714 local hPa2mmHg=0.7500615613030 - + local settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#SETTINGS local tT=string.format("%d°C",T) local tW=string.format("%.1f m/s", Ws) local tP=string.format("%.1f mmHg", P*hPa2mmHg) if settings:IsImperial() then - tT=string.format("%d°F", UTILS.CelciusToFarenheit(T)) + --tT=string.format("%d°F", UTILS.CelciusToFarenheit(T)) tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) - tP=string.format("%.2f inHg", P*hPa2inHg) + tP=string.format("%.2f inHg", P*hPa2inHg) end - - + + -- Message text. text=text..string.format("Weather Report at %s:\n", self.rangename) text=text..string.format("--------------------------------------------------\n") @@ -1692,20 +2635,61 @@ function RANGE:_DisplayRangeWeather(_unitname) else text=string.format("No range location defined for range %s.", self.rangename) end - + -- Send message to player group. - self:_DisplayMessageToGroup(unit, text, nil, true) - + self:_DisplayMessageToGroup(unit, text, nil, true, true) + -- Debug output. - self:T2(RANGE.id..text) + self:T2(self.id..text) else - self:T(RANGE.id..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname)) - end + self:T(self.id..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname)) + end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Timer Functions +--- Check status of players. +-- @param #RANGE self +-- @param #string _unitName Name of player unit. +function RANGE:_CheckPlayers() + + for playername,_playersettings in pairs(self.PlayerSettings) do + local playersettings=_playersettings --#RANGE.PlayerData + + local unitname=playersettings.unitname + local unit=UNIT:FindByName(unitname) + + if unit and unit:IsAlive() then + + if unit:IsInZone(self.rangezone) then + + ------------------------------ + -- Player INSIDE Range Zone -- + ------------------------------ + + if not playersettings.inzone then + playersettings.inzone=true + self:EnterRange(playersettings) + end + + else + + ------------------------------- + -- Player OUTSIDE Range Zone -- + ------------------------------- + + if playersettings.inzone==true then + playersettings.inzone=false + self:ExitRange(playersettings) + end + + end + end + end + +end + --- Check if player is inside a strafing zone. If he is, we start looking for hits. If he was and left the zone again, the result is stored. -- @param #RANGE self -- @param #string _unitName Name of player unit. @@ -1724,171 +2708,221 @@ function RANGE:_CheckInZone(_unitName) local _currentStrafeRun = self.strafeStatus[_unitID] if _currentStrafeRun then -- player has already registered for a strafing run. - + -- Get the current approach zone and check if player is inside. local zone=_currentStrafeRun.zone.polygon --Core.Zone#ZONE_POLYGON_BASE - + local unitheading = _unit:GetHeading() local pitheading = _currentStrafeRun.zone.heading - 180 local deltaheading = unitheading-pitheading local towardspit = math.abs(deltaheading)<=90 or math.abs(deltaheading-360)<=90 - local unitalt=_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight() - + local unitalt=_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight() + -- Check if unit is inside zone and below max height AGL. local unitinzone=_unit:IsInZone(zone) and unitalt <= self.strafemaxalt and towardspit - + -- Debug output - local text=string.format("Checking stil in zone. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _unitName, _playername, tostring(unitinzone), unitalt, deltaheading) - self:T2(RANGE.id..text) - + local text=string.format("Checking still in zone. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _unitName, _playername, tostring(unitinzone), unitalt, deltaheading) + self:T2(self.id..text) + -- Check if player is in strafe zone and below max alt. - if unitinzone then - + if unitinzone then + -- Still in zone, keep counting hits. Increase counter. _currentStrafeRun.time = _currentStrafeRun.time+1 - + else - + -- Increase counter _currentStrafeRun.time = _currentStrafeRun.time+1 - + if _currentStrafeRun.time <= 3 then - + -- Reset current run. self.strafeStatus[_unitID] = nil - + -- Message text. local _msg = string.format("%s left strafing zone %s too quickly. No Score.", _playername, _currentStrafeRun.zone.name) - + -- Send message. self:_DisplayMessageToGroup(_unit, _msg, nil, true) + if self.rangecontrol then + self.rangecontrol:NewTransmission(RANGE.Sound.RCLeftStrafePitTooQuickly.filename, RANGE.Sound.RCLeftStrafePitTooQuickly.duration, self.soundpath) + end + else - + -- Get current ammo. local _ammo=self:_GetAmmo(_unitName) - + -- Result. local _result = self.strafeStatus[_unitID] + local _sound = nil --#RANGE.Soundfile -- Judge this pass. Text is displayed on summary. if _result.hits >= _result.zone.goodPass*2 then - _result.text = "EXCELLENT PASS" + _result.text = "EXCELLENT PASS" + _sound=RANGE.Sound.RCExcellentPass elseif _result.hits >= _result.zone.goodPass then _result.text = "GOOD PASS" + _sound=RANGE.Sound.RCGoodPass elseif _result.hits >= _result.zone.goodPass/2 then _result.text = "INEFFECTIVE PASS" + _sound=RANGE.Sound.RCIneffectivePass else _result.text = "POOR PASS" + _sound=RANGE.Sound.RCPoorPass end - + -- Calculate accuracy of run. Number of hits wrt number of rounds fired. local shots=_result.ammo-_ammo local accur=0 if shots>0 then accur=_result.hits/shots*100 end - - -- Message text. - local _text=string.format("%s, %s with %d hits on target %s.", self:_myname(_unitName), _result.text, _result.hits, _result.zone.name) + + -- Message text. + local _text=string.format("%s, hits on target %s: %d", self:_myname(_unitName), _result.zone.name, _result.hits) if shots and accur then _text=_text..string.format("\nTotal rounds fired %d. Accuracy %.1f %%.", shots, accur) end - + _text=_text..string.format("\n%s", _result.text) + -- Send message. self:_DisplayMessageToGroup(_unit, _text) - + + -- Voice over. + if self.rangecontrol then + self.rangecontrol:NewTransmission(RANGE.Sound.RCHitsOnTarget.filename, RANGE.Sound.RCHitsOnTarget.duration, self.soundpath) + self.rangecontrol:Number2Transmission(string.format("%d", _result.hits)) + if shots and accur then + self.rangecontrol:NewTransmission(RANGE.Sound.RCTotalRoundsFired.filename, RANGE.Sound.RCTotalRoundsFired.duration, self.soundpath, nil, 0.2) + self.rangecontrol:Number2Transmission(string.format("%d", shots), nil, 0.2) + self.rangecontrol:NewTransmission(RANGE.Sound.RCAccuracy.filename, RANGE.Sound.RCAccuracy.duration, self.soundpath, nil, 0.2) + self.rangecontrol:Number2Transmission(string.format("%d", UTILS.Round(accur, 0))) + self.rangecontrol:NewTransmission(RANGE.Sound.RCPercent.filename, RANGE.Sound.RCPercent.duration, self.soundpath) + end + self.rangecontrol:NewTransmission(_sound.filename, _sound.duration, self.soundpath, nil, 0.5) + end + -- Set strafe status to nil. self.strafeStatus[_unitID] = nil - + -- Save stats so the player can retrieve them. local _stats = self.strafePlayerResults[_playername] or {} table.insert(_stats, _result) self.strafePlayerResults[_playername] = _stats end - + end else - + -- Check to see if we're in any of the strafing zones (first time). for _,_targetZone in pairs(self.strafeTargets) do - + -- Get the current approach zone and check if player is inside. local zonenname=_targetZone.name local zone=_targetZone.polygon --Core.Zone#ZONE_POLYGON_BASE - + -- Check if player is in zone and below max alt and flying towards the target. local unitheading = _unit:GetHeading() local pitheading = _targetZone.heading - 180 local deltaheading = unitheading-pitheading local towardspit = math.abs(deltaheading)<=90 or math.abs(deltaheading-360)<=90 - local unitalt =_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight() - + local unitalt =_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight() + -- Check if unit is inside zone and below max height AGL. local unitinzone=_unit:IsInZone(zone) and unitalt <= self.strafemaxalt and towardspit - + -- Debug info. local text=string.format("Checking zone %s. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _targetZone.name, _unitName, _playername, tostring(unitinzone), unitalt, deltaheading) - self:T2(RANGE.id..text) - + self:T2(self.id..text) + -- Player is inside zone. if unitinzone then - + -- Get ammo at the beginning of the run. local _ammo=self:_GetAmmo(_unitName) -- Init strafe status for this player. self.strafeStatus[_unitID] = {hits = 0, zone = _targetZone, time = 1, ammo=_ammo, pastfoulline=false } - + -- Rolling in! local _msg=string.format("%s, rolling in on strafe pit %s.", self:_myname(_unitName), _targetZone.name) + if self.rangecontrol then + self.rangecontrol:NewTransmission(RANGE.Sound.RCRollingInOnStrafeTarget.filename, RANGE.Sound.RCRollingInOnStrafeTarget.duration, self.soundpath) + end + -- Send message. self:_DisplayMessageToGroup(_unit, _msg, 10, true) -- We found our player. Skip remaining checks. break - - end -- unit in zone check - + + end -- unit in zone check + end -- loop over zones end end - + end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Menu Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Add menu commands for player. -- @param #RANGE self -- @param #string _unitName Name of player unit. function RANGE:_AddF10Commands(_unitName) self:F(_unitName) - + -- Get player unit and name. local _unit, playername = self:_GetPlayerUnitAndName(_unitName) - + -- Check for player unit. if _unit and playername then -- Get group and ID. local group=_unit:GetGroup() local _gid=group:GetID() - + if group and _gid then - + if not self.MenuAddedTo[_gid] then - + -- Enable switch so we don't do this twice. self.MenuAddedTo[_gid] = true - - -- Main F10 menu: F10/On the Range// - if RANGE.MenuF10[_gid] == nil then - RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range") + + -- Range root menu path. + local _rangePath=nil + + if RANGE.MenuF10Root then + + ------------------- + -- MISSION LEVEL -- + ------------------- + + _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10Root) + + else + + ----------------- + -- GROUP LEVEL -- + ----------------- + + -- Main F10 menu: F10/On the Range// + if RANGE.MenuF10[_gid] == nil then + RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range") + end + _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid]) + end - local _rangePath = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid]) + + local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Statistics", _rangePath) local _markPath = missionCommands.addSubMenuForGroup(_gid, "Mark Targets", _rangePath) local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _rangePath) @@ -1899,8 +2933,8 @@ function RANGE:_AddF10Commands(_unitName) -- F10/On the Range//Mark Targets/ missionCommands.addCommandForGroup(_gid, "Mark On Map", _markPath, self._MarkTargetsOnMap, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Illuminate Range", _markPath, self._IlluminateBombTargets, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Smoke Strafe Pits", _markPath, self._SmokeStrafeTargetBoxes, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Illuminate Range", _markPath, self._IlluminateBombTargets, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Smoke Strafe Pits", _markPath, self._SmokeStrafeTargetBoxes, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Strafe Tgts", _markPath, self._SmokeStrafeTargets, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Bomb Tgts", _markPath, self._SmokeBombTargets, self, _unitName) -- F10/On the Range//Stats/ @@ -1921,9 +2955,11 @@ function RANGE:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "White Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.White) missionCommands.addCommandForGroup(_gid, "Yellow Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Yellow) -- F10/On the Range//My Settings/ - missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) + missionCommands.addCommandForGroup(_gid, "All Messages On/Off", _settingsPath, self._MessagesToPlayerOnOff, self, _unitName) + -- F10/On the Range//Range Information missionCommands.addCommandForGroup(_gid, "General Info", _infoPath, self._DisplayRangeInfo, self, _unitName) missionCommands.addCommandForGroup(_gid, "Weather Report", _infoPath, self._DisplayRangeWeather, self, _unitName) @@ -1931,16 +2967,55 @@ function RANGE:_AddF10Commands(_unitName) missionCommands.addCommandForGroup(_gid, "Strafe Pits", _infoPath, self._DisplayStrafePits, self, _unitName) end else - self:T(RANGE.id.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) + self:E(self.id.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) end else - self:T(RANGE.id.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName) + self:E(self.id.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName) end end - + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Helper Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get the number of shells a unit currently has. +-- @param #RANGE self +-- @param #RANGE.BombTarget target Bomb target data. +-- @return Core.Point#COORDINATE Target coordinate. +function RANGE:_GetBombTargetCoordinate(target) + + local coord=nil --Core.Point#COORDINATE + + if target.type==RANGE.TargetType.UNIT then + + if not target.move then + -- Target should not move. + coord=target.coordinate + else + -- Moving target. Check if alive and get current position + if target.target and target.target:IsAlive() then + coord=target.target:GetCoordinate() + end + end + + elseif target.type==RANGE.TargetType.STATIC then + + -- Static targets dont move. + coord=target.coordinate + + elseif target.type==RANGE.TargetType.COORD then + + -- Coordinates dont move. + coord=target.coordinate + + else + self:E(self.id.."ERROR: Unknown target type.") + end + + return coord +end + --- Get the number of shells a unit currently has. -- @param #RANGE self @@ -1948,47 +3023,47 @@ end -- @return Number of shells left function RANGE:_GetAmmo(unitname) self:F2(unitname) - + -- Init counter. local ammo=0 - + local unit, playername = self:_GetPlayerUnitAndName(unitname) - + if unit and playername then - + local has_ammo=false - + local ammotable=unit:GetAmmo() self:T2({ammotable=ammotable}) - + if ammotable ~= nil then - + local weapons=#ammotable - self:T2(RANGE.id..string.format("Number of weapons %d.", weapons)) - + self:T2(self.id..string.format("Number of weapons %d.", weapons)) + for w=1,weapons do - + local Nammo=ammotable[w]["count"] local Tammo=ammotable[w]["desc"]["typeName"] - + -- We are specifically looking for shells here. if string.match(Tammo, "shell") then - + -- Add up all shells ammo=ammo+Nammo - + local text=string.format("Player %s has %d rounds ammo of type %s", playername, Nammo, Tammo) - self:T(RANGE.id..text) + self:T(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) else local text=string.format("Player %s has %d ammo of type %s", playername, Nammo, Tammo) - self:T(RANGE.id..text) + self:T(self.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) end end end end - + return ammo end @@ -1999,24 +3074,22 @@ function RANGE:_MarkTargetsOnMap(_unitName) self:F(_unitName) -- Get group. - local group=nil + local group=nil --Wrapper.Group#GROUP if _unitName then group=UNIT:FindByName(_unitName):GetGroup() end - + -- Mark bomb targets. for _,_bombtarget in pairs(self.bombingTargets) do - local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE - if _target and _target:IsAlive() then - local coord=_target:GetCoordinate() --Core.Point#COORDINATE - if group then - coord:MarkToGroup("Bomb target ".._bombtarget.name, group) - else - coord:MarkToAll("Bomb target ".._bombtarget.name) - end + local bombtarget=_bombtarget --#RANGE.BombTarget + local coord=self:_GetBombTargetCoordinate(_bombtarget) + if group then + coord:MarkToGroup(string.format("Bomb target %s:\n%s\n%s", bombtarget.name, coord:ToStringLLDMS(), coord:ToStringBULLS(group:GetCoalition())), group) + else + coord:MarkToAll(string.format("Bomb target %s", bombtarget.name)) end end - + -- Mark strafe targets. for _,_strafepit in pairs(self.strafeTargets) do for _,_target in pairs(_strafepit.targets) do @@ -2024,20 +3097,21 @@ function RANGE:_MarkTargetsOnMap(_unitName) if _target and _target:IsAlive() then local coord=_target:GetCoordinate() --Core.Point#COORDINATE if group then - coord:MarkToGroup("Strafe target ".._target:GetName(), group) + --coord:MarkToGroup("Strafe target ".._target:GetName(), group) + coord:MarkToGroup(string.format("Strafe target %s:\n%s\n%s", _target:GetName(), coord:ToStringLLDMS(), coord:ToStringBULLS(group:GetCoalition())), group) else coord:MarkToAll("Strafe target ".._target:GetName()) end end end end - + if _unitName then local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) local text=string.format("%s, %s, range targets are now marked on F10 map.", self.rangename, _playername) self:_DisplayMessageToGroup(_unit, text, 5) end - + end --- Illuminate targets. Fires illumination bombs at one random bomb and one random strafe target at a random altitude between 400 and 800 m. @@ -2051,21 +3125,21 @@ function RANGE:_IlluminateBombTargets(_unitName) for _,_bombtarget in pairs(self.bombingTargets) do local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE - if _target and _target:IsAlive() then - local coord=_target:GetCoordinate() --Core.Point#COORDINATE + local coord=self:_GetBombTargetCoordinate(_bombtarget) + if coord then table.insert(bomb, coord) end end - + if #bomb>0 then local coord=bomb[math.random(#bomb)] --Core.Point#COORDINATE local c=COORDINATE:New(coord.x,coord.y+math.random(self.illuminationminalt,self.illuminationmaxalt),coord.z) c:IlluminationBomb() end - + -- All strafe target coordinates. local strafe={} - + for _,_strafepit in pairs(self.strafeTargets) do for _,_target in pairs(_strafepit.targets) do local _target=_target --Wrapper.Positionable#POSITIONABLE @@ -2075,14 +3149,14 @@ function RANGE:_IlluminateBombTargets(_unitName) end end end - + -- Pick a random strafe target. if #strafe>0 then local coord=strafe[math.random(#strafe)] --Core.Point#COORDINATE local c=COORDINATE:New(coord.x,coord.y+math.random(self.illuminationminalt,self.illuminationmaxalt),coord.z) c:IlluminationBomb() end - + if _unitName then local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) local text=string.format("%s, %s, range targets are illuminated.", self.rangename, _playername) @@ -2096,14 +3170,14 @@ end function RANGE:_ResetRangeStats(_unitName) self:F(_unitName) - -- Get player unit and name. + -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - - if _unit and _playername then + + if _unit and _playername then self.strafePlayerResults[_playername] = nil self.bombPlayerResults[_playername] = nil local text=string.format("%s, %s, your range stats were cleared.", self.rangename, _playername) - self:DisplayMessageToGroup(_unit, text, 5) + self:DisplayMessageToGroup(_unit, text, 5, false, true) end end @@ -2113,36 +3187,47 @@ end -- @param #string _text Message text. -- @param #number _time Duration how long the message is displayed. -- @param #boolean _clear Clear up old messages. -function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear) +-- @param #boolean display If true, display message regardless of player setting "Messages Off". +function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear, display) self:F({unit=_unit, text=_text, time=_time, clear=_clear}) - + + -- Defaults _time=_time or self.Tmsg - if _clear==nil then + if _clear==nil or _clear==false then _clear=false + else + _clear=true end - - -- Group ID. - local _gid=_unit:GetGroup():GetID() - - if _gid and not self.examinerexclusive then - if _clear == true then + + -- Messages globally disabled. + if self.messages==false then + return + end + + -- Check if unit is alive. + if _unit and _unit:IsAlive() then + + -- Group ID. + local _gid=_unit:GetGroup():GetID() + + -- Get playername and player settings + local _, playername=self:_GetPlayerUnitAndName(_unit:GetName()) + local playermessage=self.PlayerSettings[playername].messages + + -- Send message to player if messages enabled and not only for the examiner. + if _gid and (playermessage==true or display) and (not self.examinerexclusive) then trigger.action.outTextForGroup(_gid, _text, _time, _clear) - else - trigger.action.outTextForGroup(_gid, _text, _time) + end + + -- Send message to examiner. + if self.examinergroupname~=nil then + local _examinerid=GROUP:FindByName(self.examinergroupname):GetID() + if _examinerid then + trigger.action.outTextForGroup(_examinerid, _text, _time, _clear) + end end end - if self.examinergroupname~=nil then - local _examinerid=GROUP:FindByName(self.examinergroupname):GetID() - if _examinerid then - if _clear == true then - trigger.action.outTextForGroup(_examinerid, _text, _time, _clear) - else - trigger.action.outTextForGroup(_examinerid, _text, _time) - end - end - end - end --- Toggle status of smoking bomb impact points. @@ -2150,7 +3235,7 @@ end -- @param #string unitname Name of the player unit. function RANGE:_SmokeBombImpactOnOff(unitname) self:F(unitname) - + local unit, playername = self:_GetPlayerUnitAndName(unitname) if unit and playername then local text @@ -2161,9 +3246,9 @@ function RANGE:_SmokeBombImpactOnOff(unitname) self.PlayerSettigs[playername].smokebombimpact=true text=string.format("%s, %s, smoking impact points of bombs is now ON.", self.rangename, playername) end - self:_DisplayMessageToGroup(unit, text, 5) + self:_DisplayMessageToGroup(unit, text, 5, false, true) end - + end --- Toggle status of time delay for smoking bomb impact points @@ -2171,7 +3256,7 @@ end -- @param #string unitname Name of the player unit. function RANGE:_SmokeBombDelayOnOff(unitname) self:F(unitname) - + local unit, playername = self:_GetPlayerUnitAndName(unitname) if unit and playername then local text @@ -2182,9 +3267,29 @@ function RANGE:_SmokeBombDelayOnOff(unitname) self.PlayerSettigs[playername].delaysmoke=true text=string.format("%s, %s, delayed smoke of bombs is now ON.", self.rangename, playername) end - self:_DisplayMessageToGroup(unit, text, 5) + self:_DisplayMessageToGroup(unit, text, 5, false, true) end - + +end + +--- Toggle display messages to player. +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +function RANGE:_MessagesToPlayerOnOff(unitname) + self:F(unitname) + + local unit, playername = self:_GetPlayerUnitAndName(unitname) + if unit and playername then + local text + if self.PlayerSettings[playername].messages==true then + text=string.format("%s, %s, display of ALL messages is now OFF.", self.rangename, playername) + else + text=string.format("%s, %s, display of ALL messages is now ON.", self.rangename, playername) + end + self:_DisplayMessageToGroup(unit, text, 5, false, true) + self.PlayerSettings[playername].messages=not self.PlayerSettings[playername].messages + end + end --- Toggle status of flaring direct hits of range targets. @@ -2192,7 +3297,7 @@ end -- @param #string unitname Name of the player unit. function RANGE:_FlareDirectHitsOnOff(unitname) self:F(unitname) - + local unit, playername = self:_GetPlayerUnitAndName(unitname) if unit and playername then local text @@ -2203,9 +3308,9 @@ function RANGE:_FlareDirectHitsOnOff(unitname) self.PlayerSettings[playername].flaredirecthits=true text=string.format("%s, %s, flaring direct hits is now ON.", self.rangename, playername) end - self:_DisplayMessageToGroup(unit, text, 5) + self:_DisplayMessageToGroup(unit, text, 5, false, true) end - + end --- Mark bombing targets with smoke. @@ -2213,21 +3318,21 @@ end -- @param #string unitname Name of the player unit. function RANGE:_SmokeBombTargets(unitname) self:F(unitname) - + for _,_bombtarget in pairs(self.bombingTargets) do local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE - if _target and _target:IsAlive() then - local coord = _target:GetCoordinate() --Core.Point#COORDINATE + local coord=self:_GetBombTargetCoordinate(_bombtarget) + if coord then coord:Smoke(self.BombSmokeColor) end end - + if unitname then local unit, playername = self:_GetPlayerUnitAndName(unitname) local text=string.format("%s, %s, bombing targets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text(self.BombSmokeColor)) self:_DisplayMessageToGroup(unit, text, 5) end - + end --- Mark strafing targets with smoke. @@ -2235,17 +3340,17 @@ end -- @param #string unitname Name of the player unit. function RANGE:_SmokeStrafeTargets(unitname) self:F(unitname) - + for _,_target in pairs(self.strafeTargets) do _target.coordinate:Smoke(self.StrafeSmokeColor) end - + if unitname then local unit, playername = self:_GetPlayerUnitAndName(unitname) local text=string.format("%s, %s, strafing tragets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text(self.StrafeSmokeColor)) self:_DisplayMessageToGroup(unit, text, 5) end - + end --- Mark approach boxes of strafe targets with smoke. @@ -2253,21 +3358,21 @@ end -- @param #string unitname Name of the player unit. function RANGE:_SmokeStrafeTargetBoxes(unitname) self:F(unitname) - + for _,_target in pairs(self.strafeTargets) do local zone=_target.polygon --Core.Zone#ZONE - zone:SmokeZone(self.StrafePitSmokeColor) + zone:SmokeZone(self.StrafePitSmokeColor, 4) for _,_point in pairs(_target.smokepoints) do _point:SmokeOrange() --Corners are smoked orange. end end - + if unitname then local unit, playername = self:_GetPlayerUnitAndName(unitname) local text=string.format("%s, %s, strafing pit approach boxes are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text(self.StrafePitSmokeColor)) self:_DisplayMessageToGroup(unit, text, 5) end - + end --- Sets the smoke color used to smoke players bomb impact points. @@ -2276,14 +3381,14 @@ end -- @param Utilities.Utils#SMOKECOLOR color ID of the smoke color. function RANGE:_playersmokecolor(_unitName, color) self:F({unitname=_unitName, color=color}) - + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) if _unit and _playername then self.PlayerSettings[_playername].smokecolor=color local text=string.format("%s, %s, your bomb impacts are now smoked in %s.", self.rangename, _playername, self:_smokecolor2text(color)) self:_DisplayMessageToGroup(_unit, text, 5) end - + end --- Sets the flare color used when player makes a direct hit on target. @@ -2292,14 +3397,14 @@ end -- @param Utilities.Utils#FLARECOLOR color ID of flare color. function RANGE:_playerflarecolor(_unitName, color) self:F({unitname=_unitName, color=color}) - + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) if _unit and _playername then self.PlayerSettings[_playername].flarecolor=color local text=string.format("%s, %s, your direct hits are now flared in %s.", self.rangename, _playername, self:_flarecolor2text(color)) self:_DisplayMessageToGroup(_unit, text, 5) end - + end --- Converts a smoke color id to text. E.g. SMOKECOLOR.Blue --> "blue". @@ -2308,7 +3413,7 @@ end -- @return #string Color text. function RANGE:_smokecolor2text(color) self:F(color) - + local txt="" if color==SMOKECOLOR.Blue then txt="blue" @@ -2321,9 +3426,9 @@ function RANGE:_smokecolor2text(color) elseif color==SMOKECOLOR.White then txt="white" else - txt=string.format("unkown color (%s)", tostring(color)) + txt=string.format("unknown color (%s)", tostring(color)) end - + return txt end @@ -2333,7 +3438,7 @@ end -- @return #string Color text. function RANGE:_flarecolor2text(color) self:F(color) - + local txt="" if color==FLARECOLOR.Green then txt="green" @@ -2344,9 +3449,9 @@ function RANGE:_flarecolor2text(color) elseif color==FLARECOLOR.Yellow then txt="yellow" else - txt=string.format("unkown color (%s)", tostring(color)) + txt=string.format("unknown color (%s)", tostring(color)) end - + return txt end @@ -2359,28 +3464,28 @@ function RANGE:_CheckStatic(name) -- Get DCS static object. local _DCSstatic=StaticObject.getByName(name) - + if _DCSstatic and _DCSstatic:isExist() then - + --Static does exist at least in DCS. Check if it also in the MOOSE DB. local _MOOSEstatic=STATIC:FindByName(name, false) - + -- If static is not yet in MOOSE DB, we add it. Can happen for cargo statics! if not _MOOSEstatic then - self:T(RANGE.id..string.format("Adding DCS static to MOOSE database. Name = %s.", name)) + self:T(self.id..string.format("Adding DCS static to MOOSE database. Name = %s.", name)) _DATABASE:AddStatic(name) end - + return true else - self:T3(RANGE.id..string.format("No static object with name %s exists.", name)) + self:T3(self.id..string.format("No static object with name %s exists.", name)) end - + -- Check if a unit has this name. if UNIT:FindByName(name) then return false else - self:T3(RANGE.id..string.format("No unit object with name %s exists.", name)) + self:T3(self.id..string.format("No unit object with name %s exists.", name)) end -- If not unit or static exist, we return nil. @@ -2396,18 +3501,18 @@ function RANGE:_GetSpeed(controllable) -- Get DCS descriptors local desc=controllable:GetDesc() - + -- Get speed local speed=0 if desc then speed=desc.speedMax*3.6 self:T({speed=speed}) end - + return speed end ---- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. -- @param #RANGE self -- @param #string _unitName Name of the player unit. -- @return Wrapper.Unit#UNIT Unit of player. @@ -2417,56 +3522,40 @@ function RANGE:_GetPlayerUnitAndName(_unitName) self:F2(_unitName) if _unitName ~= nil then - + -- Get DCS unit from its name. local DCSunit=Unit.getByName(_unitName) - + if DCSunit then - + local playername=DCSunit:getPlayerName() local unit=UNIT:Find(DCSunit) - + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) if DCSunit and unit and playername then return unit, playername end - + end - + end - + -- Return nil if we could not find a player. return nil,nil end ---- Returns a string which consits of this callsign and the player name. +--- Returns a string which consits of this callsign and the player name. -- @param #RANGE self -- @param #string unitname Name of the player unit. function RANGE:_myname(unitname) self:F2(unitname) - + local unit=UNIT:FindByName(unitname) local pname=unit:GetPlayerName() local csign=unit:GetCallsign() - - return string.format("%s (%s)", csign, pname) -end ---- Split string. Cf http://stackoverflow.com/questions/1426954/split-string-in-lua --- @param #RANGE self --- @param #string str Sting to split. --- @param #string sep Speparator for split. --- @return #table Split text. -function RANGE:_split(str, sep) - self:F2({str=str, sep=sep}) - - local result = {} - local regex = ("([^%s]+)"):format(sep) - for each in str:gmatch(regex) do - table.insert(result, each) - end - - return result + --return string.format("%s (%s)", csign, pname) + return string.format("%s", pname) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/Scoring.lua b/Moose Development/Moose/Functional/Scoring.lua index af50957e6..b5c8da81b 100644 --- a/Moose Development/Moose/Functional/Scoring.lua +++ b/Moose Development/Moose/Functional/Scoring.lua @@ -1635,7 +1635,7 @@ function SCORING:ReportScoreGroupSummary( PlayerGroup ) self:F( { ReportMissions, ScoreMissions, PenaltyMissions } ) local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions - local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + ScoreGoals + PenaltyMissions + local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions PlayerMessage = string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", diff --git a/Moose Development/Moose/Functional/Sead.lua b/Moose Development/Moose/Functional/Sead.lua index 55a791023..6edb99312 100644 --- a/Moose Development/Moose/Functional/Sead.lua +++ b/Moose Development/Moose/Functional/Sead.lua @@ -57,8 +57,10 @@ SEAD = { -- -- Defends the Russian SA installations from SEAD attacks. -- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) function SEAD:New( SEADGroupPrefixes ) + local self = BASE:Inherit( self, BASE:New() ) - self:F( SEADGroupPrefixes ) + self:F( SEADGroupPrefixes ) + if type( SEADGroupPrefixes ) == 'table' then for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix @@ -85,7 +87,29 @@ function SEAD:OnEventShot( EventData ) local SEADWeaponName = EventData.WeaponName -- return weapon type -- Start of the 2nd loop self:T( "Missile Launched = " .. SEADWeaponName ) - if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD + + --if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD + if SEADWeaponName == "weapons.missiles.X_58" --Kh-58U anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.Kh25MP_PRGS1VP" --Kh-25MP anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.X_25MP" --Kh-25MPU anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.X_28" --Kh-28 anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.X_31P" --Kh-31P anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.AGM_45A" --AGM-45A anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.AGM_45" --AGM-45B anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.AGM_88" --AGM-88C anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.AGM_122" --AGM-122 Sidearm anti-radiation missiles fired + or + SEADWeaponName == "weapons.missiles.ALARM" --ALARM anti-radiation missiles fired + then + local _evade = math.random (1,100) -- random number for chance of evading action local _targetMim = EventData.Weapon:getTarget() -- Identify target local _targetMimname = Unit.getName(_targetMim) @@ -111,47 +135,62 @@ function SEAD:OnEventShot( EventData ) self:T( _targetskill ) if self.TargetSkill[_targetskill] then if (_evade > self.TargetSkill[_targetskill].Evade) then + self:T( string.format("Evading, target skill " ..string.format(_targetskill)) ) + local _targetMim = Weapon.getTarget(SEADWeapon) local _targetMimname = Unit.getName(_targetMim) local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon)) local _targetMimcont= _targetMimgroup:getController() + routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly + local SuppressedGroups1 = {} -- unit suppressed radar off for a random time + local function SuppressionEnd1(id) id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) SuppressedGroups1[id.groupName] = nil end + local id = { groupName = _targetMimgroup, ctrl = _targetMimcont } + local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2]) + if SuppressedGroups1[id.groupName] == nil then + SuppressedGroups1[id.groupName] = { SuppressionEndTime1 = timer.getTime() + delay1, SuppressionEndN1 = SuppressionEndCounter1 --Store instance of SuppressionEnd() scheduled function - } + } + Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN) timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1) --Schedule the SuppressionEnd() function --trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20) end local SuppressedGroups = {} + local function SuppressionEnd(id) id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED) SuppressedGroups[id.groupName] = nil end + local id = { groupName = _targetMimgroup, ctrl = _targetMimcont } + local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2]) + if SuppressedGroups[id.groupName] == nil then SuppressedGroups[id.groupName] = { SuppressionEndTime = timer.getTime() + delay, SuppressionEndN = SuppressionEndCounter --Store instance of SuppressionEnd() scheduled function } + timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime) --Schedule the SuppressionEnd() function --trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20) end diff --git a/Moose Development/Moose/Functional/Suppression.lua b/Moose Development/Moose/Functional/Suppression.lua index 3df26be6d..1698f0c0a 100644 --- a/Moose Development/Moose/Functional/Suppression.lua +++ b/Moose Development/Moose/Functional/Suppression.lua @@ -5,6 +5,10 @@ -- ## Features: -- -- * Hold fire of attacked units when being fired upon. +-- * Retreat to a user defined zone. +-- * Fall back on hits. +-- * Take cover on hits. +-- * Gaussian distribution of suppression time. -- -- === -- @@ -18,7 +22,7 @@ -- -- The implementation is based on an idea and script by MBot. See the [DCS forum threat](https://forums.eagle.ru/showthread.php?t=107635) for details. -- --- In addition to suppressing the fire, conditions can be specified which let the group retreat to a defined zone, move away from the attacker +-- In addition to suppressing the fire, conditions can be specified, which let the group retreat to a defined zone, move away from the attacker -- or hide at a nearby scenery object. -- -- ==== @@ -44,6 +48,7 @@ -- @type SUPPRESSION -- @field #string ClassName Name of the class. -- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players. +-- @field #string lid String for DCS log file. -- @field #boolean flare Flare units when they get hit or die. -- @field #boolean smoke Smoke places to which the group retreats, falls back or hides. -- @field #list DCSdesc Table containing all DCS descriptors of the group. @@ -78,6 +83,8 @@ -- @field #string DefaultAlarmState Alarm state the group will go to when it is changed back from another state. Default is "Auto". -- @field #string DefaultROE ROE the group will get once suppression is over. Default is "Free". -- @field #boolean eventmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. Default true. +-- @field Core.Zone#ZONE BattleZone +-- @field #boolean AutoEngage -- @extends Core.Fsm#FSM_CONTROLLABLE -- @@ -222,45 +229,49 @@ -- -- @field #SUPPRESSION SUPPRESSION={ - ClassName = "SUPPRESSION", - Debug = false, - flare = false, - smoke = false, - DCSdesc = nil, - Type = nil, - IsInfantry=nil, - SpeedMax = nil, - Tsuppress_ave = 15, - Tsuppress_min = 5, - Tsuppress_max = 25, - TsuppressOver = nil, - IniGroupStrength = nil, - Nhit = 0, - Formation = "Off road", - Speed = 4, - MenuON = false, - FallbackON = false, - FallbackWait = 60, - FallbackDist = 100, - FallbackHeading = nil, - TakecoverON = false, - TakecoverWait = 120, - TakecoverRange = 300, - hideout = nil, - PminFlee = 10, - PmaxFlee = 90, - RetreatZone = nil, - RetreatDamage = nil, - RetreatWait = 7200, + ClassName = "SUPPRESSION", + Debug = false, + lid = nil, + flare = false, + smoke = false, + DCSdesc = nil, + Type = nil, + IsInfantry = nil, + SpeedMax = nil, + Tsuppress_ave = 15, + Tsuppress_min = 5, + Tsuppress_max = 25, + TsuppressOver = nil, + IniGroupStrength = nil, + Nhit = 0, + Formation = "Off road", + Speed = 4, + MenuON = false, + FallbackON = false, + FallbackWait = 60, + FallbackDist = 100, + FallbackHeading = nil, + TakecoverON = false, + TakecoverWait = 120, + TakecoverRange = 300, + hideout = nil, + PminFlee = 10, + PmaxFlee = 90, + RetreatZone = nil, + RetreatDamage = nil, + RetreatWait = 7200, CurrentAlarmState = "unknown", - CurrentROE = "unknown", + CurrentROE = "unknown", DefaultAlarmState = "Auto", - DefaultROE = "Weapon Free", - eventmoose = true, + DefaultROE = "Weapon Free", + eventmoose = true, } --- Enumerator of possible rules of engagement. --- @field #list ROE +-- @type SUPPRESSION.ROE +-- @field #string Hold Hold fire. +-- @field #string Free Weapon fire. +-- @field #string Return Return fire. SUPPRESSION.ROE={ Hold="Weapon Hold", Free="Weapon Free", @@ -268,7 +279,10 @@ SUPPRESSION.ROE={ } --- Enumerator of possible alarm states. --- @field #list AlarmState +-- @type SUPPRESSION.AlarmState +-- @field #string Auto Automatic. +-- @field #string Green Green. +-- @field #string Red Red. SUPPRESSION.AlarmState={ Auto="Auto", Green="Green", @@ -279,13 +293,9 @@ SUPPRESSION.AlarmState={ -- @field #string MenuF10 SUPPRESSION.MenuF10=nil ---- Some ID to identify who we are in output of the DCS.log file. --- @field #string id -SUPPRESSION.id="SUPPRESSION | " - --- PSEUDOATC version. -- @field #number version -SUPPRESSION.version="0.9.0" +SUPPRESSION.version="0.9.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -299,25 +309,24 @@ SUPPRESSION.version="0.9.0" --- Creates a new AI_suppression object. -- @param #SUPPRESSION self -- @param Wrapper.Group#GROUP group The GROUP object for which suppression should be applied. --- @return #SUPPRESSION SUPPRESSION object. --- @return nil If group does not exist or is not a ground group. +-- @return #SUPPRESSION SUPPRESSION object or *nil* if group does not exist or is not a ground group. function SUPPRESSION:New(group) - BASE:F2(group) -- Inherits from FSM_CONTROLLABLE local self=BASE:Inherit(self, FSM_CONTROLLABLE:New()) -- #SUPPRESSION -- Check that group is present. if group then - self:T(SUPPRESSION.id..string.format("SUPPRESSION version %s. Activating suppressive fire for group %s", SUPPRESSION.version, group:GetName())) + self.lid=string.format("SUPPRESSION %s | ", tostring(group:GetName())) + self:T(self.lid..string.format("SUPPRESSION version %s. Activating suppressive fire for group %s", SUPPRESSION.version, group:GetName())) else - self:E(SUPPRESSION.id.."Suppressive fire: Requested group does not exist! (Has to be a MOOSE group.)") + self:E(self.lid.."SUPPRESSION | Requested group does not exist! (Has to be a MOOSE group.)") return nil end -- Check that we actually have a GROUND group. if group:IsGround()==false then - self:E(SUPPRESSION.id..string.format("SUPPRESSION fire group %s has to be a GROUND group!", group:GetName())) + self:E(self.lid..string.format("SUPPRESSION fire group %s has to be a GROUND group!", group:GetName())) return nil end @@ -325,18 +334,16 @@ function SUPPRESSION:New(group) self:SetControllable(group) -- Get DCS descriptors of group. - local DCSgroup=Group.getByName(group:GetName()) - local DCSunit=DCSgroup:getUnit(1) - self.DCSdesc=DCSunit:getDesc() + self.DCSdesc=group:GetDCSDesc(1) -- Get max speed the group can do and convert to km/h. - self.SpeedMax=self.DCSdesc.speedMaxOffRoad*3.6 + self.SpeedMax=group:GetSpeedMax() -- Set speed to maximum. self.Speed=self.SpeedMax -- Is this infantry or not. - self.IsInfantry=DCSunit:hasAttribute("Infantry") + self.IsInfantry=group:GetUnit(1):HasAttribute("Infantry") -- Type of group. self.Type=group:GetTypeName() @@ -350,6 +357,7 @@ function SUPPRESSION:New(group) -- Transitions self:AddTransition("*", "Start", "CombatReady") + self:AddTransition("*", "Status", "*") self:AddTransition("CombatReady", "Hit", "Suppressed") self:AddTransition("Suppressed", "Hit", "Suppressed") self:AddTransition("Suppressed", "Recovered", "CombatReady") @@ -359,11 +367,45 @@ function SUPPRESSION:New(group) self:AddTransition("TakingCover", "FightBack", "CombatReady") self:AddTransition("FallingBack", "FightBack", "CombatReady") self:AddTransition("Retreating", "Retreated", "Retreated") + self:AddTransition("*", "OutOfAmmo", "*") self:AddTransition("*", "Dead", "*") + self:AddTransition("*", "Stop", "Stopped") self:AddTransition("TakingCover", "Hit", "TakingCover") self:AddTransition("FallingBack", "Hit", "FallingBack") + + --- Trigger "Status" event. + -- @function [parent=#SUPPRESSION] Status + -- @param #SUPPRESSION self + + --- Trigger "Status" event after a delay. + -- @function [parent=#SUPPRESSION] __Status + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + + --- User function for OnAfter "Status" event. + -- @function [parent=#SUPPRESSION] OnAfterStatus + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Trigger "Hit" event. + -- @function [parent=#SUPPRESSION] Hit + -- @param #SUPPRESSION self + -- @param Wrapper.Unit#UNIT Unit Unit that was hit. + -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. + + --- Trigger "Hit" event after a delay. + -- @function [parent=#SUPPRESSION] __Hit + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + -- @param Wrapper.Unit#UNIT Unit Unit that was hit. + -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. + --- User function for OnBefore "Hit" event. -- @function [parent=#SUPPRESSION] OnBeforeHit -- @param #SUPPRESSION self @@ -375,7 +417,7 @@ function SUPPRESSION:New(group) -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. -- @return #boolean - --- User function for OnAfer "Hit" event. + --- User function for OnAfter "Hit" event. -- @function [parent=#SUPPRESSION] OnAfterHit -- @param #SUPPRESSION self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. @@ -384,7 +426,16 @@ function SUPPRESSION:New(group) -- @param #string To To state. -- @param Wrapper.Unit#UNIT Unit Unit that was hit. -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. + + --- Trigger "Recovered" event. + -- @function [parent=#SUPPRESSION] Recovered + -- @param #SUPPRESSION self + + --- Trigger "Recovered" event after a delay. + -- @function [parent=#SUPPRESSION] Recovered + -- @param #number Delay Delay in seconds. + -- @param #SUPPRESSION self --- User function for OnBefore "Recovered" event. -- @function [parent=#SUPPRESSION] OnBeforeRecovered @@ -404,6 +455,17 @@ function SUPPRESSION:New(group) -- @param #string To To state. + --- Trigger "TakeCover" event. + -- @function [parent=#SUPPRESSION] TakeCover + -- @param #SUPPRESSION self + -- @param Core.Point#COORDINATE Hideout Place where the group will hide. + + --- Trigger "TakeCover" event after a delay. + -- @function [parent=#SUPPRESSION] __TakeCover + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + -- @param Core.Point#COORDINATE Hideout Place where the group will hide. + --- User function for OnBefore "TakeCover" event. -- @function [parent=#SUPPRESSION] OnBeforeTakeCover -- @param #SUPPRESSION self @@ -424,6 +486,17 @@ function SUPPRESSION:New(group) -- @param Core.Point#COORDINATE Hideout Place where the group will hide. + --- Trigger "FallBack" event. + -- @function [parent=#SUPPRESSION] FallBack + -- @param #SUPPRESSION self + -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. + + --- Trigger "FallBack" event after a delay. + -- @function [parent=#SUPPRESSION] __FallBack + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. + --- User function for OnBefore "FallBack" event. -- @function [parent=#SUPPRESSION] OnBeforeFallBack -- @param #SUPPRESSION self @@ -444,6 +517,15 @@ function SUPPRESSION:New(group) -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. + --- Trigger "Retreat" event. + -- @function [parent=#SUPPRESSION] Retreat + -- @param #SUPPRESSION self + + --- Trigger "Retreat" event after a delay. + -- @function [parent=#SUPPRESSION] __Retreat + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + --- User function for OnBefore "Retreat" event. -- @function [parent=#SUPPRESSION] OnBeforeRetreat -- @param #SUPPRESSION self @@ -462,6 +544,15 @@ function SUPPRESSION:New(group) -- @param #string To To state. + --- Trigger "Retreated" event. + -- @function [parent=#SUPPRESSION] Retreated + -- @param #SUPPRESSION self + + --- Trigger "Retreated" event after a delay. + -- @function [parent=#SUPPRESSION] __Retreated + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + --- User function for OnBefore "Retreated" event. -- @function [parent=#SUPPRESSION] OnBeforeRetreated -- @param #SUPPRESSION self @@ -480,6 +571,15 @@ function SUPPRESSION:New(group) -- @param #string To To state. + --- Trigger "FightBack" event. + -- @function [parent=#SUPPRESSION] FightBack + -- @param #SUPPRESSION self + + --- Trigger "FightBack" event after a delay. + -- @function [parent=#SUPPRESSION] __FightBack + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + --- User function for OnBefore "FlightBack" event. -- @function [parent=#SUPPRESSION] OnBeforeFightBack -- @param #SUPPRESSION self @@ -498,6 +598,41 @@ function SUPPRESSION:New(group) -- @param #string To To state. + --- Trigger "OutOfAmmo" event. + -- @function [parent=#SUPPRESSION] OutOfAmmo + -- @param #SUPPRESSION self + + --- Trigger "OutOfAmmo" event after a delay. + -- @function [parent=#SUPPRESSION] __OutOfAmmo + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + + --- User function for OnAfter "OutOfAmmo" event. + -- @function [parent=#SUPPRESSION] OnAfterOutOfAmmo + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Trigger "Dead" event. + -- @function [parent=#SUPPRESSION] Dead + -- @param #SUPPRESSION self + + --- Trigger "Dead" event after a delay. + -- @function [parent=#SUPPRESSION] __Dead + -- @param #SUPPRESSION self + -- @param #number Delay Delay in seconds. + + --- User function for OnAfter "Dead" event. + -- @function [parent=#SUPPRESSION] OnAfterDead + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + return self end @@ -524,9 +659,9 @@ function SUPPRESSION:SetSuppressionTime(Tave, Tmin, Tmax) self.Tsuppress_ave=math.max(self.Tsuppress_min) self.Tsuppress_ave=math.min(self.Tsuppress_max) - self:T(SUPPRESSION.id..string.format("Set ave suppression time to %d seconds.", self.Tsuppress_ave)) - self:T(SUPPRESSION.id..string.format("Set min suppression time to %d seconds.", self.Tsuppress_min)) - self:T(SUPPRESSION.id..string.format("Set max suppression time to %d seconds.", self.Tsuppress_max)) + self:T(self.lid..string.format("Set ave suppression time to %d seconds.", self.Tsuppress_ave)) + self:T(self.lid..string.format("Set min suppression time to %d seconds.", self.Tsuppress_min)) + self:T(self.lid..string.format("Set max suppression time to %d seconds.", self.Tsuppress_max)) end --- Set the zone to which a group retreats after being damaged too much. @@ -756,28 +891,23 @@ end --- Status of group. Current ROE, alarm state, life. -- @param #SUPPRESSION self -- @param #boolean message Send message to all players. -function SUPPRESSION:Status(message) +function SUPPRESSION:StatusReport(message) - local name=self.Controllable:GetName() - local nunits=#self.Controllable:GetUnits() + local group=self.Controllable --Wrapper.Group#GROUP + + local nunits=group:CountAliveUnits() local roe=self.CurrentROE local state=self.CurrentAlarmState local life_min, life_max, life_ave, life_ave0, groupstrength=self:_GetLife() + local ammotot=group:GetAmmunition() + local detectedG=group:GetDetectedGroupSet():CountAlive() + local detectedU=group:GetDetectedUnitSet():Count() - local text=string.format("Status of group %s\n", name) - text=text..string.format("Number of units: %d of %d\n", nunits, self.IniGroupStrength) - text=text..string.format("Current state: %s\n", self:GetState()) - text=text..string.format("ROE: %s\n", roe) - text=text..string.format("Alarm state: %s\n", state) - text=text..string.format("Hits taken: %d\n", self.Nhit) - text=text..string.format("Life min: %3.0f\n", life_min) - text=text..string.format("Life max: %3.0f\n", life_max) - text=text..string.format("Life ave: %3.0f\n", life_ave) - text=text..string.format("Life ave0: %3.0f\n", life_ave0) - text=text..string.format("Group strength: %3.0f", groupstrength) + local text=string.format("State %s, Units=%d/%d, ROE=%s, AlarmState=%s, Hits=%d, Life(min/max/ave/ave0)=%d/%d/%d/%d, Total Ammo=%d, Detected=%d/%d", + self:GetState(), nunits, self.IniGroupStrength, self.CurrentROE, self.CurrentAlarmState, self.Nhit, life_min, life_max, life_ave, life_ave0, ammotot, detectedG, detectedU) MESSAGE:New(text, 10):ToAllIf(message or self.Debug) - self:T(SUPPRESSION.id.."\n"..text) + self:I(self.lid..text) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -794,6 +924,7 @@ function SUPPRESSION:onafterStart(Controllable, From, Event, To) self:_EventFromTo("onafterStart", Event, From, To) local text=string.format("Started SUPPRESSION for group %s.", Controllable:GetName()) + self:I(self.lid..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) local rzone="not defined" @@ -854,7 +985,7 @@ function SUPPRESSION:onafterStart(Controllable, From, Event, To) text=text..string.format("Speed max = %5.1f km/h\n", self.SpeedMax) text=text..string.format("Formation = %s\n", self.Formation) text=text..string.format("******************************************************\n") - self:T(SUPPRESSION.id..text) + self:T(self.lid..text) -- Add event handler. if self.eventmoose then @@ -863,28 +994,56 @@ function SUPPRESSION:onafterStart(Controllable, From, Event, To) else world.addEventHandler(self) end - + + self:__Status(-1) end -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Before "Hit" event. (Of course, this is not really before the group got hit.) +--- After "Status" event. -- @param #SUPPRESSION self -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param Wrapper.Unit#UNIT Unit Unit that was hit. --- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. --- @return boolean -function SUPPRESSION:onbeforeHit(Controllable, From, Event, To, Unit, AttackUnit) - self:_EventFromTo("onbeforeHit", Event, From, To) +function SUPPRESSION:onafterStatus(Controllable, From, Event, To) + + -- Suppressed group. + local group=self.Controllable --Wrapper.Group#GROUP - --local Tnow=timer.getTime() - --env.info(SUPPRESSION.id..string.format("Last hit = %s %s", tostring(self.LastHit), tostring(Tnow))) + -- Check if group object exists. + if group then + + -- Number of alive units. + local nunits=group:CountAliveUnits() + + -- Check if there are units. + if nunits>0 then + + -- Retreat if completely out of ammo and retreat zone defined. + local nammo=group:GetAmmunition() + if nammo==0 then + self:OutOfAmmo() + end + + -- Status report. + self:StatusReport(false) + + -- Call status again if not "Stopped". + if self:GetState()~="Stopped" then + self:__Status(-30) + end + + else + -- Stop FSM as there are no units left. + self:Stop() + end + + else + -- Stop FSM as there group object does not exist. + self:Stop() + end - return true end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- After "Hit" event. -- @param #SUPPRESSION self @@ -925,7 +1084,7 @@ function SUPPRESSION:onafterHit(Controllable, From, Event, To, Unit, AttackUnit) text=string.format("\nGroup %s: Life min=%5.1f, max=%5.1f, ave=%5.1f, ave0=%5.1f group=%5.1f\n", Controllable:GetName(), life_min, life_max, life_ave, life_ave0, groupstrength) text=string.format("Group %s: Damage = %8.4f (%8.4f retreat threshold).\n", Controllable:GetName(), Damage, self.RetreatDamage) text=string.format("Group %s: P_Flee = %5.1f %5.1f=P_rand (P_Flee > Prand ==> Flee)\n", Controllable:GetName(), Pflee, P) - self:T(SUPPRESSION.id..text) + self:T(self.lid..text) -- Group is obviously destroyed. if Damage >= 99.9 then @@ -957,11 +1116,6 @@ function SUPPRESSION:onafterHit(Controllable, From, Event, To, Unit, AttackUnit) end end - -- Give info on current status. - if self.Debug then - self:Status() - end - end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -980,7 +1134,7 @@ function SUPPRESSION:onbeforeRecovered(Controllable, From, Event, To) local Tnow=timer.getTime() -- Debug info - self:T(SUPPRESSION.id..string.format("onbeforeRecovered: Time now: %d - Time over: %d", Tnow, self.TsuppressionOver)) + 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 @@ -1005,7 +1159,7 @@ function SUPPRESSION:onafterRecovered(Controllable, From, Event, To) -- Debug message. local text=string.format("Group %s has recovered!", Controllable:GetName()) MESSAGE:New(text, 10):ToAllIf(self.Debug) - self:T(SUPPRESSION.id..text) + self:T(self.lid..text) -- Set ROE back to default. self:_SetROE() @@ -1066,7 +1220,7 @@ function SUPPRESSION:onafterFallBack(Controllable, From, Event, To, AttackUnit) self:_EventFromTo("onafterFallback", Event, From, To) -- Debug info - self:T(SUPPRESSION.id..string.format("Group %s is falling back after %d hits.", Controllable:GetName(), self.Nhit)) + self:T(self.lid..string.format("Group %s is falling back after %d hits.", Controllable:GetName(), self.Nhit)) -- Coordinate of the attacker and attacked unit. local ACoord=AttackUnit:GetCoordinate() @@ -1161,6 +1315,25 @@ function SUPPRESSION:onafterTakeCover(Controllable, From, Event, To, Hideout) end +--- After "OutOfAmmo" event. Triggered when group is completely out of ammo. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterOutOfAmmo(Controllable, From, Event, To) + self:_EventFromTo("onafterOutOfAmmo", Event, From, To) + + -- Info to log. + self:I(self.lid..string.format("Out of ammo!")) + + -- Order retreat if retreat zone was specified. + if self.RetreatZone then + self:Retreat() + end + +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Before "Retreat" event. We check that the group is not already retreating. @@ -1174,8 +1347,8 @@ function SUPPRESSION:onbeforeRetreat(Controllable, From, Event, To) self:_EventFromTo("onbeforeRetreat", Event, From, To) if From=="Retreating" then - local text=string.format("Group %s is already retreating.") - self:T2(SUPPRESSION.id..text) + local text=string.format("Group %s is already retreating.", tostring(Controllable:GetName())) + self:T2(self.lid..text) return false else return true @@ -1195,7 +1368,7 @@ function SUPPRESSION:onafterRetreat(Controllable, From, Event, To) -- Route the group to a zone. local text=string.format("Group %s is retreating! Alarm state green.", Controllable:GetName()) MESSAGE:New(text, 10):ToAllIf(self.Debug) - self:T(SUPPRESSION.id..text) + self:T(self.lid..text) -- Get a random point in the retreat zone. local ZoneCoord=self.RetreatZone:GetRandomCoordinate() -- Core.Point#COORDINATE @@ -1267,27 +1440,54 @@ end function SUPPRESSION:onafterDead(Controllable, From, Event, To) self:_EventFromTo("onafterDead", Event, From, To) - -- Number of units left in the group. - local nunits=#self.Controllable:GetUnits() - - local text=string.format("Group %s: One of our units just died! %d units left.", self.Controllable:GetName(), nunits) - MESSAGE:New(text, 10):ToAllIf(self.Debug) - self:T(SUPPRESSION.id..text) - - -- Go to stop state. - if nunits==0 then - self:T(SUPPRESSION.id..string.format("Stopping SUPPRESSION for group %s.", Controllable:GetName())) - self:Stop() - if self.mooseevents then - self:UnHandleEvent(EVENTS.Dead) - self:UnHandleEvent(EVENTS.Hit) - else - world.removeEventHandler(self) + local group=self.Controllable --Wrapper.Group#GROUP + + if group then + + -- Number of units left in the group. + local nunits=group:CountAliveUnits() + + local text=string.format("Group %s: One of our units just died! %d units left.", self.Controllable:GetName(), nunits) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Go to stop state. + if nunits==0 then + self:Stop() end + + else + self:Stop() end end +--- After "Stop" event. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterStop(Controllable, From, Event, To) + self:_EventFromTo("onafterStop", Event, From, To) + + local text=string.format("Stopping SUPPRESSION for group %s", self.Controllable:GetName()) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Clear all pending schedules + self.CallScheduler:Clear() + + if self.mooseevents then + self:UnHandleEvent(EVENTS.Dead) + self:UnHandleEvent(EVENTS.Hit) + else + world.removeEventHandler(self) + end + +end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Event Handler ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1348,7 +1548,7 @@ function SUPPRESSION:_OnEventHit(EventData) -- Check that correct group was hit. if GroupNameTgt == GroupNameSelf then - self:T(SUPPRESSION.id..string.format("Hit event at t = %5.1f", timer.getTime())) + self:T(self.lid..string.format("Hit event at t = %5.1f", timer.getTime())) -- Flare unit that was hit. if self.flare or self.Debug then @@ -1359,11 +1559,11 @@ function SUPPRESSION:_OnEventHit(EventData) self.Nhit=self.Nhit+1 -- Info on hit times. - self:T(SUPPRESSION.id..string.format("Group %s has just been hit %d times.", self.Controllable:GetName(), self.Nhit)) + self:T(self.lid..string.format("Group %s has just been hit %d times.", self.Controllable:GetName(), self.Nhit)) --self:Status() local life=tgt:getLife()/(tgt:getLife0()+1)*100 - self:T2(SUPPRESSION.id..string.format("Target unit life = %5.1f", life)) + self:T2(self.lid..string.format("Target unit life = %5.1f", life)) -- FSM Hit event. self:__Hit(3, TgtUnit, IniUnit) @@ -1380,35 +1580,35 @@ function SUPPRESSION:_OnEventDead(EventData) local GroupNameIni=EventData.IniGroupName -- Check for correct group. - if GroupNameIni== GroupNameSelf then + if GroupNameIni==GroupNameSelf then -- Dead Unit. local IniUnit=EventData.IniUnit --Wrapper.Unit#UNIT local IniUnitName=EventData.IniUnitName if EventData.IniUnit then - self:T2(SUPPRESSION.id..string.format("Group %s: Dead MOOSE unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName)) + self:T2(self.lid..string.format("Group %s: Dead MOOSE unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName)) else - self:T2(SUPPRESSION.id..string.format("Group %s: Dead MOOSE unit DOES NOT not exist! Unit name %s.", GroupNameIni, IniUnitName)) + self:T2(self.lid..string.format("Group %s: Dead MOOSE unit DOES NOT not exist! Unit name %s.", GroupNameIni, IniUnitName)) end if EventData.IniDCSUnit then - self:T2(SUPPRESSION.id..string.format("Group %s: Dead DCS unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName)) + self:T2(self.lid..string.format("Group %s: Dead DCS unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName)) else - self:T2(SUPPRESSION.id..string.format("Group %s: Dead DCS unit DOES NOT exist! Unit name %s.", GroupNameIni, IniUnitName)) + self:T2(self.lid..string.format("Group %s: Dead DCS unit DOES NOT exist! Unit name %s.", GroupNameIni, IniUnitName)) end -- Flare unit that died. if IniUnit and (self.flare or self.Debug) then IniUnit:FlareWhite() - self:T(SUPPRESSION.id..string.format("Flare Dead MOOSE unit.")) + self:T(self.lid..string.format("Flare Dead MOOSE unit.")) end -- Flare unit that died. if EventData.IniDCSUnit and (self.flare or self.Debug) then local p=EventData.IniDCSUnit:getPosition().p trigger.action.signalFlare(p, trigger.flareColor.Yellow , 0) - self:T(SUPPRESSION.id..string.format("Flare Dead DCS unit.")) + self:T(self.lid..string.format("Flare Dead DCS unit.")) end -- Get status. @@ -1462,7 +1662,7 @@ function SUPPRESSION:_Suppress() -- Debug message. local text=string.format("Group %s is suppressed for %d seconds. Suppression ends at %d:%02d.", Controllable:GetName(), Tsuppress, self.TsuppressionOver/60, self.TsuppressionOver%60) MESSAGE:New(text, 10):ToAllIf(self.Debug) - self:T(SUPPRESSION.id..text) + self:T(self.lid..text) end @@ -1481,95 +1681,101 @@ function SUPPRESSION:_Run(fin, speed, formation, wait) local group=self.Controllable -- Wrapper.Controllable#CONTROLLABLE - -- Clear all tasks. - group:ClearTasks() + if group and group:IsAlive() then - -- Current coordinates of group. - local ini=group:GetCoordinate() - - -- Distance between current and final point. - local dist=ini:Get2DDistance(fin) - - -- Heading from ini to fin. - local heading=self:_Heading(ini, fin) - - -- Number of waypoints. - local nx - if dist <= 50 then - nx=2 - elseif dist <= 100 then - nx=3 - elseif dist <= 500 then - nx=4 - else - nx=5 - end - - -- Number of intermediate waypoints. - local dx=dist/(nx-1) + -- Clear all tasks. + group:ClearTasks() - -- Waypoint and task arrays. - local wp={} - local tasks={} - - -- First waypoint is the current position of the group. - wp[1]=ini:WaypointGround(speed, formation) - tasks[1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, 1, false) - - if self.Debug then - local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)", #wp, self.Controllable:GetName())) - end - - self:T2(SUPPRESSION.id..string.format("Number of waypoints %d", nx)) - for i=1,nx-2 do - - local x=dx*i - local coord=ini:Translate(x, heading) + -- Current coordinates of group. + local ini=group:GetCoordinate() - wp[#wp+1]=coord:WaypointGround(speed, formation) - tasks[#tasks+1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, false) + -- Distance between current and final point. + local dist=ini:Get2DDistance(fin) - self:T2(SUPPRESSION.id..string.format("%d x = %4.1f", i, x)) - if self.Debug then - local MarkerID=coord:MarkToAll(string.format("Waypoing %d of group %s", #wp, self.Controllable:GetName())) + -- Heading from ini to fin. + local heading=self:_Heading(ini, fin) + + -- Number of waypoints. + local nx + if dist <= 50 then + nx=2 + elseif dist <= 100 then + nx=3 + elseif dist <= 500 then + nx=4 + else + nx=5 end + -- Number of intermediate waypoints. + local dx=dist/(nx-1) + + -- Waypoint and task arrays. + local wp={} + local tasks={} + + -- First waypoint is the current position of the group. + wp[1]=ini:WaypointGround(speed, formation) + tasks[1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, 1, false) + + if self.Debug then + local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)", #wp, self.Controllable:GetName())) + end + + self:T2(self.lid..string.format("Number of waypoints %d", nx)) + for i=1,nx-2 do + + local x=dx*i + local coord=ini:Translate(x, heading) + + wp[#wp+1]=coord:WaypointGround(speed, formation) + tasks[#tasks+1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, false) + + self:T2(self.lid..string.format("%d x = %4.1f", i, x)) + if self.Debug then + local MarkerID=coord:MarkToAll(string.format("Waypoing %d of group %s", #wp, self.Controllable:GetName())) + end + + end + self:T2(self.lid..string.format("Total distance: %4.1f", dist)) + + -- Final waypoint. + wp[#wp+1]=fin:WaypointGround(speed, formation) + if self.Debug then + local MarkerID=fin:MarkToAll(string.format("Waypoing %d of group %s (final)", #wp, self.Controllable:GetName())) + end + + -- Task to hold. + local ConditionWait=group:TaskCondition(nil, nil, nil, nil, wait, nil) + local TaskHold = group:TaskHold() + + -- Task combo to make group hold at final waypoint. + local TaskComboFin = {} + TaskComboFin[#TaskComboFin+1] = group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, true) + TaskComboFin[#TaskComboFin+1] = group:TaskControlled(TaskHold, ConditionWait) + + -- Add final task. + tasks[#tasks+1]=group:TaskCombo(TaskComboFin) + + -- Original waypoints of the group. + local Waypoints = group:GetTemplateRoutePoints() + + -- New points are added to the default route. + for i,p in ipairs(wp) do + table.insert(Waypoints, i, wp[i]) + end + + -- Set task for all waypoints. + for i,wp in ipairs(Waypoints) do + group:SetTaskWaypoint(Waypoints[i], tasks[i]) + end + + -- Submit task and route group along waypoints. + group:Route(Waypoints) + + else + self:E(self.lid..string.format("ERROR: Group is not alive!")) end - self:T2(SUPPRESSION.id..string.format("Total distance: %4.1f", dist)) - - -- Final waypoint. - wp[#wp+1]=fin:WaypointGround(speed, formation) - if self.Debug then - local MarkerID=fin:MarkToAll(string.format("Waypoing %d of group %s (final)", #wp, self.Controllable:GetName())) - end - - -- Task to hold. - local ConditionWait=group:TaskCondition(nil, nil, nil, nil, wait, nil) - local TaskHold = group:TaskHold() - - -- Task combo to make group hold at final waypoint. - local TaskComboFin = {} - TaskComboFin[#TaskComboFin+1] = group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, true) - TaskComboFin[#TaskComboFin+1] = group:TaskControlled(TaskHold, ConditionWait) - - -- Add final task. - tasks[#tasks+1]=group:TaskCombo(TaskComboFin) - - -- Original waypoints of the group. - local Waypoints = group:GetTemplateRoutePoints() - - -- New points are added to the default route. - for i,p in ipairs(wp) do - table.insert(Waypoints, i, wp[i]) - end - - -- Set task for all waypoints. - for i,wp in ipairs(Waypoints) do - group:SetTaskWaypoint(Waypoints[i], tasks[i]) - end - - -- Submit task and route group along waypoints. - group:Route(Waypoints) end @@ -1584,7 +1790,7 @@ function SUPPRESSION._Passing_Waypoint(group, Fsm, i, final) local text=string.format("Group %s passing waypoint %d (final=%s)", group:GetName(), i, tostring(final)) MESSAGE:New(text,10):ToAllIf(Fsm.Debug) if Fsm.Debug then - env.info(SUPPRESSION.id..text) + env.info(self.lid..text) end if final then @@ -1629,7 +1835,7 @@ function SUPPRESSION:_SearchHideout() -- Place markers on every possible scenery object. local MarkerID=SceneryObject:GetCoordinate():MarkToAll(string.format("%s scenery object %s", self.Controllable:GetName(),SceneryObject:GetTypeName())) local text=string.format("%s scenery: %s, Coord %s", self.Controllable:GetName(), SceneryObject:GetTypeName(), SceneryObject:GetCoordinate():ToStringLLDMS()) - self:T2(SUPPRESSION.id..text) + self:T2(self.lid..text) end -- Add to table. @@ -1642,7 +1848,7 @@ function SUPPRESSION:_SearchHideout() if #hideouts>0 then -- Debug info. - self:T(SUPPRESSION.id.."Number of hideouts "..#hideouts) + self:T(self.lid.."Number of hideouts "..#hideouts) -- Sort results table wrt number of hits. local _sort = function(a,b) return a.distance < b.distance end @@ -1655,7 +1861,7 @@ function SUPPRESSION:_SearchHideout() Hideout=hideouts[1].object:GetCoordinate() else - self:E(SUPPRESSION.id.."No hideouts found!") + self:E(self.lid.."No hideouts found!") end return Hideout @@ -1685,7 +1891,7 @@ function SUPPRESSION:_GetLife() local groupstrength=#units/self.IniGroupStrength*100 - self.T2(SUPPRESSION.id..string.format("Group %s _GetLife nunits = %d", self.Controllable:GetName(), #units)) + self.T2(self.lid..string.format("Group %s _GetLife nunits = %d", self.Controllable:GetName(), #units)) for _,unit in pairs(units) do @@ -1702,7 +1908,7 @@ function SUPPRESSION:_GetLife() life_ave=life_ave+life if self.Debug then local text=string.format("n=%02d: Life = %3.1f, Life0 = %3.1f, min=%3.1f, max=%3.1f, ave=%3.1f, group=%3.1f", n, unit:GetLife(), unit:GetLife0(), life_min, life_max, life_ave/n,groupstrength) - self:T2(SUPPRESSION.id..text) + self:T2(self.lid..text) end end @@ -1795,13 +2001,13 @@ function SUPPRESSION:_SetROE(roe) elseif roe==SUPPRESSION.ROE.Return then group:OptionROEReturnFire() else - self:E(SUPPRESSION.id.."Unknown ROE requested: "..tostring(roe)) + self:E(self.lid.."Unknown ROE requested: "..tostring(roe)) group:OptionROEOpenFire() self.CurrentROE=SUPPRESSION.ROE.Free end local text=string.format("Group %s now has ROE %s.", self.Controllable:GetName(), self.CurrentROE) - self:T(SUPPRESSION.id..text) + self:T(self.lid..text) end --- Sets the alarm state of the group and updates the current alarm state variable. @@ -1824,13 +2030,13 @@ function SUPPRESSION:_SetAlarmState(state) elseif state==SUPPRESSION.AlarmState.Red then group:OptionAlarmStateRed() else - self:E(SUPPRESSION.id.."Unknown alarm state requested: "..tostring(state)) + self:E(self.lid.."Unknown alarm state requested: "..tostring(state)) group:OptionAlarmStateAuto() self.CurrentAlarmState=SUPPRESSION.AlarmState.Auto end local text=string.format("Group %s now has Alarm State %s.", self.Controllable:GetName(), self.CurrentAlarmState) - self:T(SUPPRESSION.id..text) + self:T(self.lid..text) end --- Print event-from-to string to DCS log file. @@ -1841,7 +2047,7 @@ end -- @param #string To To state. function SUPPRESSION:_EventFromTo(BA, Event, From, To) local text=string.format("\n%s: %s EVENT %s: %s --> %s", BA, self.Controllable:GetName(), Event, From, To) - self:T2(SUPPRESSION.id..text) + self:T2(self.lid..text) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 499e0683e..06b8c67c4 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -1,12 +1,12 @@ --- **Functional** - Simulation of logistic operations. --- +-- -- === --- +-- -- ## Features: -- --- * Holds (virtual) assests in stock and spawns them upon request. +-- * Holds (virtual) assets in stock and spawns them upon request. -- * Manages requests of assets from other warehouses. --- * Queueing system with optional priorization of requests. +-- * Queueing system with optional prioritization of requests. -- * Realistic transportation of assets between warehouses. -- * Different means of automatic transportation (planes, helicopters, APCs, self propelled). -- * Strategic components such as capturing, defending and destroying warehouses and their associated infrastructure. @@ -15,18 +15,23 @@ -- * Persistence of assets. Warehouse assets can be saved and loaded from file. -- * Can be easily interfaced to other MOOSE classes. -- --- === --- --- ## Missions: --- -- === --- --- The MOOSE warehouse concept simulates the organization and implementation of complex operations regarding the flow of assets between the point of origin and the point of consumption --- in order to meet requirements of a potential conflict. In particular, this class is concerned with maintaining army supply lines while disrupting those of the enemy, since an armed +-- +-- ## Youtube Videos: +-- +-- * [Warehouse Trailer](https://www.youtube.com/watch?v=e98jzLi5fGk) +-- * [DCS Warehouse Airbase Resources Proof Of Concept](https://www.youtube.com/watch?v=YeuGL0duEgY) +-- +-- === +-- +-- ## Missions: +-- +-- === +-- +-- The MOOSE warehouse concept simulates the organization and implementation of complex operations regarding the flow of assets between the point of origin and the point of consumption +-- in order to meet requirements of a potential conflict. In particular, this class is concerned with maintaining army supply lines while disrupting those of the enemy, since an armed -- force without resources and transportation is defenseless. -- --- Please note that his class is work in progress and in an **alpha** stage. --- -- === -- -- ### Author: **funkyfranky** @@ -35,14 +40,15 @@ -- === -- -- @module Functional.Warehouse --- @image MOOSE.JPG +-- @image Warehouse.JPG --- WAREHOUSE class. -- @type WAREHOUSE -- @field #string ClassName Name of the class. -- @field #boolean Debug If true, send debug messages to all. +-- @field #string wid Identifier of the warehouse printed before other output to DCS.log file. -- @field #boolean Report If true, send status messages to coalition. --- @field Wrapper.Static#STATIC warehouse The phyical warehouse structure. +-- @field Wrapper.Static#STATIC warehouse The phyical warehouse structure. -- @field #string alias Alias of the warehouse. Name its called when sending messages. -- @field Core.Zone#ZONE zone Zone around the warehouse. If this zone is captured, the warehouse and all its assets goes to the capturing coaliton. -- @field Wrapper.Airbase#AIRBASE airbase Airbase the warehouse belongs to. @@ -50,8 +56,7 @@ -- @field Core.Point#COORDINATE road Closest point to warehouse on road. -- @field Core.Point#COORDINATE rail Closest point to warehouse on rail. -- @field Core.Zone#ZONE spawnzone Zone in which assets are spawned. --- @field #string wid Identifier of the warehouse printed before other output to DCS.log file. --- @field #number uid Unit identifier of the warehouse. Derived from id of warehouse static element. +-- @field #number uid Unique ID of the warehouse. -- @field #number markerid ID of the warehouse marker at the airbase. -- @field #number dTstatus Time interval in seconds of updating the warehouse status and processing new events. Default 30 seconds. -- @field #number queueid Unit id of each request in the queue. Essentially a running number starting at one and incremented when a new request is added. @@ -63,12 +68,18 @@ -- @field #table defending Table holding all defending requests, i.e. self requests that were if the warehouse is under attack. Table elements are of type @{#WAREHOUSE.Pendingitem}. -- @field Core.Zone#ZONE portzone Zone defining the port of a warehouse. This is where naval assets are spawned. -- @field #table shippinglanes Table holding the user defined shipping between warehouses. --- @field #table offroadpaths Table holding user defined paths from one warehouse to another. +-- @field #table offroadpaths Table holding user defined paths from one warehouse to another. -- @field #boolean autodefence When the warehouse is under attack, automatically spawn assets to defend the warehouse. -- @field #number spawnzonemaxdist Max distance between warehouse and spawn zone. Default 5000 meters. -- @field #boolean autosave Automatically save assets to file when mission ends. -- @field #string autosavepath Path where the asset file is saved on auto save. --- @field #string autosavefilename File name of the auto asset save file. Default is auto generated from warehouse id and name. +-- @field #string autosavefile File name of the auto asset save file. Default is auto generated from warehouse id and name. +-- @field #boolean safeparking If true, parking spots for aircraft are considered as occupied if e.g. a client aircraft is parked there. Default false. +-- @field #boolean isunit If true, warehouse is represented by a unit instead of a static. +-- @field #number lowfuelthresh Low fuel threshold. Triggers the event AssetLowFuel if for any unit fuel goes below this number. +-- @field #boolean respawnafterdestroyed If true, warehouse is respawned after it was destroyed. Assets are kept. +-- @field #number respawndelay Delay before respawn in seconds. +-- @field #boolean markerOn If true, markers are displayed on the F10 map. -- @extends Core.Fsm#FSM --- Have your assets at the right place at the right time - or not! @@ -76,233 +87,240 @@ -- === -- -- # The Warehouse Concept --- +-- -- The MOOSE warehouse adds a new logistic component to the DCS World. *Assets*, i.e. ground, airborne and naval units, can be transferred from one place -- to another in a realistic and highly automatic fashion. In contrast to a "DCS warehouse" these assets have a physical representation in game. In particular, -- this means they can be destroyed during the transport and add more life to the DCS world. --- --- This comes along with some additional interesting stategic aspects since capturing/defending and destroying/protecting an enemy or your --- own warehous becomes of critical importance for the development of a conflict. --- +-- +-- This comes along with some additional interesting strategic aspects since capturing/defending and destroying/protecting an enemy or your +-- own warehouse becomes of critical importance for the development of a conflict. +-- -- In essence, creating an efficient network of warehouses is vital for the success of a battle or even the whole war. Likewise, of course, cutting off the enemy --- of important supply lines by capturing or destroying warehouses or their associated infrastructure is equally important. --- +-- of important supply lines by capturing or destroying warehouses or their associated infrastructure is equally important. +-- -- ## What is a warehouse? --- +-- -- A warehouse is an abstract object represented by a physical (static) building that can hold virtual assets in stock. -- It can (but it must not) be associated with a particular airbase. The associated airbase can be an airdrome, a Helipad/FARP or a ship. --- +-- -- If another warehouse requests assets, the corresponding troops are spawned at the warehouse and being transported to the requestor or go their -- by themselfs. Once arrived at the requesting warehouse, the assets go into the stock of the requestor and can be activated/deployed when necessary. --- +-- -- ## What assets can be stored? --- +-- -- Any kind of ground, airborne or naval asset can be stored and are spawned upon request. -- The fact that the assets live only virtually in stock and are put into the game only when needed has a positive impact on the game performance. --- It also alliviates the problem of limited parking spots at smaller airbases. --- +-- It also alliviates the problem of limited parking spots at smaller airbases. +-- -- ## What means of transportation are available? --- +-- -- Firstly, all mobile assets can be send from warehouse to another on their own. --- +-- -- * Ground vehicles will use the road infrastructure. So a good road connection for both warehouses is important but also off road connections can be added if necessary. -- * Airborne units get a flightplan from the airbase of the sending warehouse to the airbase of the receiving warehouse. This already implies that for airborne --- assets both warehouses need an airbase. If either one of the warehouses does not have an associated airbase, direct transportation of airborne assest is not possible. +-- assets both warehouses need an airbase. If either one of the warehouses does not have an associated airbase, direct transportation of airborne assets is not possible. -- * Naval units can be exchanged between warehouses which possess a port, which can be defined by the user. Also shipping lanes must be specified manually but the user since DCS does not provide these. --- * Trains (would) use the available railroad infrastructure and both warehouses must have a connection to the railroad. Unfortunately, however, trains are not yet implemented to +-- * Trains (would) use the available railroad infrastructure and both warehouses must have a connection to the railroad. Unfortunately, however, trains are not yet implemented to -- a reasonable degree in DCS at the moment and hence cannot be used yet. --- --- Furthermore, ground assets can be transferred between warehouses by transport units. These are APCs, helicopters and airplanes. The transportation process is modelled +-- +-- Furthermore, ground assets can be transferred between warehouses by transport units. These are APCs, helicopters and airplanes. The transportation process is modeled -- in a realistic way by using the corresponding cargo dispatcher classes, i.e. --- +-- -- * @{AI.AI_Cargo_Dispatcher_APC#AI_DISPATCHER_APC} --- * @{AI.AI_Cargo_Dispatcher_Helicopter#AI_DISPATCHER_HELICOPTER} +-- * @{AI.AI_Cargo_Dispatcher_Helicopter#AI_DISPATCHER_HELICOPTER} -- * @{AI.AI_Cargo_Dispatcher_Airplane#AI_DISPATCHER_AIRPLANE} --- +-- -- Depending on which cargo dispatcher is used (ground or airbore), similar considerations like in the self propelled case are necessary. Howver, note that -- the dispatchers as of yet cannot use user defined off road paths for example since they are classes of their own and use a different routing logic. --- +-- -- === --- +-- -- # Creating a Warehouse --- +-- -- A MOOSE warehouse must be represented in game by a physical *static* object. For example, the mission editor already has warehouse as static object available. -- This would be a good first choice but any static object will do. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Static.png) --- +-- -- The positioning of the warehouse static object is very important for a couple of reasons. Firstly, a warehouse needs a good infrastructure so that spawned assets -- have a proper road connection or can reach the associated airbase easily. --- +-- -- ## Constructor and Start --- +-- -- Once the static warehouse object is placed in the mission editor it can be used as a MOOSE warehouse by the @{#WAREHOUSE.New}(*warehousestatic*, *alias*) constructor, -- like for example: --- +-- -- warehouseBatumi=WAREHOUSE:New(STATIC:FindByName("Warehouse Batumi"), "My optional Warehouse Alias") -- warehouseBatumi:Start() --- +-- -- The first parameter *warehousestatic* is the static MOOSE object. By default, the name of the warehouse will be the same as the name given to the static object. --- The second parameter *alias* is optional and can be used to choose a more convenient name if desired. This will be the name the warehouse calls itself when reporting messages. --- +-- The second parameter *alias* is optional and can be used to choose a more convenient name if desired. This will be the name the warehouse calls itself when reporting messages. +-- -- Note that a warehouse also needs to be started in order to be in service. This is done with the @{#WAREHOUSE.Start}() or @{#WAREHOUSE.__Start}(*delay*) functions. -- The warehouse is now fully operational and requests are being processed. --- +-- -- # Adding Assets --- +-- -- Assets can be added to the warehouse stock by using the @{#WAREHOUSE.AddAsset}(*group*, *ngroups*, *forceattribute*, *forcecargobay*, *forceweight*, *loadradius*, *skill*, *liveries*, *assignment*) function. -- The parameter *group* has to be a MOOSE @{Wrapper.Group#GROUP}. This is also the only mandatory parameters. All other parameters are optional and can be used for fine tuning if -- nessary. The parameter *ngroups* specifies how many clones of this group are added to the stock. --- +-- -- infrantry=GROUP:FindByName("Some Infantry Group") -- warehouseBatumi:AddAsset(infantry, 5) --- --- This will add five infantry groups to the warehouse stock. Note that the group should normally be a late activated template group, +-- +-- This will add five infantry groups to the warehouse stock. Note that the group should normally be a late activated template group, -- which was defined in the mission editor. But you can also add other groups which are already spawned and present in the mission. --- +-- -- Also note that the coalition of the template group (red, blue or neutral) does not matter. The coalition of the assets is determined by the coalition of the warehouse owner. --- In other words, it is no problem to add red groups to blue warehouses and vice versa. The assets will automatically have the coalition of the warehouse. --- +-- In other words, it is no problem to add red groups to blue warehouses and vice versa. The assets will automatically have the coalition of the warehouse. +-- -- You can add assets with a delay by using the @{#WAREHOUSE.__AddAsset}(*delay*, *group*, *ngroups*, *forceattribute*, *forcecargobay*, *forceweight*, *loadradius*, *skill*, *liveries*, *assignment*), -- where *delay* is the delay in seconds before the asset is added. --- +-- -- In game, the warehouse will get a mark which is regularly updated and showing the currently available assets in stock. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Stock-Marker.png) --- +-- -- ## Optional Parameters for Fine Tuning --- +-- -- By default, the generalized attribute of the asset is determined automatically from the DCS descriptor attributes. However, this might not always result in the desired outcome. -- Therefore, it is possible, to force a generalized attribute for the asset with the third optional parameter *forceattribute*, which is of type @{#WAREHOUSE.Attribute}. --- +-- -- ### Setting the Generalized Attibute -- For example, a UH-1H Huey has in DCS the attibute of an attack helicopter. But of course, it can also transport cargo. If you want to use it for transportation, you can specify this -- manually when the asset is added --- +-- -- warehouseBatumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) --- --- This becomes important when assets are requested from other warehouses as described below. In this case, the five Hueys are now marked as transport helicopters and +-- +-- This becomes important when assets are requested from other warehouses as described below. In this case, the five Hueys are now marked as transport helicopters and -- not attack helicopters. --- --- ### Setting the Cargo Bay Weight Limit +-- +-- ### Setting the Cargo Bay Weight Limit -- You can ajust the cargo bay weight limit, in case it is not calculated correctly automatically. For example, the cargo bay of a C-17A is much smaller in DCS than that of a C-130, which is -- unrealistic. This can be corrected by the *forcecargobay* parmeter which is here set to 77,000 kg --- +-- -- warehouseBatumi:AddAsset("C-17A", nil, nil, 77000) --- +-- -- The size of the cargo bay is only important when the group is used as transport carrier for other assets. --- +-- -- ### Setting the Weight -- If an asset shall be transported by a carrier it important to note that - as in real life - a carrier can only carry cargo up to a certain weight. The weight of the -- units is automatically determined from the DCS descriptor table. -- However, in the current DCS version (2.5.3) a mortar unit has a weight of 5 tons. This confuses the transporter logic, because it appears to be too have for, e.g. all APCs. --- +-- -- As a workaround, you can manually adjust the weight by the optional *forceweight* parameter: --- +-- -- warehouseBatumi:AddAsset("Mortar Alpha", nil, nil, nil, 210) --- +-- -- In this case we set it to 210 kg. Note, the weight value set is meant for *each* unit in the group. Therefore, a group consisting of three mortars will have a total weight --- of 630 kg. This is important as groups cannot be split between carrier units when transporting, i.e. the total weight of the whole group must be smaller than the +-- of 630 kg. This is important as groups cannot be split between carrier units when transporting, i.e. the total weight of the whole group must be smaller than the -- cargo bay of the transport carrier. --- +-- -- ### Setting the Load Radius -- Boading and loading of cargo into a carrier is modeled in a realistic fashion in the AI\_CARGO\DISPATCHER classes, which are used inernally by the WAREHOUSE class. -- Meaning that troops (cargo) will board, i.e. run or drive to the carrier, and only once they are in close proximity to the transporter they will be loaded (disappear). --- +-- -- Unfortunately, there are some situations where problems can occur. For example, in DCS tanks have the strong tentendcy not to drive around obstacles but rather to roll over them. -- I have seen cases where an aircraft of the same coalition as the tank was in its way and the tank drove right through the plane waiting on a parking spot and destroying it. --- +-- -- As a workaround it is possible to set a larger load radius so that the cargo units are despawned further away from the carrier via the optional **loadradius** parameter: --- +-- -- warehouseBatumi:AddAsset("Leopard 2", nil, nil, nil, nil, 250) --- +-- -- Adding the asset like this will cause the units to be loaded into the carrier already at a distance of 250 meters. --- +-- -- ### Setting the AI Skill --- --- By default, the asset has the skill of its template group. The optional parameter *skill* allows to set a different skill when the asset is added. See the +-- +-- By default, the asset has the skill of its template group. The optional parameter *skill* allows to set a different skill when the asset is added. See the -- [hoggit page](https://wiki.hoggitworld.com/view/DCS_enum_AI) possible values of this enumerator. -- For example you can use --- +-- -- warehouseBatumi:AddAsset("Leopard 2", nil, nil, nil, nil, nil, AI.Skill.EXCELLENT) --- +-- -- do set the skill of the asset to excellent. --- +-- -- ### Setting Liveries --- +-- -- By default ,the asset uses the livery of its template group. The optional parameter *liveries* allows to define one or multiple liveries. -- If multiple liveries are given in form of a table of livery names, each asset gets a random one. --- +-- -- For example --- +-- -- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, "China UN") --- --- would spawn the asset with a chinese UN livery. --- +-- +-- would spawn the asset with a Chinese UN livery. +-- -- Or --- +-- -- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, {"China UN", "German"}) --- --- would spawn the asset with either a chinese UN or German livery. Mind the curly brackets **{}** when you want to specify multiple liveries. --- +-- +-- would spawn the asset with either a Chinese UN or German livery. Mind the curly brackets **{}** when you want to specify multiple liveries. +-- -- Four each unit type, the livery names can be found in the DCS root folder under Bazar\Liveries. You have to use the name of the livery subdirectory. The names of the liveries -- as displayed in the mission editor might be different and won't work in general. --- +-- -- ### Setting an Assignment --- +-- -- Assets can be added with a specific assignment given as a text, e.g. --- +-- -- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, nil, "Go to Warehouse Kobuleti") --- +-- -- This is helpful to establish supply chains once an asset has arrived at its (first) destination and is meant to be forwarded to another warehouse. --- +-- -- ## Retrieving the Asset --- +-- -- Once a an asset is added to a warehouse, the @{#WAREHOUSE.NewAsset} event is triggered. You can hook into this event with the @{#WAREHOUSE.OnAfterNewAsset}(*asset*, *assignment*) function. --- +-- -- The first parameter *asset* is a table of type @{#WAREHOUSE.Assetitem} and contains a lot of information about the asset. The seconed parameter *assignment* is optional and is the specific -- assignment the asset got when it was added. --- +-- -- Note that the assignment is can also be the assignment that was specified when adding a request (see next section). Once an asset that was requested from another warehouse and an assignment -- was specified in the @{#WAREHOUSE.AddRequest} function, the assignment can be checked when the asset has arrived and is added to the receiving warehouse. --- +-- -- === -- -- # Requesting Assets --- +-- -- Assets of the warehouse can be requested by other MOOSE warehouses. A request will first be scrutinized to check if can be fulfilled at all. If the request is valid, it is -- put into the warehouse queue and processed as soon as possible. -- +-- Requested assets spawn in various "Rule of Engagement Rules" (ROE) and Alerts modes. If your assets will cross into dangerous areas, be sure to change these states. You can do this in @{#WAREHOUSE:OnAfterAssetSpawned}(*From, *Event, *To, *group, *asset, *request)) function. +-- +-- Initial Spawn states is as follows: +-- GROUND: ROE, "Return Fire" Alarm, "Green" +-- AIR: ROE, "Return Fire" Reaction to Threat, "Passive Defense" +-- NAVAL ROE, "Return Fire" Alarm,"N/A" +-- -- A request can be added by the @{#WAREHOUSE.AddRequest}(*warehouse*, *AssetDescriptor*, *AssetDescriptorValue*, *nAsset*, *TransportType*, *nTransport*, *Prio*, *Assignment*) function. -- The parameters are --- +-- -- * *warehouse*: The requesting MOOSE @{#WAREHOUSE}. Assets will be delivered there. --- * *AssetDescriptor*: The descriptor to describe the asset "type". See the @{#WAREHOUSE.Descriptor} enumerator. For example, assets requested by their generalized attibute. +-- * *AssetDescriptor*: The descriptor to describe the asset "type". See the @{#WAREHOUSE.Descriptor} enumerator. For example, assets requested by their generalized attibute. -- * *AssetDescriptorValue*: The value of the asset descriptor. -- * *nAsset*: (Optional) Number of asset group requested. Default is one group. -- * *TransportType*: (Optional) The transport method used to deliver the assets to the requestor. Default is that assets go to the requesting warehouse on their own. -- * *nTransport*: (Optional) Number of asset groups used to transport the cargo assets from A to B. Default is one group. -- * *Prio*: (Optional) A number between 1 (high) and 100 (low) describing the priority of the request. Request with high priority are processed first. Default is 50, i.e. medium priority. --- * *Assignment*: (Optional) A free to choose string describing the assignment. For self requests, this can be used to assign the spawned groups to specific tasks. --- +-- * *Assignment*: (Optional) A free to choose string describing the assignment. For self requests, this can be used to assign the spawned groups to specific tasks. +-- -- ## Requesting by Generalized Attribute --- --- Generalized attributes are similar to [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). However, they are a bit more general and +-- +-- Generalized attributes are similar to [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). However, they are a bit more general and -- an asset can only have one generalized attribute by which it is characterized. --- +-- -- For example: --- +-- -- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.APC, 2) -- -- Here, warehouse Kobuleti requests 5 infantry groups from warehouse Batumi. These "cargo" assets should be transported from Batumi to Kobuleti by 2 APCS. -- Note that the warehouse at Batumi needs to have at least five infantry groups and two APC groups in their stock if the request can be processed. -- If either to few infantry or APC groups are available when the request is made, the request is held in the warehouse queue until enough cargo and -- transport assets are available. --- +-- -- Also note that the above request is for five infantry groups. So any group in stock that has the generalized attribute "GROUND_INFANTRY" can be selected for the request. --- +-- -- ### Generalized Attributes --- +-- -- Currently implemented are: -- -- * @{#WAREHOUSE.Attribute.AIR_TRANSPORTPLANE} Airplane with transport capability. This can be used to transport other assets. @@ -332,76 +350,76 @@ -- * @{#WAREHOUSE.Attribute.OTHER_UNKNOWN} Anything that does not fall into any other category. -- -- ## Requesting a Specific Unit Type --- +-- -- A more specific request could look like: --- +-- -- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.UNITTYPE, "A-10C", 2) --- +-- -- Here, Kobuleti requests a specific unit type, in particular two groups of A-10Cs. Note that the spelling is important as it must exacly be the same as -- what one get's when using the DCS unit type. --- +-- -- ## Requesting a Specific Group --- +-- -- An even more specific request would be: --- +-- -- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Group Name as in ME", 3) --- +-- -- In this case three groups named "Group Name as in ME" are requested. This explicitly request the groups named like that in the Mission Editor. --- +-- -- ## Requesting a General Category --- +-- -- On the other hand, very general and unspecifc requests can be made by the categroy descriptor. The descriptor value parameter can be any [group category](https://wiki.hoggitworld.com/view/DCS_Class_Group), i.e. --- +-- -- * Group.Category.AIRPLANE for fixed wing aircraft, -- * Group.Category.HELICOPTER for helicopters, -- * Group.Category.GROUND for all ground troops, -- * Group.Category.SHIP for naval assets, -- * Group.Category.TRAIN for trains (not implemented and not working in DCS yet). --- +-- -- For example, --- +-- -- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, 10) --- +-- -- means that Kubuleti requests 10 ground groups and does not care which ones. This could be a mix of infantry, APCs, trucks etc. --- +-- -- **Note** that these general requests should be made with *great care* due to the fact, that depending on what a warehouse has in stock a lot of different unit types can be spawned. --- +-- -- ## Requesting Relative Quantities --- --- In addition to requesting absolute numbers of assets it is possible to request relative amounts of assets currently in stock. To this end the @{#WAREHOUSE.Quantity} enumerator +-- +-- In addition to requesting absolute numbers of assets it is possible to request relative amounts of assets currently in stock. To this end the @{#WAREHOUSE.Quantity} enumerator -- was introduced: --- +-- -- * @{#WAREHOUSE.Quantity.ALL} -- * @{#WAREHOUSE.Quantity.HALF} -- * @{#WAREHOUSE.Quantity.QUARTER} -- * @{#WAREHOUSE.Quantity.THIRD} -- * @{#WAREHOUSE.Quantity.THREEQUARTERS} --- +-- -- For example, --- +-- -- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.HELICOPTER, WAREHOUSE.Quantity.HALF) --- +-- -- means that Kobuleti warehouse requests half of all available helicopters which Batumi warehouse currently has in stock. --- +-- -- # Employing Assets - The Self Request --- --- Transferring assets from one warehouse to another is important but of course once the the assets are at the "right" place it is equally important that they +-- +-- Transferring assets from one warehouse to another is important but of course once the the assets are at the "right" place it is equally important that they -- can be employed for specific tasks and assignments. --- +-- -- Assets in the warehouses stock can be used for user defined tasks quite easily. They can be spawned into the game by a "***self request***", i.e. the warehouse -- requests the assets from itself: --- +-- -- warehouseBatumi:AddRequest(warehouseBatumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5) --- +-- -- Note that the *sending* and *requesting* warehouses are *identical* in this case. --- +-- -- This would simply spawn five infantry groups in the spawn zone of the Batumi warehouse if/when they are available. --- +-- -- ## Accessing the Assets --- --- If a warehouse requests assets from itself, it triggers the event **SelfReqeuest**. The mission designer can capture this event with the associated +-- +-- If a warehouse requests assets from itself, it triggers the event **SelfReqeuest**. The mission designer can capture this event with the associated -- @{#WAREHOUSE.OnAfterSelfRequest}(*From*, *Event*, *To*, *groupset*, *request*) function. --- +-- -- --- OnAfterSelfRequest user function. Access groups spawned from the warehouse for further tasking. -- -- @param #WAREHOUSE self -- -- @param #string From From state. @@ -412,128 +430,128 @@ -- function WAREHOUSE:OnAfterSelfRequest(From, Event, To, groupset, request) -- local groupset=groupset --Core.Set#SET_GROUP -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- for _,group in pairs(groupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP -- group:SmokeGreen() -- end --- +-- -- end --- +-- -- The variable *groupset* is a @{Core.Set#SET_GROUP} object and holds all asset groups from the request. The code above shows, how the mission designer can access the groups -- for further tasking. Here, the groups are only smoked but, of course, you can use them for whatever assignment you fancy. --- +-- -- Note that airborne groups are spawned in **uncontrolled state** and need to be activated first before they can begin with their assigned tasks and missions. -- This can be done with the @{Wrapper.Controllable#CONTROLLABLE.StartUncontrolled} function as demonstrated in the example section below. --- +-- -- === --- +-- -- # Infrastructure --- +-- -- A good infrastructure is important for a warehouse to be efficient. Therefore, the location of a warehouse should be chosen with care. -- This can also help to avoid many DCS related issues such as units getting stuck in buildings, blocking taxi ways etc. --- +-- -- ## Spawn Zone --- +-- -- By default, the zone were ground assets are spawned is a circular zone around the physical location of the warehouse with a radius of 200 meters. However, the location of the -- spawn zone can be set by the @{#WAREHOUSE.SetSpawnZone}(*zone*) functions. It is advisable to choose a zone which is clear of obstacles. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Batumi.png) --- +-- -- The parameter *zone* is a MOOSE @{Core.Zone#ZONE} object. So one can, e.g., use trigger zones defined in the mission editor. If a cicular zone is not desired, one -- can use a polygon zone (see @{Core.Zone#ZONE_POLYGON}). --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_SpawnPolygon.png) --- +-- -- ## Road Connections --- +-- -- Ground assets will use a road connection to travel from one warehouse to another. Therefore, a proper road connection is necessary. --- +-- -- By default, the closest point on road to the center of the spawn zone is chosen as road connection automatically. But only, if distance between the spawn zone -- and the road connection is less than 3 km. --- +-- -- The user can set the road connection manually with the @{#WAREHOUSE.SetRoadConnection} function. This is only functional for self propelled assets at the moment -- and not if using the AI dispatcher classes since these have a different logic to find the route. --- +-- -- ## Off Road Connections --- +-- -- For ground troops it is also possible to define off road paths between warehouses if no proper road connection is available or should not be used. --- +-- -- An off road path can be defined via the @{#WAREHOUSE.AddOffRoadPath}(*remotewarehouse*, *group*, *oneway*) function, where -- *remotewarehouse* is the warehouse to which the path leads. -- The parameter *group* is a *late activated* template group. The waypoints of this group are used to define the path between the two warehouses. -- By default, the reverse paths is automatically added to get *from* the remote warehouse *to* this warehouse unless the parameter *oneway* is set to *true*. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Off-Road_Paths.png) --- +-- -- **Note** that if an off road connection is defined between two warehouses this becomes the default path, i.e. even if there is a path *on road* possible -- this will not be used. --- +-- -- Also note that you can define multiple off road connections between two warehouses. If there are multiple paths defined, the connection is chosen randomly. -- It is also possible to add the same path multiple times. By this you can influence the probability of the chosen path. For example Path1(A->B) has been -- added two times while Path2(A->B) was added only once. Hence, the group will choose Path1 with a probability of 66.6 % while Path2 is only chosen with --- a probability of 33.3 %. --- +-- a probability of 33.3 %. +-- -- ## Rail Connections --- +-- -- A rail connection is automatically defined as the closest point on a railway measured from the center of the spawn zone. But only, if the distance is less than 3 km. --- +-- -- The mission designer can manually specify a rail connection with the @{#WAREHOUSE.SetRailConnection} function. --- +-- -- **NOTE** however, that trains in DCS are currently not implemented in a way so that they can be used. --- +-- -- ## Air Connections --- +-- -- In order to use airborne assets, a warehouse needs to have an associated airbase. This can be an airdrome, a FARP/HELOPAD or a ship. --- +-- -- If there is an airbase within 3 km range of the warehouse it is automatically set as the associated airbase. A user can set an airbase manually -- with the @{#WAREHOUSE.SetAirbase} function. Keep in mind that sometimes ground units need to walk/drive from the spawn zone to the airport -- to get to their transport carriers. --- +-- -- ## Naval Connections --- +-- -- Natively, DCS does not have the concept of a port/habour or shipping lanes. So in order to have a meaningful transfer of naval units between warehouses, these have to be -- defined by the mission designer. --- +-- -- ### Defining a Port --- +-- -- A port in this context is the zone where all naval assets are spawned. This zone can be defined with the function @{#WAREHOUSE.SetPortZone}(*zone*), where the parameter -- *zone* is a MOOSE zone. So again, this can be create from a trigger zone defined in the mission editor or if a general shape is desired by a @{Core.Zone#ZONE_POLYGON}. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_PortZone.png) --- +-- -- ### Defining Shipping Lanes --- +-- -- A shipping lane between to warehouses can be defined by the @{#WAREHOUSE.AddShippingLane}(*remotewarehouse*, *group*, *oneway*) function. The first parameter *remotewarehouse* -- is the warehouse which should be connected to the present warehouse. --- +-- -- The parameter *group* should be a late activated group defined in the mission editor. The waypoints of this group are used as waypoints of the shipping lane. --- +-- -- By default, the reverse lane is automatically added to the remote warehouse. This can be disabled by setting the *oneway* parameter to *true*. --- +-- -- Similar to off road connections, you can also define multiple shipping lanes between two warehouse ports. If there are multiple lanes defined, one is chosen randomly. -- It is possible to add the same lane multiple times. By this you can influence the probability of the chosen lane. For example Lane_1(A->B) has been -- added two times while Lane_2(A->B) was added only once. Therefore, the ships will choose Lane_1 with a probability of 66.6 % while Path_2 is only chosen with --- a probability of 33.3 %. --- --- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_ShippingLane.png) --- +-- a probability of 33.3 %. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_ShippingLane.png) +-- -- === -- -- # Why is my request not processed? -- --- For each request, the warehouse class logic does a lot of consistancy and validation checks under the hood. +-- For each request, the warehouse class logic does a lot of consistency and validation checks under the hood. -- This helps to circumvent a lot of DCS issues and shortcomings. For example, it is checked that enough free -- parking spots at an airport are available *before* the assets are spawned. --- However, this also means that sometimes a request is deemed to be *invalid* in which case they are deleted +-- However, this also means that sometimes a request is deemed to be *invalid* in which case they are deleted -- from the queue or considered to be valid but cannot be executed at this very moment. --- +-- -- ## Invalid Requests --- --- Invalid request are requests which can **never** be processes because there is some logical or physical argument against it. +-- +-- Invalid request are requests which can **never** be processes because there is some logical or physical argument against it. -- (Or simply because that feature was not implemented (yet).) --- --- * All airborne assets need an associated airbase of any kind on the sending *and* receiving warhouse. +-- +-- * All airborne assets need an associated airbase of any kind on the sending *and* receiving warehouse. -- * Airplanes need an airdrome at the sending and receiving warehouses. -- * Not enough parking spots of the right terminal type at the sending warehouse. This avoids planes spawning on runways or on top of each other. -- * No parking spots of the right terminal type at the receiving warehouse. This avoids DCS despawning planes on landing if they have no valid parking spot. @@ -545,112 +563,113 @@ -- * If transport by airplane, both warehouses must have and airdrome. -- * If transport by APC, both warehouses must have a road connection. -- * If transport by helicopter, the sending airbase must have an associated airbase (airdrome or FARP). --- +-- -- All invalid requests are cancelled and **removed** from the warehouse queue! --- +-- -- ## Temporarily Unprocessable Requests --- --- Temporarily unprocessable requests are possible in priciple, but cannot be processed at the given time the warehouse checks its queue. --- +-- +-- Temporarily unprocessable requests are possible in principle, but cannot be processed at the given time the warehouse checks its queue. +-- -- * No enough parking spaces are available for all requested assets but the airbase has enough parking spots in total so that this request is possible once other aircraft have taken off. -- * The requesting warehouse is not in state "Running" (could be paused, not yet started or under attack). -- * Not enough cargo assets available at this moment. -- * Not enough free parking spots for all cargo or transport airborne assets at the moment. -- * Not enough transport assets to carry all cargo assets. --- +-- -- Temporarily unprocessable requests are held in the queue. If at some point in time, the situation changes so that these requests can be processed, they are executed. --- +-- -- ## Cargo Bay and Weight Limitations --- --- The transporation of cargo is handled by the AI\_Dispatcher classes. These take the cargo bay of a carrier and the weight of +-- +-- The transportation of cargo is handled by the AI\_Dispatcher classes. These take the cargo bay of a carrier and the weight of -- the cargo into account so that a carrier can only load a realistic amount of cargo. --- +-- -- However, if troops are supposed to be transported between warehouses, there is one important limitations one has to keep in mind. --- This is that **cargo asset groups cannot be split** and devided into separate carrier units! --- +-- This is that **cargo asset groups cannot be split** and divided into separate carrier units! +-- -- For example, a TPz Fuchs has a cargo bay large enough to carry up to 10 soldiers at once, which is a realistic number. -- If a group consisting of more than ten soldiers needs to be transported, it cannot be loaded into the APC. --- Even if two APCs are available, which could in principle carry up to 20 soldiers, a group of, let's say 12 soldiers will not --- be split into a group of ten soldiers using the first APC and a group two soldiers using the second APC. --- +-- Even if two APCs are available, which could in principle carry up to 20 soldiers, a group of, let's say 12 soldiers will not +-- be split into a group of ten soldiers using the first APC and a group two soldiers using the second APC. +-- -- In other words, **there must be at least one carrier unit available that has a cargo bay large enough to load the heaviest cargo group!** -- The warehouse logic will automatically search all available transport assets for a large enough carrier. -- But if none is available, the request will be queued until a suitable carrier becomes available. --- +-- -- The only realistic solution in this case is to either provide a transport carrier with a larger cargo bay or to reduce the number of soldiers -- in the group. --- +-- -- A better way would be to have two groups of max. 10 soldiers each and one TPz Fuchs for transport. In this case, the first group is -- loaded and transported to the receiving warehouse. Once this is done, the carrier will drive back and pick up the remaining -- group. --- +-- -- As an artificial workaround one can manually set the cargo bay size to a larger value or alternatively reduce the weight of the cargo -- when adding the assets via the @{#WAREHOUSE.AddAsset} function. This might even be unavoidable if, for example, a SAM group -- should be transported since SAM sites only work when all units are in the same group. --- +-- -- ## Processing Speed --- +-- -- A warehouse has a limited speed to process requests. Each time the status of the warehouse is updated only one requests is processed. -- The time interval between status updates is 30 seconds by default and can be adjusted via the @{#WAREHOUSE.SetStatusUpdate}(*interval*) function. -- However, the status is also updated on other occasions, e.g. when a new request was added. --- +-- -- === --- +-- -- # Strategic Considerations --- +-- -- Due to the fact that a warehouse holds (or can hold) a lot of valuable assets, it makes a (potentially) juicy target for enemy attacks. --- There are several interesting situations, which can occurr. --- +-- There are several interesting situations, which can occur. +-- -- ## Capturing a Warehouses Airbase --- +-- -- If a warehouse has an associated airbase, it can be captured by the enemy. In this case, the warehouse looses its ability so employ all airborne assets and is also cut-off -- from supply by airplanes. Supply of ground troops via helicopters is still possible, because they deliver the troops into the spawn zone. --- --- Technically, the capturing of the airbase is triggered by the DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) event. +-- +-- Technically, the capturing of the airbase is triggered by the DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) event. -- So the capturing takes place when only enemy ground units are in the airbase zone whilst no ground units of the present airbase owner are in that zone. --- +-- -- The warehouse will also create an event **AirbaseCaptured**, which can be captured by the @{#WAREHOUSE.OnAfterAirbaseCaptured} function. So the warehouse chief can react on -- this attack and for example deploy ground groups to re-capture its airbase. --- +-- -- When an airbase is re-captured the event **AirbaseRecaptured** is triggered and can be captured by the @{#WAREHOUSE.OnAfterAirbaseRecaptured} function. -- This can be used to put the defending assets back into the warehouse stock. --- +-- -- ## Capturing the Warehouse --- +-- -- A warehouse can be captured by the enemy coalition. If enemy ground troops enter the warehouse zone the event **Attacked** is triggered which can be captured by the -- @{#WAREHOUSE.OnAfterAttacked} event. By default the warehouse zone circular zone with a radius of 500 meters located at the center of the physical warehouse. --- The warehouse zone can be set via the @{#WAREHOUSE.SetWarehouseZone}(*zone*) function. The parameter *zone* must also be a cirular zone. --- +-- The warehouse zone can be set via the @{#WAREHOUSE.SetWarehouseZone}(*zone*) function. The parameter *zone* must also be a circular zone. +-- -- The @{#WAREHOUSE.OnAfterAttacked} function can be used by the mission designer to react to the enemy attack. For example by deploying some or all ground troops -- currently in stock to defend the warehouse. Note that the warehouse also has a self defence option which can be enabled by the @{#WAREHOUSE.SetAutoDefenceOn}() -- function. In this case, the warehouse will automatically spawn all ground troops. If the spawn zone is further away from the warehouse zone, all mobile troops --- are routed to the warehouse zone. --- +-- are routed to the warehouse zone. The self request which is triggered on an automatic defence has the assignment "AutoDefence". So you can use this to +-- give orders to the groups that were spawned using the @{#WAREHOUSE.OnAfterSelfRequest} function. +-- -- If only ground troops of the enemy coalition are present in the warehouse zone, the warehouse and all its assets falls into the hands of the enemy. -- In this case the event **Captured** is triggered which can be captured by the @{#WAREHOUSE.OnAfterCaptured} function. --- --- The warehouse turns to the capturing coalition, i.e. its physical representation, and all assets as well. In paticular, all requests to the warehouse will --- spawn assets beloning to the new owner. --- --- If the enemy troops could be defeated, i.e. no more troops of the opposite coalition are in the warehouse zone, the event **Defeated** is triggered and +-- +-- The warehouse turns to the capturing coalition, i.e. its physical representation, and all assets as well. In particular, all requests to the warehouse will +-- spawn assets belonging to the new owner. +-- +-- If the enemy troops could be defeated, i.e. no more troops of the opposite coalition are in the warehouse zone, the event **Defeated** is triggered and -- the @{#WAREHOUSE.OnAfterDefeated} function can be used to adapt to the new situation. For example putting back all spawned defender troops back into -- the warehouse stock. Note that if the automatic defence is enabled, all defenders are automatically put back into the warehouse on the **Defeated** event. --- +-- -- ## Destroying a Warehouse --- --- If an enemy destroy the physical warehouse structure, the warehouse will of course stop all its services. In priciple, all assets contained in the warehouse are +-- +-- If an enemy destroy the physical warehouse structure, the warehouse will of course stop all its services. In principle, all assets contained in the warehouse are -- gone as well. So a warehouse should be properly defended. --- +-- -- Upon destruction of the warehouse, the event **Destroyed** is triggered, which can be captured by the @{#WAREHOUSE.OnAfterDestroyed} function. --- So the mission designer can intervene at this point and for example choose to spawn all or paricular types of assets before the warehouse is gone for good. +-- So the mission designer can intervene at this point and for example choose to spawn all or particular types of assets before the warehouse is gone for good. -- -- === --- +-- -- # Hook in and Take Control --- +-- -- The Finite State Machine implementation allows mission designers to hook into important events and add their own code. -- Most of these events have already been mentioned but here is the list at a glance: --- +-- -- * "NotReadyYet" --> "Start" --> "Running" (Starting the warehouse) -- * "*" --> "Status" --> "*" (status updated in regular intervals) -- * "*" --> "AddAsset" --> "*" (adding a new asset to the warehouse stock) @@ -667,28 +686,30 @@ -- * "Attacked" --> "Captured" --> "Running" (warehouse was captured by the enemy) -- * "*" --> "AirbaseCaptured" --> "*" (airbase belonging to the warehouse was captured by the enemy) -- * "*" --> "AirbaseRecaptured" --> "*" (airbase was re-captured) --- * "*" --> "AssetDead" --> "*" (a whole asset group is dead) +-- * "*" --> "AssetSpawned" --> "*" (an asset has been spawned into the world) +-- * "*" --> "AssetLowFuel" --> "*" (an asset is running low on fuel) +-- * "*" --> "AssetDead" --> "*" (a whole asset, i.e. all its units/groups, is dead) -- * "*" --> "Destroyed" --> "Destroyed" (warehouse was destroyed) -- * "Running" --> "Pause" --> "Paused" (warehouse is paused) -- * "Paused" --> "Unpause" --> "Running" (warehouse is unpaused) -- * "*" --> "Stop" --> "Stopped" (warehouse is stopped) --- +-- -- The transitions are of the general form "From State" --> "Event" --> "To State". The "*" star denotes that the transition is possible from *any* state. -- Some transitions, however, are only allowed from certain "From States". For example, no requests can be processed if the warehouse is in "Paused" or "Destroyed" or "Stopped" state. -- -- Mission designers can capture the events with OnAfterEvent functions, e.g. @{#WAREHOUSE.OnAfterDelivered} or @{#WAREHOUSE.OnAfterAirbaseCaptured}. --- +-- -- === --- +-- -- # Persistence of Assets --- +-- -- Assets in stock of a warehouse can be saved to a file on your hard drive and then loaded from that file at a later point. This enables to restart the mission -- and restore the warehouse stock. --- +-- -- ## Prerequisites --- +-- -- **Important** By default, DCS does not allow for writing data to files. Therefore, one first has to comment out the line "sanitizeModule('io')", i.e. --- +-- -- do -- sanitizeModule('os') -- --sanitizeModule('io') @@ -698,62 +719,62 @@ -- end -- -- in the file "MissionScripting.lua", which is located in the subdirectory "Scripts" of your DCS installation root directory. --- +-- -- ### Don't! --- +-- -- Do not use **semi-colons** or **equal signs** in the group names of your assets as these are used as separators in the saved and loaded files texts. -- If you do, it will cause problems and give you a headache! --- +-- -- ## Save Assets --- +-- -- Saving asset data to file is achieved by the @{WAREHOUSE.Save}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the -- warehouse data is saved. If you do not specify a path, the file is saved your the DCS installation root directory. -- The parameter *filename* is optional and defines the name of the saved file. By default this is automatically created from the warehouse id and name, for example -- "Warehouse-1234_Batumi.txt". --- +-- -- warehouseBatumi:Save("D:\\My Warehouse Data\\") --- +-- -- This will save all asset data to in "D:\\My Warehouse Data\\Warehouse-1234_Batumi.txt". --- +-- -- ### Automatic Save at Mission End --- +-- -- The assets can be saved automatically when the mission is ended via the @{WAREHOUSE.SetSaveOnMissionEnd}(*path*, *filename*) function, i.e. --- +-- -- warehouseBatumi:SetSaveOnMissionEnd("D:\\My Warehouse Data\\") --- +-- -- ## Load Assets --- +-- -- Loading assets data from file is achieved by the @{WAREHOUSE.Load}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the -- warehouse data is loaded from. If you do not specify a path, the file is loaded from your the DCS installation root directory. -- The parameter *filename* is optional and defines the name of the file to load. By default this is automatically generated from the warehouse id and name, for example -- "Warehouse-1234_Batumi.txt". --- +-- -- Note that the warehouse **must not be started** and in the *Running* state in order to load the assets. In other words, loading should happen after the -- @{#WAREHOUSE.New} command is specified in the code but before the @{#WAREHOUSE.Start} command is given. --- +-- -- Loading the assets is done by --- +-- -- warehouseBatumi:New(STATIC:FindByName("Warehouse Batumi")) -- warehouseBatumi:Load("D:\\My Warehouse Data\\") -- warehouseBatumi:Start() --- +-- -- This sequence loads all assets from file. If a warehouse was captured in the last mission, it also respawns the static warehouse structure with the right coaliton. -- However, it due to DCS limitations it is not possible to set the airbase coalition. This has to be done manually in the mission editor. Or alternatively, one could -- spawn some ground units via a self request and let them capture the airbase. --- +-- -- === -- -- # Examples --- +-- -- This section shows some examples how the WAREHOUSE class is used in practice. This is one of the best ways to explain things, in my opinion. --- +-- -- But first, let me introduce a convenient way to define several warehouses in a table. This is absolutely *not necessary* but quite handy if you have -- multiple WAREHOUSE objects in your mission. --- +-- -- ## Example 0: Setting up a Warehouse Array --- +-- -- If you have multiple warehouses, you can put them in a table. This makes it easier to access them or to loop over them. --- +-- -- -- Define Warehouses. -- local warehouse={} -- -- Blue warehouses @@ -771,118 +792,118 @@ -- warehouse.Sochi = WAREHOUSE:New(STATIC:FindByName("Warehouse Sochi"), "Sochi") --Functional.Warehouse#WAREHOUSE -- -- Remarks: --- +-- -- * I defined the array as local, i.e. local warehouse={}. This is personal preference and sometimes causes trouble with the lua garbage collection. You can also define it as a global array/table! -- * The "--Functional.Warehouse#WAREHOUSE" at the end is only to have the LDT intellisense working correctly. If you don't use LDT (which you should!), it can be omitted. -- -- **NOTE** that all examples below need this bit or code at the beginning - or at least the warehouses which are used. --- +-- -- The example mission is based on the same template mission, which has defined a lot of airborne, ground and naval assets as templates. Only few of those are used here. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Assets.png) --- +-- -- ## Example 1: Self Request --- +-- -- Ground troops are taken from the Batumi warehouse stock and spawned in its spawn zone. After a short delay, they are added back to the warehouse stock. -- Also a new request is made. Hence, the groups will be spawned, added back to the warehouse, spawned again and so on and so forth... --- +-- -- -- Start warehouse Batumi. -- warehouse.Batumi:Start() --- +-- -- -- Add five groups of infantry as assets. -- warehouse.Batumi:AddAsset(GROUP:FindByName("Infantry Platoon Alpha"), 5) --- +-- -- -- Add self request for three infantry at Batumi. -- warehouse.Batumi:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 3) --- --- +-- +-- -- --- Self request event. Triggered once the assets are spawned in the spawn zone or at the airbase. -- function warehouse.Batumi:OnAfterSelfRequest(From, Event, To, groupset, request) -- local mygroupset=groupset --Core.Set#SET_GROUP --- +-- -- -- Loop over all groups spawned from that request. -- for _,group in pairs(mygroupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP --- +-- -- -- Gree smoke on spawned group. -- group:SmokeGreen() --- +-- -- -- Put asset back to stock after 10 seconds. --- warehouse.Batumi:__AddAsset(10, group) +-- warehouse.Batumi:__AddAsset(10, group) -- end --- +-- -- -- Add new self request after 20 seconds. -- warehouse.Batumi:__AddRequest(20, warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 3) --- +-- -- end -- -- ## Example 2: Self propelled Ground Troops --- +-- -- Warehouse Berlin, which is a FARP near Batumi, requests infantry and troop transports from the warehouse at Batumi. --- The groups are spawned at Batumi and move by themselfs from Batumi to Berlin using the roads. +-- The groups are spawned at Batumi and move by themselves from Batumi to Berlin using the roads. -- Once the troops have arrived at Berlin, the troops are automatically added to the warehouse stock of Berlin. -- While on the road, Batumi has requested back two APCs from Berlin. Since Berlin does not have the assets in stock, -- the request is queued. After the troops have arrived, Berlin is sending back the APCs to Batumi. --- +-- -- -- Start Warehouse at Batumi. -- warehouse.Batumi:Start() --- +-- -- -- Add 20 infantry groups and ten APCs as assets at Batumi. -- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) -- warehouse.Batumi:AddAsset("TPz Fuchs", 10) --- --- -- Start Warehouse Berlin. +-- +-- -- Start Warehouse Berlin. -- warehouse.Berlin:Start() --- +-- -- -- Warehouse Berlin requests 10 infantry groups and 5 APCs from warehouse Batumi. -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 10) -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, 5) --- +-- -- -- Request from Batumi for 2 APCs. Initially these are not in stock. When they become available, the request is executed. --- warehouse.Berlin:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, 2) +-- warehouse.Berlin:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, 2) -- -- ## Example 3: Self Propelled Airborne Assets --- +-- -- Warehouse Senaki receives a high priority request from Kutaisi for one Yak-52s. At the same time, Kobuleti requests half of -- all available Yak-52s. Request from Kutaisi is first executed and then Kobuleti gets half of the remaining assets. -- Additionally, London requests one third of all available UH-1H Hueys from Senaki. --- Once the units have arrived they are added to the stock of the receiving warehouses and can be used for further assignments. --- +-- Once the units have arrived they are added to the stock of the receiving warehouses and can be used for further assignments. +-- -- -- Start warehouses -- warehouse.Senaki:Start() -- warehouse.Kutaisi:Start() -- warehouse.Kobuleti:Start() -- warehouse.London:Start() --- +-- -- -- Add assets to Senaki warehouse. -- warehouse.Senaki:AddAsset("Yak-52", 10) -- warehouse.Senaki:AddAsset("Huey", 6) --- +-- -- -- Kusaisi requests 3 Yak-52 form Senaki while Kobuleti wants all the rest. -- warehouse.Senaki:AddRequest(warehouse.Kutaisi, WAREHOUSE.Descriptor.GROUPNAME, "Yak-52", 1, nil, nil, 10) -- warehouse.Senaki:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Yak-52", WAREHOUSE.Quantity.HALF, nil, nil, 70) --- +-- -- -- FARP London wants 1/3 of the six available Hueys. -- warehouse.Senaki:AddRequest(warehouse.London, WAREHOUSE.Descriptor.GROUPNAME, "Huey", WAREHOUSE.Quantity.THIRD) -- -- ## Example 4: Transport of Assets by APCs --- +-- -- Warehouse at FARP Berlin requests five infantry groups from Batumi. These assets shall be transported using two APC groups. --- Infantry and APC are spawned in the spawn zone at Batumi. The APCs have a cargo bay large enough to pick up four of the +-- Infantry and APC are spawned in the spawn zone at Batumi. The APCs have a cargo bay large enough to pick up four of the -- five infantry groups in the first run and will bring them to Berlin. There, they unboard and walk to the warehouse where they will be added to the stock. -- Meanwhile the APCs go back to Batumi and one will pick up the last remaining soldiers. --- Once the APCs have completed their mission, they return to Batumi and are added back to stock. --- +-- Once the APCs have completed their mission, they return to Batumi and are added back to stock. +-- -- -- Start Warehouse at Batumi. -- warehouse.Batumi:Start() --- --- -- Start Warehouse Berlin. +-- +-- -- Start Warehouse Berlin. -- warehouse.Berlin:Start() --- +-- -- -- Add 20 infantry groups and five APCs as assets at Batumi. -- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) -- warehouse.Batumi:AddAsset("TPz Fuchs", 5) --- +-- -- -- Warehouse Berlin requests 5 infantry groups from warehouse Batumi using 2 APCs for transport. -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.APC, 2) -- @@ -898,13 +919,13 @@ -- -- Start Warehouses. -- warehouse.Batumi:Start() -- warehouse.Berlin:Start() --- +-- -- -- Add 20 infantry groups as assets at Batumi. -- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) --- +-- -- -- Add five Hueys for transport. Note that a Huey in DCS is an attack and not a transport helo. So we force this attribute! -- warehouse.Batumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) --- +-- -- -- Warehouse Berlin requests 5 infantry groups from warehouse Batumi using all available helos for transport. -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.HELICOPTER, WAREHOUSE.Quantity.ALL) -- @@ -917,53 +938,53 @@ -- -- Start warehouses. -- warehouse.Batumi:Start() -- warehouse.Kobuleti:Start() --- +-- -- -- Add assets to Batumi warehouse. -- warehouse.Batumi:AddAsset("C-130", 1) -- warehouse.Batumi:AddAsset("TPz Fuchs", 3) --- +-- -- warehouse.Batumi:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, WAREHOUSE.Quantity.ALL, WAREHOUSE.TransportType.AIRPLANE) -- -- ## Example 7: Capturing Airbase and Warehouse --- +-- -- A red BMP has made it through our defence lines and drives towards our unprotected airbase at Senaki. --- Once the BMP captures the airbase (DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) is evaluated) +-- Once the BMP captures the airbase (DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) is evaluated) -- the warehouse at Senaki lost its air infrastructure and it is not possible any more to spawn airborne units. All requests for airborne units are rejected and cancelled in this case. --- +-- -- The red BMP then drives further to the warehouse. Once it enters the warehouse zone (500 m radius around the warehouse building), the warehouse is -- considered to be under attack. This triggers the event **Attacked**. The @{#WAREHOUSE.OnAfterAttacked} function can be used to react to this situation. -- Here, we only broadcast a distress call and launch a flare. However, it would also be reasonable to spawn all or selected ground troops in order to defend -- the warehouse. Note, that the warehouse has a self defence option which can be activated via the @{#WAREHOUSE.SetAutoDefenceOn}() function. If activated, -- *all* ground assets are automatically spawned and assigned to defend the warehouse. Once/if the attack is defeated, these assets go automatically back -- into the warehouse stock. --- --- If the red coalition manages to capture our warehouse, all assets go into their possession. Now red tries to steal three F/A-18 flights and send them to +-- +-- If the red coalition manages to capture our warehouse, all assets go into their possession. Now red tries to steal three F/A-18 flights and send them to -- Sukhumi. These aircraft will be spawned and begin to taxi. However, ... --- --- A blue Bradley is in the area and will attemt to recapture the warehouse. It might also catch the red F/A-18s before they take off. --- --- -- Start warehouses. +-- +-- A blue Bradley is in the area and will attempt to recapture the warehouse. It might also catch the red F/A-18s before they take off. +-- +-- -- Start warehouses. -- warehouse.Senaki:Start() -- warehouse.Sukhumi:Start() --- +-- -- -- Add some assets. -- warehouse.Senaki:AddAsset("TPz Fuchs", 5) -- warehouse.Senaki:AddAsset("Infantry Platoon Alpha", 10) -- warehouse.Senaki:AddAsset("F/A-18C 2ship", 10) --- +-- -- -- Enable auto defence, i.e. spawn all group troups into the spawn zone. -- --warehouse.Senaki:SetAutoDefenceOn() --- +-- -- -- Activate Red BMP trying to capture the airfield and the warehouse. -- local red1=GROUP:FindByName("Red BMP-80 Senaki"):Activate() --- +-- -- -- The red BMP first drives to the airbase which gets captured and changes from blue to red. --- -- This triggers the "AirbaseCaptured" event where you can hook in and do things. +-- -- This triggers the "AirbaseCaptured" event where you can hook in and do things. -- function warehouse.Senaki:OnAfterAirbaseCaptured(From, Event, To, Coalition) -- -- This request cannot be processed since the warehouse has lost its airbase. In fact it is deleted from the queue. -- warehouse.Senaki:AddRequest(warehouse.Senaki,WAREHOUSE.Descriptor.CATEGORY, Group.Category.AIRPLANE, 1) -- end --- +-- -- -- Now the red BMP also captures the warehouse. This triggers the "Captured" event where you can hook in. -- -- So now the warehouse and the airbase are both red and aircraft can be spawned again. -- function warehouse.Senaki:OnAfterCaptured(From, Event, To, Coalition, Country) @@ -976,63 +997,63 @@ -- elseif Coalition==coalition.side.BLUE then -- warehouse.Senaki.warehouse:SmokeBlue() -- end --- +-- -- -- Activate a blue vehicle to re-capture the warehouse. It will drive to the warehouse zone and kill the red intruder. -- local blue1=GROUP:FindByName("blue1"):Activate() -- end -- -- ## Example 8: Destroying a Warehouse --- +-- -- FARP Berlin requests a Huey from Batumi warehouse. This helo is deployed and will be delivered. -- After 30 seconds into the mission we create and (artificial) big explosion - or a terrorist attack if you like - which completely destroys the -- the warehouse at Batumi. All assets are gone and requests cannot be processed anymore. --- +-- -- -- Start Batumi and Berlin warehouses. -- warehouse.Batumi:Start() -- warehouse.Berlin:Start() --- +-- -- -- Add some assets. -- warehouse.Batumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) -- warehouse.Berlin:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) --- +-- -- -- Big explosion at the warehose. It has a very nice damage model by the way :) -- local function DestroyWarehouse() -- warehouse.Batumi:GetCoordinate():Explosion(999) -- end -- SCHEDULER:New(nil, DestroyWarehouse, {}, 30) --- +-- -- -- First request is okay since warehouse is still alive. -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1) --- --- -- These requests should both not be processed any more since the warehouse at Batumi is destroyed. +-- +-- -- These requests should both not be processed any more since the warehouse at Batumi is destroyed. -- warehouse.Batumi:__AddRequest(35, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1) -- warehouse.Berlin:__AddRequest(40, warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1) -- -- ## Example 9: Self Propelled Naval Assets --- +-- -- Kobuleti requests all naval assets from Batumi. -- However, before naval assets can be exchanged, both warehouses need a port and at least one shipping lane defined by the user. -- See the @{#WAREHOUSE.SetPortZone}() and @{#WAREHOUSE.AddShippingLane}() functions. -- We do not want to spawn them all at once, because this will probably be a disaster -- in the port zone. Therefore, each ship is spawned with a delay of five minutes. --- +-- -- Batumi has quite a selection of different ships (for testing). --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Naval_Assets.png) --- +-- -- -- Start warehouses. -- warehouse.Batumi:Start() -- warehouse.Kobuleti:Start() --- +-- -- -- Define ports. These are polygon zones created by the waypoints of late activated units. -- warehouse.Batumi:SetPortZone(ZONE_POLYGON:NewFromGroupName("Warehouse Batumi Port Zone", "Warehouse Batumi Port Zone")) -- warehouse.Kobuleti:SetPortZone(ZONE_POLYGON:NewFromGroupName("Warehouse Kobuleti Port Zone", "Warehouse Kobuleti Port Zone")) --- +-- -- -- Shipping lane. Again, the waypoints of late activated units are taken as points defining the shipping lane. -- -- Some units will take lane 1 while others will take lane two. But both lead from Batumi to Kobuleti port. -- warehouse.Batumi:AddShippingLane(warehouse.Kobuleti, GROUP:FindByName("Warehouse Batumi-Kobuleti Shipping Lane 1")) -- warehouse.Batumi:AddShippingLane(warehouse.Kobuleti, GROUP:FindByName("Warehouse Batumi-Kobuleti Shipping Lane 2")) --- +-- -- -- Large selection of available naval units in DCS. -- warehouse.Batumi:AddAsset("Speedboat") -- warehouse.Batumi:AddAsset("Perry") @@ -1055,11 +1076,11 @@ -- warehouse.Batumi:AddAsset("Ivanov") -- warehouse.Batumi:AddAsset("Yantai") -- warehouse.Batumi:AddAsset("Type 052C") --- warehouse.Batumi:AddAsset("Guangzhou") --- +-- warehouse.Batumi:AddAsset("Guangzhou") +-- -- -- Get Number of ships at Batumi. -- local nships=warehouse.Batumi:GetNumberOfAssets(WAREHOUSE.Descriptor.CATEGORY, Group.Category.SHIP) --- +-- -- -- Send one ship every 3 minutes (ships do not evade each other well, so we need a bit space between them). -- for i=1, nships do -- warehouse.Batumi:__AddRequest(180*(i-1)+10, warehouse.Kobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.SHIP, 1) @@ -1067,129 +1088,129 @@ -- -- ## Example 10: Warehouse on Aircraft Carrier -- --- This example shows how to spawn assets from a warehouse located on an aircraft carrier. The warehouse must still be represented by a --- physical static object. However, on a carrier space is limit so we take a smaller static. In priciple one could also take something +-- This example shows how to spawn assets from a warehouse located on an aircraft carrier. The warehouse must still be represented by a +-- physical static object. However, on a carrier space is limit so we take a smaller static. In priciple one could also take something -- like a windsock. --- +-- -- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Carrier.png) --- +-- -- USS Stennis requests F/A-18s from Batumi. At the same time Kobuleti requests F/A-18s from the Stennis which currently does not have any. -- So first, Batumi delivers the fighters to the Stennis. After they arrived they are deployed again and send to Kobuleti. --- +-- -- -- Start warehouses. --- warehouse.Batumi:Start() +-- warehouse.Batumi:Start() -- warehouse.Stennis:Start() -- warehouse.Kobuleti:Start() --- +-- -- -- Add F/A-18 2-ship flight to Batmi. -- warehouse.Batumi:AddAsset("F/A-18C 2ship", 1) --- +-- -- -- USS Stennis requests F/A-18 from Batumi. -- warehouse.Batumi:AddRequest(warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "F/A-18C 2ship") --- +-- -- -- Kobuleti requests F/A-18 from USS Stennis. -- warehouse.Stennis:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "F/A-18C 2ship") -- -- ## Example 11: Aircraft Carrier - Rescue Helo and Escort --- +-- -- After 10 seconds we make a self request for a rescue helicopter. Note, that the @{#WAREHOUSE.AddRequest} function has a parameter which lets you -- specify an "Assignment". This can be later used to identify the request and take the right actions. --- +-- -- Once the request is processed, the @{#WAREHOUSE.OnAfterSelfRequest} function is called. This is where we hook in and postprocess the spawned assets. -- In particular, we use the @{AI.AI_Formation#AI_FORMATION} class to make some nice escorts for our carrier. --- +-- -- When the resue helo is spawned, we can check that this is the correct asset and make the helo go into formation with the carrier. -- Once the helo runs out of fuel, it will automatically return to the ship and land. For the warehouse, this means that the "cargo", i.e. the helicopter -- has been delivered - assets can be delivered to other warehouses and to the same warehouse - hence a *self* request. -- When that happens, the **Delivered** event is triggered and the @{#WAREHOUSE.OnAfterDelivered} function called. This can now be used to spawn -- a fresh helo. Effectively, there we created an infinite, never ending loop. So a rescue helo will be up at all times. --- +-- -- After 30 and 45 seconds requests for five groups of armed speedboats are made. These will be spawned in the port zone right behind the carrier. -- The first five groups will go port of the carrier an form a left wing formation. The seconds groups will to the analogue on the starboard side. -- **Note** that in order to spawn naval assets a warehouse needs a port (zone). Since the carrier and hence the warehouse is mobile, we define a moving --- zone as @{Core.Zone#ZONE_UNIT} with the carrier as reference unit. The "port" of the Stennis at its stern so all naval assets are spawned behing the carrier. --- +-- zone as @{Core.Zone#ZONE_UNIT} with the carrier as reference unit. The "port" of the Stennis at its stern so all naval assets are spawned behind the carrier. +-- -- -- Start warehouse on USS Stennis. -- warehouse.Stennis:Start() --- +-- -- -- Aircraft carrier gets a moving zone right behind it as port. -- warehouse.Stennis:SetPortZone(ZONE_UNIT:New("Warehouse Stennis Port Zone", UNIT:FindByName("USS Stennis"), 100, {rho=250, theta=180, relative_to_unit=true})) --- +-- -- -- Add speedboat assets. -- warehouse.Stennis:AddAsset("Speedboat", 10) -- warehouse.Stennis:AddAsset("CH-53E", 1) --- +-- -- -- Self request of speed boats. -- warehouse.Stennis:__AddRequest(10, warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "CH-53E", 1, nil, nil, nil, "Rescue Helo") -- warehouse.Stennis:__AddRequest(30, warehouse.Stennis, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.NAVAL_ARMEDSHIP, 5, nil, nil, nil, "Speedboats Left") -- warehouse.Stennis:__AddRequest(45, warehouse.Stennis, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.NAVAL_ARMEDSHIP, 5, nil, nil, nil, "Speedboats Right") --- +-- -- --- Function called after self request --- function warehouse.Stennis:OnAfterSelfRequest(From, Event, To,_groupset, request) +-- function warehouse.Stennis:OnAfterSelfRequest(From, Event, To,_groupset, request) -- local groupset=_groupset --Core.Set#SET_GROUP -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- -- USS Stennis is the mother ship. -- local Mother=UNIT:FindByName("USS Stennis") --- +-- -- -- Get assignment of the request. -- local assignment=warehouse.Stennis:GetAssignment(request) --- +-- -- if assignment=="Speedboats Left" then --- +-- -- -- Define AI Formation object. -- -- Note that this has to be a global variable or the garbage collector will remove it for some reason! -- CarrierFormationLeft = AI_FORMATION:New(Mother, groupset, "Left Formation with Carrier", "Escort Carrier.") --- +-- -- -- Formation parameters. --- CarrierFormationLeft:FormationLeftWing(200 ,50, 0, 0, 500, 50) +-- CarrierFormationLeft:FormationLeftWing(200 ,50, 0, 0, 500, 50) -- CarrierFormationLeft:__Start(2) --- +-- -- for _,group in pairs(groupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP --- group:FlareRed() --- end --- +-- group:FlareRed() +-- end +-- -- elseif assignment=="Speedboats Right" then --- +-- -- -- Define AI Formation object. -- -- Note that this has to be a global variable or the garbage collector will remove it for some reason! -- CarrierFormationRight = AI_FORMATION:New(Mother, groupset, "Right Formation with Carrier", "Escort Carrier.") --- +-- -- -- Formation parameters. --- CarrierFormationRight:FormationRightWing(200 ,50, 0, 0, 500, 50) +-- CarrierFormationRight:FormationRightWing(200 ,50, 0, 0, 500, 50) -- CarrierFormationRight:__Start(2) --- +-- -- for _,group in pairs(groupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP --- group:FlareGreen() --- end --- +-- group:FlareGreen() +-- end +-- -- elseif assignment=="Rescue Helo" then --- +-- -- -- Start uncontrolled helo. -- local group=groupset:GetFirst() --Wrapper.Group#GROUP -- group:StartUncontrolled() --- +-- -- -- Define AI Formation object. -- CarrierFormationHelo = AI_FORMATION:New(Mother, groupset, "Helo Formation with Carrier", "Fly Formation.") --- +-- -- -- Formation parameters. -- CarrierFormationHelo:FormationCenterWing(-150, 50, 20, 50, 100, 50) -- CarrierFormationHelo:__Start(2) --- +-- -- end --- +-- -- --- When the helo is out of fuel, it will return to the carrier and should be delivered. -- function warehouse.Stennis:OnAfterDelivered(From,Event,To,request) -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- -- So we start another request. -- if request.assignment=="Rescue Helo" then -- warehouse.Stennis:__AddRequest(10, warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "CH-53E", 1, nil, nil, nil, "Rescue Helo") -- end -- end --- +-- -- end -- -- ## Example 12: Pause a Warehouse @@ -1208,72 +1229,72 @@ -- -- -- Start Warehouse at Batumi. -- warehouse.Batumi:Start() --- --- -- Start Warehouse Berlin. +-- +-- -- Start Warehouse Berlin. -- warehouse.Berlin:Start() --- +-- -- -- Add 20 infantry groups and 5 tank platoons as assets at Batumi. -- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) --- +-- -- -- Pause the warehouse after 10 seconds -- warehouse.Batumi:__Pause(10) --- +-- -- -- Add a request from Berlin after 15 seconds. A request can be added but not be processed while warehouse is paused. -- warehouse.Batumi:__AddRequest(15, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 1) --- +-- -- -- New asset added after 20 seconds. This is possible even if the warehouse is paused. -- warehouse.Batumi:__AddAsset(20, "Abrams", 5) --- +-- -- -- Unpause warehouse after 30 seconds. Now the request from Berlin can be processed. -- warehouse.Batumi:__Unpause(30) --- +-- -- -- Pause warehouse Berlin -- warehouse.Berlin:__Pause(60) --- +-- -- -- After 90 seconds request from Berlin for tanks. -- warehouse.Batumi:__AddRequest(90, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TANK, 1) --- +-- -- -- After 120 seconds unpause Berlin. -- warehouse.Berlin:__Unpause(120) -- -- ## Example 13: Battlefield Air Interdiction --- +-- -- This example show how to couple the WAREHOUSE class with the @{AI.AI_Bai} class. --- Four enemy targets have been located at the famous Kobuleti X. All three available Viggen 2-ship flights are assigned to kill at least one of the BMPs to complete their mission. +-- Four enemy targets have been located at the famous Kobuleti X. All three available Viggen 2-ship flights are assigned to kill at least one of the BMPs to complete their mission. -- -- -- Start Warehouse at Kobuleti. -- warehouse.Kobuleti:Start() --- +-- -- -- Add three 2-ship groups of Viggens. -- warehouse.Kobuleti:AddAsset("Viggen 2ship", 3) --- +-- -- -- Self request for all Viggen assets. -- warehouse.Kobuleti:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Viggen 2ship", WAREHOUSE.Quantity.ALL, nil, nil, nil, "BAI") --- +-- -- -- Red targets at Kobuleti X (late activated). -- local RedTargets=GROUP:FindByName("Red IVF Alpha") --- +-- -- -- Activate the targets. -- RedTargets:Activate() --- +-- -- -- Do something with the spawned aircraft. -- function warehouse.Kobuleti:OnAfterSelfRequest(From,Event,To,groupset,request) -- local groupset=groupset --Core.Set#SET_GROUP -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- if request.assignment=="BAI" then --- +-- -- for _,group in pairs(groupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP --- +-- -- -- Start uncontrolled aircraft. -- group:StartUncontrolled() --- +-- -- local BAI=AI_BAI_ZONE:New(ZONE:New("Patrol Zone Kobuleti"), 500, 1000, 500, 600, ZONE:New("Patrol Zone Kobuleti")) --- +-- -- -- Tell the program to use the object (in this case called BAIPlane) as the group to use in the BAI function -- BAI:SetControllable(group) --- +-- -- -- Function checking if targets are still alive -- local function CheckTargets() -- local nTargets=RedTargets:GetSize() @@ -1285,76 +1306,76 @@ -- else -- MESSAGE:New("BAI Mission: The required red targets are destroyed.", 30):ToAll() -- BAI:__Accomplish(1) -- Now they should fly back to the patrolzone and patrol. --- end +-- end -- end --- +-- -- -- Start scheduler to monitor number of targets. -- local Check, CheckScheduleID = SCHEDULER:New(nil, CheckTargets, {}, 60, 60) --- +-- -- -- When the targets in the zone are destroyed, (see scheduled function), the planes will return home ... -- function BAI:OnAfterAccomplish( Controllable, From, Event, To ) -- MESSAGE:New( "BAI Mission: Sending the Viggens back to base.", 30):ToAll() -- Check:Stop(CheckScheduleID) -- BAI:__RTB(1) -- end --- +-- -- -- Start BAI -- BAI:Start() --- +-- -- -- Engage after 5 minutes. -- BAI:__Engage(300) --- +-- -- -- RTB after 30 min max. -- BAI:__RTB(-30*60) --- +-- -- end -- end --- +-- -- end -- -- ## Example 14: Strategic Bombing --- --- This example shows how to employ stategic bombers in a mission. Three B-52s are lauched at Kobuleti with the assignment to wipe out the enemy warehouse at Sukhumi. +-- +-- This example shows how to employ strategic bombers in a mission. Three B-52s are launched at Kobuleti with the assignment to wipe out the enemy warehouse at Sukhumi. -- The bombers will get a flight path and make their approach from the South at an altitude of 5000 m ASL. After their bombing run, they will return to Kobuleti and -- added back to stock. --- +-- -- -- Start warehouses --- warehouse.Kobuleti:Start() +-- warehouse.Kobuleti:Start() -- warehouse.Sukhumi:Start() --- +-- -- -- Add a strategic bomber assets -- warehouse.Kobuleti:AddAsset("B-52H", 3) --- --- -- Request bombers for specific task of bombing Sukhumi warehouse. +-- +-- -- Request bombers for specific task of bombing Sukhumi warehouse. -- warehouse.Kobuleti:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_BOMBER, WAREHOUSE.Quantity.ALL, nil, nil, nil, "Bomb Sukhumi") --- --- -- Specify assignment after bombers have been spawned. +-- +-- -- Specify assignment after bombers have been spawned. -- function warehouse.Kobuleti:OnAfterSelfRequest(From, Event, To, groupset, request) -- local groupset=groupset --Core.Set#SET_GROUP --- +-- -- -- Get assignment of this request. -- local assignment=warehouse.Kobuleti:GetAssignment(request) --- +-- -- if assignment=="Bomb Sukhumi" then --- +-- -- for _,_group in pairs(groupset:GetSet()) do -- local group=_group --Wrapper.Group#GROUP --- +-- -- -- Start uncontrolled aircraft. -- group:StartUncontrolled() --- +-- -- -- Target coordinate! -- local ToCoord=warehouse.Sukhumi:GetCoordinate():SetAltitude(5000) --- +-- -- -- Home coordinate. -- local HomeCoord=warehouse.Kobuleti:GetCoordinate():SetAltitude(3000) --- +-- -- -- Task bomb Sukhumi warehouse using all bombs (2032) from direction 180 at altitude 5000 m. -- local task=group:TaskBombing(warehouse.Sukhumi:GetCoordinate():GetVec2(), false, "All", nil , 180, 5000, 2032) --- --- -- Define waypoints. +-- +-- -- Define waypoints. -- local WayPoints={} --- +-- -- -- Take off position. -- WayPoints[1]=warehouse.Kobuleti:GetCoordinate():WaypointAirTakeOffParking() -- -- Begin bombing run 20 km south of target. @@ -1363,16 +1384,16 @@ -- WayPoints[3]=HomeCoord:WaypointAirTurningPoint() -- -- Land at homebase. Bombers are added back to stock and can be employed in later assignments. -- WayPoints[4]=warehouse.Kobuleti:GetCoordinate():WaypointAirLanding() --- +-- -- -- Route bombers. -- group:Route(WayPoints) -- end --- +-- -- end -- end -- -- ## Example 15: Defining Off-Road Paths --- +-- -- For self propelled assets it is possible to define custom off-road paths from one warehouse to another via the @{#WAREHOUSE.AddOffRoadPath} function. -- The waypoints of a path are taken from late activated units. In this example, two paths have been defined between the warehouses Kobuleti and FARP London. -- Trucks are spawned at each warehouse and are guided along the paths to the other warehouse. @@ -1381,21 +1402,21 @@ -- -- Start warehouses -- warehouse.Kobuleti:Start() -- warehouse.London:Start() --- +-- -- -- Define a polygon zone as spawn zone at Kobuleti. -- warehouse.Kobuleti:SetSpawnZone(ZONE_POLYGON:New("Warehouse Kobuleti Spawn Zone", GROUP:FindByName("Warehouse Kobuleti Spawn Zone"))) --- +-- -- -- Add assets. -- warehouse.Kobuleti:AddAsset("M978", 20) -- warehouse.London:AddAsset("M818", 20) --- +-- -- -- Off two road paths from Kobuleti to London. The reverse path from London to Kobuleti is added automatically. -- warehouse.Kobuleti:AddOffRoadPath(warehouse.London, GROUP:FindByName("Warehouse Kobuleti-London OffRoad Path 1")) -- warehouse.Kobuleti:AddOffRoadPath(warehouse.London, GROUP:FindByName("Warehouse Kobuleti-London OffRoad Path 2")) --- --- -- London requests all available trucks from Kobuleti. +-- +-- -- London requests all available trucks from Kobuleti. -- warehouse.Kobuleti:AddRequest(warehouse.London, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TRUCK, WAREHOUSE.Quantity.ALL) --- +-- -- -- Kobuleti requests all available trucks from London. -- warehouse.London:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TRUCK, WAREHOUSE.Quantity.HALF) -- @@ -1404,62 +1425,62 @@ -- Warehouse at FARP Berlin is located at the front line and sends infantry groups to the battle zone. -- Whenever a group dies, a new group is send from the warehouse to the battle zone. -- Additionally, for each dead group, Berlin requests resupply from Batumi. --- +-- -- -- Start warehouses. -- warehouse.Batumi:Start() -- warehouse.Berlin:Start() --- +-- -- -- Front line warehouse. -- warehouse.Berlin:AddAsset("Infantry Platoon Alpha", 6) --- +-- -- -- Resupply warehouse. -- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 50) --- +-- -- -- Battle zone near FARP Berlin. This is where the action is! -- local BattleZone=ZONE:New("Virtual Battle Zone") --- +-- -- -- Send infantry groups to the battle zone. Two groups every ~60 seconds. -- for i=1,2 do -- local time=(i-1)*60+10 -- warehouse.Berlin:__AddRequest(time, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 2, nil, nil, nil, "To Battle Zone") -- end --- +-- -- -- Take care of the spawned units. -- function warehouse.Berlin:OnAfterSelfRequest(From,Event,To,groupset,request) -- local groupset=groupset --Core.Set#SET_GROUP -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- -- Get assignment of this request. -- local assignment=warehouse.Berlin:GetAssignment(request) --- +-- -- if assignment=="To Battle Zone" then --- +-- -- for _,group in pairs(groupset:GetSet()) do -- local group=group --Wrapper.Group#GROUP --- +-- -- -- Route group to Battle zone. -- local ToCoord=BattleZone:GetRandomCoordinate() -- group:RouteGroundOnRoad(ToCoord, group:GetSpeedMax()*0.8) --- +-- -- -- After 3-5 minutes we create an explosion to destroy the group. -- SCHEDULER:New(nil, Explosion, {group, 50}, math.random(180, 300)) -- end --- +-- -- end --- +-- -- end --- +-- -- -- An asset has died ==> request resupply for it. -- function warehouse.Berlin:OnAfterAssetDead(From, Event, To, asset, request) -- local asset=asset --Functional.Warehouse#WAREHOUSE.Assetitem -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- -- Get assignment. -- local assignment=warehouse.Berlin:GetAssignment(request) --- +-- -- -- Request resupply for dead asset from Batumi. -- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, asset.attribute, nil, nil, nil, nil, "Resupply") --- +-- -- -- Send asset to Battle zone either now or when they arrive. -- warehouse.Berlin:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, asset.attribute, 1, nil, nil, nil, assignment) -- end @@ -1473,12 +1494,12 @@ -- Once infantry has arrived at Batumi, it will walk by itself to warehouse Pampa. -- The mortars can only be transported once the Mi-8 helos are available again, i.e. when the infantry has been delivered. -- Once the mortars arrive at Batumi, they will be transported by APCs to Pampa. --- +-- -- -- Start warehouses. -- warehouse.Kobuleti:Start() -- warehouse.Batumi:Start() -- warehouse.Pampa:Start() --- +-- -- -- Add assets to Kobuleti warehouse, which is our main hub. -- warehouse.Kobuleti:AddAsset("C-130", 2) -- warehouse.Kobuleti:AddAsset("C-17A", 2, nil, 77000) @@ -1486,32 +1507,32 @@ -- warehouse.Kobuleti:AddAsset("Leopard 2", 10, nil, nil, 62000, 500) -- warehouse.Kobuleti:AddAsset("Mortar Alpha", 10, nil, nil, 210) -- warehouse.Kobuleti:AddAsset("Infantry Platoon Alpha", 20) --- +-- -- -- Transports at Batumi. -- warehouse.Batumi:AddAsset("SPz Marder", 2) -- warehouse.Batumi:AddAsset("TPz Fuchs", 2) --- +-- -- -- Tanks transported by plane from from Kobuleti to Batumi. -- warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TANK, 2, WAREHOUSE.TransportType.AIRPLANE, 2, 10, "Assets for Pampa") -- -- Artillery transported by helicopter from Kobuleti to Batumi. -- warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_ARTILLERY, 2, WAREHOUSE.TransportType.HELICOPTER, 2, 30, "Assets for Pampa via APC") -- -- Infantry transported by helicopter from Kobuleti to Batumi. -- warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 8, WAREHOUSE.TransportType.HELICOPTER, 2, 20, "Assets for Pampa") --- +-- -- --- Function handling assets delivered from Kobuleti warehouse. -- function warehouse.Kobuleti:OnAfterDelivered(From, Event, To, request) -- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem --- +-- -- -- Get assignment. -- local assignment=warehouse.Kobuleti:GetAssignment(request) --- +-- -- -- Check if these assets were meant for Warehouse Pampa. -- if assignment=="Assets for Pampa via APC" then -- -- Forward everything that arrived at Batumi to Pampa via APC. -- warehouse.Batumi:AddRequest(warehouse.Pampa, WAREHOUSE.Descriptor.ATTRIBUTE, request.cargoattribute, request.ndelivered, WAREHOUSE.TransportType.APC, WAREHOUSE.Quantity.ALL) -- end -- end --- +-- -- -- Forward all mobile ground assets to Pampa once they arrived. -- function warehouse.Batumi:OnAfterNewAsset(From, Event, To, asset, assignment) -- local asset=asset --Functional.Warehouse#WAREHOUSE.Assetitem @@ -1527,6 +1548,7 @@ WAREHOUSE = { ClassName = "WAREHOUSE", Debug = false, + lid = nil, Report = true, warehouse = nil, alias = nil, @@ -1536,7 +1558,6 @@ WAREHOUSE = { road = nil, rail = nil, spawnzone = nil, - wid = nil, uid = nil, markerid = nil, dTstatus = 30, @@ -1555,6 +1576,11 @@ WAREHOUSE = { autosave = false, autosavepath = nil, autosavefile = nil, + saveparking = false, + isunit = false, + lowfuelthresh = 0.15, + respawnafterdestroyed=false, + respawndelay = nil, } --- Item of the warehouse stock table. @@ -1577,7 +1603,12 @@ WAREHOUSE = { -- @field #number loadradius Distance when cargo is loaded into the carrier. -- @field DCS#AI.Skill skill Skill of AI unit. -- @field #string livery Livery of the asset. --- @field #string assignment Assignment of the asset. This could, e.g., be used in the @{#WAREHOUSE.OnAfterNewAsset) funktion. +-- @field #string assignment Assignment of the asset. This could, e.g., be used in the @{#WAREHOUSE.OnAfterNewAsset) function. +-- @field #boolean spawned If true, asset was spawned into the cruel world. If false, it is still in stock. +-- @field #string spawngroupname Name of the spawned group. +-- @field #boolean iscargo If true, asset is cargo. If false asset is transport. Nil if in stock. +-- @field #number rid The request ID of this asset. +-- @field #boolean arrived If true, asset arrived at its destination. --- Item of the warehouse queue table. -- @type WAREHOUSE.Queueitem @@ -1611,6 +1642,7 @@ WAREHOUSE = { -- @field Core.Set#SET_CARGO transportcargoset Set of cargo objects. -- @field #table carriercargo Table holding the cargo groups of each carrier unit. -- @field #number ntransporthome Number of transports back home. +-- @field #boolean lowfuel If true, at least one asset group is low on fuel. -- @extends #WAREHOUSE.Queueitem --- Descriptors enumerator describing the type of the asset. @@ -1619,11 +1651,15 @@ WAREHOUSE = { -- @field #string UNITTYPE Typename of the DCS unit, e.g. "A-10C". -- @field #string ATTRIBUTE Generalized attribute @{#WAREHOUSE.Attribute}. -- @field #string CATEGORY Asset category of type DCS#Group.Category, i.e. GROUND, AIRPLANE, HELICOPTER, SHIP, TRAIN. +-- @field #string ASSIGNMENT Assignment of asset when it was added. +-- @field #string ASSETLIST List of specific assets gives as a table of assets. Mind the curly brackets {}. WAREHOUSE.Descriptor = { GROUPNAME="templatename", UNITTYPE="unittype", ATTRIBUTE="attribute", CATEGORY="category", + ASSIGNMENT="assignment", + ASSETLIST="assetlist," } --- Generalized asset attributes. Can be used to request assets with certain general characteristics. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit. @@ -1701,7 +1737,7 @@ WAREHOUSE.TransportType = { --- Warehouse quantity enumerator for selecting number of assets, e.g. all, half etc. of what is in stock rather than an absolute number. -- @type WAREHOUSE.Quantity -- @field #string ALL All "all" assets currently in stock. --- @field #string THREEQUARTERS Three quarters "3/4" of assets in stock. +-- @field #string THREEQUARTERS Three quarters "3/4" of assets in stock. -- @field #string HALF Half "1/2" of assets in stock. -- @field #string THIRD One third "1/3" of assets in stock. -- @field #string QUARTER One quarter "1/4" of assets in stock. @@ -1714,19 +1750,21 @@ WAREHOUSE.Quantity = { } --- Warehouse database. Note that this is a global array to have easier exchange between warehouses. --- @type WAREHOUSE.db +-- @type _WAREHOUSEDB -- @field #number AssetID Unique ID of each asset. This is a running number, which is increased each time a new asset is added. --- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}. +-- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}.# +-- @field #number WarehouseID Unique ID of the warehouse. Running number. -- @field #table Warehouses Table holding all defined @{#WAREHOUSE} objects by their unique ids. -WAREHOUSE.db = { - AssetID = 0, - Assets = {}, - Warehouses = {} +_WAREHOUSEDB = { + AssetID = 0, + Assets = {}, + WarehouseID = 0, + Warehouses = {} } --- Warehouse class version. -- @field #string version -WAREHOUSE.version="0.6.4" +WAREHOUSE.version="1.0.2" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO: Warehouse todo list. @@ -1734,13 +1772,13 @@ WAREHOUSE.version="0.6.4" -- TODO: Add check if assets "on the move" are stationary. Can happen if ground units get stuck in buildings. If stationary auto complete transport by adding assets to request warehouse? Time? -- TODO: Optimize findpathonroad. Do it only once (first time) and safe paths between warehouses similar to off-road paths. --- TODO: Spawn assets only virtually, i.e. remove requested assets from stock but do NOT spawn them ==> Interface to A2A dispatcher! Maybe do a negative sign on asset number? --- TODO: Test capturing a neutral warehouse. +-- NOGO: Spawn assets only virtually, i.e. remove requested assets from stock but do NOT spawn them ==> Interface to A2A dispatcher! Maybe do a negative sign on asset number? -- TODO: Make more examples: ARTY, CAP, ... -- TODO: Check also general requests like all ground. Is this a problem for self propelled if immobile units are among the assets? Check if transport. -- TODO: Handle the case when units of a group die during the transfer. -- TODO: Added habours as interface for transport to from warehouses? Could make a rudimentary shipping dispatcher. --- TODO: Add save/load capability of warehouse <==> percistance after mission restart. Difficult in lua! +-- DONE: Test capturing a neutral warehouse. +-- DONE: Add save/load capability of warehouse <==> persistance after mission restart. Difficult in lua! -- DONE: Get cargo bay and weight from CARGO_GROUP and GROUP. No necessary any more! -- DONE: Add possibility to set weight and cargo bay manually in AddAsset function as optional parameters. -- DONE: Check overlapping aircraft sometimes. @@ -1759,7 +1797,7 @@ WAREHOUSE.version="0.6.4" -- DONE: Warehouse re-capturing not working?! -- DONE: Naval assets dont go back into stock once arrived. -- DONE: Take cargo weight into consideration, when selecting transport assets. --- DONE: Add ports for spawning naval assets. +-- DONE: Add ports for spawning naval assets. -- DONE: Add shipping lanes between warehouses. -- DONE: Handle cases with immobile units <== should be handled by dispatcher classes. -- DONE: Handle cases for aircraft carriers and other ships. Place warehouse on carrier possible? On others probably not - exclude them? @@ -1787,23 +1825,31 @@ WAREHOUSE.version="0.6.4" --- The WAREHOUSE constructor. Creates a new WAREHOUSE object from a static object. Parameters like the coalition and country are taken from the static object structure. -- @param #WAREHOUSE self --- @param Wrapper.Static#STATIC warehouse The physical structure of the warehouse. --- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static +-- @param Wrapper.Static#STATIC warehouse The physical structure representing the warehouse. +-- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static -- @return #WAREHOUSE self function WAREHOUSE:New(warehouse, alias) BASE:T({warehouse=warehouse}) - + -- Check if just a string was given and convert to static. if type(warehouse)=="string" then - warehouse=STATIC:FindByName(warehouse, true) + local warehousename=warehouse + warehouse=UNIT:FindByName(warehousename) + if warehouse==nil then + --env.info(string.format("No warehouse unit with name %s found trying static.", tostring(warehousename))) + warehouse=STATIC:FindByName(warehousename, true) + self.isunit=false + else + self.isunit=true + end end - + -- Nil check. if warehouse==nil then BASE:E("ERROR: Warehouse does not exist!") return nil end - + -- Set alias. self.alias=alias or warehouse:GetName() @@ -1814,29 +1860,43 @@ function WAREHOUSE:New(warehouse, alias) local self = BASE:Inherit(self, FSM:New()) -- #WAREHOUSE -- Set some string id for output to DCS.log file. - self.wid=string.format("WAREHOUSE %s | ", self.alias) + self.lid=string.format("WAREHOUSE %s | ", self.alias) -- Set some variables. self.warehouse=warehouse - self.uid=tonumber(warehouse:GetID()) - -- Closest of the same coalition but within a certain range. + -- Increase global warehouse counter. + _WAREHOUSEDB.WarehouseID=_WAREHOUSEDB.WarehouseID+1 + + -- Set unique ID for this warehouse. + self.uid=_WAREHOUSEDB.WarehouseID + + -- Coalition of the warehouse. + self.coalition=self.warehouse:GetCoalition() + + -- Country of the warehouse. + self.countryid=self.warehouse:GetCountry() + + -- Closest of the same coalition but within 5 km range. local _airbase=self:GetCoordinate():GetClosestAirbase(nil, self:GetCoalition()) - if _airbase and _airbase:GetCoordinate():Get2DDistance(self:GetCoordinate()) < 3000 then + if _airbase and _airbase:GetCoordinate():Get2DDistance(self:GetCoordinate()) <= 5000 then self:SetAirbase(_airbase) end - + -- Define warehouse and default spawn zone. self.zone=ZONE_RADIUS:New(string.format("Warehouse zone %s", self.warehouse:GetName()), warehouse:GetVec2(), 500) self.spawnzone=ZONE_RADIUS:New(string.format("Warehouse %s spawn zone", self.warehouse:GetName()), warehouse:GetVec2(), 250) + -- Defaults + self:SetMarker(true) + -- Add warehouse to database. - WAREHOUSE.db.Warehouses[self.uid]=self - + _WAREHOUSEDB.Warehouses[self.uid]=self + ----------------------- --- FSM Transitions --- ----------------------- - + -- Start State. self:SetStartState("NotReadyYet") @@ -1844,38 +1904,49 @@ function WAREHOUSE:New(warehouse, alias) -- From State --> Event --> To State self:AddTransition("NotReadyYet", "Load", "Loaded") -- Load the warehouse state from scatch. self:AddTransition("Stopped", "Load", "Loaded") -- Load the warehouse state stopped state. + self:AddTransition("NotReadyYet", "Start", "Running") -- Start the warehouse from scratch. - self:AddTransition("Loaded", "Start", "Running") -- Start the warehouse when loaded from disk. + self:AddTransition("Loaded", "Start", "Running") -- Start the warehouse when loaded from disk. + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("*", "AddAsset", "*") -- Add asset to warehouse stock. self:AddTransition("*", "NewAsset", "*") -- New asset was added to warehouse stock. + self:AddTransition("*", "AddRequest", "*") -- New request from other warehouse. self:AddTransition("Running", "Request", "*") -- Process a request. Only in running mode. + self:AddTransition("Running", "RequestSpawned", "*") -- Assets of request were spawned. self:AddTransition("Attacked", "Request", "*") -- Process a request. Only in running mode. + self:AddTransition("*", "Unloaded", "*") -- Cargo has been unloaded from the carrier (unused ==> unnecessary?). + self:AddTransition("*", "AssetSpawned", "*") -- Asset has been spawned into the world. + self:AddTransition("*", "AssetLowFuel", "*") -- Asset is low on fuel. + self:AddTransition("*", "Arrived", "*") -- Cargo or transport group has arrived. + self:AddTransition("*", "Delivered", "*") -- All cargo groups of a request have been delivered to the requesting warehouse. self:AddTransition("Running", "SelfRequest", "*") -- Request to warehouse itself. Requested assets are only spawned but not delivered anywhere. self:AddTransition("Attacked", "SelfRequest", "*") -- Request to warehouse itself. Also possible when warehouse is under attack! - self:AddTransition("Running", "Pause", "Paused") -- Pause the processing of new requests. Still possible to add assets and requests. - self:AddTransition("Paused", "Unpause", "Running") -- Unpause the warehouse. Queued requests are processed again. + self:AddTransition("Running", "Pause", "Paused") -- Pause the processing of new requests. Still possible to add assets and requests. + self:AddTransition("Paused", "Unpause", "Running") -- Unpause the warehouse. Queued requests are processed again. self:AddTransition("*", "Stop", "Stopped") -- Stop the warehouse. self:AddTransition("Stopped", "Restart", "Running") -- Restart the warehouse when it was stopped before. self:AddTransition("Loaded", "Restart", "Running") -- Restart the warehouse when assets were loaded from file before. - self:AddTransition("*", "Save", "*") -- TODO Save the warehouse state to disk. + self:AddTransition("*", "Save", "*") -- Save the warehouse state to disk. self:AddTransition("*", "Attacked", "Attacked") -- Warehouse is under attack by enemy coalition. self:AddTransition("Attacked", "Defeated", "Running") -- Attack by other coalition was defeated! - self:AddTransition("*", "ChangeCountry", "*") -- Change country (and coalition) of the warehouse. Warehouse is respawned! + self:AddTransition("*", "ChangeCountry", "*") -- Change country (and coalition) of the warehouse. Warehouse is respawned! self:AddTransition("Attacked", "Captured", "Running") -- Warehouse was captured by another coalition. It must have been attacked first. self:AddTransition("*", "AirbaseCaptured", "*") -- Airbase was captured by other coalition. self:AddTransition("*", "AirbaseRecaptured", "*") -- Airbase was re-captured from other coalition. self:AddTransition("*", "AssetDead", "*") -- An asset group died. self:AddTransition("*", "Destroyed", "Destroyed") -- Warehouse was destroyed. All assets in stock are gone and warehouse is stopped. - + self:AddTransition("Destroyed", "Respawn", "Running") -- Respawn warehouse after it was destroyed. + ------------------------ --- Pseudo Functions --- ------------------------ - + --- Triggers the FSM event "Start". Starts the warehouse. Initializes parameters and starts event handlers. -- @function [parent=#WAREHOUSE] Start -- @param #WAREHOUSE self @@ -1903,6 +1974,22 @@ function WAREHOUSE:New(warehouse, alias) -- @param #WAREHOUSE self -- @param #number delay Delay in seconds. + --- Triggers the FSM event "Respawn". + -- @function [parent=#WAREHOUSE] Respawn + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Respawn" after a delay. + -- @function [parent=#WAREHOUSE] __Respawn + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + --- On after "Respawn" event user function. + -- @function [parent=#WAREHOUSE] OnAfterRespawn + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + --- Triggers the FSM event "Pause". Pauses the warehouse. Assets can still be added and requests be made. However, requests are not processed. -- @function [parent=#WAREHOUSE] Pause -- @param #WAREHOUSE self @@ -2013,19 +2100,35 @@ function WAREHOUSE:New(warehouse, alias) -- @function [parent=#WAREHOUSE] Request -- @param #WAREHOUSE self -- @param #WAREHOUSE.Queueitem Request Information table of the request. - + --- Triggers the FSM event "Request" after a delay. Executes a request from the queue if possible. -- @function [parent=#WAREHOUSE] __Request -- @param #WAREHOUSE self -- @param #number Delay Delay in seconds. -- @param #WAREHOUSE.Queueitem Request Information table of the request. + --- On before "Request" user function. The necessary cargo and transport assets will be spawned. Time to set some additional asset parameters. + -- @function [parent=#WAREHOUSE] OnBeforeRequest + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Queueitem Request Information table of the request. + + --- On after "Request" user function. The necessary cargo and transport assets were spawned. + -- @function [parent=#WAREHOUSE] OnAfterRequest + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Queueitem Request Information table of the request. + --- Triggers the FSM event "Arrived" when a group has arrived at the destination warehouse. -- This function should always be called from the sending and not the receiving warehouse. -- If the group is a cargo asset, it is added to the receiving warehouse. If the group is a transporter it - -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once - -- all cargo was delivered. + -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once + -- all cargo was delivered. -- @function [parent=#WAREHOUSE] Arrived -- @param #WAREHOUSE self -- @param Wrapper.Group#GROUP group Group that has arrived. @@ -2033,7 +2136,7 @@ function WAREHOUSE:New(warehouse, alias) --- Triggers the FSM event "Arrived" after a delay when a group has arrived at the destination. -- This function should always be called from the sending and not the receiving warehouse. -- If the group is a cargo asset, it is added to the receiving warehouse. If the group is a transporter it - -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once + -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once -- @function [parent=#WAREHOUSE] __Arrived -- @param #WAREHOUSE self -- @param #number delay Delay in seconds. @@ -2088,24 +2191,24 @@ function WAREHOUSE:New(warehouse, alias) --- On after "SelfRequest" event. Request was initiated from the warehouse to itself. Groups are simply spawned at the warehouse or the associated airbase. -- All requested assets are passed as a @{Core.Set#SET_GROUP} and can be used for further tasks or in other MOOSE classes. -- Note that airborne assets are spawned in uncontrolled state so they do not simply "fly away" after spawning. - -- + -- -- @usage -- --- Self request event. Triggered once the assets are spawned in the spawn zone or at the airbase. -- function mywarehouse:OnAfterSelfRequest(From, Event, To, groupset, request) -- local groupset=groupset --Core.Set#SET_GROUP - -- + -- -- -- Loop over all groups spawned from that request. -- for _,group in pairs(groupset:GetSetObjects()) do -- local group=group --Wrapper.Group#GROUP - -- + -- -- -- Gree smoke on spawned group. -- group:SmokeGreen() - -- + -- -- -- Activate uncontrolled airborne group if necessary. -- group:StartUncontrolled() -- end - -- end - -- + -- end + -- -- @function [parent=#WAREHOUSE] OnAfterSelfRequest -- @param #WAREHOUSE self -- @param #string From From state. @@ -2159,7 +2262,7 @@ function WAREHOUSE:New(warehouse, alias) -- @function [parent=#WAREHOUSE] ChangeCountry -- @param #WAREHOUSE self -- @param DCS#country.id Country New country id of the warehouse. - + --- Triggers the FSM event "ChangeCountry" after a delay so the warehouse is respawned with the new country. -- @function [parent=#WAREHOUSE] __ChangeCountry -- @param #WAREHOUSE self @@ -2180,7 +2283,7 @@ function WAREHOUSE:New(warehouse, alias) -- @param #WAREHOUSE self -- @param DCS#coalition.side Coalition Coalition side which captured the warehouse. -- @param DCS#country.id Country Country id which has captured the warehouse. - + --- Triggers the FSM event "Captured" with a delay when a warehouse has been captured by another coalition. -- @function [parent=#WAREHOUSE] __Captured -- @param #WAREHOUSE self @@ -2196,13 +2299,13 @@ function WAREHOUSE:New(warehouse, alias) -- @param #string To To state. -- @param DCS#coalition.side Coalition Coalition side which captured the warehouse, i.e. a number of @{DCS#coalition.side} enumerator. -- @param DCS#country.id Country Country id which has captured the warehouse, i.e. a number @{DCS#country.id} enumerator. - -- + -- --- Triggers the FSM event "AirbaseCaptured" when the airbase of the warehouse has been captured by another coalition. -- @function [parent=#WAREHOUSE] AirbaseCaptured -- @param #WAREHOUSE self -- @param DCS#coalition.side Coalition Coalition side which captured the airbase, i.e. a number of @{DCS#coalition.side} enumerator. - + --- Triggers the FSM event "AirbaseCaptured" with a delay when the airbase of the warehouse has been captured by another coalition. -- @function [parent=#WAREHOUSE] __AirbaseCaptured -- @param #WAREHOUSE self @@ -2222,7 +2325,7 @@ function WAREHOUSE:New(warehouse, alias) -- @param #WAREHOUSE self -- @function [parent=#WAREHOUSE] AirbaseRecaptured -- @param DCS#coalition.side Coalition Coalition which re-captured the airbase, i.e. the same as the current warehouse owner coalition. - + --- Triggers the FSM event "AirbaseRecaptured" with a delay when the airbase of the warehouse has been re-captured from the other coalition. -- @function [parent=#WAREHOUSE] __AirbaseRecaptured -- @param #WAREHOUSE self @@ -2264,7 +2367,7 @@ function WAREHOUSE:New(warehouse, alias) --- Triggers the FSM event "Destroyed" when the warehouse was destroyed. Services are stopped. -- @function [parent=#WAREHOUSE] Destroyed -- @param #WAREHOUSE self - + --- Triggers the FSM event "Destroyed" with a delay when the warehouse was destroyed. Services are stopped. -- @function [parent=#WAREHOUSE] __Destroyed -- @param #WAREHOUSE self @@ -2278,12 +2381,61 @@ function WAREHOUSE:New(warehouse, alias) -- @param #string To To state. + --- Triggers the FSM event "AssetSpawned" when the warehouse has spawned an asset. + -- @function [parent=#WAREHOUSE] AssetSpawned + -- @param #WAREHOUSE self + -- @param Wrapper.Group#GROUP group the group that was spawned. + -- @param #WAREHOUSE.Assetitem asset The asset that was spawned. + -- @param #WAREHOUSE.Pendingitem request The request of the spawned asset. + + --- Triggers the FSM event "AssetSpawned" with a delay when the warehouse has spawned an asset. + -- @function [parent=#WAREHOUSE] __AssetSpawned + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Group#GROUP group the group that was spawned. + -- @param #WAREHOUSE.Assetitem asset The asset that was spawned. + -- @param #WAREHOUSE.Pendingitem request The request of the spawned asset. + + --- On after "AssetSpawned" event user function. Called when the warehouse has spawned an asset. + -- @function [parent=#WAREHOUSE] OnAfterAssetSpawned + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#GROUP group the group that was spawned. + -- @param #WAREHOUSE.Assetitem asset The asset that was spawned. + -- @param #WAREHOUSE.Pendingitem request The request of the spawned asset. + + + --- Triggers the FSM event "AssetLowFuel" when an asset runs low on fuel + -- @function [parent=#WAREHOUSE] AssetLowFuel + -- @param #WAREHOUSE self + -- @param #WAREHOUSE.Assetitem asset The asset that is low on fuel. + -- @param #WAREHOUSE.Pendingitem request The request of the asset that is low on fuel. + + --- Triggers the FSM event "AssetLowFuel" with a delay when an asset runs low on fuel. + -- @function [parent=#WAREHOUSE] __AssetLowFuel + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #WAREHOUSE.Assetitem asset The asset that is low on fuel. + -- @param #WAREHOUSE.Pendingitem request The request of the asset that is low on fuel. + + --- On after "AssetLowFuel" event user function. Called when the an asset is low on fuel. + -- @function [parent=#WAREHOUSE] OnAfterAssetLowFuel + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Assetitem asset The asset that is low on fuel. + -- @param #WAREHOUSE.Pendingitem request The request of the asset that is low on fuel. + + --- Triggers the FSM event "Save" when the warehouse assets are saved to file on disk. -- @function [parent=#WAREHOUSE] Save -- @param #WAREHOUSE self -- @param #string path Path where the file is saved. Default is the DCS installation root directory. -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. - + --- Triggers the FSM event "Save" with a delay when the warehouse assets are saved to a file. -- @function [parent=#WAREHOUSE] __Save -- @param #WAREHOUSE self @@ -2306,7 +2458,7 @@ function WAREHOUSE:New(warehouse, alias) -- @param #WAREHOUSE self -- @param #string path Path where the file is located. Default is the DCS installation root directory. -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. - + --- Triggers the FSM event "Load" with a delay when the warehouse assets are loaded from disk. -- @function [parent=#WAREHOUSE] __Load -- @param #WAREHOUSE self @@ -2363,6 +2515,32 @@ function WAREHOUSE:SetReportOff() return self end +--- Enable safe parking option, i.e. parking spots at an airbase will be considered as occupied when a client aircraft is parked there (even if the client slot is not taken by a player yet). +-- Note that also incoming aircraft can reserve/occupie parking spaces. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetSafeParkingOn() + self.safeparking=true + return self +end + +--- Disable safe parking option. Note that is the default setting. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetSafeParkingOff() + self.safeparking=false + return self +end + +--- Set low fuel threshold. If one unit of an asset has less fuel than this number, the event AssetLowFuel will be fired. +-- @param #WAREHOUSE self +-- @param #number threshold Relative low fuel threshold, i.e. a number in [0,1]. Default 0.15 (15%). +-- @return #WAREHOUSE self +function WAREHOUSE:SetLowFuelThreshold(threshold) + self.lowfuelthresh=threshold or 0.15 + return self +end + --- Set interval of status updates. Note that normally only one request can be processed per time interval. -- @param #WAREHOUSE self -- @param #number timeinterval Time interval in seconds. @@ -2393,7 +2571,7 @@ function WAREHOUSE:SetWarehouseZone(zone) return self end ---- Set auto defence on. When the warehouse is under attack, all ground assets are spawned automatically and will defend the warehouse zone. +--- Set auto defence on. When the warehouse is under attack, all ground assets are spawned automatically and will defend the warehouse zone. -- @param #WAREHOUSE self -- @return #WAREHOUSE self function WAREHOUSE:SetAutoDefenceOn() @@ -2401,7 +2579,7 @@ function WAREHOUSE:SetAutoDefenceOn() return self end ---- Set auto defence off. This is the default. +--- Set auto defence off. This is the default. -- @param #WAREHOUSE self -- @return #WAREHOUSE self function WAREHOUSE:SetAutoDefenceOff() @@ -2409,7 +2587,38 @@ function WAREHOUSE:SetAutoDefenceOff() return self end ---- Set auto defence off. This is the default. +--- Set valid parking spot IDs. +-- @param #WAREHOUSE self +-- @param #table ParkingIDs Table of numbers. +-- @return #WAREHOUSE self +function WAREHOUSE:SetParkingIDs(ParkingIDs) + if type(ParkingIDs)~="table" then + ParkingIDs={ParkingIDs} + end + self.parkingIDs=ParkingIDs + return self +end + +--- Check parking ID. +-- @param #WAREHOUSE self +-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot. +-- @return #boolean If true, parking is valid. +function WAREHOUSE:_CheckParkingValid(spot) + if self.parkingIDs==nil then + return true + end + + for _,id in pairs(self.parkingIDs or {}) do + if spot.TerminalID==id then + return true + end + end + + return false +end + + +--- Enable auto save of warehouse assets at mission end event. -- @param #WAREHOUSE self -- @param #string path Path where to save the asset data file. -- @param #string filename File name. Default is generated automatically from warehouse id. @@ -2421,6 +2630,28 @@ function WAREHOUSE:SetSaveOnMissionEnd(path, filename) return self end +--- Show or don't show markers on the F10 map displaying the Warehouse stock and road/rail connections. +-- @param #WAREHOUSE self +-- @param #boolean switch If true (or nil), markers are on. If false, markers are not displayed. +-- @return #WAREHOUSE self +function WAREHOUSE:SetMarker(switch) + if switch==false then + self.markerOn=false + else + self.markerOn=true + end + return self +end + +--- Set respawn after destroy. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetRespawnAfterDestroyed(delay) + self.respawnafterdestroyed=true + self.respawndelay=delay + return self +end + --- Set the airbase belonging to this warehouse. -- Note that it has to be of the same coalition as the warehouse. @@ -2441,7 +2672,7 @@ end --- Set the connection of the warehouse to the road. -- Ground assets spawned in the warehouse spawn zone will first go to this point and from there travel on road to the requesting warehouse. -- Note that by default the road connection is set to the closest point on road from the center of the spawn zone if it is withing 3000 meters. --- Also note, that if the parameter "coordinate" is passed as nil, any road connection is disabled and ground assets cannot travel of be transportet on the ground. +-- Also note, that if the parameter "coordinate" is passed as nil, any road connection is disabled and ground assets cannot travel of be transportet on the ground. -- @param #WAREHOUSE self -- @param Core.Point#COORDINATE coordinate The road connection. Technically, the closest point on road from this coordinate is determined by DCS API function. So this point must not be exactly on the road. -- @return #WAREHOUSE self @@ -2469,7 +2700,7 @@ function WAREHOUSE:SetRailConnection(coordinate) end --- Set the port zone for this warehouse. --- The port zone is the zone, where all naval assets of the warehouse are spawned. +-- The port zone is the zone, where all naval assets of the warehouse are spawned. -- @param #WAREHOUSE self -- @param Core.Zone#ZONE zone The zone defining the naval port of the warehouse. -- @return #WAREHOUSE self @@ -2499,10 +2730,10 @@ function WAREHOUSE:AddShippingLane(remotewarehouse, group, oneway) -- Initial and final coordinates are random points within the port zones. local startcoord=self.portzone:GetRandomCoordinate() local finalcoord=remotewarehouse.portzone:GetRandomCoordinate() - + -- Create new lane from waypoints of the template group. local lane=self:_NewLane(group, startcoord, finalcoord) - + -- Debug info. Marks along shipping lane. if self.Debug then for i=1,#lane do @@ -2511,29 +2742,29 @@ function WAREHOUSE:AddShippingLane(remotewarehouse, group, oneway) coord:MarkToCoalition(text, self:GetCoalition()) end end - + -- Name of the remote warehouse. local remotename=remotewarehouse.warehouse:GetName() - + -- Create new table if no shipping lane exists yet. if self.shippinglanes[remotename]==nil then self.shippinglanes[remotename]={} - end - + end + -- Add shipping lane. table.insert(self.shippinglanes[remotename], lane) - + -- Add shipping lane in the opposite direction. if not oneway then remotewarehouse:AddShippingLane(self, group, true) end - + return self end --- Add an off-road path from this warehouse to another and back. --- The start and end points are automatically set to one random point in the respective spawn zones of the two warehouses. +-- The start and end points are automatically set to one random point in the respective spawn zones of the two warehouses. -- By default, the reverse path is also added as path from the remote warehouse to this warehouse. -- @param #WAREHOUSE self -- @param #WAREHOUSE remotewarehouse The remote warehouse to which the path leads. @@ -2545,15 +2776,15 @@ function WAREHOUSE:AddOffRoadPath(remotewarehouse, group, oneway) -- Initial and final points are random points within the spawn zone. local startcoord=self.spawnzone:GetRandomCoordinate() local finalcoord=remotewarehouse.spawnzone:GetRandomCoordinate() - + -- Create new path from template group waypoints. local path=self:_NewLane(group, startcoord, finalcoord) - + if path==nil then - self:E(self.wid.."ERROR: Offroad path could not be added. Group present in ME?") + self:E(self.lid.."ERROR: Offroad path could not be added. Group present in ME?") return end - + -- Debug info. Marks along path. if path and self.Debug then for i=1,#path do @@ -2562,23 +2793,23 @@ function WAREHOUSE:AddOffRoadPath(remotewarehouse, group, oneway) coord:MarkToCoalition(text, self:GetCoalition()) end end - + -- Name of the remote warehouse. local remotename=remotewarehouse.warehouse:GetName() - + -- Create new table if no shipping lane exists yet. if self.offroadpaths[remotename]==nil then self.offroadpaths[remotename]={} - end - + end + -- Add off road path. table.insert(self.offroadpaths[remotename], path) - - -- Add off road path in the opposite direction (if not forbidden). + + -- Add off road path in the opposite direction (if not forbidden). if not oneway then remotewarehouse:AddOffRoadPath(self, group, true) end - + return self end @@ -2596,19 +2827,19 @@ function WAREHOUSE:_NewLane(group, startcoord, finalcoord) -- Get route from template. local lanepoints=group:GetTemplateRoutePoints() - + -- First and last waypoints local laneF=lanepoints[1] local laneL=lanepoints[#lanepoints] - + -- Get corresponding coordinates. local coordF=COORDINATE:New(laneF.x, 0, laneF.y) local coordL=COORDINATE:New(laneL.x, 0, laneL.y) - + -- Figure out which point is closer to the port of this warehouse. local distF=startcoord:Get2DDistance(coordF) local distL=startcoord:Get2DDistance(coordL) - + -- Add the lane. Need to take care of the wrong "direction". lane={} if distF0 then - + -- Check if coalition is right. local samecoalition=anycoalition or Coalition==warehouse:GetCoalition() - + -- Check that warehouse is in service. if samecoalition and not (warehouse:IsNotReadyYet() or warehouse:IsStopped() or warehouse:IsDestroyed()) then - + -- Get number of assets. Whole stock is returned if no descriptor/value is given. local nassets=warehouse:GetNumberOfAssets(Descriptor, DescriptorValue) - - --env.info(string.format(" FF warehouse %s nassets = %d for %s=%s", warehouse.alias, nassets, tostring(Descriptor), tostring(DescriptorValue))) - + + --env.info(string.format("FF warehouse %s nassets = %d for %s=%s", warehouse.alias, nassets, tostring(Descriptor), tostring(DescriptorValue))) + -- Assume we have enough. local enough=true -- If specifc assets need to be present... if Descriptor and DescriptorValue then -- Check that enough assets (default 1) are available. enough = nassets>=MinAssets - end - + end + -- Check distance. if enough and (distmin==nil or dist Need to do a lot of checks. - + -- All transports are dead but there is still cargo left ==> Put cargo back into stock. for _,_group in pairs(request.transportgroupset:GetSetObjects()) do local group=_group --Wrapper.Group#GROUP - + -- Check if group is alive. if group and group:IsAlive() then - + -- Check if group is in the spawn zone? local category=group:GetCategory() - + -- Get current speed. local speed=group:GetVelocityKMH() local notmoving=speed<1 - + -- Closest airbase. local airbase=group:GetCoordinate():GetClosestAirbase():GetName() local athomebase=self.airbase and self.airbase:GetName()==airbase - + -- On ground local onground=not group:InAir() - + -- In spawn zone. local inspawnzone=group:IsPartlyOrCompletelyInZone(self.spawnzone) - - -- Check conditions for being back home. + + -- Check conditions for being back home. local ishome=false if category==Group.Category.GROUND or category==Group.Category.HELICOPTER then -- Units go back to the spawn zone, helicopters land and they should not move any more. @@ -3321,70 +3564,70 @@ function WAREHOUSE:_JobDone() -- Planes need to be on ground at their home airbase and should not move any more. ishome=athomebase and onground and notmoving end - + -- Debug text. local text=string.format("Group %s: speed=%d km/h, onground=%s , airbase=%s, spawnzone=%s ==> ishome=%s", group:GetName(), speed, tostring(onground), airbase, tostring(inspawnzone), tostring(ishome)) - self:T(self.wid..text) - + self:I(self.lid..text) + if ishome then -- Info message. local text=string.format("Warehouse %s: Transport group arrived back home and no cargo left for request id=%d.\nSending transport group %s back to stock.", self.alias, request.uid, group:GetName()) - self:_InfoMessage(text) - + self:_InfoMessage(text) + -- Debug smoke. if self.Debug then group:SmokeRed() end - + -- Group arrived. self:Arrived(group) end - end + end end - + end - + else - + if ntransport==0 and request.ntransport>0 then ----------------------------------- -- Still cargo but no transports -- ----------------------------------- - + local ncargoalive=0 - + -- All transports are dead but there is still cargo left ==> Put cargo back into stock. for _,_group in pairs(request.cargogroupset:GetSetObjects()) do --local group=group --Wrapper.Group#GROUP - + -- These groups have been respawned as cargo, i.e. their name changed! local groupname=_group:GetName() local group=GROUP:FindByName(groupname.."#CARGO") - + -- Check if group is alive. if group and group:IsAlive() then - + -- Check if group is in spawn zone? if group:IsPartlyOrCompletelyInZone(self.spawnzone) then - -- Debug smoke. + -- Debug smoke. if self.Debug then group:SmokeBlue() - end + end -- Add asset group back to stock. self:AddAsset(group) ncargoalive=ncargoalive+1 end end - + end -- Info message. - self:_InfoMessage(string.format("Warehouse %s: All transports of request id=%s dead! Putting remaining %s cargo assets back into warehouse!", self.alias, request.uid, ncargoalive)) + self:_InfoMessage(string.format("Warehouse %s: All transports of request id=%s dead! Putting remaining %s cargo assets back into warehouse!", self.alias, request.uid, ncargoalive)) end end - + end -- loop over requests -- Remove pending requests if done. @@ -3401,15 +3644,15 @@ function WAREHOUSE:_CheckAssetStatus() local function _CheckGroup(_request, _group) local request=_request --#WAREHOUSE.Pendingitem local group=_group --Wrapper.Group#GROUP - + if group and group:IsAlive() then - + -- Category of group. local category=group:GetCategory() - + for _,_unit in pairs(group:GetUnits()) do local unit=_unit --Wrapper.Unit#UNIT - + if unit and unit:IsAlive() then local unitid=unit:GetID() local life9=unit:GetLife() @@ -3417,16 +3660,16 @@ function WAREHOUSE:_CheckAssetStatus() local life=life9/life0*100 local speed=unit:GetVelocityMPS() local onground=unit:InAir() - + local problem=false if life<10 then - self:T(string.format("Unit %s is heavily damaged!", unit:GetName())) + self:T(string.format("Unit %s is heavily damaged!", unit:GetName())) end if speed<1 and unit:GetSpeedMax()>1 and onground then self:T(string.format("Unit %s is not moving!", unit:GetName())) problem=true end - + if problem then if request.assetproblem[unitid] then local deltaT=timer.getAbsTime()-request.assetproblem[unitid] @@ -3439,33 +3682,33 @@ function WAREHOUSE:_CheckAssetStatus() end end end - + end end end - + for _,request in pairs(self.pending) do local request=request --#WAREHOUSE.Pendingitem - + -- Cargo groups. if request.cargogroupset then for _,_group in pairs(request.cargogroupset:GetSet()) do local group=_group --Wrapper.Group#GROUP - + _CheckGroup(request, group) - + end end - + -- Transport groups. if request.transportgroupset then for _,group in pairs(request.transportgroupset:GetSet()) do - - _CheckGroup(request, group) + + _CheckGroup(request, group) end end - + end end @@ -3486,112 +3729,150 @@ end -- @param DCS#AI.Skill skill Skill of the asset. -- @param #table liveries Table of livery names. When the asset is spawned one livery is chosen randomly. -- @param #string assignment A free to choose string specifying an assignment for the asset. This can be used with the @{#WAREHOUSE.OnAfterNewAsset} function. -function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, skill, liveries, assignment) +-- @param #table other (Optional) Table of other useful data. Can be collected via WAREHOUSE.OnAfterNewAsset() function for example +function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, skill, liveries, assignment, other) self:T({group=group, ngroups=ngroups, forceattribute=forceattribute, forcecargobay=forcecargobay, forceweight=forceweight}) -- Set default. local n=ngroups or 1 - + -- Handle case where just a string is passed. if type(group)=="string" then group=GROUP:FindByName(group) end - + if liveries and type(liveries)=="string" then liveries={liveries} end - + if group then - + -- Try to get UIDs from group name. Is this group a known or a new asset? local wid,aid,rid=self:_GetIDsFromGroup(group) - + if wid and aid and rid then + --------------------------- -- This is a KNOWN asset -- --------------------------- - + -- Get the original warehouse this group belonged to. local warehouse=self:FindWarehouseInDB(wid) + if warehouse then + local request=warehouse:_GetRequestOfGroup(group, warehouse.pending) + if request then - + -- Increase number of cargo delivered and transports home. local istransport=warehouse:_GroupIsTransport(group,request) + if istransport==true then request.ntransporthome=request.ntransporthome+1 request.transportgroupset:Remove(group:GetName(), true) - self:T3(warehouse.wid..string.format("Transport %d of %s returned home.", request.ntransporthome, tostring(request.ntransport))) + local ntrans=request.transportgroupset:Count() + self:T2(warehouse.lid..string.format("Transport %d of %s returned home. TransportSet=%d", request.ntransporthome, tostring(request.ntransport), ntrans)) elseif istransport==false then request.ndelivered=request.ndelivered+1 - request.cargogroupset:Remove(self:_GetNameWithOut(group), true) - self:T3(warehouse.wid..string.format("Cargo %d of %s delivered.", request.ndelivered, tostring(request.nasset))) + local namewo=self:_GetNameWithOut(group) + request.cargogroupset:Remove(namewo, true) + local ncargo=request.cargogroupset:Count() + self:T2(warehouse.lid..string.format("Cargo %s: %d of %s delivered. CargoSet=%d", namewo, request.ndelivered, tostring(request.nasset), ncargo)) else - self:T(warehouse.wid..string.format("WARNING: Group %s is neither cargo nor transport!", group:GetName())) + self:E(warehouse.lid..string.format("WARNING: Group %s is neither cargo nor transport! Need to investigate...", group:GetName())) end - - end - - -- If no assignment was given we take the assignment of the request if there is any. - if assignment==nil and request.assignment~=nil then - assignment=request.assignment + + -- If no assignment was given we take the assignment of the request if there is any. + if assignment==nil and request.assignment~=nil then + assignment=request.assignment + end + end end -- Get the asset from the global DB. local asset=self:FindAssetInDB(group) - -- Set livery. - if liveries then - asset.livery=liveries[math.random(#liveries)] - end - - -- Set skill. - asset.skill=skill - -- Note the group is only added once, i.e. the ngroups parameter is ignored here. -- This is because usually these request comes from an asset that has been transfered from another warehouse and hence should only be added once. - if asset~=nil then + if asset~=nil then self:_DebugMessage(string.format("Warehouse %s: Adding KNOWN asset uid=%d with attribute=%s to stock.", self.alias, asset.uid, asset.attribute), 5) + + -- Set livery. + if liveries then + if type(liveries)=="table" then + asset.livery=liveries[math.random(#liveries)] + else + asset.livery=liveries + end + end + + -- Set skill. + asset.skill=skill or asset.skill + + -- Asset now belongs to this warehouse. Set warehouse ID. + asset.wid=self.uid + + -- No request associated with this asset. + asset.rid=nil + + -- Asset is not spawned. + asset.spawned=false + asset.iscargo=nil + asset.arrived=nil + + -- Add asset to stock. table.insert(self.stock, asset) - self:NewAsset(asset, assignment or "") + + -- Trigger New asset event. + self:__NewAsset(0.1, asset, assignment or "") else self:_ErrorMessage(string.format("ERROR: Known asset could not be found in global warehouse db!"), 0) - end - + end + else + ------------------------- -- This is a NEW asset -- ------------------------- - + -- Debug info. - self:_DebugMessage(string.format("Warehouse %s: Adding %d NEW assets of group %s to stock.", self.alias, n, tostring(group:GetName())), 5) - + self:_DebugMessage(string.format("Warehouse %s: Adding %d NEW assets of group %s to stock", self.alias, n, tostring(group:GetName())), 5) + -- This is a group that is not in the db yet. Add it n times. - local assets=self:_RegisterAsset(group, n, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill) - + local assets=self:_RegisterAsset(group, n, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill, assignment) + -- Add created assets to stock of this warehouse. for _,asset in pairs(assets) do + + -- Asset belongs to this warehouse. Set warehouse ID. + asset.wid=self.uid + + -- No request associated with this asset. + asset.rid=nil + + -- Add asset to stock. table.insert(self.stock, asset) - self:NewAsset(asset, assignment or "") - end - - end - + + -- Trigger NewAsset event. Delay a bit for OnAfterNewAsset functions to work properly. + self:__NewAsset(0.1, asset, assignment or "") + end + + end + -- Destroy group if it is alive. if group:IsAlive()==true then - self:_DebugMessage(string.format("Destroying group %s.", group:GetName()), 5) + self:_DebugMessage(string.format("Removing group %s", group:GetName()), 5) -- Setting parameter to false, i.e. creating NO dead or remove unit event, seems to not confuse the dispatcher logic. group:Destroy(false) end - + else - self:E(self.wid.."ERROR: Unknown group added as asset!") + self:E(self.lid.."ERROR: Unknown group added as asset!") + self:E({unknowngroup=group}) end - - -- Update status. - --self:__Status(-1) + end --- Register new asset in globase warehouse data base. @@ -3604,13 +3885,14 @@ end -- @param #number loadradius Radius in meters when cargo is loaded into the carrier. -- @param #table liveries Table of liveries. -- @param DCS#AI.Skill skill Skill of AI. +-- @param #string assignment Assignment attached to the asset item. -- @return #table A table containing all registered assets. -function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill) +function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill, assignment) self:F({groupname=group:GetName(), ngroups=ngroups, forceattribute=forceattribute, forcecargobay=forcecargobay, forceweight=forceweight}) -- Set default. local n=ngroups or 1 - + -- Get the size of an object. local function _GetObjectSize(DCSdesc) if DCSdesc.box then @@ -3620,18 +3902,20 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, return math.max(x,z), x , y, z end return 0,0,0,0 - end - + end + -- Get name of template group. local templategroupname=group:GetName() - + local Descriptors=group:GetUnit(1):GetDesc() local Category=group:GetCategory() local TypeName=group:GetTypeName() local SpeedMax=group:GetSpeedMax() local RangeMin=group:GetRange() local smax,sx,sy,sz=_GetObjectSize(Descriptors) - + + --self:E(Descriptors) + -- Get weight and cargo bay size in kg. local weight=0 local cargobay={} @@ -3640,31 +3924,31 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, for _i,_unit in pairs(group:GetUnits()) do local unit=_unit --Wrapper.Unit#UNIT local Desc=unit:GetDesc() - + -- Weight. We sum up all units in the group. local unitweight=forceweight or Desc.massEmpty if unitweight then weight=weight+unitweight end - + local cargomax=0 local massfuel=Desc.fuelMassMax or 0 local massempty=Desc.massEmpty or 0 local massmax=Desc.massMax or 0 - + -- Calcuate cargo bay limit value. cargomax=massmax-massfuel-massempty - self:T3(self.wid..string.format("Unit name=%s: mass empty=%.1f kg, fuel=%.1f kg, max=%.1f kg ==> cargo=%.1f kg", unit:GetName(), unitweight, massfuel, massmax, cargomax)) - + self:T3(self.lid..string.format("Unit name=%s: mass empty=%.1f kg, fuel=%.1f kg, max=%.1f kg ==> cargo=%.1f kg", unit:GetName(), unitweight, massfuel, massmax, cargomax)) + -- Cargo bay size. local bay=forcecargobay or unit:GetCargoBayFreeWeight() - + -- Add bay size to table. table.insert(cargobay, bay) - + -- Sum up total bay size. cargobaytot=cargobaytot+bay - + -- Get max bay size. if bay>cargobaymax then cargobaymax=bay @@ -3680,20 +3964,20 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, -- Add this n times to the table. for i=1,n do local asset={} --#WAREHOUSE.Assetitem - + -- Increase asset unique id counter. - WAREHOUSE.db.AssetID=WAREHOUSE.db.AssetID+1 - + _WAREHOUSEDB.AssetID=_WAREHOUSEDB.AssetID+1 + -- Set parameters. - asset.uid=WAREHOUSE.db.AssetID + asset.uid=_WAREHOUSEDB.AssetID asset.templatename=templategroupname asset.template=UTILS.DeepCopy(_DATABASE.Templates.Groups[templategroupname].Template) asset.category=Category asset.unittype=TypeName - asset.nunits=#asset.template.units + asset.nunits=#asset.template.units asset.range=RangeMin asset.speedmax=SpeedMax - asset.size=smax + asset.size=smax asset.weight=weight asset.DCSdesc=Descriptors asset.attribute=attribute @@ -3705,14 +3989,17 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, asset.livery=liveries[math.random(#liveries)] end asset.skill=skill - + asset.assignment=assignment + asset.spawned=false + asset.spawngroupname=string.format("%s_AID-%d", templategroupname, asset.uid) + if i==1 then self:_AssetItemInfo(asset) end - + -- Add asset to global db. - WAREHOUSE.db.Assets[asset.uid]=asset - + _WAREHOUSEDB.Assets[asset.uid]=asset + -- Add asset to the table that is retured. table.insert(assets,asset) end @@ -3726,21 +4013,22 @@ end function WAREHOUSE:_AssetItemInfo(asset) -- Info about asset: local text=string.format("\nNew asset with id=%d for warehouse %s:\n", asset.uid, self.alias) - text=text..string.format("Template name = %s\n", asset.templatename) - text=text..string.format("Unit type = %s\n", asset.unittype) - text=text..string.format("Attribute = %s\n", asset.attribute) - text=text..string.format("Category = %d\n", asset.category) - text=text..string.format("Units # = %d\n", asset.nunits) - text=text..string.format("Speed max = %5.2f km/h\n", asset.speedmax) - text=text..string.format("Range max = %5.2f km\n", asset.range/1000) - text=text..string.format("Size max = %5.2f m\n", asset.size) - text=text..string.format("Weight total = %5.2f kg\n", asset.weight) - text=text..string.format("Cargo bay tot = %5.2f kg\n", asset.cargobaytot) - text=text..string.format("Cargo bay max = %5.2f kg\n", asset.cargobaymax) - text=text..string.format("Load radius = %s m\n", tostring(asset.loadradius)) - text=text..string.format("Skill = %s\n", tostring(asset.skill)) - text=text..string.format("Livery = %s", tostring(asset.livery)) - self:T(self.wid..text) + text=text..string.format("Spawngroup name= %s\n", asset.spawngroupname) + text=text..string.format("Template name = %s\n", asset.templatename) + text=text..string.format("Unit type = %s\n", asset.unittype) + text=text..string.format("Attribute = %s\n", asset.attribute) + text=text..string.format("Category = %d\n", asset.category) + text=text..string.format("Units # = %d\n", asset.nunits) + text=text..string.format("Speed max = %5.2f km/h\n", asset.speedmax) + text=text..string.format("Range max = %5.2f km\n", asset.range/1000) + text=text..string.format("Size max = %5.2f m\n", asset.size) + text=text..string.format("Weight total = %5.2f kg\n", asset.weight) + text=text..string.format("Cargo bay tot = %5.2f kg\n", asset.cargobaytot) + text=text..string.format("Cargo bay max = %5.2f kg\n", asset.cargobaymax) + text=text..string.format("Load radius = %s m\n", tostring(asset.loadradius)) + text=text..string.format("Skill = %s\n", tostring(asset.skill)) + text=text..string.format("Livery = %s", tostring(asset.livery)) + self:I(self.lid..text) self:T({DCSdesc=asset.DCSdesc}) self:T3({Template=asset.template}) end @@ -3751,9 +4039,9 @@ end -- @param #string Event Event. -- @param #string To To state. -- @param #WAREHOUSE.Assetitem asset The asset that has just been added. --- @parma #string assignment The (optional) assignment for the asset. +-- @param #string assignment The (optional) assignment for the asset. function WAREHOUSE:onafterNewAsset(From, Event, To, asset, assignment) - self:T(self.wid..string.format("New asset %s id=%d with assignment %s.", asset.templatename, asset.uid, assignment)) + self:T(self.lid..string.format("New asset %s id=%d with assignment %s.", tostring(asset.templatename), asset.uid, tostring(assignment))) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3773,12 +4061,12 @@ end -- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets. -- @return #boolean If true, request is okay at first glance. function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescriptor, AssetDescriptorValue, nAsset, TransportType, nTransport, Assignment, Prio) - + -- Request is okay. local okay=true - + if AssetDescriptor==WAREHOUSE.Descriptor.ATTRIBUTE then - + -- Check if a valid attibute was given. local gotit=false for _,attribute in pairs(WAREHOUSE.Attribute) do @@ -3790,7 +4078,7 @@ function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescripto self:_ErrorMessage("ERROR: Invalid request. Asset attribute is unknown!", 5) okay=false end - + elseif AssetDescriptor==WAREHOUSE.Descriptor.CATEGORY then -- Check if a valid category was given. @@ -3804,38 +4092,52 @@ function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescripto self:_ErrorMessage("ERROR: Invalid request. Asset category is unknown!", 5) okay=false end - + elseif AssetDescriptor==WAREHOUSE.Descriptor.GROUPNAME then - + if type(AssetDescriptorValue)~="string" then self:_ErrorMessage("ERROR: Invalid request. Asset template name must be passed as a string!", 5) - okay=false + okay=false end - + elseif AssetDescriptor==WAREHOUSE.Descriptor.UNITTYPE then if type(AssetDescriptorValue)~="string" then self:_ErrorMessage("ERROR: Invalid request. Asset unit type must be passed as a string!", 5) - okay=false + okay=false end - + + elseif AssetDescriptor==WAREHOUSE.Descriptor.ASSIGNMENT then + + if type(AssetDescriptorValue)~="string" then + self:_ErrorMessage("ERROR: Invalid request. Asset assignment type must be passed as a string!", 5) + okay=false + end + + elseif AssetDescriptor==WAREHOUSE.Descriptor.ASSETLIST then + + if type(AssetDescriptorValue)~="table" then + self:_ErrorMessage("ERROR: Invalid request. Asset assignment type must be passed as a table!", 5) + okay=false + end + else - self:_ErrorMessage("ERROR: Invalid request. Asset descriptor is not ATTRIBUTE, CATEGORY, GROUPNAME or UNITTYPE!", 5) + self:_ErrorMessage("ERROR: Invalid request. Asset descriptor is not ATTRIBUTE, CATEGORY, GROUPNAME, UNITTYPE or ASSIGNMENT!", 5) okay=false end -- Warehouse is stopped? if self:IsStopped() then self:_ErrorMessage("ERROR: Invalid request. Warehouse is stopped!", 0) - okay=false + okay=false end -- Warehouse is destroyed? - if self:IsDestroyed() then + if self:IsDestroyed() and not self.respawnafterdestroyed then self:_ErrorMessage("ERROR: Invalid request. Warehouse is destroyed!", 0) okay=false end - + return okay end @@ -3849,9 +4151,9 @@ end -- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc. -- @param #number nAsset Number of groups requested that match the asset specification. -- @param #WAREHOUSE.TransportType TransportType Type of transport. --- @param #number nTransport Number of transport units requested. +-- @param #number nTransport Number of transport units requested. -- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low. --- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets. +-- @param #string Assignment A keyword or text that can later be used to identify this request and postprocess the assets. function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor, AssetDescriptorValue, nAsset, TransportType, nTransport, Prio, Assignment) -- Defaults. @@ -3870,8 +4172,8 @@ function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor local toself=false if self.warehouse:GetName()==warehouse.warehouse:GetName() then toself=true - end - + end + -- Increase id. self.queueid=self.queueid+1 @@ -3887,22 +4189,20 @@ function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor ntransport=nTransport, assignment=tostring(Assignment), airbase=warehouse:GetAirbase(), - category=warehouse:GetAirbaseCategory(), + category=warehouse:GetAirbaseCategory(), ndelivered=0, ntransporthome=0, assets={}, toself=toself, } --#WAREHOUSE.Queueitem - + -- Add request to queue. table.insert(self.queue, request) - - local text=string.format("Warehouse %s: New request from warehouse %s.\nDescriptor %s=%s, #assets=%s; Transport=%s, #transports =%s.", + + local text=string.format("Warehouse %s: New request from warehouse %s.\nDescriptor %s=%s, #assets=%s; Transport=%s, #transports =%s.", self.alias, warehouse.alias, request.assetdesc, tostring(request.assetdescval), tostring(request.nasset), request.transporttype, tostring(request.ntransport)) self:_DebugMessage(text, 5) - -- Update status - --self:__Status(-1) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3922,36 +4222,38 @@ function WAREHOUSE:onbeforeRequest(From, Event, To, Request) -- Shortcut to cargoassets. local _assets=Request.cargoassets - + if Request.nasset==0 then local text=string.format("Warehouse %s: Request denied! Zero assets were requested.", self.alias) self:_InfoMessage(text, 10) return false end - + -- Check if destination is in range for all requested assets. for _,_asset in pairs(_assets) do local asset=_asset --#WAREHOUSE.Assetitem - -- Check if destination is in range. - if asset.range1 then @@ -4458,35 +4718,57 @@ function WAREHOUSE:onafterUnloaded(From, Event, To, group) self:Arrived(group) elseif group:IsShip() then -- Not sure if naval units will be allowed as cargo even though it might be possible. Best put them into warehouse immediately. - self:Arrived(group) + self:Arrived(group) end - + else - self:E(self.wid..string.format("ERROR unloaded Cargo group is not alive!")) - end + self:E(self.lid..string.format("ERROR unloaded Cargo group is not alive!")) + end +end + +--- On before "Arrived" event. Triggered when a group has arrived at its destination warehouse. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group that was delivered. +function WAREHOUSE:onbeforeArrived(From, Event, To, group) + + local asset=self:FindAssetInDB(group) + + if asset then + if asset.arrived==true then + -- Asset already arrived (e.g. if multiple units trigger the event via landing). + return false + else + asset.arrived=true --ensure this is not called again from the same asset group. + return true + end + end + end --- On after "Arrived" event. Triggered when a group has arrived at its destination warehouse. -- The routine should be called by the warehouse sending this asset and not by the receiving warehouse. -- It is checked if this asset is cargo (or self propelled) or transport. If it is cargo it is put into the stock of receiving warehouse. --- If it is a transporter it is put back into the sending warehouse since transports are supposed to return their home warehouse. +-- If it is a transporter it is put back into the sending warehouse since transports are supposed to return their home warehouse. -- @param #WAREHOUSE self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. -- @param Wrapper.Group#GROUP group The group that was delivered. function WAREHOUSE:onafterArrived(From, Event, To, group) - + -- Debug message and smoke. if self.Debug then group:SmokeOrange() end - + -- Get pending request this group belongs to. local request=self:_GetRequestOfGroup(group, self.pending) if request then - + -- Get the right warehouse to put the asset into -- Transports go back to the warehouse which called this function while cargo goes into the receiving warehouse. local warehouse=request.warehouse @@ -4496,36 +4778,37 @@ function WAREHOUSE:onafterArrived(From, Event, To, group) elseif istransport==false then warehouse=request.warehouse else - self:E(self.wid..string.format("ERROR: Group %s is neither cargo nor transport", group:GetName())) + self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport", group:GetName())) return end - + -- Debug message. self:_DebugMessage(string.format("Group %s arrived at warehouse %s!", tostring(group:GetName()), warehouse.alias), 5) - + -- Route mobile ground group to the warehouse. Group has 60 seconds to get there or it is despawned and added as asset to the new warehouse regardless. if group:IsGround() and group:GetSpeedMax()>1 then group:RouteGroundTo(warehouse:GetCoordinate(), group:GetSpeedMax()*0.3, "Off Road") end - -- Increase number of cargo delivered and transports home. - local istransport=warehouse:_GroupIsTransport(group,request) + -- NOTE: This is done in the AddAsset() function. Dont know, why we do it also here. + --[[ if istransport==true then request.ntransporthome=request.ntransporthome+1 request.transportgroupset:Remove(group:GetName(), true) - self:T2(warehouse.wid..string.format("Transport %d of %s returned home.", request.ntransporthome, tostring(request.ntransport))) + self:T2(warehouse.lid..string.format("Transport %d of %s returned home.", request.ntransporthome, tostring(request.ntransport))) elseif istransport==false then request.ndelivered=request.ndelivered+1 request.cargogroupset:Remove(self:_GetNameWithOut(group), true) - self:T2(warehouse.wid..string.format("Cargo %d of %s delivered.", request.ndelivered, tostring(request.nasset))) + self:T2(warehouse.lid..string.format("Cargo %d of %s delivered.", request.ndelivered, tostring(request.nasset))) else - self:E(warehouse.wid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) - end - + self:E(warehouse.lid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) + end + ]] + -- Move asset from pending queue into new warehouse. warehouse:__AddAsset(60, group) end - + end --- On after "Delivered" event. Triggered when all asset groups have reached their destination. Corresponding request is deleted from the pending queue. @@ -4544,10 +4827,10 @@ function WAREHOUSE:onafterDelivered(From, Event, To, request) if self.Debug then self:_Fireworks(request.warehouse:GetCoordinate()) end - + -- Set delivered status for this request uid. self.delivered[request.uid]=true - + end @@ -4564,7 +4847,7 @@ function WAREHOUSE:onafterSelfRequest(From, Event, To, groupset, request) -- Debug info. self:_DebugMessage(string.format("Assets spawned at warehouse %s after self request!", self.alias)) - + -- Debug info. for _,_group in pairs(groupset:GetSetObjects()) do local group=_group --Wrapper.Group#GROUP @@ -4572,7 +4855,7 @@ function WAREHOUSE:onafterSelfRequest(From, Event, To, groupset, request) group:FlareGreen() end end - + -- Add a "defender request" to be able to despawn all assets once defeated. if self:IsAttacked() then @@ -4584,13 +4867,13 @@ function WAREHOUSE:onafterSelfRequest(From, Event, To, groupset, request) if group:IsGround() and speedmax>1 and group:IsNotInZone(self.zone) then group:RouteGroundTo(self.zone:GetRandomCoordinate(), 0.8*speedmax, "Off Road") end - end - end - + end + end + -- Add request to defenders. table.insert(self.defending, request) end - + end --- On after "Attacked" event. Warehouse is under attack by an another coalition. @@ -4605,29 +4888,29 @@ function WAREHOUSE:onafterAttacked(From, Event, To, Coalition, Country) -- Warning. local text=string.format("Warehouse %s: We are under attack!", self.alias) self:_InfoMessage(text) - + -- Debug smoke. if self.Debug then self:GetCoordinate():SmokeOrange() - end - + end + -- Spawn all ground units in the spawnzone? if self.autodefence then local nground=self:GetNumberOfAssets(WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND) local text=string.format("Warehouse auto defence activated.\n") - + if nground>0 then text=text..string.format("Deploying all %d ground assets.", nground) - + -- Add self request. - self:AddRequest(self, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, WAREHOUSE.Quantity.ALL, nil, nil , 0) + self:AddRequest(self, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, WAREHOUSE.Quantity.ALL, nil, nil , 0, "AutoDefence") else - text=text..string.format("No ground assets currently available.") + text=text..string.format("No ground assets currently available.") end self:_InfoMessage(text) else local text=string.format("Warehouse auto defence inactive.") - self:I(self.wid..text) + self:I(self.lid..text) end end @@ -4645,33 +4928,48 @@ function WAREHOUSE:onafterDefeated(From, Event, To) -- Debug smoke. if self.Debug then self:GetCoordinate():SmokeGreen() - end + end -- Auto defence: put assets back into stock. if self.autodefence then for _,request in pairs(self.defending) do - - -- Route defenders back to warehoue (for visual reasons only) and put them back into stock. + + -- Route defenders back to warehoue (for visual reasons only) and put them back into stock. for _,_group in pairs(request.cargogroupset:GetSetObjects()) do local group=_group --Wrapper.Group#GROUP - + -- Get max speed of group and route it back slowly to the warehouse. local speed=group:GetSpeedMax() if group:IsGround() and speed>1 then group:RouteGroundTo(self:GetCoordinate(), speed*0.3) - end - + end + -- Add asset group back to stock after 60 seconds. self:__AddAsset(60, group) end - + end - + self.defending=nil self.defending={} end end +--- Respawn warehouse. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function WAREHOUSE:onafterRespawn(From, Event, To) + + -- Info message. + local text=string.format("Respawning warehouse %s", self.alias) + self:_InfoMessage(text) + + -- Respawn warehouse. + self.warehouse:ReSpawn() + +end --- On before "ChangeCountry" event. Checks whether a change of country is necessary by comparing the actual country to the the requested one. -- @param #WAREHOUSE self @@ -4686,8 +4984,8 @@ function WAREHOUSE:onbeforeChangeCountry(From, Event, To, Country) -- Message. local text=string.format("Warehouse %s: request to change country %d-->%d", self.alias, currentCountry, Country) self:_DebugMessage(text, 10) - - -- Check if current or requested coalition or country match. + + -- Check if current or requested coalition or country match. if currentCountry~=Country then return true end @@ -4695,39 +4993,43 @@ function WAREHOUSE:onbeforeChangeCountry(From, Event, To, Country) return false end - --- On after "ChangeCountry" event. Warehouse is respawned with the specified country. All queued requests are deleted and the owned airbase is reset if the coalition is changed by changing the -- country. -- @param #WAREHOUSE self -- @param #string From From state. -- @param #string Event Event. -- @param #string To To state. --- @param DCS#country.id Country which has captured the warehouse. +-- @param DCS#country.id Country Country which has captured the warehouse. function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) local CoalitionOld=self:GetCoalition() - -- Respawn warehouse with new coalition/country. self.warehouse:ReSpawn(Country) - + local CoalitionNew=self:GetCoalition() - + -- Delete all waiting requests because they are not valid any more. self.queue=nil self.queue={} + + if self.airbasename then + + -- Get airbase of this warehouse. + local airbase=AIRBASE:FindByName(self.airbasename) - -- Airbase could have been captured before and already belongs to the new coalition. - local airbase=AIRBASE:FindByName(self.airbasename) - local airbasecoaltion=airbase:GetCoalition() + -- Get coalition of the airbase. + local airbaseCoalition=airbase:GetCoalition() - if CoalitionNew==airbasecoaltion then - -- Airbase already owned by the coalition that captured the warehouse. Airbase can be used by this warehouse. - self.airbase=airbase - else - -- Airbase is owned by other coalition. So this warehouse does not have an airbase unil it is captured. - self.airbase=nil + if CoalitionNew==airbaseCoalition then + -- Airbase already owned by the coalition that captured the warehouse. Airbase can be used by this warehouse. + self.airbase=airbase + else + -- Airbase is owned by other coalition. So this warehouse does not have an airbase until it is captured. + self.airbase=nil + end + end - + -- Debug smoke. if self.Debug then if CoalitionNew==coalition.side.RED then @@ -4736,7 +5038,21 @@ function WAREHOUSE:onafterChangeCountry(From, Event, To, Country) self:GetCoordinate():SmokeBlue() end end - + +end + +--- On before "Captured" event. Warehouse has been captured by another coalition. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#coalition.side Coalition which captured the warehouse. +-- @param DCS#country.id Country which has captured the warehouse. +function WAREHOUSE:onbeforeCaptured(From, Event, To, Coalition, Country) + + -- Warehouse respawned. + self:ChangeCountry(Country) + end --- On after "Captured" event. Warehouse has been captured by another coalition. @@ -4751,10 +5067,7 @@ function WAREHOUSE:onafterCaptured(From, Event, To, Coalition, Country) -- Message. local text=string.format("Warehouse %s: We were captured by enemy coalition (side=%d)!", self.alias, Coalition) self:_InfoMessage(text) - - -- Change coalition and country of warehouse static. - self:ChangeCoaliton(Coalition, Country) - + end @@ -4778,7 +5091,7 @@ function WAREHOUSE:onafterAirbaseCaptured(From, Event, To, Coalition) self.airbase:GetCoordinate():SmokeBlue() end end - + -- Set airbase to nil and category to no airbase. self.airbase=nil end @@ -4795,9 +5108,9 @@ function WAREHOUSE:onafterAirbaseRecaptured(From, Event, To, Coalition) local text=string.format("Warehouse %s: We recaptured our airbase %s from the enemy (coalition=%d)!", self.alias, self.airbasename, Coalition) self:_InfoMessage(text) - -- Set airbase and category. + -- Set airbase and category. self.airbase=AIRBASE:FindByName(self.airbasename) - + -- Debug smoke. if self.Debug then if Coalition==coalition.side.RED then @@ -4806,11 +5119,69 @@ function WAREHOUSE:onafterAirbaseRecaptured(From, Event, To, Coalition) self.airbase:GetCoordinate():SmokeBlue() end end - + end +--- On before "AssetSpawned" event. Checks whether the asset was already set to "spawned" for groups with multiple units. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group spawned. +-- @param #WAREHOUSE.Assetitem asset The asset that is dead. +-- @param #WAREHOUSE.Pendingitem request The request of the dead asset. +function WAREHOUSE:onbeforeAssetSpawned(From, Event, To, group, asset, request) + if asset.spawned then + --return false + else + --return true + end + + return true +end ---- On after "AssetDead" event triggerd when an asset group died. +--- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group spawned. +-- @param #WAREHOUSE.Assetitem asset The asset that is dead. +-- @param #WAREHOUSE.Pendingitem request The request of the dead asset. +function WAREHOUSE:onafterAssetSpawned(From, Event, To, group, asset, request) + local text=string.format("Asset %s from request id=%d was spawned!", asset.spawngroupname, request.uid) + self:I(self.lid..text) + + -- Sete asset state to spawned. + asset.spawned=true + + -- Check if all assets groups are spawned and trigger events. + local n=0 + for _,_asset in pairs(request.assets) do + local assetitem=_asset --#WAREHOUSE.Assetitem + + -- Debug info. + self:T2(self.lid..string.format("Asset %s spawned %s as %s", assetitem.templatename, tostring(assetitem.spawned), tostring(assetitem.spawngroupname))) + + if assetitem.spawned then + n=n+1 + else + self:E(self.lid.."FF What?! This should not happen!") + end + + end + + -- Trigger event. + if n==request.nasset+request.ntransport then + self:T3(self.lid..string.format("All assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned. Calling RequestSpawned", n, request.nasset, request.ntransport, request.uid)) + self:RequestSpawned(request, request.cargogroupset, request.transportgroupset) + else + self:T3(self.lid..string.format("Not all assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned YET", n, request.nasset, request.ntransport, request.uid)) + end + +end + +--- On after "AssetDead" event triggered when an asset group died. -- @param #WAREHOUSE self -- @param #string From From state. -- @param #string Event Event. @@ -4819,7 +5190,7 @@ end -- @param #WAREHOUSE.Pendingitem request The request of the dead asset. function WAREHOUSE:onafterAssetDead(From, Event, To, asset, request) local text=string.format("Asset %s from request id=%d is dead!", asset.templatename, request.uid) - self:T(self.wid..text) + self:T(self.lid..text) self:_DebugMessage(text) end @@ -4832,22 +5203,41 @@ end function WAREHOUSE:onafterDestroyed(From, Event, To) -- Message. - local text=string.format("Warehouse %s was destroyed! Assets lost %d.", self.alias, #self.stock) + local text=string.format("Warehouse %s was destroyed! Assets lost %d. Respawn=%s", self.alias, #self.stock, tostring(self.respawnafterdestroyed)) self:_InfoMessage(text) - - -- Remove all table entries from waiting queue and stock. - for k,_ in pairs(self.queue) do - self.queue[k]=nil - end - for k,_ in pairs(self.stock) do - self.stock[k]=nil - end - --self.queue=nil - --self.queue={} + if self.respawnafterdestroyed then - --self.stock=nil - --self.stock={} + if self.respawndelay then + self:Pause() + self:__Respawn(self.respawndelay) + else + self:Respawn() + end + + else + + -- Remove all table entries from waiting queue and stock. + for k,_ in pairs(self.queue) do + self.queue[k]=nil + end + + for k,_ in pairs(self.stock) do + --self.stock[k]=nil + end + + for k=#self.stock,1,-1 do + --local asset=self.stock[k] --#WAREHOUSE.Assetitem + --self:AssetDead(asset, nil) + self.stock[k]=nil + end + + --self.queue=nil + --self.queue={} + + --self.stock=nil + --self.stock={} + end end @@ -4866,32 +5256,32 @@ function WAREHOUSE:onafterSave(From, Event, To, path, filename) f:write(data) f:close() end - + -- Set file name. filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias) - + -- Set path. if path~=nil then filename=path.."\\"..filename end - + -- Info local text=string.format("Saving warehouse assets to file %s", filename) MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report) - self:I(self.wid..text) - + self:I(self.lid..text) + local warehouseassets="" warehouseassets=warehouseassets..string.format("coalition=%d\n", self:GetCoalition()) warehouseassets=warehouseassets..string.format("country=%d\n", self:GetCountry()) - + -- Loop over all assets in stock. for _,_asset in pairs(self.stock) do local asset=_asset -- #WAREHOUSE.Assetitem - + -- Loop over asset parameters. local assetstring="" for key,value in pairs(asset) do - + -- Only save keys which are needed to restore the asset. if key=="templatename" or key=="attribute" or key=="cargobay" or key=="weight" or key=="loadradius" or key=="livery" or key=="skill" or key=="assignment" then local name @@ -4904,13 +5294,13 @@ function WAREHOUSE:onafterSave(From, Event, To, path, filename) end self:I(string.format("Loaded asset: %s", assetstring)) end - + -- Add asset string. warehouseassets=warehouseassets..assetstring.."\n" end -- Save file. - _savefile(filename, warehouseassets) + _savefile(filename, warehouseassets) end @@ -4927,25 +5317,25 @@ function WAREHOUSE:onbeforeLoad(From, Event, To, path, filename) local function _fileexists(name) local f=io.open(name,"r") - if f~=nil then + if f~=nil then io.close(f) - return true - else + return true + else return false end end -- Set file name. filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias) - + -- Set path. if path~=nil then filename=path.."\\"..filename end - + -- Check if file exists. local exists=_fileexists(filename) - + if exists then return true else @@ -4974,40 +5364,40 @@ function WAREHOUSE:onafterLoad(From, Event, To, path, filename) -- Set file name. filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias) - + -- Set path. if path~=nil then filename=path.."\\"..filename end - + -- Info local text=string.format("Loading warehouse assets from file %s", filename) MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report) - self:I(self.wid..text) + self:I(self.lid..text) -- Load asset data from file. local data=_loadfile(filename) -- Split by line break. local assetdata=UTILS.Split(data,"\n") - + -- Coalition and coutrny. local Coalition local Country - + -- Loop over asset lines. local assets={} for _,asset in pairs(assetdata) do - + -- Parameters are separated by semi-colons local descriptors=UTILS.Split(asset,";") - + local asset={} local isasset=false for _,descriptor in pairs(descriptors) do - + local keyval=UTILS.Split(descriptor,"=") - + if #keyval==2 then if keyval[1]=="coalition" then @@ -5017,20 +5407,20 @@ function WAREHOUSE:onafterLoad(From, Event, To, path, filename) -- Get country id. Country=tonumber(keyval[2]) else - + -- This is an asset. isasset=true - + local key=keyval[1] local val=keyval[2] - - --env.info(string.format("FF asset key=%s val=%s", key, val)) - + + --env.info(string.format("FF asset key=%s val=%s", key, val)) + -- Livery or skill could be "nil". if val=="nil" then val=nil - end - + end + -- Convert string to number where necessary. if key=="cargobay" or key=="weight" or key=="loadradius" then asset[key]=tonumber(val) @@ -5038,25 +5428,25 @@ function WAREHOUSE:onafterLoad(From, Event, To, path, filename) asset[key]=val end end - + end end - + -- Add to table. if isasset then table.insert(assets, asset) end end - + -- Respawn warehouse with prev coalition if necessary. if Country~=self:GetCountry() then - self:T(self.wid..string.format("Changing warehouse country %d-->%d on loading assets.", self:GetCountry(), Country)) + self:T(self.lid..string.format("Changing warehouse country %d-->%d on loading assets.", self:GetCountry(), Country)) self:ChangeCountry(Country) end - + for _,_asset in pairs(assets) do local asset=_asset --#WAREHOUSE.Assetitem - + local group=GROUP:FindByName(asset.templatename) if group then self:AddAsset(group, 1, asset.attribute, asset.cargobay, asset.weight, asset.loadradius, asset.skill, asset.livery, asset.assignment) @@ -5074,97 +5464,80 @@ end --- Spawns requested assets at warehouse or associated airbase. -- @param #WAREHOUSE self -- @param #WAREHOUSE.Queueitem Request Information table of the request. --- @return Core.Set#SET_GROUP Set of groups that were spawned. function WAREHOUSE:_SpawnAssetRequest(Request) self:F2({requestUID=Request.uid}) - -- Shortcut to cargo assets. - local _assetstock=Request.cargoassets + -- Shortcut to cargo assets. + local cargoassets=Request.cargoassets - -- General type and category. - local _cargotype=Request.cargoattribute --#WAREHOUSE.Attribute - local _cargocategory=Request.cargocategory --DCS#Group.Category - -- Now we try to find all parking spots for all cargo groups in advance. Due to the for loop, the parking spots do not get updated while spawning. local Parking={} - if _cargocategory==Group.Category.AIRPLANE or _cargocategory==Group.Category.HELICOPTER then - Parking=self:_FindParkingForAssets(self.airbase,_assetstock) or {} + if Request.cargocategory==Group.Category.AIRPLANE or Request.cargocategory==Group.Category.HELICOPTER then + Parking=self:_FindParkingForAssets(self.airbase, cargoassets) or {} end - + -- Spawn aircraft in uncontrolled state. local UnControlled=true - - -- Create an empty group set. - local _groupset=SET_GROUP:New() - -- Table for all spawned assets. - local _assets={} - -- Loop over cargo requests. - for i=1,#_assetstock do + for i=1,#cargoassets do -- Get stock item. - local _assetitem=_assetstock[i] --#WAREHOUSE.Assetitem + local asset=cargoassets[i] --#WAREHOUSE.Assetitem + + -- Set asset status to not spawned until we capture its birth event. + asset.spawned=false + asset.iscargo=true - -- Alias of the group. - local _alias=self:_Alias(_assetitem, Request) + -- Set request ID. + asset.rid=Request.uid + + -- Spawn group name. + local _alias=asset.spawngroupname + + --Request add asset by id. + Request.assets[asset.uid]=asset -- Spawn an asset group. - local _group=nil --Wrapper.Group#GROUP - if _assetitem.category==Group.Category.GROUND then - - -- Spawn ground troops. - _group=self:_SpawnAssetGroundNaval(_alias,_assetitem, Request, self.spawnzone) - - elseif _assetitem.category==Group.Category.AIRPLANE or _assetitem.category==Group.Category.HELICOPTER then - + local _group=nil --Wrapper.Group#GROUP + if asset.category==Group.Category.GROUND then + + -- Spawn ground troops. + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone) + + elseif asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then + -- Spawn air units. - if Parking[_assetitem.uid] then - _group=self:_SpawnAssetAircraft(_alias,_assetitem, Request, Parking[_assetitem.uid], UnControlled) + if Parking[asset.uid] then + _group=self:_SpawnAssetAircraft(_alias, asset, Request, Parking[asset.uid], UnControlled) else - _group=self:_SpawnAssetAircraft(_alias,_assetitem, Request, nil, UnControlled) + _group=self:_SpawnAssetAircraft(_alias, asset, Request, nil, UnControlled) end - - elseif _assetitem.category==Group.Category.TRAIN then - + + elseif asset.category==Group.Category.TRAIN then + -- Spawn train. if self.rail then --TODO: Rail should only get one asset because they would spawn on top! + + -- Spawn naval assets. + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone) end - - self:E(self.wid.."ERROR: Spawning of TRAIN assets not possible yet!") - - elseif _assetitem.category==Group.Category.SHIP then - + + --self:E(self.lid.."ERROR: Spawning of TRAIN assets not possible yet!") + + elseif asset.category==Group.Category.SHIP then + -- Spawn naval assets. - _group=self:_SpawnAssetGroundNaval(_alias,_assetitem, Request, self.portzone) - + _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.portzone) + else - self:E(self.wid.."ERROR: Unknown asset category!") + self:E(self.lid.."ERROR: Unknown asset category!") end - -- Add group to group set and asset list. - if _group then - _groupset:AddGroup(_group) - table.insert(_assets, _assetitem) - else - self:E(self.wid.."ERROR: Cargo asset could not be spawned!") - end - end - -- Delete spawned items from warehouse stock. - for _,_asset in pairs(_assets) do - local asset=_asset --#WAREHOUSE.Assetitem - Request.assets[asset.uid]=asset - self:_DeleteStockItem(asset) - end - - -- Overwrite the assets with the actually spawned ones. - Request.cargoassets=_assets - - return _groupset -end +end --- Spawn a ground or naval asset in the corresponding spawn zone of the warehouse. @@ -5177,23 +5550,28 @@ end -- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned. function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aioff) - if asset and (asset.category==Group.Category.GROUND or asset.category==Group.Category.SHIP) then - + if asset and (asset.category==Group.Category.GROUND or asset.category==Group.Category.SHIP or asset.category==Group.Category.TRAIN) then + -- Prepare spawn template. - local template=self:_SpawnAssetPrepareTemplate(asset, alias) - + local template=self:_SpawnAssetPrepareTemplate(asset, alias) + -- Initial spawn point. - template.route.points[1]={} - + template.route.points[1]={} + -- Get a random coordinate in the spawn zone. local coord=spawnzone:GetRandomCoordinate() + -- For trains, we use the rail connection point. + if asset.category==Group.Category.TRAIN then + coord=self.rail + end + -- Translate the position of the units. for i=1,#template.units do - + -- Unit template. local unit = template.units[i] - + -- Translate position. local SX = unit.x or 0 local SY = unit.y or 0 @@ -5201,40 +5579,40 @@ function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aiof local BY = asset.template.route.points[1].y local TX = coord.x + (SX-BX) local TY = coord.z + (SY-BY) - + template.units[i].x = TX template.units[i].y = TY - + if asset.livery then unit.livery_id = asset.livery end if asset.skill then unit.skill= asset.skill - end - + end + end - + template.route.points[1].x = coord.x template.route.points[1].y = coord.z - + template.x = coord.x template.y = coord.z template.alt = coord.y - + -- Spawn group. local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP - + -- Activate group. Should only be necessary for late activated groups. --group:Activate() - + -- Switch AI off if desired. This works only for ground and naval groups. if aioff then group:SetAIOff() end - + return group end - + return nil end @@ -5250,55 +5628,60 @@ end function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled, hotstart) if asset and asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then - + -- Prepare the spawn template. local template=self:_SpawnAssetPrepareTemplate(asset, alias) - + -- Set route points. if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then - + -- Get flight path if the group goes to another warehouse by itself. - template.route.points=self:_GetFlightplan(asset, self.airbase, request.warehouse.airbase) - + if request.toself then + local wp=self.airbase:GetCoordinate():WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, 0, false, self.airbase, {}, "Parking") + template.route.points={wp} + else + template.route.points=self:_GetFlightplan(asset, self.airbase, request.warehouse.airbase) + end + else - + -- Cold start (default). local _type=COORDINATE.WaypointType.TakeOffParking local _action=COORDINATE.WaypointAction.FromParkingArea - + -- Hot start. if hotstart then _type=COORDINATE.WaypointType.TakeOffParkingHot _action=COORDINATE.WaypointAction.FromParkingAreaHot end - + -- First route point is the warehouse airbase. template.route.points[1]=self.airbase:GetCoordinate():WaypointAir("BARO",_type,_action, 0, true, self.airbase, nil, "Spawnpoint") - + end - + -- Get airbase ID and category. local AirbaseID = self.airbase:GetID() local AirbaseCategory = self:GetAirbaseCategory() - + -- Check enough parking spots. if AirbaseCategory==Airbase.Category.HELIPAD or AirbaseCategory==Airbase.Category.SHIP then - + --TODO Figure out what's necessary in this case. - + else - + if #parking<#template.units then local text=string.format("ERROR: Not enough parking! Free parking = %d < %d aircraft to be spawned.", #parking, #template.units) self:_DebugMessage(text) return nil end - + end - + -- Position the units. for i=1,#template.units do - + -- Unit template. local unit = template.units[i] @@ -5306,67 +5689,76 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol -- Helipads we take the position of the airbase location, since the exact location of the spawn point does not make sense. local coord=self.airbase:GetCoordinate() - + unit.x=coord.x unit.y=coord.z unit.alt=coord.y - + unit.parking_id = nil unit.parking = nil - + else - + local coord=parking[i].Coordinate --Core.Point#COORDINATE local terminal=parking[i].TerminalID --#number - + if self.Debug then coord:MarkToAll(string.format("Spawnplace unit %s terminal %d.", unit.name, terminal)) end - + unit.x=coord.x unit.y=coord.z unit.alt=coord.y - + unit.parking_id = nil unit.parking = terminal - + end - + if asset.livery then unit.livery_id = asset.livery end + if asset.skill then unit.skill= asset.skill end + + if asset.payload then + unit.payload=asset.payload.pylons + end + if asset.modex then + unit.onboard_num=asset.modex[i] + end + if asset.callsign then + unit.callsign=asset.callsign[i] + end + end - + -- And template position. template.x = template.units[1].x template.y = template.units[1].y - + -- DCS bug workaround. Spawning helos in uncontrolled state on carriers causes a big spash! -- See https://forums.eagle.ru/showthread.php?t=219550 -- Should be solved in latest OB update 2.5.3.21708 --if AirbaseCategory == Airbase.Category.SHIP and asset.category==Group.Category.HELICOPTER then -- uncontrolled=false --end - + -- Uncontrolled spawning. template.uncontrolled=uncontrolled - + -- Debug info. self:T2({airtemplate=template}) - + -- Spawn group. local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP - - -- Activate group - should only be necessary for late activated groups. - --group:Activate() - + return group end - + return nil end @@ -5380,24 +5772,27 @@ function WAREHOUSE:_SpawnAssetPrepareTemplate(asset, alias) -- Create an own copy of the template! local template=UTILS.DeepCopy(asset.template) - + -- Set unique name. template.name=alias - - -- Set current(!) coalition and country. + + -- Set current(!) coalition and country. template.CoalitionID=self:GetCoalition() template.CountryID=self:GetCountry() - + -- Nillify the group ID. template.groupId=nil - -- For group units, visible needs to be false. - if asset.category==Group.Category.GROUND then - --template.visible=false - end - -- No late activation. template.lateActivation=false + + if asset.missionTask then + self:I(self.lid..string.format("Setting mission task to %s", tostring(asset.missionTask))) + template.task=asset.missionTask + end + + -- No predefined task. + --template.taskSelected=false -- Set and empty route. template.route = {} @@ -5406,16 +5801,16 @@ function WAREHOUSE:_SpawnAssetPrepareTemplate(asset, alias) -- Handle units. for i=1,#template.units do - + -- Unit template. local unit = template.units[i] - + -- Nillify the unit ID. unit.unitId=nil - + -- Set unit name: -01, -02, ... unit.name=string.format("%s-%02d", template.name , i) - + end return template @@ -5430,65 +5825,71 @@ end -- @param #WAREHOUSE self -- @param Wrapper.Group#GROUP group The ground group to be routed -- @param #WAREHOUSE.Queueitem request The request for this group. --- @param #number Speed Speed in km/h to drive to the destination coordinate. Default is 60% of max possible speed the unit can go. function WAREHOUSE:_RouteGround(group, request) if group and group:IsAlive() then -- Set speed to 70% of max possible. local _speed=group:GetSpeedMax()*0.7 - + -- Route waypoints. local Waypoints={} - - -- Check if an off road path has been defined. + + -- Check if an off road path has been defined. local hasoffroad=self:HasConnectionOffRoad(request.warehouse, self.Debug) - + -- Check if any off road paths have be defined. They have priority! if hasoffroad then -- Get off road path to remote warehouse. If more have been defined, pick one randomly. local remotename=request.warehouse.warehouse:GetName() local path=self.offroadpaths[remotename][math.random(#self.offroadpaths[remotename])] - + -- Loop over user defined shipping lanes. for i=1,#path do - + -- Shortcut and coordinate intellisense. local coord=path[i] --Core.Point#COORDINATE - + -- Get waypoint for coordinate. local Waypoint=coord:WaypointGround(_speed, "Off Road") - + -- Add waypoint to route. - table.insert(Waypoints, Waypoint) - end - + table.insert(Waypoints, Waypoint) + end + else - + -- Waypoints for road-to-road connection. Waypoints = group:TaskGroundOnRoad(request.warehouse.road, _speed, "Off Road", false, self.road) - + -- First waypoint = current position of the group. local FromWP=group:GetCoordinate():WaypointGround(_speed, "Off Road") table.insert(Waypoints, 1, FromWP) - - -- Final coordinate. - local ToWP=request.warehouse.spawnzone:GetRandomCoordinate():WaypointGround(_speed, "Off Road") - table.insert(Waypoints, #Waypoints+1, ToWP) - + + -- Final coordinate. Note, this can lead to errors if the final WP is too close the the point on the road. The vehicle will stop driving and not reach the final WP! + --local ToCO=request.warehouse.spawnzone:GetRandomCoordinate() + --local ToWP=ToCO:WaypointGround(_speed, "Off Road") + --table.insert(Waypoints, #Waypoints+1, ToWP) + end - + + for n,wp in ipairs(Waypoints) do + env.info(n) + local tf=self:_SimpleTaskFunctionWP("warehouse:_PassingWaypoint",group, n, #Waypoints) + group:SetTaskWaypoint(wp, tf) + end + -- Task function triggering the arrived event at the last waypoint. - local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) + --local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) -- Put task function on last waypoint. - local Waypoint = Waypoints[#Waypoints] - group:SetTaskWaypoint(Waypoint, TaskFunction) + --local Waypoint = Waypoints[#Waypoints] + --group:SetTaskWaypoint(Waypoint, TaskFunction) -- Route group to destination. group:Route(Waypoints, 1) - + -- Set ROE and alaram state. group:OptionROEReturnFire() group:OptionAlarmStateGreen() @@ -5506,47 +5907,47 @@ function WAREHOUSE:_RouteNaval(group, request) -- Set speed to 80% of max possible. local _speed=group:GetSpeedMax()*0.8 - + -- Get shipping lane to remote warehouse. If more have been defined, pick one randomly. local remotename=request.warehouse.warehouse:GetName() local lane=self.shippinglanes[remotename][math.random(#self.shippinglanes[remotename])] - + if lane then - + -- Route waypoints. local Waypoints={} - + -- Loop over user defined shipping lanes. for i=1,#lane do - + -- Shortcut and coordinate intellisense. local coord=lane[i] --Core.Point#COORDINATE - + -- Get waypoint for coordinate. local Waypoint=coord:WaypointGround(_speed) - + -- Add waypoint to route. - table.insert(Waypoints, Waypoint) + table.insert(Waypoints, Waypoint) end - + -- Task function triggering the arrived event at the last waypoint. local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) - + -- Put task function on last waypoint. local Waypoint = Waypoints[#Waypoints] group:SetTaskWaypoint(Waypoint, TaskFunction) - + -- Route group to destination. - group:Route(Waypoints, 1) - + group:Route(Waypoints, 1) + -- Set ROE (Naval units dont have and alaram state.) group:OptionROEReturnFire() - + else -- This should not happen! Existance of shipping lane was checked before executing this request. - self:E(self.wid..string.format("ERROR: No shipping lane defined for Naval asset!")) + self:E(self.lid..string.format("ERROR: No shipping lane defined for Naval asset!")) end - + end end @@ -5558,21 +5959,22 @@ end function WAREHOUSE:_RouteAir(aircraft) if aircraft and aircraft:IsAlive()~=nil then - + -- Debug info. - self:T2(self.wid..string.format("RouteAir aircraft group %s alive=%s", aircraft:GetName(), tostring(aircraft:IsAlive()))) - + self:T2(self.lid..string.format("RouteAir aircraft group %s alive=%s", aircraft:GetName(), tostring(aircraft:IsAlive()))) + -- Give start command to activate uncontrolled aircraft within the next 60 seconds. local starttime=math.random(60) + aircraft:StartUncontrolled(starttime) - + -- Debug info. - self:T2(self.wid..string.format("RouteAir aircraft group %s alive=%s (after start command)", aircraft:GetName(), tostring(aircraft:IsAlive()))) - + self:T2(self.lid..string.format("RouteAir aircraft group %s alive=%s (after start command)", aircraft:GetName(), tostring(aircraft:IsAlive()))) + -- Set ROE and alaram state. aircraft:OptionROEReturnFire() aircraft:OptionROTPassiveDefense() - + else self:E(string.format("ERROR: aircraft %s cannot be routed since it does not exist or is not alive %s!", tostring(aircraft:GetName()), tostring(aircraft:IsAlive()))) end @@ -5609,33 +6011,142 @@ end -- @param Wrapper.Group#GROUP group The group that arrived. function WAREHOUSE:_Arrived(group) self:_DebugMessage(string.format("Group %s arrived!", tostring(group:GetName()))) - + if group then --Trigger "Arrived event. self:__Arrived(1, group) end - + +end + +--- Task function for when passing a waypoint. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group that arrived. +-- @param #number n Waypoint passed. +-- @param #number N Final waypoint. +function WAREHOUSE:_PassingWaypoint(group, n, N) + self:T(self.lid..string.format("Group %s passing waypoint %d of %d!", tostring(group:GetName()), n, N)) + + -- Final waypoint reached. + if n==N then + self:__Arrived(1, group) + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event handler functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Get a warehouse asset from its unique id. +-- @param #WAREHOUSE self +-- @param #number id Asset ID. +-- @return #WAREHOUSE.Assetitem The warehouse asset. +function WAREHOUSE:GetAssetByID(id) + if id then + return _WAREHOUSEDB.Assets[id] + else + return nil + end +end + +--- Get a warehouse asset from its name. +-- @param #WAREHOUSE self +-- @param #string GroupName Spawn group name. +-- @return #WAREHOUSE.Assetitem The warehouse asset. +function WAREHOUSE:GetAssetByName(GroupName) + + local name=self:_GetNameWithOut(GroupName) + local _,aid,_=self:_GetIDsFromGroup(GROUP:FindByName(name)) + + if aid then + return _WAREHOUSEDB.Assets[aid] + else + return nil + end +end + +--- Get a warehouse request from its unique id. +-- @param #WAREHOUSE self +-- @param #number id Request ID. +-- @return #WAREHOUSE.Pendingitem The warehouse requested - either queued or pending. +-- @return #boolean If *true*, request is queued, if *false*, request is pending, if *nil*, request could not be found. +function WAREHOUSE:GetRequestByID(id) + + if id then + + for _,_request in pairs(self.queue) do + local request=_request --#WAREHOUSE.Queueitem + if request.uid==id then + return request, true + end + end + + for _,_request in pairs(self.pending) do + local request=_request --#WAREHOUSE.Pendingitem + if request.uid==id then + return request, false + end + end + + end + + return nil,nil +end + --- Warehouse event function, handling the birth of a unit. -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventBirth(EventData) - self:T3(self.wid..string.format("Warehouse %s (id=%s) captured event birth!", self.alias, self.uid)) - + self:T3(self.lid..string.format("Warehouse %s (id=%s) captured event birth!", self.alias, self.uid)) + if EventData and EventData.IniGroup then local group=EventData.IniGroup + -- Note: Remember, group:IsAlive might(?) not return true here. local wid,aid,rid=self:_GetIDsFromGroup(group) + if wid==self.uid then - self:T(self.wid..string.format("Warehouse %s captured event birth of its asset unit %s.", self.alias, EventData.IniUnitName)) + + -- Get asset and request from id. + local asset=self:GetAssetByID(aid) + local request=self:GetRequestByID(rid) + + -- Debug message. + self:T(self.lid..string.format("Warehouse %s captured event birth of its asset unit %s. spawned=%s", self.alias, EventData.IniUnitName, tostring(asset.spawned))) + + -- Birth is triggered for each unit. We need to make sure not to call this too often! + if not asset.spawned then + + -- Remove asset from stock. + self:_DeleteStockItem(asset) + + -- Set spawned switch. + asset.spawned=true + asset.spawngroupname=group:GetName() + + -- Add group. + if asset.iscargo==true then + request.cargogroupset=request.cargogroupset or SET_GROUP:New() + request.cargogroupset:AddGroup(group) + else + request.transportgroupset=request.transportgroupset or SET_GROUP:New() + request.transportgroupset:AddGroup(group) + end + + -- Set warehouse state. + group:SetState(group, "WAREHOUSE", self) + + -- Asset spawned FSM function. + --self:__AssetSpawned(1, group, asset, request) + self:AssetSpawned(group, asset, request) + + end + else --self:T3({wid=wid, uid=self.uid, match=(wid==self.uid), tw=type(wid), tu=type(self.uid)}) end + end end @@ -5645,15 +6156,15 @@ end -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventEngineStartup(EventData) - self:T3(self.wid..string.format("Warehouse %s captured event engine startup!",self.alias)) + self:T3(self.lid..string.format("Warehouse %s captured event engine startup!",self.alias)) if EventData and EventData.IniGroup then local group=EventData.IniGroup local wid,aid,rid=self:_GetIDsFromGroup(group) if wid==self.uid then - self:T(self.wid..string.format("Warehouse %s captured event engine startup of its asset unit %s.", self.alias, EventData.IniUnitName)) + self:T(self.lid..string.format("Warehouse %s captured event engine startup of its asset unit %s.", self.alias, EventData.IniUnitName)) end - end + end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5662,15 +6173,15 @@ end -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventTakeOff(EventData) - self:T3(self.wid..string.format("Warehouse %s captured event takeoff!",self.alias)) - + self:T3(self.lid..string.format("Warehouse %s captured event takeoff!",self.alias)) + if EventData and EventData.IniGroup then local group=EventData.IniGroup local wid,aid,rid=self:_GetIDsFromGroup(group) if wid==self.uid then - self:T(self.wid..string.format("Warehouse %s captured event takeoff of its asset unit %s.", self.alias, EventData.IniUnitName)) + self:T(self.lid..string.format("Warehouse %s captured event takeoff of its asset unit %s.", self.alias, EventData.IniUnitName)) end - end + end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5679,20 +6190,20 @@ end -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventLanding(EventData) - self:T3(self.wid..string.format("Warehouse %s captured event landing!", self.alias)) - + self:T3(self.lid..string.format("Warehouse %s captured event landing!", self.alias)) + if EventData and EventData.IniGroup then local group=EventData.IniGroup - + -- Try to get UIDs from group name. local wid,aid,rid=self:_GetIDsFromGroup(group) - + -- Check that this group belongs to this warehouse. if wid~=nil and wid==self.uid then - + -- Debug info. - self:T(self.wid..string.format("Warehouse %s captured event landing of its asset unit %s.", self.alias, EventData.IniUnitName)) - + self:T(self.lid..string.format("Warehouse %s captured event landing of its asset unit %s.", self.alias, EventData.IniUnitName)) + end end end @@ -5703,15 +6214,15 @@ end -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventEngineShutdown(EventData) - self:T3(self.wid..string.format("Warehouse %s captured event engine shutdown!", self.alias)) - + self:T3(self.lid..string.format("Warehouse %s captured event engine shutdown!", self.alias)) + if EventData and EventData.IniGroup then local group=EventData.IniGroup local wid,aid,rid=self:_GetIDsFromGroup(group) if wid==self.uid then - self:T(self.wid..string.format("Warehouse %s captured event engine shutdown of its asset unit %s.", self.alias, EventData.IniUnitName)) + self:T(self.lid..string.format("Warehouse %s captured event engine shutdown of its asset unit %s.", self.alias, EventData.IniUnitName)) end - end + end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5722,49 +6233,57 @@ end function WAREHOUSE:_OnEventArrived(EventData) if EventData and EventData.IniUnit then - + -- Unit that arrived. local unit=EventData.IniUnit - + -- Check if unit is alive and on the ground. Engine shutdown can also be triggered in other situations! if unit and unit:IsAlive()==true and unit:InAir()==false then - + -- Get group. local group=EventData.IniGroup - - -- Get unique IDs from group name. + + -- Get unique IDs from group name. local wid,aid,rid=self:_GetIDsFromGroup(group) - + -- If all IDs are good we can assume it is a warehouse asset. if wid~=nil and aid~=nil and rid~=nil then - + -- Check that warehouse ID is right. if self.uid==wid then - + local request=self:_GetRequestOfGroup(group, self.pending) - local istransport=self:_GroupIsTransport(group,request) - - -- Check if engine shutdown happend at right airbase because the event is also triggered in other situations. - local rightairbase=group:GetCoordinate():GetClosestAirbase():GetName()==request.warehouse:GetAirbase():GetName() - - -- Check that group is cargo and not transport. - if istransport==false and rightairbase then - - -- Debug info. - local text=string.format("Air asset group %s from warehouse %s arrived at its destination.", group:GetName(), self.alias) - self:_InfoMessage(text) - - -- Trigger arrived event for this group. Note that each unit of a group will trigger this event. So the onafterArrived function needs to take care of that. - -- Actually, we only take the first unit of the group that arrives. If it does, we assume the whole group arrived, which might not be the case, since - -- some units might still be taxiing or whatever. Therefore, we add 10 seconds for each additional unit of the group until the first arrived event is triggered. - local nunits=#group:GetUnits() - local dt=10*(nunits-1)+1 -- one unit = 1 sec, two units = 11 sec, three units = 21 sec before we call the group arrived. - self:__Arrived(dt, group) - + + -- Better check that the request still exists, because for a group with more units, the + if request then + + local istransport=self:_GroupIsTransport(group, request) + + -- Get closest airbase. + -- Note, this crashed at somepoint when the Tarawa was in the mission. Don't know why. Deleting the Tarawa and adding it again solved the problem. + local closest=group:GetCoordinate():GetClosestAirbase() + + -- Check if engine shutdown happend at right airbase because the event is also triggered in other situations. + local rightairbase=closest:GetName()==request.warehouse:GetAirbase():GetName() + + -- Check that group is cargo and not transport. + if istransport==false and rightairbase then + + -- Debug info. + local text=string.format("Air asset group %s from warehouse %s arrived at its destination.", group:GetName(), self.alias) + self:_InfoMessage(text) + + -- Trigger arrived event for this group. Note that each unit of a group will trigger this event. So the onafterArrived function needs to take care of that. + -- Actually, we only take the first unit of the group that arrives. If it does, we assume the whole group arrived, which might not be the case, since + -- some units might still be taxiing or whatever. Therefore, we add 10 seconds for each additional unit of the group until the first arrived event is triggered. + local nunits=#group:GetUnits() + local dt=10*(nunits-1)+1 -- one unit = 1 sec, two units = 11 sec, three units = 21 sec before we call the group arrived. + self:__Arrived(dt, group) + end + end - end - + else self:T3(string.format("Group that arrived did not belong to a warehouse. Warehouse ID=%s, Asset ID=%s, Request ID=%s.", tostring(wid), tostring(aid), tostring(rid))) end @@ -5779,53 +6298,53 @@ end -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventCrashOrDead(EventData) - self:T3(self.wid..string.format("Warehouse %s captured event dead or crash!", self.alias)) - + self:T3(self.lid..string.format("Warehouse %s captured event dead or crash!", self.alias)) + if EventData then - + -- Check if warehouse was destroyed. We compare the name of the destroyed unit. - if EventData.IniUnitName then + if EventData.IniUnitName then local warehousename=self.warehouse:GetName() if EventData.IniUnitName==warehousename then self:_DebugMessage(string.format("Warehouse %s alias %s was destroyed!", warehousename, self.alias)) - + -- Trigger Destroyed event. self:Destroyed() end end - - --self:I(self.wid..string.format("Warehouse %s captured event dead or crash or unit %s.", self.alias, tostring(EventData.IniUnitName))) - - -- Check if an asset unit was destroyed. + + --self:I(self.lid..string.format("Warehouse %s captured event dead or crash or unit %s.", self.alias, tostring(EventData.IniUnitName))) + + -- Check if an asset unit was destroyed. if EventData.IniGroup then - - -- Group initiating the event. + + -- Group initiating the event. local group=EventData.IniGroup - + -- Get warehouse, asset and request IDs from the group name. local wid,aid,rid=self:_GetIDsFromGroup(group) - + -- Check that we have the right warehouse. if wid==self.uid then - + -- Debug message. - self:T(self.wid..string.format("Warehouse %s captured event dead or crash of its asset unit %s.", self.alias, EventData.IniUnitName)) - + self:T(self.lid..string.format("Warehouse %s captured event dead or crash of its asset unit %s.", self.alias, EventData.IniUnitName)) + -- Loop over all pending requests and get the one belonging to this unit. for _,request in pairs(self.pending) do local request=request --#WAREHOUSE.Pendingitem - + -- This is the right request. if request.uid==rid then - + -- Update cargo and transport group sets of this request. We need to know if this job is finished. self:_UnitDead(EventData.IniUnit, request) - - end + + end end end end - end + end end --- A unit of a group just died. Update group sets in request. @@ -5835,43 +6354,30 @@ end -- @param #WAREHOUSE.Pendingitem request Request that needs to be updated. function WAREHOUSE:_UnitDead(deadunit, request) - -- Flare unit - deadunit:FlareRed() - + -- Flare unit. + if self.Debug then + deadunit:FlareRed() + end + -- Group the dead unit belongs to. local group=deadunit:GetGroup() - - -- Check if this was the last unit of the group ==> whole group dead. + + -- Number of alive units in group. + local nalive=group:CountAliveUnits() + + -- Whole group is dead? local groupdead=true - local nunits=0 - local nunits0=0 - if group then - -- Get current size of group and substract the unit that just died because it is not counted yet! - nunits=group:GetSize()-1 - nunits0=group:GetInitialSize() - - if nunits > 0 then - groupdead=false - end + if nalive>0 then + groupdead=false end - - + -- Here I need to get rid of the #CARGO at the end to obtain the original name again! local unitname=self:_GetNameWithOut(deadunit) local groupname=self:_GetNameWithOut(group) - - -- Debug message. - local text=string.format("Unit %s died! #units=%d/%d ==> Group dead=%s (IsAlive=%s).", unitname, nunits, nunits0, tostring(groupdead), tostring(group:IsAlive())) - self:T2(self.wid..text) - -- Check if this really works as expected! - if nunits<0 then - self:E(self.wid.."ERROR: Number of units negative! This should not happen.") - end - -- Group is dead! if groupdead then - self:T(self.wid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(group,request)))) + self:T(self.lid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(group,request)))) if self.Debug then group:SmokeWhite() end @@ -5879,71 +6385,71 @@ function WAREHOUSE:_UnitDead(deadunit, request) local asset=self:FindAssetInDB(group) self:AssetDead(asset, request) end - - - -- Not sure what this does actually and if it would be better to set it to true. + + + -- Dont trigger a Remove event for the group sets. local NoTriggerEvent=true - + if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then - + --- -- Easy case: Group can simply be removed from the cargogroupset. --- - - -- Remove dead group from carg group set. + + -- Remove dead group from cargo group set. if groupdead==true then request.cargogroupset:Remove(groupname, NoTriggerEvent) - self:T(self.wid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count())) + self:T(self.lid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count())) end - + else - + --- - -- Complicated case: Dead unit could be: + -- Complicated case: Dead unit could be: -- 1.) A Cargo unit (e.g. waiting to be picked up). -- 2.) A Transport unit which itself holds cargo groups. - --- - + --- + -- Check if this a cargo or transport group. local istransport=self:_GroupIsTransport(group,request) - + if istransport==true then - + -- Get the carrier unit table holding the cargo groups inside this carrier. local cargogroupnames=request.carriercargo[unitname] - + if cargogroupnames then - + -- Loop over all groups inside the destroyed carrier ==> all dead. for _,cargoname in pairs(cargogroupnames) do request.cargogroupset:Remove(cargoname, NoTriggerEvent) - self:T(self.wid..string.format("Removed transported cargo %s inside dead carrier %s: ncargo=%d", cargoname, unitname, request.cargogroupset:Count())) + self:T(self.lid..string.format("Removed transported cargo %s inside dead carrier %s: ncargo=%d", cargoname, unitname, request.cargogroupset:Count())) end - + end - + -- Whole carrier group is dead. Remove it from the carrier group set. if groupdead then request.transportgroupset:Remove(groupname, NoTriggerEvent) - self:T(self.wid..string.format("Removed transport %s: ntransport=%d", groupname, request.transportgroupset:Count())) - end - + self:T(self.lid..string.format("Removed transport %s: ntransport=%d", groupname, request.transportgroupset:Count())) + end + elseif istransport==false then - + -- This must have been an alive cargo group that was killed outside the carrier, e.g. waiting to be transported or waiting to be put back. -- Remove dead group from cargo group set. if groupdead==true then request.cargogroupset:Remove(groupname, NoTriggerEvent) - self:T(self.wid..string.format("Removed transported cargo %s outside carrier: ncargo=%d", groupname, request.cargogroupset:Count())) + self:T(self.lid..string.format("Removed transported cargo %s outside carrier: ncargo=%d", groupname, request.cargogroupset:Count())) -- This as well? --request.transportcargoset:RemoveCargosByName(RemoveCargoNames) end - - else - self:E(self.wid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) + + else + self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName())) end end - + end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -5953,30 +6459,30 @@ end -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventBaseCaptured(EventData) - self:T3(self.wid..string.format("Warehouse %s captured event base captured!",self.alias)) - + self:T3(self.lid..string.format("Warehouse %s captured event base captured!",self.alias)) + -- This warehouse does not have an airbase and never had one. So it could not have been captured. if self.airbasename==nil then return end - + if EventData and EventData.Place then - + -- Place is the airbase that was captured. local airbase=EventData.Place --Wrapper.Airbase#AIRBASE - + -- Check that this airbase belongs or did belong to this warehouse. if EventData.PlaceName==self.airbasename then - + -- New coalition of airbase after it was captured. local NewCoalitionAirbase=airbase:GetCoalition() - + -- Debug info - self:T(self.wid..string.format("Airbase of warehouse %s (coalition ID=%d) was captured! New owner coalition ID=%d.",self.alias, self:GetCoalition(), NewCoalitionAirbase)) - + self:T(self.lid..string.format("Airbase of warehouse %s (coalition ID=%d) was captured! New owner coalition ID=%d.",self.alias, self:GetCoalition(), NewCoalitionAirbase)) + -- So what can happen? -- Warehouse is blue, airbase is blue and belongs to warehouse and red captures it ==> self.airbase=nil - -- Warehouse is blue, airbase is blue self.airbase is nil and blue (re-)captures it ==> self.airbase=Event.Place + -- Warehouse is blue, airbase is blue self.airbase is nil and blue (re-)captures it ==> self.airbase=Event.Place if self.airbase==nil then -- New coalition is the same as of the warehouse ==> warehouse previously lost this airbase and now it was re-captured. if NewCoalitionAirbase == self:GetCoalition() then @@ -5988,7 +6494,7 @@ function WAREHOUSE:_OnEventBaseCaptured(EventData) self:AirbaseCaptured(NewCoalitionAirbase) end end - + end end end @@ -5998,8 +6504,8 @@ end -- @param #WAREHOUSE self -- @param Core.Event#EVENTDATA EventData Event data. function WAREHOUSE:_OnEventMissionEnd(EventData) - self:T3(self.wid..string.format("Warehouse %s captured event mission end!",self.alias)) - + self:T3(self.lid..string.format("Warehouse %s captured event mission end!",self.alias)) + if self.autosave then self:Save(self.autosavepath, self.autosavefile) end @@ -6016,35 +6522,35 @@ function WAREHOUSE:_CheckConquered() -- Get coordinate and radius to check. local coord=self.zone:GetCoordinate() local radius=self.zone:GetRadius() - + -- Scan units in zone. local gotunits,_,_,units,_,_=coord:ScanObjects(radius, true, false, false) - + local Nblue=0 local Nred=0 local Nneutral=0 - + local CountryBlue=nil local CountryRed=nil local CountryNeutral=nil - + if gotunits then -- Loop over all units. for _,_unit in pairs(units) do local unit=_unit --Wrapper.Unit#UNIT - + local distance=coord:Get2DDistance(unit:GetCoordinate()) - + -- Filter only alive groud units. Also check distance again, because the scan routine might give some larger distances. if unit:IsGround() and unit:IsAlive() and distance <= radius then - + -- Get coalition and country. local _coalition=unit:GetCoalition() local _country=unit:GetCountry() - + -- Debug info. - self:T2(self.wid..string.format("Unit %s in warehouse zone of radius=%d m. Coalition=%d, country=%d. Distance = %d m.",unit:GetName(), radius,_coalition,_country, distance)) - + self:T2(self.lid..string.format("Unit %s in warehouse zone of radius=%d m. Coalition=%d, country=%d. Distance = %d m.",unit:GetName(), radius,_coalition,_country, distance)) + -- Add up units for each side. if _coalition==coalition.side.BLUE then Nblue=Nblue+1 @@ -6056,15 +6562,15 @@ function WAREHOUSE:_CheckConquered() Nneutral=Nneutral+1 CountryNeutral=_country end - - end + + end end end - + -- Debug info. - self:T(self.wid..string.format("Ground troops in warehouse zone: blue=%d, red=%d, neutral=%d", Nblue, Nred, Nneutral)) - - + self:T(self.lid..string.format("Ground troops in warehouse zone: blue=%d, red=%d, neutral=%d", Nblue, Nred, Nneutral)) + + -- Figure out the new coalition if any. -- Condition is that only units of one coalition are within the zone. local newcoalition=self:GetCoalition() @@ -6088,7 +6594,7 @@ function WAREHOUSE:_CheckConquered() self:Captured(newcoalition, newcountry) return end - + -- Before a warehouse can be captured, it has to be attacked. -- That is, even if only enemy units are present it is not immediately captured in order to spawn all ground assets for defence. if self:GetCoalition()==coalition.side.BLUE then @@ -6099,7 +6605,7 @@ function WAREHOUSE:_CheckConquered() -- Blue warehouse was under attack by blue but no more blue units in zone. if self:IsAttacked() and Nred==0 then self:Defeated() - end + end elseif self:GetCoalition()==coalition.side.RED then -- Red Warehouse is running and we have blue units in the zone. if self:IsRunning() and Nblue>0 then @@ -6117,7 +6623,7 @@ function WAREHOUSE:_CheckConquered() self:Attacked(coalition.side.BLUE, CountryBlue) end end - + end --- Checks if the associated airbase still belongs to the warehouse. @@ -6125,26 +6631,26 @@ end function WAREHOUSE:_CheckAirbaseOwner() -- The airbasename is set at start and not deleted if the airbase was captured. if self.airbasename then - + local airbase=AIRBASE:FindByName(self.airbasename) local airbasecurrentcoalition=airbase:GetCoalition() - + if self.airbase then - + -- Warehouse has lost its airbase. if self:GetCoalition()~=airbasecurrentcoalition then self.airbase=nil end - + else - + -- Warehouse has re-captured the airbase. if self:GetCoalition()==airbasecurrentcoalition then self.airbase=airbase - end - + end + end - + end end @@ -6154,59 +6660,59 @@ end -- @param #table queue The queue which is holding the requests to check. -- @return #boolean If true, request can be executed. If false, something is not right. function WAREHOUSE:_CheckRequestConsistancy(queue) - self:T3(self.wid..string.format("Number of queued requests = %d", #queue)) + self:T3(self.lid..string.format("Number of queued requests = %d", #queue)) -- Requests to delete. local invalid={} - + for _,_request in pairs(queue) do local request=_request --#WAREHOUSE.Queueitem - + -- Debug info. - self:T2(self.wid..string.format("Checking request id=%d.", request.uid)) - + self:T2(self.lid..string.format("Checking request id=%d.", request.uid)) + -- Let's assume everything is fine. local valid=true - + -- Check if at least one asset was requested. if request.nasset==0 then - self:E(self.wid..string.format("ERROR: INVALID request. Request for zero assets not possible. Can happen when, e.g. \"all\" ground assets are requests but none in stock.")) + self:E(self.lid..string.format("ERROR: INVALID request. Request for zero assets not possible. Can happen when, e.g. \"all\" ground assets are requests but none in stock.")) valid=false end - + -- Request from enemy coalition? if self:GetCoalition()~=request.warehouse:GetCoalition() then - self:E(self.wid..string.format("ERROR: INVALID request. Requesting warehouse is of wrong coaltion! Own coalition %s != %s of requesting warehouse.", self:GetCoalitionName(), request.warehouse:GetCoalitionName())) + self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is of wrong coaltion! Own coalition %s != %s of requesting warehouse.", self:GetCoalitionName(), request.warehouse:GetCoalitionName())) valid=false end - + -- Is receiving warehouse stopped? if request.warehouse:IsStopped() then - self:E(self.wid..string.format("ERROR: INVALID request. Requesting warehouse is stopped!")) - valid=false + self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is stopped!")) + valid=false end -- Is receiving warehouse destroyed? - if request.warehouse:IsDestroyed() then - self:E(self.wid..string.format("ERROR: INVALID request. Requesting warehouse is destroyed!")) - valid=false + if request.warehouse:IsDestroyed() and not self.respawnafterdestroyed then + self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is destroyed!")) + valid=false end - + -- Add request as unvalid and delete it later. if valid==false then - self:E(self.wid..string.format("Got invalid request id=%d.", request.uid)) - table.insert(invalid, request) + self:E(self.lid..string.format("Got invalid request id=%d.", request.uid)) + table.insert(invalid, request) else - self:T3(self.wid..string.format("Got valid request id=%d.", request.uid)) - end + self:T3(self.lid..string.format("Got valid request id=%d.", request.uid)) + end end -- Delete invalid requests. for _,_request in pairs(invalid) do - self:E(self.wid..string.format("Deleting INVALID request id=%d.",_request.uid)) + self:E(self.lid..string.format("Deleting INVALID request id=%d.",_request.uid)) self:_DeleteQueueItem(_request, self.queue) end - + end --- Check if a request is valid in general. If not, it will be removed from the queue. @@ -6219,12 +6725,12 @@ function WAREHOUSE:_CheckRequestValid(request) -- Check if number of requested assets is in stock. local _assets,_nassets,_enough=self:_FilterStock(self.stock, request.assetdesc, request.assetdescval, request.nasset) - + -- No assets in stock? Checks cannot be performed. if #_assets==0 then return true end - + -- Convert relative to absolute number if necessary. local nasset=request.nasset if type(request.nasset)=="string" then @@ -6234,10 +6740,10 @@ function WAREHOUSE:_CheckRequestValid(request) -- Debug check, request.nasset might be a string Quantity enumerator. local text=string.format("Request valid? Number of assets: requested=%s=%d, selected=%d, total=%d, enough=%s.", tostring(request.nasset), nasset,#_assets,_nassets, tostring(_enough)) self:T(text) - + -- First asset. Is representative for all filtered items in stock. local asset=_assets[1] --#WAREHOUSE.Assetitem - + -- Asset is air, ground etc. local asset_plane = asset.category==Group.Category.AIRPLANE local asset_helo = asset.category==Group.Category.HELICOPTER @@ -6250,158 +6756,160 @@ function WAREHOUSE:_CheckRequestValid(request) -- Assume everything is okay. local valid=true - + -- Category of the requesting warehouse airbase. local requestcategory=request.warehouse:GetAirbaseCategory() - + if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then ------------------------------------------- -- Case where the units go my themselves -- ------------------------------------------- if asset_air then - + if asset_plane then - + -- No airplane to or from FARPS. if requestcategory==Airbase.Category.HELIPAD or self:GetAirbaseCategory()==Airbase.Category.HELIPAD then self:E("ERROR: Incorrect request. Asset airplane requested but warehouse or requestor is HELIPAD/FARP!") valid=false end - + -- Category SHIP is not general enough! Fighters can go to carriers. Which fighters, is there an attibute? -- Also for carriers, attibute? - + elseif asset_helo then - + -- Helos need a FARP or AIRBASE or SHIP for spawning. Also at the the receiving warehouse. So even if they could go there they "cannot" be spawned again. -- Unless I allow spawning of helos in the the spawn zone. But one should place at least a FARP there. if self:GetAirbaseCategory()==-1 or requestcategory==-1 then self:E("ERROR: Incorrect request. Helos need a AIRBASE/HELIPAD/SHIP as home/destination base!") - valid=false + valid=false end - + end - + -- All aircraft need an airbase of any type at depature and destination. if self.airbase==nil or request.airbase==nil then - + self:E("ERROR: Incorrect request. Either warehouse or requesting warehouse does not have any kind of airbase!") valid=false - + else - + -- Check if enough parking spots are available. This checks the spots available in general, i.e. not the free spots. -- TODO: For FARPS/ships, is it possible to send more assets than parking spots? E.g. a FARPS has only four (or even one). -- TODO: maybe only check if spots > 0 for the necessary terminal type? At least for FARPS. - + -- Get necessary terminal type. - local termtype=self:_GetTerminal(asset.attribute) - + local termtype_dep=asset.terminalType or self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) + local termtype_des=asset.terminalType or self:_GetTerminal(asset.attribute, request.warehouse:GetAirbaseCategory()) + -- Get number of parking spots. - local np_departure=self.airbase:GetParkingSpotsNumber(termtype) - local np_destination=request.airbase:GetParkingSpotsNumber(termtype) - + local np_departure=self.airbase:GetParkingSpotsNumber(termtype_dep) + local np_destination=request.airbase:GetParkingSpotsNumber(termtype_des) + -- Debug info. - self:T(string.format("Asset attribute = %s, terminal type = %d, spots at departure = %d, destination = %d", asset.attribute, termtype, np_departure, np_destination)) - + self:T(string.format("Asset attribute = %s, DEPARTURE: terminal type = %d, spots = %d, DESTINATION: terminal type = %d, spots = %d", asset.attribute, termtype_dep, np_departure, termtype_des, np_destination)) + -- Not enough parking at sending warehouse. --if (np_departure < request.nasset) and not (self.category==Airbase.Category.SHIP or self.category==Airbase.Category.HELIPAD) then if np_departure < nasset then - self:E(string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype, np_departure, nasset)) - valid=false + self:E(string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype_dep, np_departure, nasset)) + valid=false end -- No parking at requesting warehouse. if np_destination == 0 then - self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse. Available spots = %d!", termtype, np_destination)) - valid=false - end - + self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse. Available spots = %d!", termtype_des, np_destination)) + valid=false + end + end - + elseif asset_ground then - + -- Check that both spawn zones are not in water. local inwater=self.spawnzone:GetCoordinate():IsSurfaceTypeWater() or request.warehouse.spawnzone:GetCoordinate():IsSurfaceTypeWater() - + if inwater then self:E("ERROR: Incorrect request. Ground asset requested but at least one spawn zone is in water!") + --valid=false valid=false end - + -- No ground assets directly to or from ships. -- TODO: May needs refinement if warehouse is on land and requestor is ship in harbour?! --if (requestcategory==Airbase.Category.SHIP or self:GetAirbaseCategory()==Airbase.Category.SHIP) then -- self:E("ERROR: Incorrect request. Ground asset requested but warehouse or requestor is SHIP!") -- valid=false --end - + if asset_train then - + -- Check if there is a valid path on rail. local hasrail=self:HasConnectionRail(request.warehouse) if not hasrail then self:E("ERROR: Incorrect request. No valid path on rail for train assets!") valid=false end - + else - + if self.warehouse:GetName()~=request.warehouse.warehouse:GetName() then - + -- Check if there is a valid path on road. local hasroad=self:HasConnectionRoad(request.warehouse) - + -- Check if there is a valid off road path. local hasoffroad=self:HasConnectionOffRoad(request.warehouse) - + if not (hasroad or hasoffroad) then self:E("ERROR: Incorrect request. No valid path on or off road for ground assets!") valid=false end - + end - + end - + elseif asset_naval then - + -- Check shipping lane. local shippinglane=self:HasConnectionNaval(request.warehouse) - + if not shippinglane then self:E("ERROR: Incorrect request. No shipping lane has been defined between warehouses!") valid=false - end - + end + end - - else + + else ------------------------------- -- Assests need a transport --- ------------------------------- if request.transporttype==WAREHOUSE.TransportType.AIRPLANE then - + -- Airplanes only to AND from airdromes. if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME or requestcategory~=Airbase.Category.AIRDROME then self:E("ERROR: Incorrect request. Warehouse or requestor does not have an airdrome. No transport by plane possible!") valid=false end - + --TODO: Not sure if there are any transport planes that can land on a carrier? - + elseif request.transporttype==WAREHOUSE.TransportType.APC then - + -- Transport by ground units. - + -- No transport to or from ships if self:GetAirbaseCategory()==Airbase.Category.SHIP or requestcategory==Airbase.Category.SHIP then self:E("ERROR: Incorrect request. Warehouse or requestor is SHIP. No transport by APC possible!") valid=false end - + -- Check if there is a valid path on road. local hasroad=self:HasConnectionRoad(request.warehouse) if not hasroad then @@ -6410,37 +6918,37 @@ function WAREHOUSE:_CheckRequestValid(request) end elseif request.transporttype==WAREHOUSE.TransportType.HELICOPTER then - + -- Transport by helicopters ==> need airbase for spawning but not for delivering to the spawn zone of the receiver. if self:GetAirbaseCategory()==-1 then self:E("ERROR: Incorrect request. Warehouse has no airbase. Transport by helicopter not possible!") valid=false end - + elseif request.transporttype==WAREHOUSE.TransportType.SHIP then - + -- Transport by ship. self:E("ERROR: Incorrect request. Transport by SHIP not implemented yet!") valid=false - + elseif request.transporttype==WAREHOUSE.TransportType.TRAIN then - + -- Transport by train. self:E("ERROR: Incorrect request. Transport by TRAIN not implemented yet!") valid=false - + else -- No match. self:E("ERROR: Incorrect request. Transport type unknown!") valid=false end - + -- Airborne assets: check parking situation. if request.transporttype==WAREHOUSE.TransportType.AIRPLANE or request.transporttype==WAREHOUSE.TransportType.HELICOPTER then - + -- Check if number of requested assets is in stock. - local _assets,_nassets,_enough=self:_FilterStock(self.stock, WAREHOUSE.Descriptor.ATTRIBUTE, request.transporttype, request.ntransport) - + local _assets,_nassets,_enough=self:_FilterStock(self.stock, WAREHOUSE.Descriptor.ATTRIBUTE, request.transporttype, request.ntransport, true) + -- Convert relative to absolute number if necessary. local nasset=request.ntransport if type(request.ntransport)=="string" then @@ -6452,49 +6960,50 @@ function WAREHOUSE:_CheckRequestValid(request) self:T(text) -- Get necessary terminal type for helos or transport aircraft. - local termtype=self:_GetTerminal(request.transporttype) - + local termtype=self:_GetTerminal(request.transporttype, self:GetAirbaseCategory()) + -- Get number of parking spots. local np_departure=self.airbase:GetParkingSpotsNumber(termtype) - + -- Debug info. - self:T(self.wid..string.format("Transport attribute = %s, terminal type = %d, spots at departure = %d.", request.transporttype, termtype, np_departure)) - + self:T(self.lid..string.format("Transport attribute = %s, terminal type = %d, spots at departure = %d.", request.transporttype, termtype, np_departure)) + -- Not enough parking at sending warehouse. --if (np_departure < request.nasset) and not (self.category==Airbase.Category.SHIP or self.category==Airbase.Category.HELIPAD) then if np_departure < nasset then - self:E(self.wid..string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype, np_departure, nasset)) + self:E(self.lid..string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype, np_departure, nasset)) valid=false end - + -- Planes also need parking at the receiving warehouse. if request.transporttype==WAREHOUSE.TransportType.AIRPLANE then - + -- Total number of parking spots for transport planes at destination. + termtype=self:_GetTerminal(request.transporttype, request.warehouse:GetAirbaseCategory()) local np_destination=request.airbase:GetParkingSpotsNumber(termtype) -- Debug info. - self:T(self.wid..string.format("Transport attribute = %s: total # of spots (type=%d) at destination = %d.", asset.attribute, termtype, np_destination)) - + self:T(self.lid..string.format("Transport attribute = %s: total # of spots (type=%d) at destination = %d.", asset.attribute, termtype, np_destination)) + -- No parking at requesting warehouse. if np_destination == 0 then self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse for transports. Available spots = %d!", termtype, np_destination)) - valid=false + valid=false end end - + end - + end - + -- Add request as unvalid and delete it later. if valid==false then - self:E(self.wid..string.format("ERROR: Got invalid request id=%d.", request.uid)) + self:E(self.lid..string.format("ERROR: Got invalid request id=%d.", request.uid)) else - self:T3(self.wid..string.format("Request id=%d valid :)", request.uid)) + self:T3(self.lid..string.format("Request id=%d valid :)", request.uid)) end - + return valid end @@ -6510,129 +7019,129 @@ function WAREHOUSE:_CheckRequestNow(request) if (request.warehouse:IsRunning()==false) and not (request.toself and self:IsAttacked()) then local text=string.format("Warehouse %s: Request denied! Receiving warehouse %s is not running. Current state %s.", self.alias, request.warehouse.alias, request.warehouse:GetState()) self:_InfoMessage(text, 5) - + return false end - + -- If no transport is requested, assets need to be mobile unless it is a self request. local onlymobile=false if type(request.transport)=="number" and request.ntransport==0 and not request.toself then onlymobile=true end - + -- Check if number of requested assets is in stock. local _assets,_nassets,_enough=self:_FilterStock(self.stock, request.assetdesc, request.assetdescval, request.nasset, onlymobile) - - + + -- Check if enough assets are in stock. if not _enough then local text=string.format("Warehouse %s: Request ID=%d denied! Not enough (cargo) assets currently available.", self.alias, request.uid) self:_InfoMessage(text, 5) - text=string.format("Enough=%s, #_assets=%d, _nassets=%d, request.nasset=%s", tostring(_enough), #_assets,_nassets, tostring(request.nasset)) - self:T(self.wid..text) + text=string.format("Enough=%s, #assets=%d, nassets=%d, request.nasset=%s", tostring(_enough), #_assets,_nassets, tostring(request.nasset)) + self:T(self.lid..text) return false end - + local _transports local _assetattribute local _assetcategory - + -- Check if at least one (cargo) asset is available. if _nassets>0 then -- Get the attibute of the requested asset. _assetattribute=_assets[1].attribute - _assetcategory=_assets[1].category - - -- Check available parking for air asset units. + _assetcategory=_assets[1].category + + -- Check available parking for air asset units. if self.airbase and (_assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER) then - + local Parking=self:_FindParkingForAssets(self.airbase,_assets) - + --if Parking==nil and not (self.category==Airbase.Category.HELIPAD) then if Parking==nil then local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all requested assets at the moment.", self.alias) self:_InfoMessage(text, 5) - + return false end - + end - + -- Add this here or gettransport fails request.cargoassets=_assets - - end - + + end + -- Check that a transport units. if request.transporttype ~= WAREHOUSE.TransportType.SELFPROPELLED then -- Get best transports for this asset pack. _transports=self:_GetTransportsForAssets(request) - + -- Check if at least one transport asset is available. if #_transports>0 then - + -- Get the attibute of the transport units. local _transportattribute=_transports[1].attribute local _transportcategory=_transports[1].category - + -- Check available parking for transport units. if self.airbase and (_transportcategory==Group.Category.AIRPLANE or _transportcategory==Group.Category.HELICOPTER) then local Parking=self:_FindParkingForAssets(self.airbase,_transports) if Parking==nil then local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all transports at the moment.", self.alias) self:_InfoMessage(text, 5) - + return false end end - + else -- Not enough or the right transport carriers. local text=string.format("Warehouse %s: Request denied! Not enough transport carriers available at the moment.", self.alias) self:_InfoMessage(text, 5) - - return false - end + + return false + end else - + -- Self propelled case. Nothing to do for now. - + -- Ground asset checks. if _assetcategory==Group.Category.GROUND then - + -- Distance between warehouse and spawn zone. local dist=self.warehouse:GetCoordinate():Get2DDistance(self.spawnzone:GetCoordinate()) - + -- Check min dist to spawn zone. if dist>self.spawnzonemaxdist then -- Not close enough to spawn zone. local text=string.format("Warehouse %s: Request denied! Not close enough to spawn zone. Distance = %d m. We need to be at least within %d m range to spawn.", self.alias, dist, self.spawnzonemaxdist) - self:_InfoMessage(text, 5) + self:_InfoMessage(text, 5) return false end - + end - + end -- Set chosen cargo assets. request.cargoassets=_assets request.cargoattribute=_assets[1].attribute - request.cargocategory=_assets[1].category + request.cargocategory=_assets[1].category request.nasset=#_assets -- Debug info: - local text=string.format("Selected cargo assets, attibute=%s, category=%d:\n", request.cargoattribute, request.cargocategory) + local text=string.format("Selected cargo assets, attibute=%s, category=%d:\n", request.cargoattribute, request.cargocategory) for _i,_asset in pairs(_assets) do local asset=_asset --#WAREHOUSE.Assetitem text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d",_i, asset.templatename, asset.unittype, asset.category, asset.nunits) end - self:T(self.wid..text) + self:T(self.lid..text) if request.transporttype ~= WAREHOUSE.TransportType.SELFPROPELLED then @@ -6641,154 +7150,154 @@ function WAREHOUSE:_CheckRequestNow(request) request.transportattribute=_transports[1].attribute request.transportcategory=_transports[1].category request.ntransport=#_transports - + -- Debug info: - local text=string.format("Selected transport assets, attibute=%s, category=%d:\n", request.transportattribute, request.transportcategory) + local text=string.format("Selected transport assets, attibute=%s, category=%d:\n", request.transportattribute, request.transportcategory) for _i,_asset in pairs(_transports) do local asset=_asset --#WAREHOUSE.Assetitem text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d\n",_i, asset.templatename, asset.unittype, asset.category, asset.nunits) end - self:T(self.wid..text) - + self:T(self.lid..text) + end - + return true end ----Get (optimized) transport carriers for the given assets to be transported. +---Get (optimized) transport carriers for the given assets to be transported. -- @param #WAREHOUSE self -- @param #WAREHOUSE.Pendingitem Chosen request. function WAREHOUSE:_GetTransportsForAssets(request) -- Get all transports of the requested type in stock. - local transports=self:_FilterStock(self.stock, WAREHOUSE.Descriptor.ATTRIBUTE, request.transporttype) + local transports=self:_FilterStock(self.stock, WAREHOUSE.Descriptor.ATTRIBUTE, request.transporttype, nil, true) -- Copy asset. local cargoassets=UTILS.DeepCopy(request.cargoassets) local cargoset=request.transportcargoset -- TODO: Get weight and cargo bay from CARGO_GROUP - --local cargogroup=CARGO_GROUP:New(CargoGroup,Type,Name,LoadRadius,NearRadius) + --local cargogroup=CARGO_GROUP:New(CargoGroup,Type,Name,LoadRadius,NearRadius) --cargogroup:GetWeight() - + -- Sort transport carriers w.r.t. cargo bay size. local function sort_transports(a,b) return a.cargobaymax>b.cargobaymax end - + -- Sort cargo assets w.r.t. weight in assending order. local function sort_cargoassets(a,b) return a.weight>b.weight end - + -- Sort tables. table.sort(transports, sort_transports) table.sort(cargoassets, sort_cargoassets) - + -- Total cargo bay size of all groups. - self:T2(self.wid.."Transport capability:") + self:T2(self.lid.."Transport capability:") local totalbay=0 for i=1,#transports do local transport=transports[i] --#WAREHOUSE.Assetitem - for j=1,transport.nunits do + for j=1,transport.nunits do totalbay=totalbay+transport.cargobay[j] - self:T2(self.wid..string.format("Cargo bay = %d (unit=%d)", transport.cargobay[j], j)) + self:T2(self.lid..string.format("Cargo bay = %d (unit=%d)", transport.cargobay[j], j)) end end - self:T2(self.wid..string.format("Total capacity = %d", totalbay)) + self:T2(self.lid..string.format("Total capacity = %d", totalbay)) -- Total cargo weight of all assets to transports. - self:T2(self.wid.."Cargo weight:") + self:T2(self.lid.."Cargo weight:") local totalcargoweight=0 for i=1,#cargoassets do local asset=cargoassets[i] --#WAREHOUSE.Assetitem totalcargoweight=totalcargoweight+asset.weight - self:T2(self.wid..string.format("weight = %d", asset.weight)) - end - self:T2(self.wid..string.format("Total weight = %d", totalcargoweight)) - + self:T2(self.lid..string.format("weight = %d", asset.weight)) + end + self:T2(self.lid..string.format("Total weight = %d", totalcargoweight)) + -- Transports used. local used_transports={} - + -- Loop over all transport groups, largest cargobaymax to smallest. for i=1,#transports do - + -- Shortcut for carrier and cargo bay local transport=transports[i] - -- Cargo put into carrier. + -- Cargo put into carrier. local putintocarrier={} - + -- Cargo assigned to this transport group? local used=false - + -- Loop over all units for k=1,transport.nunits do - + -- Get cargo bay of this carrier. local cargobay=transport.cargobay[k] - + -- Loop over cargo assets. for j,asset in pairs(cargoassets) do local asset=asset --#WAREHOUSE.Assetitem - + -- How many times does the cargo fit into the carrier? local delta=cargobay-asset.weight --env.info(string.format("k=%d, j=%d delta=%d cargobay=%d weight=%d", k, j, delta, cargobay, asset.weight)) - - --self:E(self.wid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight)) - + + --self:E(self.lid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight)) + -- Cargo fits into carrier if delta>=0 then -- Reduce remaining cargobay. cargobay=cargobay-asset.weight - self:T3(self.wid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight)) - + self:T3(self.lid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight)) + -- Remember this cargo and remove it so it does not get loaded into other carriers. table.insert(putintocarrier, j) - + -- This transport group is used. used=true - else - self:T2(self.wid..string.format("Carrier unit %s too small for cargo asset %s ==> cannot be used! Cargo bay - asset weight = %d kg", transport.templatename, asset.templatename, delta)) + else + self:T2(self.lid..string.format("Carrier unit %s too small for cargo asset %s ==> cannot be used! Cargo bay - asset weight = %d kg", transport.templatename, asset.templatename, delta)) end - - end -- loop over assets + + end -- loop over assets end -- loop over units - + -- Remove cargo assets from list. Needs to be done back-to-front in order not to confuse the loop. for j=#putintocarrier,1, -1 do - + local nput=putintocarrier[j] local cargo=cargoassets[nput] - + -- Need to check if multiple units in a group and the group has already been removed! -- TODO: This might need to be improved but is working okay so far. if cargo then -- Remove this group because it was used. - self:T2(self.wid..string.format("Cargo id=%d assigned for carrier id=%d", cargo.uid, transport.uid)) + self:T2(self.lid..string.format("Cargo id=%d assigned for carrier id=%d", cargo.uid, transport.uid)) table.remove(cargoassets, nput) end end - + -- Cargo was assined for this carrier. if used then table.insert(used_transports, transport) end - + -- Convert relative quantity (all, half) to absolute number if necessary. local ntrans=self:_QuantityRel2Abs(request.ntransport, #transports) - + -- Max number of transport groups reached? if #used_transports >= ntrans then request.ntransport=#used_transports break end end - + -- Debug info. local text=string.format("Used Transports for request %d to warehouse %s:\n", request.uid, request.warehouse.alias) - local totalcargobay=0 + local totalcargobay=0 for _i,_transport in pairs(used_transports) do local transport=_transport --#WAREHOUSE.Assetitem text=text..string.format("%d) %s: cargobay tot = %d kg, cargobay max = %d kg, nunits=%d\n", _i, transport.unittype, transport.cargobaytot, transport.cargobaymax, transport.nunits) @@ -6800,7 +7309,7 @@ function WAREHOUSE:_GetTransportsForAssets(request) text=text..string.format("Total cargo bay capacity = %.1f kg\n", totalcargobay) text=text..string.format("Total cargo weight = %.1f kg\n", totalcargoweight) text=text..string.format("Minimum number of runs = %.1f", totalcargoweight/totalcargobay) - self:_DebugMessage(text) + self:_DebugMessage(text) return used_transports end @@ -6823,7 +7332,7 @@ function WAREHOUSE:_QuantityRel2Abs(relative, ntot) elseif relative==WAREHOUSE.Quantity.HALF then nabs=UTILS.Round(ntot/2) elseif relative==WAREHOUSE.Quantity.THIRD then - nabs=UTILS.Round(ntot/3) + nabs=UTILS.Round(ntot/3) elseif relative==WAREHOUSE.Quantity.QUARTER then nabs=UTILS.Round(ntot/4) else @@ -6832,8 +7341,8 @@ function WAREHOUSE:_QuantityRel2Abs(relative, ntot) else nabs=relative end - - self:T2(self.wid..string.format("Relative %s: tot=%d, abs=%.2f", tostring(relative), ntot, nabs)) + + self:T2(self.lid..string.format("Relative %s: tot=%d, abs=%.2f", tostring(relative), ntot, nabs)) return nabs end @@ -6848,24 +7357,24 @@ function WAREHOUSE:_CheckQueue() -- Search for a request we can execute. local request=nil --#WAREHOUSE.Queueitem - + local invalid={} local gotit=false for _,_qitem in ipairs(self.queue) do local qitem=_qitem --#WAREHOUSE.Queueitem - + -- Check if request is valid in general. local valid=self:_CheckRequestValid(qitem) - + -- Check if request is possible now. local okay=false - if valid then + if valid then okay=self:_CheckRequestNow(qitem) else -- Remember invalid request and delete later in order not to confuse the loop. table.insert(invalid, qitem) end - + -- Get the first valid request that can be executed now. if okay and valid and not gotit then request=qitem @@ -6873,10 +7382,10 @@ function WAREHOUSE:_CheckQueue() break end end - + -- Delete invalid requests. for _,_request in pairs(invalid) do - self:T(self.wid..string.format("Deleting invalid request id=%d.",_request.uid)) + self:T(self.lid..string.format("Deleting invalid request id=%d.",_request.uid)) self:_DeleteQueueItem(_request, self.queue) end @@ -6897,28 +7406,63 @@ function WAREHOUSE:_SimpleTaskFunction(Function, group) -- Task script. local DCSScript = {} - --DCSScript[#DCSScript+1] = string.format('env.info(\"WAREHOUSE: Simple task function called!\") ') - DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". - DCSScript[#DCSScript+1] = string.format("local mystatic = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. - DCSScript[#DCSScript+1] = string.format('local warehouse = mystatic:GetState(mystatic, \"WAREHOUSE\") ') -- Get the warehouse self object from the static. - DCSScript[#DCSScript+1] = string.format('%s(mygroup)', Function) -- Call the function, e.g. myfunction.(warehouse,mygroup) + + DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". + if self.isunit then + DCSScript[#DCSScript+1] = string.format("local mywarehouse = UNIT:FindByName(\"%s\") ", warehouse) -- The unit that holds the warehouse self object. + else + DCSScript[#DCSScript+1] = string.format("local mywarehouse = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. + end + DCSScript[#DCSScript+1] = string.format('local warehouse = mywarehouse:GetState(mywarehouse, \"WAREHOUSE\") ') -- Get the warehouse self object from the static. + DCSScript[#DCSScript+1] = string.format('%s(mygroup)', Function) -- Call the function, e.g. myfunction.(warehouse,mygroup) -- Create task. local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) - + + return DCSTask +end + +--- Simple task function. Can be used to call a function which has the warehouse and the executing group as parameters. +-- @param #WAREHOUSE self +-- @param #string Function The name of the function to call passed as string. +-- @param Wrapper.Group#GROUP group The group which is meant. +-- @param #number n Waypoint passed. +-- @param #number N Final waypoint number. +function WAREHOUSE:_SimpleTaskFunctionWP(Function, group, n, N) + self:F2({Function}) + + -- Name of the warehouse (static) object. + local warehouse=self.warehouse:GetName() + local groupname=group:GetName() + + -- Task script. + local DCSScript = {} + + DCSScript[#DCSScript+1] = string.format('local mygroup = GROUP:FindByName(\"%s\") ', groupname) -- The group that executes the task function. Very handy with the "...". + if self.isunit then + DCSScript[#DCSScript+1] = string.format("local mywarehouse = UNIT:FindByName(\"%s\") ", warehouse) -- The unit that holds the warehouse self object. + else + DCSScript[#DCSScript+1] = string.format("local mywarehouse = STATIC:FindByName(\"%s\") ", warehouse) -- The static that holds the warehouse self object. + end + DCSScript[#DCSScript+1] = string.format('local warehouse = mywarehouse:GetState(mywarehouse, \"WAREHOUSE\") ') -- Get the warehouse self object from the static. + DCSScript[#DCSScript+1] = string.format('%s(mygroup, %d, %d)', Function, n ,N) -- Call the function, e.g. myfunction.(warehouse,mygroup) + + -- Create task. + local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) + return DCSTask end --- Get the proper terminal type based on generalized attribute of the group. --@param #WAREHOUSE self --@param #WAREHOUSE.Attribute _attribute Generlized attibute of unit. +--@param #number _category Airbase category. --@return Wrapper.Airbase#AIRBASE.TerminalType Terminal type for this group. -function WAREHOUSE:_GetTerminal(_attribute) +function WAREHOUSE:_GetTerminal(_attribute, _category) -- Default terminal is "large". local _terminal=AIRBASE.TerminalType.OpenBig - - + if _attribute==WAREHOUSE.Attribute.AIR_FIGHTER then -- Fighter ==> small. _terminal=AIRBASE.TerminalType.FighterAircraft @@ -6928,8 +7472,17 @@ function WAREHOUSE:_GetTerminal(_attribute) elseif _attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO then -- Helicopter. _terminal=AIRBASE.TerminalType.HelicopterUsable + else + --_terminal=AIRBASE.TerminalType.OpenMedOrBig end - + + -- For ships, we allow medium spots for all fixed wing aircraft. There are smaller tankers and AWACS aircraft that can use a carrier. + if _category==Airbase.Category.SHIP then + if not (_attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO) then + _terminal=AIRBASE.TerminalType.OpenMedOrBig + end + end + return _terminal end @@ -6955,27 +7508,49 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local safedist=(l1/2+l2/2)*1.05 -- 5% safety margine added to safe distance! local safe = (dist > safedist) self:T3(string.format("l1=%.1f l2=%.1f s=%.1f d=%.1f ==> safe=%s", l1,l2,safedist,dist,tostring(safe))) - return safe + return safe end - + + -- Get client coordinates. + local function _clients() + local clients=_DATABASE.CLIENTS + local coords={} + for clientname, client in pairs(clients) do + local template=_DATABASE:GetGroupTemplateFromUnitName(clientname) + local units=template.units + for i,unit in pairs(units) do + local coord=COORDINATE:New(unit.x, unit.alt, unit.y) + coords[unit.name]=coord + --[[ + local airbase=coord:GetClosestAirbase() + local _,TermID, dist, spot=coord:GetClosestParkingSpot(airbase) + if dist<=10 then + env.info(string.format("Found client %s on parking spot %d at airbase %s", unit.name, TermID, airbase:GetName())) + end + ]] + end + end + return coords + end + -- 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! for _,parkingspot in pairs(parkingdata) do - + -- Coordinate of the parking spot. local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE local _termid=parkingspot.TerminalID - + -- Scan a radius of 100 meters around the spot. local _,_,_,_units,_statics,_sceneries=_spot:ScanObjects(scanradius, scanunits, scanstatics, scanscenery) - -- Check all units. + -- Check all units. for _,_unit in pairs(_units) do local unit=_unit --Wrapper.Unit#UNIT local _coord=unit:GetCoordinate() @@ -6983,7 +7558,13 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) 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() @@ -6992,7 +7573,7 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) 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() @@ -7001,63 +7582,57 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) local _size=self:_GetObjectSize(scenery) table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="scenery"}) end - - --[[ - -- TODO Clients? Unoccupied client aircraft are also important! Are they already included in scanned units maybe? - local clients=_DATABASE.CLIENTS - for _,_client in pairs(clients) do - local client=_client --Wrapper.Client#CLIENT - env.info(string.format("FF Client name %s", client:GetName())) - local unit=UNIT:FindByName(client:GetName()) - --local unit=client:GetClientGroupUnit() - local _coord=unit:GetCoordinate() - local _name=unit:GetName() - local _size=self:_GetObjectSize(client:GetClientGroupDCSUnit()) - table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="client"}) - end - ]] + end - + -- Parking data for all assets. local parking={} -- Loop over all assets that need a parking psot. for _,asset in pairs(assets) do local _asset=asset --#WAREHOUSE.Assetitem - + -- Get terminal type of this asset - local terminaltype=self:_GetTerminal(asset.attribute) - + local terminaltype=asset.terminalType or self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) + -- Asset specific parking. parking[_asset.uid]={} - + -- Loop over all units - each one needs a spot. for i=1,_asset.nunits do - + -- Loop over all parking spots. local gotit=false - for _,_parkingspot in pairs(parkingdata) do + 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 - + if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) and self:_CheckParkingValid(parkingspot) then + -- Coordinate of the parking spot. local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE local _termid=parkingspot.TerminalID local _toac=parkingspot.TOAC - + --env.info(string.format("FF asset=%s (id=%d): needs terminal type=%d, id=%d, #obstacles=%d", _asset.templatename, _asset.uid, terminaltype, _termid, #obstacles)) - - -- Loop over all obstacles. + local free=true local problem=nil + + -- Safe parking using TO_AC from DCS result. + self:I(self.lid..string.format("Parking spot %d TOAC=%s (safe park=%s).", _termid, tostring(_toac), tostring(self.safeparking))) + if self.safeparking and _toac then + free=false + self:I(self.lid..string.format("Parking spot %d is occupied by other aircraft taking off (TOAC).", _termid)) + end + + -- Loop over all obstacles. for _,obstacle in pairs(obstacles) do - + -- Check if aircraft overlaps with any obstacle. local dist=_spot:Get2DDistance(obstacle.coord) local safe=_overlap(_asset.size, obstacle.size, dist) - + -- Spot is blocked. if not safe then --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE", _asset.templatename, _asset.uid, _termid, dist)) @@ -7068,46 +7643,46 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) else --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", _asset.templatename, _asset.uid, _termid, dist)) end - + end - + -- Check if spot is free if free then - + -- Add parkingspot for this asset unit. table.insert(parking[_asset.uid], parkingspot) - - self:T(self.wid..string.format("Parking spot #%d is free for asset id=%d!", _termid, _asset.uid)) - + + self:I(self.lid..string.format("Parking spot %d is free for asset id=%d!", _termid, _asset.uid)) + -- Add the unit as obstacle so that this spot will not be available for the next unit. table.insert(obstacles, {coord=_spot, size=_asset.size, name=_asset.templatename, type="asset"}) - + gotit=true break - + else - + -- Debug output for occupied spots. - self:T(self.wid..string.format("Parking spot #%d is occupied or not big enough!", _termid)) + self:I(self.lid..string.format("Parking spot %d is occupied or not big enough!", _termid)) if self.Debug then local coord=problem.coord --Core.Point#COORDINATE local text=string.format("Obstacle blocking spot #%d is %s type %s with size=%.1f m and distance=%.1f m.", _termid, problem.name, problem.type, problem.size, problem.dist) coord:MarkToAll(string.format(text)) end - + end - + end -- check terminal type end -- loop over parking spots - + -- No parking spot for at least one asset :( if not gotit then - self:T(self.wid..string.format("WARNING: No free parking spot for asset id=%d",_asset.uid)) + self:I(self.lid..string.format("WARNING: No free parking spot for asset id=%d",_asset.uid)) return nil - end + end end -- loop over asset units end -- loop over asset groups - + return parking end @@ -7121,7 +7696,7 @@ function WAREHOUSE:_GetRequestOfGroup(group, queue) -- Get warehouse, asset and request ID from group name. local wid,aid,rid=self:_GetIDsFromGroup(group) - + -- Find the request. for _,_request in pairs(queue) do local request=_request --#WAREHOUSE.Queueitem @@ -7129,7 +7704,7 @@ function WAREHOUSE:_GetRequestOfGroup(group, queue) return request end end - + end --- Is the group a used as transporter for a given request? @@ -7139,81 +7714,55 @@ end -- @return #boolean True if group is transport, false if group is cargo and nil otherwise. function WAREHOUSE:_GroupIsTransport(group, request) - -- Name of the group under question. - local groupname=self:_GetNameWithOut(group) - - if request.transportgroupset then - local transporters=request.transportgroupset:GetSetObjects() + local asset=self:FindAssetInDB(group) - for _,transport in pairs(transporters) do - if transport:GetName()==groupname then - return true + if asset and asset.iscargo~=nil then + return not asset.iscargo + else + + -- Name of the group under question. + local groupname=self:_GetNameWithOut(group) + + if request.transportgroupset then + local transporters=request.transportgroupset:GetSetObjects() + + for _,transport in pairs(transporters) do + if transport:GetName()==groupname then + return true + end + end + end + + if request.cargogroupset then + local cargos=request.cargogroupset:GetSetObjects() + + for _,cargo in pairs(cargos) do + if self:_GetNameWithOut(cargo)==groupname then + return false + end end end end - - if request.cargogroupset then - local cargos=request.cargogroupset:GetSetObjects() - - for _,cargo in pairs(cargos) do - if self:_GetNameWithOut(cargo)==groupname then - return false - end - end - end - + return nil end ---- Creates a unique name for spawned assets. From the group name the original warehouse, global asset and the request can be derived. --- @param #WAREHOUSE self --- @param #WAREHOUSE.Assetitem _assetitem Asset for which the name is created. --- @param #WAREHOUSE.Queueitem _queueitem (Optional) Request specific name. --- @return #string Alias name "UnitType\_WID-%d\_AID-%d\_RID-%d" -function WAREHOUSE:_Alias(_assetitem,_queueitem) - return self:_alias(_assetitem.unittype, self.uid, _assetitem.uid,_queueitem.uid) -end - ---- Creates a unique name for spawned assets. From the group name the original warehouse, global asset and the request can be derived. --- @param #WAREHOUSE self --- @param #string unittype Type of unit. --- @param #number wid Warehouse id. --- @param #number aid Asset item id. --- @param #number qid Queue/request item id. --- @return #string Alias name "UnitType\_WID-%d\_AID-%d\_RID-%d" -function WAREHOUSE:_alias(unittype, wid, aid, qid) - local _alias=string.format("%s_WID-%d_AID-%d", unittype, wid, aid) - if qid then - _alias=_alias..string.format("_RID-%d", qid) - end - return _alias -end - --- Get group name without any spawn or cargo suffix #CARGO etc. -- @param #WAREHOUSE self -- @param Wrapper.Group#GROUP group The group from which the info is gathered. -- @return #string Name of the object without trailing #... function WAREHOUSE:_GetNameWithOut(group) - if group then - local name - if type(group)=="string" then - name=group - else - name=group:GetName() - end - local namewithout=UTILS.Split(name, "#")[1] - if namewithout then - return namewithout - else - return name - end - end - if type(group)=="string" then - return group + + local groupname=type(group)=="string" and group or group:GetName() + + if groupname:find("CARGO") then + local name=groupname:gsub("#CARGO", "") + return name else - return group:GetName() + return groupname end + end @@ -7227,16 +7776,16 @@ function WAREHOUSE:_GetIDsFromGroup(group) ---@param #string text The text to analyse. local function analyse(text) - + -- Get rid of #0001 tail from spawn. local unspawned=UTILS.Split(text, "#")[1] - - -- Split keywords. + + -- Split keywords. local keywords=UTILS.Split(unspawned, "_") local _wid=nil -- warehouse UID local _aid=nil -- asset UID local _rid=nil -- request UID - + -- Loop over keys. for _,keys in pairs(keywords) do local str=UTILS.Split(keys, "-") @@ -7248,26 +7797,35 @@ function WAREHOUSE:_GetIDsFromGroup(group) _aid=tonumber(val) elseif key:find("RID") then _rid=tonumber(val) - end + end end - + return _wid,_aid,_rid end - + if group then - + -- Group name local name=group:GetName() - - -- Get ids + + -- Get asset id from group name. local wid,aid,rid=analyse(name) - + + -- Get Asset. + local asset=self:GetAssetByID(aid) + + -- Get warehouse and request id from asset table. + if asset then + wid=asset.wid + rid=asset.rid + end + -- Debug info - self:T3(self.wid..string.format("Group Name = %s", tostring(name))) - self:T3(self.wid..string.format("Warehouse ID = %s", tostring(wid))) - self:T3(self.wid..string.format("Asset ID = %s", tostring(aid))) - self:T3(self.wid..string.format("Request ID = %s", tostring(rid))) - + self:T(self.lid..string.format("Group Name = %s", tostring(name))) + self:T(self.lid..string.format("Warehouse ID = %s", tostring(wid))) + self:T(self.lid..string.format("Asset ID = %s", tostring(aid))) + self:T(self.lid..string.format("Request ID = %s", tostring(rid))) + return wid,aid,rid else self:E("WARNING: Group not found in GetIDsFromGroup() function!") @@ -7275,6 +7833,78 @@ function WAREHOUSE:_GetIDsFromGroup(group) end + +--- Get warehouse id, asset id and request id from group name (alias). +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group from which the info is gathered. +-- @return #number Warehouse ID. +-- @return #number Asset ID. +-- @return #number Request ID. +function WAREHOUSE:_GetIDsFromGroupOLD(group) + + ---@param #string text The text to analyse. + local function analyse(text) + + -- Get rid of #0001 tail from spawn. + local unspawned=UTILS.Split(text, "#")[1] + + -- Split keywords. + local keywords=UTILS.Split(unspawned, "_") + local _wid=nil -- warehouse UID + local _aid=nil -- asset UID + local _rid=nil -- request UID + + -- Loop over keys. + for _,keys in pairs(keywords) do + local str=UTILS.Split(keys, "-") + local key=str[1] + local val=str[2] + if key:find("WID") then + _wid=tonumber(val) + elseif key:find("AID") then + _aid=tonumber(val) + elseif key:find("RID") then + _rid=tonumber(val) + end + end + + return _wid,_aid,_rid + end + + if group then + + -- Group name + local name=group:GetName() + + -- Get ids + local wid,aid,rid=analyse(name) + + -- Debug info + self:T3(self.lid..string.format("Group Name = %s", tostring(name))) + self:T3(self.lid..string.format("Warehouse ID = %s", tostring(wid))) + self:T3(self.lid..string.format("Asset ID = %s", tostring(aid))) + self:T3(self.lid..string.format("Request ID = %s", tostring(rid))) + + return wid,aid,rid + else + self:E("WARNING: Group not found in GetIDsFromGroup() function!") + end + +end + +--- Filter stock assets by descriptor and attribute. +-- @param #WAREHOUSE self +-- @param #string descriptor Descriptor describing the filtered assets. +-- @param attribute Value of the descriptor. +-- @param #number nmax (Optional) Maximum number of items that will be returned. Default nmax=nil is all matching items are returned. +-- @param #boolean mobile (Optional) If true, filter only mobile assets. +-- @return #table Filtered assets in stock with the specified descriptor value. +-- @return #number Total number of (requested) assets available. +-- @return #boolean If true, enough assets are available. +function WAREHOUSE:FilterStock(descriptor, attribute, nmax, mobile) + return self:_FilterStock(self.stock, descriptor, attribute, nmax, mobile) +end + --- Filter stock assets by table entry. -- @param #WAREHOUSE self -- @param #table stock Table holding all assets in stock of the warehouse. Each entry is of type @{#WAREHOUSE.Assetitem}. @@ -7295,6 +7925,25 @@ function WAREHOUSE:_FilterStock(stock, descriptor, attribute, nmax, mobile) -- Filtered array. local filtered={} + + -- A specific list of assets was required. + if descriptor==WAREHOUSE.Descriptor.ASSETLIST then + + -- Count total number in stock. + local ntot=0 + for _,_rasset in pairs(attribute) do + local rasset=_rasset --#WAREHOUSE.Assetitem + for _,_asset in ipairs(stock) do + local asset=_asset --#WAREHOUSE.Assetitem + if rasset.uid==asset.uid then + table.insert(filtered, asset) + break + end + end + end + + return filtered, #filtered, #filtered>=#attribute + end -- Count total number in stock. local ntot=0 @@ -7307,34 +7956,34 @@ function WAREHOUSE:_FilterStock(stock, descriptor, attribute, nmax, mobile) end end end - + -- Treat case where ntot=0, i.e. no assets at all. if ntot==0 then return filtered, ntot, false end - + -- Convert relative to absolute number if necessary. nmax=self:_QuantityRel2Abs(nmax,ntot) -- Loop over stock items. for _i,_asset in ipairs(stock) do local asset=_asset --#WAREHOUSE.Assetitem - + -- Check if asset has the right attribute. if asset[descriptor]==attribute then - + -- Check if asset has to be mobile. if (mobile and asset.speedmax>0) or (not mobile) then - + -- Add asset to filtered table. table.insert(filtered, asset) - + -- Break loop if nmax was reached. if nmax~=nil and #filtered>=nmax then return filtered, ntot, true end - - end + + end end end @@ -7367,24 +8016,24 @@ function WAREHOUSE:_GetAttribute(group) local attribute=WAREHOUSE.Attribute.OTHER_UNKNOWN --#WAREHOUSE.Attribute if group then - + ----------- --- Air --- - ----------- + ----------- -- Planes local transportplane=group:HasAttribute("Transports") and group:HasAttribute("Planes") local awacs=group:HasAttribute("AWACS") - local fighter=group:HasAttribute("Fighters") or group:HasAttribute("Interceptors") or group:HasAttribute("Multirole fighters") or (group:HasAttribute("Bombers") and not group:HasAttribute("Strategic bombers")) + local 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") + local tanker=group:HasAttribute("Tankers") + local uav=group:HasAttribute("UAVs") -- Helicopters local transporthelo=group:HasAttribute("Transport helicopters") local attackhelicopter=group:HasAttribute("Attack helicopters") -------------- --- Ground --- - -------------- + -------------- -- Ground local apc=group:HasAttribute("Infantry carriers") local truck=group:HasAttribute("Trucks") and group:GetCategory()==Group.Category.GROUND @@ -7399,13 +8048,13 @@ function WAREHOUSE:_GetAttribute(group) ------------- --- Naval --- - ------------- + ------------- -- Ships local aircraftcarrier=group:HasAttribute("Aircraft Carriers") local warship=group:HasAttribute("Heavy armed ships") local armedship=group:HasAttribute("Armed ships") local unarmedship=group:HasAttribute("Unarmed ships") - + -- Define attribute. Order is important. if transportplane then @@ -7439,7 +8088,7 @@ function WAREHOUSE:_GetAttribute(group) elseif sam then attribute=WAREHOUSE.Attribute.GROUND_SAM elseif truck then - attribute=WAREHOUSE.Attribute.GROUND_TRUCK + attribute=WAREHOUSE.Attribute.GROUND_TRUCK elseif train then attribute=WAREHOUSE.Attribute.GROUND_TRAIN elseif aircraftcarrier then @@ -7447,7 +8096,7 @@ function WAREHOUSE:_GetAttribute(group) elseif warship then attribute=WAREHOUSE.Attribute.NAVAL_WARSHIP elseif armedship then - attribute=WAREHOUSE.Attribute.NAVAL_ARMEDSHIP + attribute=WAREHOUSE.Attribute.NAVAL_ARMEDSHIP elseif unarmedship then attribute=WAREHOUSE.Attribute.NAVAL_UNARMEDSHIP else @@ -7459,7 +8108,7 @@ function WAREHOUSE:_GetAttribute(group) attribute=WAREHOUSE.Attribute.AIR_OTHER else attribute=WAREHOUSE.Attribute.OTHER_UNKNOWN - end + end end end @@ -7482,7 +8131,7 @@ function WAREHOUSE:_GetObjectSize(DCSobject) return math.max(x,z), x , y, z end return 0,0,0,0 -end +end --- Returns the number of assets for each generalized attribute. -- @param #WAREHOUSE self @@ -7526,11 +8175,27 @@ end -- @param #table queue The queue from which the item should be deleted. function WAREHOUSE:_DeleteQueueItem(qitem, queue) self:F({qitem=qitem, queue=queue}) - + for i=1,#queue do local _item=queue[i] --#WAREHOUSE.Queueitem if _item.uid==qitem.uid then - self:T(self.wid..string.format("Deleting queue item id=%d.", qitem.uid)) + self:T(self.lid..string.format("Deleting queue item id=%d.", qitem.uid)) + table.remove(queue,i) + break + end + end +end + +--- Delete item from queue. +-- @param #WAREHOUSE self +-- @param #number qitemID ID of queue item to be removed. +-- @param #table queue The queue from which the item should be deleted. +function WAREHOUSE:_DeleteQueueItemByID(qitemID, queue) + + for i=1,#queue do + local _item=queue[i] --#WAREHOUSE.Queueitem + if _item.uid==qitemID then + self:T(self.lid..string.format("Deleting queue item id=%d.", qitemID)) table.remove(queue,i) break end @@ -7548,6 +8213,75 @@ function WAREHOUSE:_SortQueue() table.sort(self.queue, _sort) end +--- Checks fuel on all pening assets. +-- @param #WAREHOUSE self +function WAREHOUSE:_CheckFuel() + + for i,qitem in ipairs(self.pending) do + local qitem=qitem --#WAREHOUSE.Pendingitem + + if qitem.transportgroupset then + for _,_group in pairs(qitem.transportgroupset:GetSet()) do + local group=_group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + -- Get min fuel of group. + local fuel=group:GetFuelMin() + + -- Debug info. + self:T2(self.lid..string.format("Transport group %s min fuel state = %.2f", group:GetName(), fuel)) + + -- Check if fuel is below threshold for first time. + if fuel0 then - local attribute=tostring(UTILS.Split(_attribute, "_")[2]) - text=text..string.format("%s=%d, ", attribute,_count) + -- Create a mark with the current assets in stock. + if self.markerid~=nil then + trigger.action.removeMark(self.markerid) end + + -- Get assets in stock. + local _data=self:GetStockInfo(self.stock) + + -- Text. + local text=string.format("Warehouse state: %s\nTotal assets in stock %d:\n", self:GetState(), #self.stock) + + for _attribute,_count in pairs(_data) do + if _count>0 then + local attribute=tostring(UTILS.Split(_attribute, "_")[2]) + text=text..string.format("%s=%d, ", attribute,_count) + end + end + + -- Create/update marker at warehouse in F10 map. + self.markerid=self:GetCoordinate():MarkToCoalition(text, self:GetCoalition(), true) + end - -- Create/update marker at warehouse in F10 map. - self.markerid=self:GetCoordinate():MarkToCoalition(text, self:GetCoalition(), true) end --- Display stock items of warehouse. @@ -7688,7 +8436,7 @@ end -- @param #table stock Table holding all assets in stock of the warehouse. Each entry is of type @{#WAREHOUSE.Assetitem}. function WAREHOUSE:_DisplayStockItems(stock) - local text=self.wid..string.format("Warehouse %s stock assets:", self.alias) + local text=self.lid..string.format("Warehouse %s stock assets:", self.alias) for _i,_stock in pairs(stock) do local mystock=_stock --#WAREHOUSE.Assetitem local name=mystock.templatename @@ -7701,7 +8449,7 @@ function WAREHOUSE:_DisplayStockItems(stock) local speed=mystock.speedmax local uid=mystock.uid local unittype=mystock.unittype - local weight=mystock.weight + local weight=mystock.weight local attribute=mystock.attribute text=text..string.format("\n%02d) uid=%d, name=%s, unittype=%s, category=%d, attribute=%s, nunits=%d, speed=%.1f km/h, range=%.1f km, size=%.1f m, weight=%.1f kg, cargobax max=%.1f kg tot=%.1f kg", _i, uid, name, unittype, category, attribute, nunits, speed, range/1000, size, weight, cargobaymax, cargobaytot) @@ -7734,7 +8482,7 @@ function WAREHOUSE:_InfoMessage(text, duration) if duration>0 then MESSAGE:New(text, duration):ToCoalitionIf(self:GetCoalition(), self.Debug or self.Report) end - self:I(self.wid..text) + self:I(self.lid..text) end @@ -7747,7 +8495,7 @@ function WAREHOUSE:_DebugMessage(text, duration) if duration>0 then MESSAGE:New(text, duration):ToAllIf(self.Debug) end - self:T(self.wid..text) + self:T(self.lid..text) end --- Error message. Message send to all (if duration > 0). Text self:E(text) added to DCS.log file. @@ -7759,7 +8507,7 @@ function WAREHOUSE:_ErrorMessage(text, duration) if duration>0 then MESSAGE:New(text, duration):ToAll() end - self:E(self.wid..text) + self:E(self.lid..text) end @@ -7777,23 +8525,23 @@ function WAREHOUSE:_GetMaxHeight(D, alphaC, alphaD, Hdep, Hdest, Deltahhold) local Hhold=Hdest+Deltahhold local hdest=Hdest-Hdep local hhold=hdest+Deltahhold - + local Dp=math.sqrt(D^2 + hhold^2) - + local alphaS=math.atan(hdest/D) -- slope angle local alphaH=math.atan(hhold/D) -- angle to holding point (could be necative!) - + local alphaCp=alphaC-alphaH -- climb angle with slope local alphaDp=alphaD+alphaH -- descent angle with slope - + -- ASA triangle. local gammap=math.pi-alphaCp-alphaDp local sCp=Dp*math.sin(alphaDp)/math.sin(gammap) local sDp=Dp*math.sin(alphaCp)/math.sin(gammap) - + -- Max height from departure. local hmax=sCp*math.sin(alphaC) - + -- Debug info. if self.Debug then env.info(string.format("Hdep = %.3f km", Hdep/1000)) @@ -7817,14 +8565,14 @@ function WAREHOUSE:_GetMaxHeight(D, alphaC, alphaD, Hdep, Hdest, Deltahhold) env.info() env.info(string.format("hmax = %.3f km", hmax/1000)) env.info() - + -- Descent height local hdescent=hmax-hhold - + local dClimb = hmax/math.tan(alphaC) local dDescent = (hmax-hhold)/math.tan(alphaD) local dCruise = D-dClimb-dDescent - + env.info(string.format("hmax = %.3f km", hmax/1000)) env.info(string.format("hdescent = %.3f km", hdescent/1000)) env.info(string.format("Dclimb = %.3f km", dClimb/1000)) @@ -7832,84 +8580,84 @@ function WAREHOUSE:_GetMaxHeight(D, alphaC, alphaD, Hdep, Hdest, Deltahhold) env.info(string.format("Ddescent = %.3f km", dDescent/1000)) env.info() end - + return hmax end ---- Make a flight plan from a departure to a destination airport. +--- Make a flight plan from a departure to a destination airport. -- @param #WAREHOUSE self --- @param #WAREHOUSE.Assetitem asset +-- @param #WAREHOUSE.Assetitem asset -- @param Wrapper.Airbase#AIRBASE departure Departure airbase. -- @param Wrapper.Airbase#AIRBASE destination Destination airbase. -- @return #table Table of flightplan waypoints. --- @return #table Table of flightplan coordinates. +-- @return #table Table of flightplan coordinates. function WAREHOUSE:_GetFlightplan(asset, departure, destination) - + -- Parameters in SI units (m/s, m). local Vmax=asset.speedmax/3.6 local Range=asset.range local category=asset.category local ceiling=asset.DCSdesc.Hmax local Vymax=asset.DCSdesc.VyMax - + -- Max cruise speed 90% of max speed. local VxCruiseMax=0.90*Vmax -- Min cruise speed 70% of max cruise or 600 km/h whichever is lower. local VxCruiseMin = math.min(VxCruiseMax*0.70, 166) - + -- Cruise speed (randomized). Expectation value at midpoint between min and max. local VxCruise = UTILS.RandomGaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin, (VxCruiseMax-VxCruiseMax)/4, VxCruiseMin, VxCruiseMax) - + -- Climb speed 90% ov Vmax but max 720 km/h. local VxClimb = math.min(Vmax*0.90, 200) - + -- Descent speed 60% of Vmax but max 500 km/h. local VxDescent = math.min(Vmax*0.60, 140) - + -- Holding speed is 90% of descent speed. local VxHolding = VxDescent*0.9 - + -- Final leg is 90% of holding speed. local VxFinal = VxHolding*0.9 - + -- Reasonably civil climb speed Vy=1500 ft/min = 7.6 m/s but max aircraft specific climb rate. local VyClimb=math.min(7.6, Vymax) - + -- Climb angle in rad. --local AlphaClimb=math.asin(VyClimb/VxClimb) local AlphaClimb=math.rad(4) - + -- Descent angle in rad. Moderate 4 degrees. local AlphaDescent=math.rad(4) - + -- Expected cruise level (peak of Gaussian distribution) local FLcruise_expect=150*RAT.unit.FL2m - if category==Group.Category.HELICOPTER then + if category==Group.Category.HELICOPTER then FLcruise_expect=1000 -- 1000 m ASL end - + ------------------------- --- DEPARTURE AIRPORT --- ------------------------- - + -- Coordinates of departure point. local Pdeparture=departure:GetCoordinate() - + -- Height ASL of departure point. local H_departure=Pdeparture.y - - --------------------------- + + --------------------------- --- DESTINATION AIRPORT --- --------------------------- - + -- Position of destination airport. local Pdestination=destination:GetCoordinate() - + -- Height ASL of destination airport/zone. local H_destination=Pdestination.y - + ----------------------------- --- DESCENT/HOLDING POINT --- ----------------------------- @@ -7917,26 +8665,26 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) -- Get a random point between 5 and 10 km away from the destination. local Rhmin=5000 local Rhmax=10000 - + -- For helos we set a distance between 500 to 1000 m. - if category==Group.Category.HELICOPTER then + if category==Group.Category.HELICOPTER then Rhmin=500 Rhmax=1000 end - + -- Coordinates of the holding point. y is the land height at that point. local Pholding=Pdestination:GetRandomCoordinateInRadius(Rhmax, Rhmin) -- Distance from holding point to final destination (not used). local d_holding=Pholding:Get2DDistance(Pdestination) - + -- AGL height of holding point. local H_holding=Pholding.y - + --------------- --- GENERAL --- --------------- - + -- We go directly to the holding point not the destination airport. From there, planes are guided by DCS to final approach. local heading=Pdeparture:HeadingTo(Pholding) local d_total=Pdeparture:Get2DDistance(Pholding) @@ -7944,46 +8692,46 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) ------------------------------ --- Holding Point Altitude --- ------------------------------ - + -- Holding point altitude. For planes between 1600 and 2400 m AGL. For helos 160 to 240 m AGL. local h_holding=1200 if category==Group.Category.HELICOPTER then h_holding=150 end h_holding=UTILS.Randomize(h_holding, 0.2) - + -- Max holding altitude. local DeltaholdingMax=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, 0) - + if h_holding>DeltaholdingMax then h_holding=math.abs(DeltaholdingMax) end - + -- This is the height ASL of the holding point we want to fly to. local Hh_holding=H_holding+h_holding - + --------------------------- --- Max Flight Altitude --- - --------------------------- - + --------------------------- + -- Get max flight altitude relative to H_departure. local h_max=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, h_holding) -- Max flight level ASL aircraft can reach for given angles and distance. local FLmax = h_max+H_departure - - --CRUISE + + --CRUISE -- Min cruise alt is just above holding point at destination or departure height, whatever is larger. local FLmin=math.max(H_departure, Hh_holding) - + -- Ensure that FLmax not above its service ceiling. FLmax=math.min(FLmax, ceiling) - + -- If the route is very short we set FLmin a bit lower than FLmax. if FLmin>FLmax then FLmin=FLmax end - + -- Expected cruise altitude - peak of gaussian distribution. if FLcruise_expectFLmax then FLcruise_expect=FLmax end - + -- Set cruise altitude. Selected from Gaussian distribution but limited to FLmin and FLmax. local FLcruise=UTILS.RandomGaussian(FLcruise_expect, math.abs(FLmax-FLmin)/4, FLmin, FLmax) -- Climb and descent heights. local h_climb = FLcruise - H_departure local h_descent = FLcruise - Hh_holding - + -- Get distances. local d_climb = h_climb/math.tan(AlphaClimb) local d_descent = h_descent/math.tan(AlphaDescent) local d_cruise = d_total-d_climb-d_descent - + -- Debug. local text=string.format("Flight plan:\n") text=text..string.format("Vx max = %.2f km/h\n", Vmax*3.6) @@ -8030,8 +8778,8 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) text=text..string.format("FL max = %.3f km\n", FLmax/1000) text=text..string.format("Ceiling = %.3f km\n", ceiling/1000) text=text..string.format("Max range = %.3f km\n", Range/1000) - self:T(self.wid..text) - + self:T(self.lid..text) + -- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back. if d_cruise<0 then d_cruise=100 @@ -8044,32 +8792,32 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) -- Waypoints and coordinates local wp={} local c={} - + --- Departure/Take-off c[#c+1]=Pdeparture - wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb, true, departure, nil, "Departure") - + wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb*3.6, true, departure, nil, "Departure") + --- Begin of Cruise local Pcruise=Pdeparture:Translate(d_climb, heading) Pcruise.y=FLcruise c[#c+1]=Pcruise - wp[#wp+1]=Pcruise:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise, true, nil, nil, "Cruise") + wp[#wp+1]=Pcruise:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise*3.6, true, nil, nil, "Cruise") - --- Descent + --- Descent local Pdescent=Pcruise:Translate(d_cruise, heading) Pdescent.y=FLcruise c[#c+1]=Pdescent - wp[#wp+1]=Pdescent:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent, true, nil, nil, "Descent") - + wp[#wp+1]=Pdescent:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent*3.6, true, nil, nil, "Descent") + --- Holding point Pholding.y=H_holding+h_holding c[#c+1]=Pholding - wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding, true, nil, nil, "Holding") + wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding*3.6, true, nil, nil, "Holding") - --- Final destination. + --- Final destination. c[#c+1]=Pdestination - wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal, true, destination, nil, "Final Destination") - + wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal*3.6, true, destination, nil, "Final Destination") + -- Mark points at waypoints for debugging. if self.Debug then @@ -8080,9 +8828,9 @@ function WAREHOUSE:_GetFlightplan(asset, departure, destination) dist=coord:Get2DDistance(c[i-1]) end coord:MarkToAll(string.format("Waypoint %i, distance = %.2f km",i, dist/1000)) - end + end end - + return wp,c end @@ -8090,42 +8838,3 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---[[ - --- Departure/Take-off - c[#c+1]=Pdeparture - wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb, true, departure, nil, "Departure") - - --- Climb - local Pclimb=Pdeparture:Translate(d_climb/2, heading) - Pclimb.y=H_departure+(FLcruise-H_departure)/2 - c[#c+1]=Pclimb - wp[#wp+1]=Pclimb:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxClimb, true, nil, nil, "Climb") - - --- Begin of Cruise - local Pcruise1=Pclimb:Translate(d_climb/2, heading) - Pcruise1.y=FLcruise - c[#c+1]=Pcruise1 - wp[#wp+1]=Pcruise1:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise, true, nil, nil, "Begin of Cruise") - - --- End of Cruise - local Pcruise2=Pcruise1:Translate(d_cruise, heading) - Pcruise2.y=FLcruise - c[#c+1]=Pcruise2 - wp[#wp+1]=Pcruise2:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise, true, nil, nil, "End of Cruise") - - --- Descent - local Pdescent=Pcruise2:Translate(d_descent/2, heading) - Pdescent.y=FLcruise-(FLcruise-(h_holding+H_holding))/2 - c[#c+1]=Pdescent - wp[#wp+1]=Pcruise2:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent, true, nil, nil, "Descent") - - --- Holding point - Pholding.y=H_holding+h_holding - c[#c+1]=Pholding - wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding, true, nil, nil, "Holding") - - --- Final destination. - c[#c+1]=Pdestination - wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal, true, destination, nil, "Final Destination") -]] diff --git a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua index 61625bc4f..e6a07e323 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -39,7 +39,7 @@ -- === -- -- ### Author: **FlightControl** --- ### Contributions: **Millertime** - Concept +-- ### Contributions: **Millertime** - Concept, **funkyfranky** -- -- === -- @@ -49,6 +49,15 @@ do -- ZONE_CAPTURE_COALITION --- @type ZONE_CAPTURE_COALITION + -- @field #string ClassName Name of the class. + -- @field #number MarkBlue ID of blue F10 mark. + -- @field #number MarkRed ID of red F10 mark. + -- @field #number StartInterval Time in seconds after the status monitor is started. + -- @field #number RepeatInterval Time in seconds after which the zone status is updated. + -- @field #boolean HitsOn If true, hit events are monitored and trigger the "Attack" event when a defending unit is hit. + -- @field #number HitTimeLast Time stamp in seconds when the last unit inside the zone was hit. + -- @field #number HitTimeAttackOver Time interval in seconds before the zone goes from "Attacked" to "Guarded" state after the last hit. + -- @field #boolean MarkOn If true, create marks of zone status on F10 map. -- @extends Functional.ZoneGoalCoalition#ZONE_GOAL_COALITION @@ -197,8 +206,7 @@ do -- ZONE_CAPTURE_COALITION -- -- ### IMPORTANT -- - -- **Each capture zone object must have the monitoring process started specifically. - -- The monitoring process is NOT started by default!!!** + -- **Each capture zone object must have the monitoring process started specifically. The monitoring process is NOT started by default!** -- -- -- # Full Example @@ -338,29 +346,48 @@ do -- ZONE_CAPTURE_COALITION -- -- @field #ZONE_CAPTURE_COALITION ZONE_CAPTURE_COALITION = { - ClassName = "ZONE_CAPTURE_COALITION", + ClassName = "ZONE_CAPTURE_COALITION", + MarkBlue = nil, + MarkRed = nil, + StartInterval = nil, + RepeatInterval = nil, + HitsOn = nil, + HitTimeLast = nil, + HitTimeAttackOver = nil, + MarkOn = nil, } - - --- @field #table ZONE_CAPTURE_COALITION.States - ZONE_CAPTURE_COALITION.States = {} - + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor and Start/Stop Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- ZONE_CAPTURE_COALITION Constructor. -- @param #ZONE_CAPTURE_COALITION self -- @param Core.Zone#ZONE Zone A @{Zone} object with the goal to be achieved. -- @param DCSCoalition.DCSCoalition#coalition Coalition The initial coalition owning the zone. + -- @param #table UnitCategories Table of unit categories. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). Default {Unit.Category.GROUND_UNIT}. + -- @param #table ObjectCategories Table of unit categories. See [DCS Class Object](https://wiki.hoggitworld.com/view/DCS_Class_Object). Default {Object.Category.UNIT, Object.Category.STATIC}, i.e. all UNITS and STATICS. -- @return #ZONE_CAPTURE_COALITION -- @usage -- -- AttackZone = ZONE:New( "AttackZone" ) -- - -- ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( AttackZone, coalition.side.RED ) -- Create a new ZONE_CAPTURE_COALITION object of zone AttackZone with ownership RED coalition. + -- ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( AttackZone, coalition.side.RED, {UNITS ) -- Create a new ZONE_CAPTURE_COALITION object of zone AttackZone with ownership RED coalition. -- ZoneCaptureCoalition:__Guard( 1 ) -- Start the Guarding of the AttackZone. -- - function ZONE_CAPTURE_COALITION:New( Zone, Coalition ) + function ZONE_CAPTURE_COALITION:New( Zone, Coalition, UnitCategories, ObjectCategories ) - local self = BASE:Inherit( self, ZONE_GOAL_COALITION:New( Zone, Coalition ) ) -- #ZONE_CAPTURE_COALITION - - self:F( { Zone = Zone, Coalition = Coalition } ) + local self = BASE:Inherit( self, ZONE_GOAL_COALITION:New( Zone, Coalition, UnitCategories ) ) -- #ZONE_CAPTURE_COALITION + self:F( { Zone = Zone, Coalition = Coalition, UnitCategories = UnitCategories, ObjectCategories = ObjectCategories } ) + + self:SetObjectCategories(ObjectCategories) + + -- Default is no smoke. + self:SetSmokeZone(false) + -- Default is F10 marks ON. + self:SetMarkZone(true) + -- Start in state "Empty". + self:SetStartState("Empty") do @@ -545,178 +572,12 @@ do -- ZONE_CAPTURE_COALITION -- @param #ZONE_CAPTURE_COALITION self -- @param #number Delay - -- We check if a unit within the zone is hit. - -- If it is, then we must move the zone to attack state. - self:HandleEvent( EVENTS.Hit, self.OnEventHit ) + -- ZoneGoal objects are added to the _DATABASE.ZONES_GOAL and SET_ZONE_GOAL sets. + _EVENTDISPATCHER:CreateEventNewZoneGoal(self) return self end - - --- @param #ZONE_CAPTURE_COALITION self - function ZONE_CAPTURE_COALITION:onenterCaptured() - - self:GetParent( self, ZONE_CAPTURE_COALITION ).onenterCaptured( self ) - - self.Goal:Achieved() - end - - - function ZONE_CAPTURE_COALITION:IsGuarded() - - local IsGuarded = self.Zone:IsAllInZoneOfCoalition( self.Coalition ) - self:F( { IsGuarded = IsGuarded } ) - return IsGuarded - end - - - function ZONE_CAPTURE_COALITION:IsEmpty() - - local IsEmpty = self.Zone:IsNoneInZone() - self:F( { IsEmpty = IsEmpty } ) - return IsEmpty - end - - - function ZONE_CAPTURE_COALITION:IsCaptured() - - local IsCaptured = self.Zone:IsAllInZoneOfOtherCoalition( self.Coalition ) - self:F( { IsCaptured = IsCaptured } ) - return IsCaptured - end - - - function ZONE_CAPTURE_COALITION:IsAttacked() - - local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) - self:F( { IsAttacked = IsAttacked } ) - return IsAttacked - end - - - - --- Mark. - -- @param #ZONE_CAPTURE_COALITION self - function ZONE_CAPTURE_COALITION:Mark() - - local Coord = self.Zone:GetCoordinate() - local ZoneName = self:GetZoneName() - local State = self:GetState() - - if self.MarkRed and self.MarkBlue then - self:F( { MarkRed = self.MarkRed, MarkBlue = self.MarkBlue } ) - Coord:RemoveMark( self.MarkRed ) - Coord:RemoveMark( self.MarkBlue ) - end - - if self.Coalition == coalition.side.BLUE then - self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Blue\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) - self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Blue\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) - else - self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Red\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) - self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Red\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) - end - end - - --- Bound. - -- @param #ZONE_CAPTURE_COALITION self - function ZONE_CAPTURE_COALITION:onenterGuarded() - - --self:GetParent( self ):onenterGuarded() - - if self.Coalition == coalition.side.BLUE then - --elf.ProtectZone:BoundZone( 12, country.id.USA ) - else - --self.ProtectZone:BoundZone( 12, country.id.RUSSIA ) - end - - self:Mark() - - end - - function ZONE_CAPTURE_COALITION:onenterCaptured() - - --self:GetParent( self ):onenterCaptured() - - local NewCoalition = self.Zone:GetScannedCoalition() - self:F( { NewCoalition = NewCoalition } ) - self:SetCoalition( NewCoalition ) - - self:Mark() - end - - - function ZONE_CAPTURE_COALITION:onenterEmpty() - - --self:GetParent( self ):onenterEmpty() - - self:Mark() - end - - - function ZONE_CAPTURE_COALITION:onenterAttacked() - - --self:GetParent( self ):onenterAttacked() - - self:Mark() - end - - - --- When started, check the Coalition status. - -- @param #ZONE_CAPTURE_COALITION self - function ZONE_CAPTURE_COALITION:onafterGuard() - - --self:F({BASE:GetParent( self )}) - --BASE:GetParent( self ).onafterGuard( self ) - - if not self.SmokeScheduler then - self.SmokeScheduler = self:ScheduleRepeat( 1, 1, 0.1, nil, self.StatusSmoke, self ) - end - end - - - function ZONE_CAPTURE_COALITION:IsCaptured() - - local IsCaptured = self.Zone:IsAllInZoneOfOtherCoalition( self.Coalition ) - self:F( { IsCaptured = IsCaptured } ) - return IsCaptured - end - - - function ZONE_CAPTURE_COALITION:IsAttacked() - - local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition ) - self:F( { IsAttacked = IsAttacked } ) - return IsAttacked - end - - - --- Check status Coalition ownership. - -- @param #ZONE_CAPTURE_COALITION self - function ZONE_CAPTURE_COALITION:StatusZone() - - local State = self:GetState() - self:F( { State = self:GetState() } ) - - self:GetParent( self, ZONE_CAPTURE_COALITION ).StatusZone( self ) - - if State ~= "Guarded" and self:IsGuarded() then - self:Guard() - end - - if State ~= "Empty" and self:IsEmpty() then - self:Empty() - end - - if State ~= "Attacked" and self:IsAttacked() then - self:Attack() - end - - if State ~= "Captured" and self:IsCaptured() then - self:Capture() - end - - end --- Starts the zone capturing monitoring process. -- This process can be CPU intensive, ensure that you specify reasonable time intervals for the monitoring process. @@ -727,6 +588,7 @@ do -- ZONE_CAPTURE_COALITION -- @param #ZONE_CAPTURE_COALITION self -- @param #number StartInterval (optional) Specifies the start time interval in seconds when the zone state will be checked for the first time. -- @param #number RepeatInterval (optional) Specifies the repeat time interval in seconds when the zone state will be checked repeatedly. + -- @return #ZONE_CAPTURE_COALITION self -- @usage -- -- -- Setup the zone. @@ -741,13 +603,23 @@ do -- ZONE_CAPTURE_COALITION -- function ZONE_CAPTURE_COALITION:Start( StartInterval, RepeatInterval ) - StartInterval = StartInterval or 15 - RepeatInterval = RepeatInterval or 15 + self.StartInterval = StartInterval or 1 + self.RepeatInterval = RepeatInterval or 15 if self.ScheduleStatusZone then self:ScheduleStop( self.ScheduleStatusZone ) end - self.ScheduleStatusZone = self:ScheduleRepeat( StartInterval, RepeatInterval, 0.1, nil, self.StatusZone, self ) + + -- Start Status scheduler. + self.ScheduleStatusZone = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 0.1, nil, self.StatusZone, self ) + + -- We check if a unit within the zone is hit. If it is, then we must move the zone to attack state. + self:HandleEvent(EVENTS.Hit, self.OnEventHit) + + -- Create mark on F10 map. + self:Mark() + + return self end @@ -789,24 +661,281 @@ do -- ZONE_CAPTURE_COALITION function ZONE_CAPTURE_COALITION:Stop() if self.ScheduleStatusZone then - self:ScheduleStop( self.ScheduleStatusZone ) + self:ScheduleStop(self.ScheduleStatusZone) end + + if self.SmokeScheduler then + self:ScheduleStop(self.SmokeScheduler) + end + + self:UnHandleEvent(EVENTS.Hit) + end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + --- Set whether hit events of defending units are monitored and trigger "Attack" events. + -- @param #ZONE_CAPTURE_COALITION self + -- @param #boolean Switch If *true*, hit events are monitored. If *false* or *nil*, hit events are not monitored. + -- @param #number TimeAttackOver (Optional) Time in seconds after an attack is over after the last hit and the zone state goes to "Guarded". Default is 300 sec = 5 min. + -- @return #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:SetMonitorHits(Switch, TimeAttackOver) + self.HitsOn=Switch + self.HitTimeAttackOver=TimeAttackOver or 5*60 + return self + end + + --- Set whether marks on the F10 map are shown, which display the current zone status. + -- @param #ZONE_CAPTURE_COALITION self + -- @param #boolean Switch If *true* or *nil*, marks are shown. If *false*, marks are not displayed. + -- @return #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:SetMarkZone(Switch) + if Switch==nil or Switch==true then + self.MarkOn=true + else + self.MarkOn=false + end + return self + end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DCS Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + - --- @param #ZONE_CAPTURE_COALITION self + --- Monitor hit events. + -- @param #ZONE_CAPTURE_COALITION self -- @param Core.Event#EVENTDATA EventData The event data. function ZONE_CAPTURE_COALITION:OnEventHit( EventData ) - local UnitHit = EventData.TgtUnit - - if UnitHit then - if UnitHit:IsInZone( self.Zone ) then - self:Attack() + 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.Coalition then + + -- Update last hit time. + self.HitTimeLast=timer.getTime() + + -- Only trigger attacked event if not already in state "Attacked". + if self:GetState()~="Attacked" then + self:F2("Hit ==> Attack") + self:Attack() + end + end + end end - - -end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Event Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + --- On after "Guard" event. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onafterGuard() + self:F2("After Guard") + + if self.SmokeZone and not self.SmokeScheduler then + self.SmokeScheduler = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 0.1, nil, self.StatusSmoke, self ) + end + + end + + --- On enter "Guarded" state. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onenterGuarded() + self:F2("Enter Guarded") + self:Mark() + end + + --- On enter "Captured" state. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onenterCaptured() + self:F2("Enter Captured") + + -- Get new coalition. + local NewCoalition = self:GetScannedCoalition() + self:F( { NewCoalition = NewCoalition } ) + + -- Set new owner of zone. + self:SetCoalition(NewCoalition) + + -- Update mark. + self:Mark() + + -- Goal achieved. + self.Goal:Achieved() + end + + --- On enter "Empty" state. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onenterEmpty() + self:F2("Enter Empty") + self:Mark() + end + + --- On enter "Attacked" state. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:onenterAttacked() + self:F2("Enter Attacked") + self:Mark() + end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Status Check Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + --- Check if zone is "Empty". + -- @param #ZONE_CAPTURE_COALITION self + -- @return #boolean self:IsNoneInZone() + function ZONE_CAPTURE_COALITION:IsEmpty() + + local IsEmpty = self:IsNoneInZone() + self:F( { IsEmpty = IsEmpty } ) + + return IsEmpty + end + + --- Check if zone is "Guarded", i.e. only one (the defending) coaliton is present inside the zone. + -- @param #ZONE_CAPTURE_COALITION self + -- @return #boolean self:IsAllInZoneOfCoalition( self.Coalition ) + function ZONE_CAPTURE_COALITION:IsGuarded() + + local IsGuarded = self:IsAllInZoneOfCoalition( self.Coalition ) + self:F( { IsGuarded = IsGuarded } ) + + return IsGuarded + end + + --- Check if zone is "Captured", i.e. another coalition took control over the zone and is the only one present. + -- @param #ZONE_CAPTURE_COALITION self + -- @return #boolean self:IsAllInZoneOfOtherCoalition( self.Coalition ) + function ZONE_CAPTURE_COALITION:IsCaptured() + + local IsCaptured = self:IsAllInZoneOfOtherCoalition( self.Coalition ) + self:F( { IsCaptured = IsCaptured } ) + + return IsCaptured + end + + --- Check if zone is "Attacked", i.e. another coaliton entered the zone. + -- @param #ZONE_CAPTURE_COALITION self + -- @return #boolean self:IsSomeInZoneOfCoalition( self.Coalition ) + function ZONE_CAPTURE_COALITION:IsAttacked() + + local IsAttacked = self:IsSomeInZoneOfCoalition( self.Coalition ) + self:F( { IsAttacked = IsAttacked } ) + + return IsAttacked + end + + + --- Check status Coalition ownership. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:StatusZone() + + -- Get FSM state. + local State = self:GetState() + + -- Scan zone in parent class ZONE_GOAL_COALITION + self:GetParent( self, ZONE_CAPTURE_COALITION ).StatusZone( self ) + + local Tnow=timer.getTime() + + -- Check if zone is guarded. + if State ~= "Guarded" and self:IsGuarded() then + + -- Check that there was a sufficient amount of time after the last hit before going back to "Guarded". + if self.HitTimeLast==nil or Tnow>=self.HitTimeLast+self.HitTimeAttackOver then + self:Guard() + self.HitTimeLast=nil + end + end + + -- Check if zone is empty. + if State ~= "Empty" and self:IsEmpty() then + self:Empty() + end + + -- Check if zone is attacked. + if State ~= "Attacked" and self:IsAttacked() then + self:Attack() + end + + -- Check if zone is captured. + if State ~= "Captured" and self:IsCaptured() then + self:Capture() + end + + -- Count stuff in zone. + local unitset=self:GetScannedSetUnit() + local nRed=0 + local nBlue=0 + for _,object in pairs(unitset:GetSet()) do + local coal=object:GetCoalition() + if object:IsAlive() then + if coal==coalition.side.RED then + nRed=nRed+1 + elseif coal==coalition.side.BLUE then + nBlue=nBlue+1 + end + end + end + + -- Status text. + local text=string.format("CAPTURE ZONE %s: Owner=%s (Previous=%s): #blue=%d, #red=%d, Status %s", self:GetZoneName(), self:GetCoalitionName(), UTILS.GetCoalitionName(self:GetPreviousCoalition()), nBlue, nRed, State) + local NewState = self:GetState() + if NewState~=State then + text=text..string.format(" --> %s", NewState) + end + self:I(text) + + end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + --- Update Mark on F10 map. + -- @param #ZONE_CAPTURE_COALITION self + function ZONE_CAPTURE_COALITION:Mark() + + if self.MarkOn then + + local Coord = self:GetCoordinate() + local ZoneName = self:GetZoneName() + local State = self:GetState() + + -- Remove marks. + if self.MarkRed then + Coord:RemoveMark(self.MarkRed) + end + if self.MarkBlue then + Coord:RemoveMark(self.MarkBlue) + end + + -- Create new marks for each coaliton. + if self.Coalition == coalition.side.BLUE then + self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Blue\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) + self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Blue\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + elseif self.Coalition == coalition.side.RED then + self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Red\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State ) + self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Red\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + else + self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Neutral\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Neutral\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State ) + end + + end + + end + +end diff --git a/Moose Development/Moose/Functional/ZoneGoal.lua b/Moose Development/Moose/Functional/ZoneGoal.lua index 0caabc8a3..cd55b2603 100644 --- a/Moose Development/Moose/Functional/ZoneGoal.lua +++ b/Moose Development/Moose/Functional/ZoneGoal.lua @@ -8,6 +8,7 @@ -- === -- -- ### Author: **FlightControl** +-- ### Contributions: **funkyfranky** -- -- === -- @@ -17,10 +18,16 @@ do -- Zone --- @type ZONE_GOAL - -- @extends Core.Fsm#FSM + -- @field #string ClassName Name of the class. + -- @field Core.Goal#GOAL Goal The goal object. + -- @field #number SmokeTime Time stamp in seconds when the last smoke of the zone was triggered. + -- @field Core.Scheduler#SCHEDULER SmokeScheduler Scheduler responsible for smoking the zone. + -- @field #number SmokeColor Color of the smoke. + -- @field #boolean SmokeZone If true, smoke zone. + -- @extends Core.Zone#ZONE_RADIUS - -- Models processes that have a Goal with a defined achievement involving a Zone. + --- Models processes that have a Goal with a defined achievement involving a Zone. -- Derived classes implement the ways how the achievements can be realized. -- -- ## 1. ZONE_GOAL constructor @@ -39,24 +46,41 @@ do -- Zone -- -- @field #ZONE_GOAL ZONE_GOAL = { - ClassName = "ZONE_GOAL", + ClassName = "ZONE_GOAL", + Goal = nil, + SmokeTime = nil, + SmokeScheduler = nil, + SmokeColor = nil, + SmokeZone = nil, } --- ZONE_GOAL Constructor. -- @param #ZONE_GOAL self - -- @param Core.Zone#ZONE_BASE Zone A @{Zone} object with the goal to be achieved. + -- @param Core.Zone#ZONE_RADIUS Zone A @{Zone} object with the goal to be achieved. -- @return #ZONE_GOAL function ZONE_GOAL:New( Zone ) - local self = BASE:Inherit( self, FSM:New() ) -- #ZONE_GOAL + local self = BASE:Inherit( self, ZONE_RADIUS:New( Zone:GetName(), Zone:GetVec2(), Zone:GetRadius() ) ) -- #ZONE_GOAL self:F( { Zone = Zone } ) - self.Zone = Zone -- Core.Zone#ZONE_BASE + -- Goal object. self.Goal = GOAL:New() self.SmokeTime = nil + + -- Set smoke ON. + self:SetSmokeZone(true) self:AddTransition( "*", "DestroyedUnit", "*" ) + + --- DestroyedUnit event. + -- @function [parent=#ZONE_GOAL] DestroyedUnit + -- @param #ZONE_GOAL self + + --- DestroyedUnit delayed event + -- @function [parent=#ZONE_GOAL] __DestroyedUnit + -- @param #ZONE_GOAL self + -- @param #number delay Delay in seconds. --- DestroyedUnit Handler OnAfter for ZONE_GOAL -- @function [parent=#ZONE_GOAL] OnAfterDestroyedUnit @@ -70,52 +94,64 @@ do -- Zone return self end - --- Get the Zone + --- Get the Zone. -- @param #ZONE_GOAL self - -- @return Core.Zone#ZONE_BASE + -- @return #ZONE_GOAL function ZONE_GOAL:GetZone() - return self.Zone + return self end - --- Get the name of the ProtectZone + --- Get the name of the Zone. -- @param #ZONE_GOAL self -- @return #string function ZONE_GOAL:GetZoneName() - return self.Zone:GetName() + return self:GetName() end - --- Smoke the center of theh zone. + --- Activate smoking of zone with the color or the current owner. -- @param #ZONE_GOAL self - -- @param #SMOKECOLOR.Color SmokeColor - function ZONE_GOAL:Smoke( SmokeColor ) - + -- @param #boolean switch If *true* or *nil* activate smoke. If *false* or *nil*, no smoke. + -- @return #ZONE_GOAL + function ZONE_GOAL:SetSmokeZone(switch) + self.SmokeZone=switch + --[[ + if switch==nil or switch==true then + self.SmokeZone=true + else + self.SmokeZone=false + end + ]] + return self + end + + --- Set the smoke color. + -- @param #ZONE_GOAL self + -- @param DCS#SMOKECOLOR.Color SmokeColor + function ZONE_GOAL:Smoke( SmokeColor ) self:F( { SmokeColor = SmokeColor} ) self.SmokeColor = SmokeColor end - --- Flare the center of the zone. + --- Flare the zone boundary. -- @param #ZONE_GOAL self - -- @param #SMOKECOLOR.Color FlareColor + -- @param DCS#SMOKECOLOR.Color FlareColor function ZONE_GOAL:Flare( FlareColor ) - self.Zone:FlareZone( FlareColor, math.random( 1, 360 ) ) + self:FlareZone( FlareColor, 30) end --- When started, check the Smoke and the Zone status. -- @param #ZONE_GOAL self function ZONE_GOAL:onafterGuard() - - --self:GetParent( self ):onafterStart() - self:F("Guard") - - --self:ScheduleRepeat( 15, 15, 0.1, nil, self.StatusZone, self ) - if not self.SmokeScheduler then - self.SmokeScheduler = self:ScheduleRepeat( 1, 1, 0.1, nil, self.StatusSmoke, self ) + + -- Start smoke + if self.SmokeZone and not self.SmokeScheduler then + self.SmokeScheduler = self:ScheduleRepeat(1, 1, 0.1, nil, self.StatusSmoke, self) end end @@ -123,44 +159,54 @@ do -- Zone --- Check status Smoke. -- @param #ZONE_GOAL self function ZONE_GOAL:StatusSmoke() - self:F({self.SmokeTime, self.SmokeColor}) - local CurrentTime = timer.getTime() - - if self.SmokeTime == nil or self.SmokeTime + 300 <= CurrentTime then - if self.SmokeColor then - self.Zone:GetCoordinate():Smoke( self.SmokeColor ) - --self.SmokeColor = nil - self.SmokeTime = CurrentTime + if self.SmokeZone then + + -- Current time. + local CurrentTime = timer.getTime() + + -- Restart smoke every 5 min. + if self.SmokeTime == nil or self.SmokeTime + 300 <= CurrentTime then + if self.SmokeColor then + self:GetCoordinate():Smoke( self.SmokeColor ) + self.SmokeTime = CurrentTime + end end + end + end --- @param #ZONE_GOAL self - -- @param Core.Event#EVENTDATA EventData + -- @param Core.Event#EVENTDATA EventData Event data table. function ZONE_GOAL:__Destroyed( EventData ) self:F( { "EventDead", EventData } ) self:F( { EventData.IniUnit } ) - local Vec3 = EventData.IniDCSUnit:getPosition().p - self:F( { Vec3 = Vec3 } ) - local ZoneGoal = self:GetZone() - self:F({ZoneGoal}) - if EventData.IniDCSUnit then - if ZoneGoal:IsVec3InZone(Vec3) then + + local Vec3 = EventData.IniDCSUnit:getPosition().p + self:F( { Vec3 = Vec3 } ) + + if Vec3 and self:IsVec3InZone(Vec3) then + local PlayerHits = _DATABASE.HITS[EventData.IniUnitName] + if PlayerHits then + for PlayerName, PlayerHit in pairs( PlayerHits.Players or {} ) do self.Goal:AddPlayerContribution( PlayerName ) self:DestroyedUnit( EventData.IniUnitName, PlayerName ) end + end + end end + end diff --git a/Moose Development/Moose/Functional/ZoneGoalCoalition.lua b/Moose Development/Moose/Functional/ZoneGoalCoalition.lua index ba86ffe95..6296b4835 100644 --- a/Moose Development/Moose/Functional/ZoneGoalCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneGoalCoalition.lua @@ -17,6 +17,11 @@ do -- ZoneGoal --- @type ZONE_GOAL_COALITION + -- @field #string ClassName Name of the Class. + -- @field #number Coalition The current coalition ID of the zone owner. + -- @field #number PreviousCoalition The previous owner of the zone. + -- @field #table UnitCategories Table of unit categories that are able to capture and hold the zone. Default is only GROUND units. + -- @field #table ObjectCategories Table of object categories that are able to hold a zone. Default is UNITS and STATICS. -- @extends Functional.ZoneGoal#ZONE_GOAL @@ -37,7 +42,11 @@ do -- ZoneGoal -- -- @field #ZONE_GOAL_COALITION ZONE_GOAL_COALITION = { - ClassName = "ZONE_GOAL_COALITION", + ClassName = "ZONE_GOAL_COALITION", + Coalition = nil, + PreviousCoaliton = nil, + UnitCategories = nil, + ObjectCategories = nil, } --- @field #table ZONE_GOAL_COALITION.States @@ -46,27 +55,70 @@ do -- ZoneGoal --- ZONE_GOAL_COALITION Constructor. -- @param #ZONE_GOAL_COALITION self -- @param Core.Zone#ZONE Zone A @{Zone} object with the goal to be achieved. - -- @param DCSCoalition.DCSCoalition#coalition Coalition The initial coalition owning the zone. + -- @param DCSCoalition.DCSCoalition#coalition Coalition The initial coalition owning the zone. Default coalition.side.NEUTRAL. + -- @param #table UnitCategories Table of unit categories. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). Default {Unit.Category.GROUND_UNIT}. -- @return #ZONE_GOAL_COALITION - function ZONE_GOAL_COALITION:New( Zone, Coalition ) + function ZONE_GOAL_COALITION:New( Zone, Coalition, UnitCategories ) + if not Zone then + BASE:E("ERROR: No Zone specified in ZONE_GOAL_COALITON!") + return nil + end + + -- Inherit ZONE_GOAL. local self = BASE:Inherit( self, ZONE_GOAL:New( Zone ) ) -- #ZONE_GOAL_COALITION self:F( { Zone = Zone, Coalition = Coalition } ) - self:SetCoalition( Coalition ) - - + -- Set initial owner. + self:SetCoalition( Coalition or coalition.side.NEUTRAL) + + -- Set default unit and object categories for the zone scan. + self:SetUnitCategories(UnitCategories) + self:SetObjectCategories() + return self end --- Set the owning coalition of the zone. -- @param #ZONE_GOAL_COALITION self - -- @param DCSCoalition.DCSCoalition#coalition Coalition + -- @param DCSCoalition.DCSCoalition#coalition Coalition The coalition ID, e.g. *coalition.side.RED*. + -- @return #ZONE_GOAL_COALITION function ZONE_GOAL_COALITION:SetCoalition( Coalition ) + self.PreviousCoalition=self.Coalition or Coalition self.Coalition = Coalition + return self + end + + --- Set the owning coalition of the zone. + -- @param #ZONE_GOAL_COALITION self + -- @param #table UnitCategories Table of unit categories. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). Default {Unit.Category.GROUND_UNIT}. + -- @return #ZONE_GOAL_COALITION + function ZONE_GOAL_COALITION:SetUnitCategories( UnitCategories ) + + if UnitCategories and type(UnitCategories)~="table" then + UnitCategories={UnitCategories} + end + + self.UnitCategories=UnitCategories or {Unit.Category.GROUND_UNIT} + + return self end + --- Set the owning coalition of the zone. + -- @param #ZONE_GOAL_COALITION self + -- @param #table ObjectCategories Table of unit categories. See [DCS Class Object](https://wiki.hoggitworld.com/view/DCS_Class_Object). Default {Object.Category.UNIT, Object.Category.STATIC}, i.e. all UNITS and STATICS. + -- @return #ZONE_GOAL_COALITION + function ZONE_GOAL_COALITION:SetObjectCategories( ObjectCategories ) + + if ObjectCategories and type(ObjectCategories)~="table" then + ObjectCategories={ObjectCategories} + end + + self.ObjectCategories=ObjectCategories or {Object.Category.UNIT, Object.Category.STATIC} + + return self + end --- Get the owning coalition of the zone. -- @param #ZONE_GOAL_COALITION self @@ -75,37 +127,38 @@ do -- ZoneGoal return self.Coalition end + --- Get the previous coaliton, i.e. the one owning the zone before the current one. + -- @param #ZONE_GOAL_COALITION self + -- @return DCSCoalition.DCSCoalition#coalition Coalition. + function ZONE_GOAL_COALITION:GetPreviousCoalition() + return self.PreviousCoalition + end + --- Get the owning coalition name of the zone. -- @param #ZONE_GOAL_COALITION self -- @return #string Coalition name. function ZONE_GOAL_COALITION:GetCoalitionName() - - if self.Coalition == coalition.side.BLUE then - return "Blue" - end - - if self.Coalition == coalition.side.RED then - return "Red" - end - - if self.Coalition == coalition.side.NEUTRAL then - return "Neutral" - end - - return "" + return UTILS.GetCoalitionName(self.Coalition) end --- Check status Coalition ownership. -- @param #ZONE_GOAL_COALITION self + -- @return #ZONE_GOAL_COALITION function ZONE_GOAL_COALITION:StatusZone() + -- Get current state. local State = self:GetState() - self:F( { State = self:GetState() } ) - self.Zone:Scan( { Object.Category.UNIT, Object.Category.STATIC } ) + -- Debug text. + local text=string.format("Zone state=%s, Owner=%s, Scanning...", State, self:GetCoalitionName()) + self:F(text) + + -- Scan zone. + self:Scan( self.ObjectCategories, self.UnitCategories ) + return self end end diff --git a/Moose Development/Moose/Moose.lua b/Moose Development/Moose/Globals.lua similarity index 84% rename from Moose Development/Moose/Moose.lua rename to Moose Development/Moose/Globals.lua index 05f3b8cf3..bdde44b6d 100644 --- a/Moose Development/Moose/Moose.lua +++ b/Moose Development/Moose/Globals.lua @@ -5,7 +5,7 @@ _EVENTDISPATCHER = EVENT:New() -- Core.Event#EVENT --- Declare the timer dispatcher based on the SCHEDULEDISPATCHER class -_SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.Timer#SCHEDULEDISPATCHER +_SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.ScheduleDispatcher#SCHEDULEDISPATCHER --- Declare the main database object, which is used internally by the MOOSE classes. _DATABASE = DATABASE:New() -- Core.Database#DATABASE diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua new file mode 100644 index 000000000..22f23bb75 --- /dev/null +++ b/Moose Development/Moose/Modules.lua @@ -0,0 +1,125 @@ +__Moose.Include( 'Scripts/Moose/Utilities/Enums.lua' ) +__Moose.Include( 'Scripts/Moose/Utilities/Routines.lua' ) +__Moose.Include( 'Scripts/Moose/Utilities/Utils.lua' ) + +__Moose.Include( 'Scripts/Moose/Core/Base.lua' ) +__Moose.Include( 'Scripts/Moose/Core/UserFlag.lua' ) +__Moose.Include( 'Scripts/Moose/Core/UserSound.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Report.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Scheduler.lua' ) +__Moose.Include( 'Scripts/Moose/Core/ScheduleDispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Event.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Settings.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Menu.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Zone.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Zone_Detection.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Database.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Set.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Point.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Velocity.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Message.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Fsm.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Radio.lua' ) +__Moose.Include( 'Scripts/Moose/Core/RadioQueue.lua' ) +__Moose.Include( 'Scripts/Moose/Core/RadioSpeech.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Spawn.lua' ) +__Moose.Include( 'Scripts/Moose/Core/SpawnStatic.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Goal.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Spot.lua' ) + +__Moose.Include( 'Scripts/Moose/Wrapper/Object.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Identifiable.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Positionable.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Controllable.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Group.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Unit.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Client.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Static.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Airbase.lua' ) +__Moose.Include( 'Scripts/Moose/Wrapper/Scenery.lua' ) + +__Moose.Include( 'Scripts/Moose/Cargo/Cargo.lua' ) +__Moose.Include( 'Scripts/Moose/Cargo/CargoUnit.lua' ) +__Moose.Include( 'Scripts/Moose/Cargo/CargoSlingload.lua' ) +__Moose.Include( 'Scripts/Moose/Cargo/CargoCrate.lua' ) +__Moose.Include( 'Scripts/Moose/Cargo/CargoGroup.lua' ) + +__Moose.Include( 'Scripts/Moose/Functional/Scoring.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/CleanUp.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Movement.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Sead.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Escort.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/MissileTrainer.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/ATC_Ground.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Detection.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/DetectionZones.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Designate.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/RAT.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Range.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/ZoneGoal.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/ZoneGoalCoalition.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/ZoneCaptureCoalition.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Artillery.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Suppression.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/PseudoATC.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Warehouse.lua' ) +__Moose.Include( 'Scripts/Moose/Functional/Fox.lua' ) + +__Moose.Include( 'Scripts/Moose/Ops/Airboss.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/RecoveryTanker.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/RescueHelo.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/ATIS.lua' ) + +__Moose.Include( 'Scripts/Moose/AI/AI_Balancer.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Air.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Air_Patrol.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Air_Engage.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2A_Patrol.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2A_Cap.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2A_Gci.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2A_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_BAI.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_CAS.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_SEAD.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_A2G_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Patrol.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cap.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cas.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Bai.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Formation.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Escort.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Escort_Request.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Escort_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Escort_Dispatcher_Request.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_APC.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Helicopter.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Airplane.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher_APC.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua' ) +__Moose.Include( 'Scripts/Moose/AI/AI_Cargo_Dispatcher_Airplane.lua' ) + +__Moose.Include( 'Scripts/Moose/Actions/Act_Assign.lua' ) +__Moose.Include( 'Scripts/Moose/Actions/Act_Route.lua' ) +__Moose.Include( 'Scripts/Moose/Actions/Act_Account.lua' ) +__Moose.Include( 'Scripts/Moose/Actions/Act_Assist.lua' ) + +__Moose.Include( 'Scripts/Moose/Tasking/CommandCenter.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Mission.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/TaskInfo.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Manager.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/DetectionManager.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_A2G_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_A2G.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_A2A_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_A2A.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Cargo.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Cargo_Transport.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Cargo_CSAR.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Cargo_Dispatcher.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Capture_Zone.lua' ) +__Moose.Include( 'Scripts/Moose/Tasking/Task_Capture_Dispatcher.lua' ) + +__Moose.Include( 'Scripts/Moose/Globals.lua' ) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua new file mode 100644 index 000000000..db2fa22a9 --- /dev/null +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -0,0 +1,2177 @@ +--- **Ops** - Automatic Terminal Information Service (ATIS). +-- +-- === +-- +-- **Main Features:** +-- +-- * Wind direction and speed +-- * Visibility +-- * Cloud coverage, base and ceiling +-- * Temprature +-- * Dew point (approximate as there is no relative humidity in DCS yet) +-- * Pressure QNH/QFE +-- * Weather phenomena: rain, thunderstorm, fog, dust +-- * Active runway based on wind direction +-- * Tower frequencies +-- * More than 180 voice overs +-- * Airbase names pronounced in locale accent (russian, US, french, arabic) +-- * Option to present information in imperial or metric units +-- * Runway length and airfield elevation (optional) +-- * Frequencies/channels of nav aids (ILS, VOR, NDB, TACAN, PRMG, RSBN) (optional) +-- +-- === +-- +-- ## Youtube Videos: +-- +-- * [ATIS v0.1 Caucasus - Batumi (WIP)](https://youtu.be/MdH9FmbNabo) +-- * [ATIS v0.2 Nevada - Nellis AFB (WIP)](https://youtu.be/8CT_9AoPrTk) +-- * [ATIS v0.3 Persion Gulf - Abu Dhabi/Dubai International](https://youtu.be/NjkKvPz6ovM) +-- * [ATIS Airport Names Sound Check](https://youtu.be/qIE_OUQNAc0) +-- +-- === +-- +-- ## Missions: Example missions can be found [here](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20ATIS) +-- +-- === +-- +-- ## Sound files: Check out the pinned messages in the Moose discord #ops-atis channel. +-- +-- === +-- +-- Automatic terminal information service, or ATIS, is a continuous broadcast of recorded aeronautical information in busier terminal areas, *i.e.* airports and their immediate surroundings. +-- ATIS broadcasts contain essential information, such as current weather information, active runways, and any other information required by the pilots. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Ops.Atis +-- @image OPS_ATIS.png + + +--- ATIS class. +-- @type ATIS +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string theatre DCS map name. +-- @field #string airbasename The name of the airbase. +-- @field Wrapper.Airbase#AIRBASE airbase The airbase object. +-- @field #number frequency Radio frequency in MHz. +-- @field #number modulation Radio modulation 0=AM or 1=FM. +-- @field #number power Radio power in Watts. Default 100 W. +-- @field Core.RadioQueue#RADIOQUEUE radioqueue Radio queue for broadcasing messages. +-- @field #string soundpath Path to sound files. +-- @field #string relayunitname Name of the radio relay unit. +-- @field #table towerfrequency Table with tower frequencies. +-- @field #string activerunway The active runway specified by the user. +-- @field #number subduration Duration how long subtitles are displayed in seconds. +-- @field #boolean metric If true, use metric units. If false, use imperial (default). +-- @field #boolean PmmHg If true, give pressure in millimeters of Mercury. Default is inHg for imperial and hecto Pascal (=mili Bars) for metric units. +-- @field #boolean TDegF If true, give temperature in degrees Fahrenheit. Default is in degrees Celsius independent of chosen unit system. +-- @field #number zuludiff Time difference local vs. zulu in hours. +-- @field #number magvar Magnetic declination/variation at the airport in degrees. +-- @field #table ils Table of ILS frequencies (can be runway specific). +-- @field #table ndbinner Table of inner NDB frequencies (can be runway specific). +-- @field #table ndbouter Table of outer NDB frequencies (can be runway specific). +-- @field #number tacan TACAN channel. +-- @field #number vor VOR frequency. +-- @field #number rsbn RSBN channel. +-- @field #table prmg PRMG channels (can be runway specific). +-- @field #boolean rwylength If true, give info on runway length. +-- @field #boolean elevation If true, give info on airfield elevation. +-- @field #table runwaymag Table of magnetic runway headings. +-- @field #number runwaym2t Optional correction for magnetic to true runway heading conversion (and vice versa) in degrees. +-- @field #boolean windtrue Report true (from) heading of wind. Default is magnetic. +-- @field #boolean altimeterQNH Report altimeter QNH. +-- @field #boolean usemarker Use mark on the F10 map. +-- @field #number markerid Numerical ID of the F10 map mark point. +-- @field #number relHumidity Relative humidity (used to approximately calculate the dew point). +-- @extends Core.Fsm#FSM + +--- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde +-- +-- === +-- +-- ![Banner Image](..\Presentations\ATIS\ATIS_Main.png) +-- +-- # The ATIS Concept +-- +-- Automatic terminal information service, or ATIS, is a continuous broadcast of recorded aeronautical information in busier terminal areas, *i.e.* airports and their immediate surroundings. +-- ATIS broadcasts contain essential information, such as current weather information, active runways, and any other information required by the pilots. +-- +-- # DCS Limitations +-- +-- Unfortunately, the DCS API only allow to get the temperature, pressure as well as wind direction and speed. Therefore, some other information such as cloud coverage, base and ceiling are not available +-- when dynamic weather is used. +-- +-- # Scripting +-- +-- The lua script to create an ATIS at an airport is pretty easy: +-- +-- -- ATIS at Batumi Airport on 143.00 MHz AM. +-- atisBatumi=ATIS:New("Batumi", 143.00) +-- atisBatumi:Start() +-- +-- The @{#ATIS.New}(*airbasename*, *frequency*) creates a new ATIS object. The parameter *airbasename* is the name of the airbase or airport. Note that this has to be spelled exactly as in the DCS mission editor. +-- The parameter *frequency* is the frequency the ATIS broadcasts in MHz. +-- +-- Broadcasting is started via the @{#ATIS.Start}() function. The start can be delayed by useing @{#ATIS.__Start}(*delay*), where *delay* is the delay in seconds. +-- +-- ## Subtitles +-- +-- Currently, DCS allows for displaying subtitles of radio transmissions only from airborne units, *i.e.* airplanes and helicopters. Therefore, if you want to have subtitles, it is necessary to place an +-- additonal aircraft on the ATIS airport and set it to uncontrolled. This unit can then function as a radio relay to transmit messages with subtitles. These subtitles will only be displayed, if the +-- player has tuned in the correct ATIS frequency. +-- +-- Radio transmissions via an airborne unit can be set via the @{#ATIS.SetRadioRelayUnitName}(*unitname*) function, where the parameter *unitname* is the name of the unit passed as string, *e.g.* +-- +-- atisBatumi:SetRadioRelayUnitName("Radio Relay Batumi") +-- +-- With a unit set in the mission editor with name "Radio Relay Batumi". +-- +-- **Note** that you should use a different relay unit for each ATIS! +-- +-- By default, subtitles are displayed for 10 seconds. This can be changed using @{#ATIS.SetSubtitleDuration}(*duration*) with *duration* being the duration in seconds. +-- Setting a *duration* of 0 will completely disable all subtitles. +-- +-- ## Active Runway +-- +-- By default, the currently active runway is determined automatically by analysing the wind direction. Therefore, you should obviously set the wind speed to be greater zero in your mission. +-- +-- Note however, there are a few special cases, where automatic detection does not yield the correct or desired result. +-- For example, there are airports with more than one runway facing in the same direction (usually denoted left and right). In this case, there is obviously no *unique* result depending on the wind vector. +-- +-- If the automatic runway detection fails, the active runway can be specified manually in the script via the @{#ATIS.SetActiveRunway}(*runway*) function. +-- The parameter *runway* is a string which can be used to specify the runway heading and, if applicable, whether the left or right runway is in use. +-- +-- For example, setting runway 21L would be +-- +-- atisNellis:SetActiveRunway("21L") +-- +-- The script will examine the string and search for the characters "L" (left) and "R" (right). +-- +-- If only left or right should be set and the direction determined by the wind vector, the runway heading can be left out, *e.g.* +-- +-- atisAbuDhabi:SetActiveRunway("L") +-- +-- The first two digits of the runway are determined by converting the *true* runway heading into its magnetic heading. The magnetic declination (or variation) is assumed to be constant on the given map. +-- An explicit correction factor can be set via @{#ATIS.SetRunwayCorrectionMagnetic2True}. +-- +-- ## Tower Frequencies +-- +-- The tower frequency (or frequencies) can also be included in the ATIS information. However, there is no way to get these automatically. Therefore, it is necessary to manually specify them in the script via the +-- @{#ATIS.SetTowerFrequencies}(*frequencies*) function. The parameter *frequencies* can be a plain number if only one frequency is necessary or it can be a table of frequencies. +-- +-- ## Nav Aids +-- +-- Frequencies or channels of navigation aids can be specified by the user and are then provided as additional information. Unfortunately, it is **not possible** to aquire this information via the DCS API +-- we have access to. +-- +-- As they say, all road lead to Rome but (for me) the easiest way to obtain the available nav aids data of an airport, is to start a mission and click on an airport symbol. +-- +-- For example, the *AIRDROME DATA* for **Batumi** reads: +-- +-- * **TACAN** *16X* - set via @{#ATIS.SetTACAN} +-- * **VOR** *N/A* - set via @{#ATIS.SetVOR} +-- * **RSBN** *N/A* - set via @{#ATIS.SetRSBN} +-- * **ATC** *260.000*, *131.000*, *40.400*, *4.250* - set via @{#ATIS.SetTowerFrequencies} +-- * **Runways** *31* and *13* - automatic but can be set manually via @{#ATIS.SetRunwayHeadingsMagnetic} +-- * **ILS** *110.30* for runway *13* - set via @{#ATIS.AddILS} +-- * **PRMG** *N/A* - set via @{#ATIS.AddPRMG} +-- * **OUTER NDB** *N/A* - set via @{#ATIS.AddNDBouter} +-- * **INNER NDB** *N/A* - set via @{#ATIS.AddNDBinner} +-- +-- ![Banner Image](..\Presentations\ATIS\NavAid_Batumi.png) +-- +-- And the *AIRDROME DATA* for **Kobuleti** reads: +-- +-- * **TACAN** *67X* - set via @{#ATIS.SetTACAN} +-- * **VOR** *N/A* - set via @{#ATIS.SetVOR} +-- * **RSBN** *N/A* - set via @{#ATIS.SetRSBN} +-- * **ATC** *262.000*, *133.000*, *40.800*, *4.350* - set via @{#ATIS.SetTowerFrequencies} +-- * **Runways** *25* and *07* - automatic but can be set manually via @{#ATIS.SetRunwayHeadingsMagnetic} +-- * **ILS** *111.50* for runway *07* - set via @{#ATIS.AddILS} +-- * **PRMG** *N/A* - set via @{#ATIS.AddPRMG} +-- * **OUTER NDB** *870.00* - set via @{#ATIS.AddNDBouter} +-- * **INNER NDB** *490.00* - set via @{#ATIS.AddNDBinner} +-- +-- ![Banner Image](..\Presentations\ATIS\NavAid_Kobuleti.png) +-- +-- ### TACAN +-- +-- The TACtical Air Navigation system [(TACAN)](https://en.wikipedia.org/wiki/Tactical_air_navigation_system) channel can be set via the @{#ATIS.SetTACAN}(*channel*) function, where *channel* is the TACAN channel. Band is always assumed to be X-ray. +-- +-- ### VOR +-- +-- The Very high frequency Omni-directional Range [(VOR)](https://en.wikipedia.org/wiki/VHF_omnidirectional_range) frequency can be set via the @{#ATIS.SetVOR}(*frequency*) function, where *frequency* is the VOR frequency. +-- +-- ### ILS +-- +-- The Instrument Landing System [(ILS)](https://en.wikipedia.org/wiki/Instrument_landing_system) frequency can be set via the @{#ATIS.AddILS}(*frequency*, *runway*) function, where *frequency* is the ILS frequency and *runway* the two letter string of the corresponding runway, *e.g.* "31". +-- If the parameter *runway* is omitted (nil) then the frequency is supposed to be valid for all runways of the airport. +-- +-- ### NDB +-- +-- Inner and outer Non-Directional (radio) Beacons [NDBs](https://en.wikipedia.org/wiki/Non-directional_beacon) can be set via the @{#ATIS.AddNDBinner}(*frequency*, *runway*) and @{#ATIS.AddNDBouter}(*frequency*, *runway*) functions, respectively. +-- +-- In both cases, the parameter *frequency* is the NDB frequency and *runway* the two letter string of the corresponding runway, *e.g.* "31". +-- If the parameter *runway* is omitted (nil) then the frequency is supposed to be valid for all runways of the airport. +-- +-- ## RSBN +-- +-- The RSBN channel can be set via the @{#ATIS.SetRSBN}(*channel*) function. +-- +-- ## PRMG +-- +-- The PRMG channel can be set via the @{#ATIS.AddPRMG}(*channel*, *runway*) function for each *runway*. +-- +-- ## Unit System +-- +-- By default, information is given in imperial units, *i.e.* wind speed in knots, pressure in inches of mercury, visibility in Nautical miles, etc. +-- +-- If you prefer metric units, you can enable this via the @{#ATIS.SetMetricUnits}() function, +-- +-- atisBatumi:SetMetricUnits() +-- +-- With this, wind speed is given in meters per second, pressure in hecto Pascal (mbar), visibility in kilometers etc. +-- +-- # Sound Files +-- +-- More than 180 individual sound files have been created using a text-to-speech program. All ATIS information is given with en-US accent. +-- +-- Check out the pinned messages in the Moose discord #ops-atis channel. +-- +-- To include the files, open the mission (.miz) file with, *e.g.*, 7-zip. Then just drag-n-drop the file into the miz. +-- +-- ![Banner Image](..\Presentations\ATIS\ATIS_SoundFolder.png) +-- +-- **Note** that the default folder name is *ATIS Soundfiles/*. If you want to change it, you can use the @{#ATIS.SetSoundfilesPath}(*path*), where *path* is the path of the directory. This must end with a slash "/"! +-- +-- # Marks on the F10 Map +-- +-- You can place marks on the F10 map via the @{#ATIS.SetMapMarks}() function. These will contain info about the ATIS frequency, the currently active runway and some basic info about the weather (wind, pressure and temperature). +-- +-- # Examples +-- +-- ## Caucasus: Batumi +-- +-- -- ATIS Batumi Airport on 143.00 MHz AM. +-- atisBatumi=ATIS:New(AIRBASE.Caucasus.Batumi, 143.00) +-- atisBatumi:SetRadioRelayUnitName("Radio Relay Batumi") +-- atisBatumi:Start() +-- +-- ## Nevada: Nellis AFB +-- +-- -- ATIS Nellis AFB on 270.10 MHz AM. +-- atisNellis=ATIS:New(AIRBASE.Nevada.Nellis_AFB, 270.1) +-- atisNellis:SetRadioRelayUnitName("Radio Relay Nellis") +-- atisNellis:SetActiveRunway("21L") +-- atisNellis:SetTowerFrequencies({327.000, 132.550}) +-- atisNellis:SetTACAN(12) +-- atisNellis:AddILS(109.1, "21") +-- atisNellis:Start() +-- +-- ## Persian Gulf: Abu Dhabi International Airport +-- +-- -- ATIS Abu Dhabi International on 125.1 MHz AM. +-- atisAbuDhabi=ATIS:New(AIRBASE.PersianGulf.Abu_Dhabi_International_Airport, 125.1) +-- atisAbuDhabi:SetRadioRelayUnitName("Radio Relay Abu Dhabi International Airport") +-- atisAbuDhabi:SetMetricUnits() +-- atisAbuDhabi:SetActiveRunway("L") +-- atisAbuDhabi:SetTowerFrequencies({250.5, 119.2}) +-- atisAbuDhabi:SetVOR(114.25) +-- atisAbuDhabi:Start() +-- +-- +-- @field #ATIS +ATIS = { + ClassName = "ATIS", + Debug = false, + lid = nil, + theatre = nil, + airbasename = nil, + airbase = nil, + frequency = nil, + modulation = nil, + power = nil, + radioqueue = nil, + soundpath = nil, + relayunitname = nil, + towerfrequency = nil, + activerunway = nil, + subduration = nil, + metric = nil, + PmmHg = nil, + TDegF = nil, + zuludiff = nil, + magvar = nil, + ils = {}, + ndbinner = {}, + ndbouter = {}, + vor = nil, + tacan = nil, + rsbn = nil, + prmg = {}, + rwylength = nil, + elevation = nil, + runwaymag = {}, + runwaym2t = nil, + windtrue = nil, + altimeterQNH = nil, + usemarker = nil, + markerid = nil, + relHumidity = nil, +} + +--- NATO alphabet. +-- @type ATIS.Alphabet +ATIS.Alphabet = { + [1] = "Alfa", + [2] = "Bravo", + [3] = "Charlie", + [4] = "Delta", + [5] = "Echo", + [6] = "Delta", + [7] = "Echo", + [8] = "Foxtrot", + [9] = "Golf", + [10] = "Hotel", + [11] = "India", + [12] = "Juliett", + [13] = "Kilo", + [14] = "Lima", + [15] = "Mike", + [16] = "November", + [17] = "Oscar", + [18] = "Papa", + [19] = "Quebec", + [20] = "Romeo", + [21] = "Sierra", + [22] = "Tango", + [23] = "Uniform", + [24] = "Victor", + [25] = "Whiskey", + [26] = "Xray", + [27] = "Yankee", + [28] = "Zulu", +} + +--- Runway correction for converting true to magnetic heading. +-- @type ATIS.RunwayM2T +-- @field #number Caucasus 0° (East). +-- @field #number Nevada +12° (East). +-- @field #number Normandy -10° (West). +-- @field #number PersianGulf +2° (East). +ATIS.RunwayM2T={ + Caucasus=0, + Nevada=12, + Normany=-10, + PersianGulf=2, +} + +--- Nav point data. +-- @type ATIS.NavPoint +-- @field #number frequency Nav point frequency. +-- @field #string runway Runway, *e.g.* "21". +-- @field #boolean leftright If true, runway has left "L" and right "R" runways. + +--- Sound file data. +-- @type ATIS.Soundfile +-- @field #string filename Name of the file +-- @field #number duration Duration in seconds. + +--- Sound files. +-- @type ATIS.Sound +-- @field #ATIS.Soundfile ActiveRunway +-- @field #ATIS.Soundfile AdviceOnInitial +-- @field #ATIS.Soundfile Airport +-- @field #ATIS.Soundfile Altimeter +-- @field #ATIS.Soundfile At +-- @field #ATIS.Soundfile CloudBase +-- @field #ATIS.Soundfile CloudCeiling +-- @field #ATIS.Soundfile CloudsBroken +-- @field #ATIS.Soundfile CloudsFew +-- @field #ATIS.Soundfile CloudsNo +-- @field #ATIS.Soundfile CloudsNotAvailable +-- @field #ATIS.Soundfile CloudsOvercast +-- @field #ATIS.Soundfile CloudsScattered +-- @field #ATIS.Soundfile Decimal +-- @field #ATIS.Soundfile DegreesCelsius +-- @field #ATIS.Soundfile DegreesFahrenheit +-- @field #ATIS.Soundfile DewPoint +-- @field #ATIS.Soundfile Dust +-- @field #ATIS.Soundfile Elevation +-- @field #ATIS.Soundfile EndOfInformation +-- @field #ATIS.Soundfile Feet +-- @field #ATIS.Soundfile Fog +-- @field #ATIS.Soundfile Gusting +-- @field #ATIS.Soundfile HectoPascal +-- @field #ATIS.Soundfile Hundred +-- @field #ATIS.Soundfile InchesOfMercury +-- @field #ATIS.Soundfile Information +-- @field #ATIS.Soundfile Kilometers +-- @field #ATIS.Soundfile Knots +-- @field #ATIS.Soundfile Left +-- @field #ATIS.Soundfile MegaHertz +-- @field #ATIS.Soundfile Meters +-- @field #ATIS.Soundfile MetersPerSecond +-- @field #ATIS.Soundfile MillimetersOfMercury +-- @field #ATIS.Soundfile N0 +-- @field #ATIS.Soundfile N1 +-- @field #ATIS.Soundfile N2 +-- @field #ATIS.Soundfile N3 +-- @field #ATIS.Soundfile N4 +-- @field #ATIS.Soundfile N5 +-- @field #ATIS.Soundfile N6 +-- @field #ATIS.Soundfile N7 +-- @field #ATIS.Soundfile N8 +-- @field #ATIS.Soundfile N9 +-- @field #ATIS.Soundfile NauticalMiles +-- @field #ATIS.Soundfile None +-- @field #ATIS.Soundfile QFE +-- @field #ATIS.Soundfile QNH +-- @field #ATIS.Soundfile Rain +-- @field #ATIS.Soundfile Right +-- @field #ATIS.Soundfile Snow +-- @field #ATIS.Soundfile SnowStorm +-- @field #ATIS.Soundfile Temperature +-- @field #ATIS.Soundfile Thousand +-- @field #ATIS.Soundfile ThunderStorm +-- @field #ATIS.Soundfile TimeLocal +-- @field #ATIS.Soundfile TimeZulu +-- @field #ATIS.Soundfile TowerFrequency +-- @field #ATIS.Soundfile Visibilty +-- @field #ATIS.Soundfile WeatherPhenomena +-- @field #ATIS.Soundfile WindFrom +-- @field #ATIS.Soundfile ILSFrequency +-- @field #ATIS.Soundfile InnerNDBFrequency +-- @field #ATIS.Soundfile OuterNDBFrequency +-- @field #ATIS.Soundfile PRMGChannel +-- @field #ATIS.Soundfile RSBNChannel +-- @field #ATIS.Soundfile RunwayLength +-- @field #ATIS.Soundfile TACANChannel +-- @field #ATIS.Soundfile VORFrequency +ATIS.Sound = { + ActiveRunway={filename="ActiveRunway.ogg", duration=0.99}, + AdviceOnInitial={filename="AdviceOnInitial.ogg", duration=3.00}, + Airport={filename="Airport.ogg", duration=0.66}, + Altimeter={filename="Altimeter.ogg", duration=0.68}, + At={filename="At.ogg", duration=0.41}, + CloudBase={filename="CloudBase.ogg", duration=0.82}, + CloudCeiling={filename="CloudCeiling.ogg", duration=0.61}, + CloudsBroken={filename="CloudsBroken.ogg", duration=1.07}, + CloudsFew={filename="CloudsFew.ogg", duration=0.99}, + CloudsNo={filename="CloudsNo.ogg", duration=1.01}, + CloudsNotAvailable={filename="CloudsNotAvailable.ogg", duration=2.35}, + CloudsOvercast={filename="CloudsOvercast.ogg", duration=0.83}, + CloudsScattered={filename="CloudsScattered.ogg", duration=1.18}, + Decimal={filename="Decimal.ogg", duration=0.54}, + DegreesCelsius={filename="DegreesCelsius.ogg", duration=1.27}, + DegreesFahrenheit={filename="DegreesFahrenheit.ogg", duration=1.23}, + DewPoint={filename="DewPoint.ogg", duration=0.65}, + Dust={filename="Dust.ogg", duration=0.54}, + Elevation={filename="Elevation.ogg", duration=0.78}, + EndOfInformation={filename="EndOfInformation.ogg", duration=1.15}, + Feet={filename="Feet.ogg", duration=0.45}, + Fog={filename="Fog.ogg", duration=0.47}, + Gusting={filename="Gusting.ogg", duration=0.55}, + HectoPascal={filename="HectoPascal.ogg", duration=1.15}, + Hundred={filename="Hundred.ogg", duration=0.47}, + InchesOfMercury={filename="InchesOfMercury.ogg", duration=1.16}, + Information={filename="Information.ogg", duration=0.85}, + Kilometers={filename="Kilometers.ogg", duration=0.78}, + Knots={filename="Knots.ogg", duration=0.59}, + Left={filename="Left.ogg", duration=0.54}, + MegaHertz={filename="MegaHertz.ogg", duration=0.87}, + Meters={filename="Meters.ogg", duration=0.59}, + MetersPerSecond={filename="MetersPerSecond.ogg", duration=1.14}, + MillimetersOfMercury={filename="MillimetersOfMercury.ogg", duration=1.53}, + Minus={filename="Minus.ogg", duration=0.64}, + N0={filename="N-0.ogg", duration=0.55}, + N1={filename="N-1.ogg", duration=0.41}, + N2={filename="N-2.ogg", duration=0.37}, + N3={filename="N-3.ogg", duration=0.41}, + N4={filename="N-4.ogg", duration=0.37}, + N5={filename="N-5.ogg", duration=0.43}, + N6={filename="N-6.ogg", duration=0.55}, + N7={filename="N-7.ogg", duration=0.43}, + N8={filename="N-8.ogg", duration=0.38}, + N9={filename="N-9.ogg", duration=0.55}, + NauticalMiles={filename="NauticalMiles.ogg", duration=1.04}, + None={filename="None.ogg", duration=0.43}, + QFE={filename="QFE.ogg", duration=0.63}, + QNH={filename="QNH.ogg", duration=0.71}, + Rain={filename="Rain.ogg", duration=0.41}, + Right={filename="Right.ogg", duration=0.44}, + Snow={filename="Snow.ogg", duration=0.48}, + SnowStorm={filename="SnowStorm.ogg", duration=0.82}, + Temperature={filename="Temperature.ogg", duration=0.64}, + Thousand={filename="Thousand.ogg", duration=0.55}, + ThunderStorm={filename="ThunderStorm.ogg", duration=0.81}, + TimeLocal={filename="TimeLocal.ogg", duration=0.90}, + TimeZulu={filename="TimeZulu.ogg", duration=0.86}, + TowerFrequency={filename="TowerFrequency.ogg", duration=1.19}, + Visibilty={filename="Visibility.ogg", duration=0.79}, + WeatherPhenomena={filename="WeatherPhenomena.ogg", duration=1.07}, + WindFrom={filename="WindFrom.ogg", duration=0.60}, + ILSFrequency={filename="ILSFrequency.ogg", duration=1.30}, + InnerNDBFrequency={filename="InnerNDBFrequency.ogg", duration=1.56}, + OuterNDBFrequency={filename="OuterNDBFrequency.ogg", duration=1.59}, + RunwayLength={filename="RunwayLength.ogg", duration=0.91}, + VORFrequency={filename="VORFrequency.ogg", duration=1.38}, + TACANChannel={filename="TACANChannel.ogg", duration=0.88}, + PRMGChannel={filename="PRMGChannel.ogg", duration=1.18}, + RSBNChannel={filename="RSBNChannel.ogg", duration=1.14}, +} + + +--- ATIS table containing all defined ATISes. +-- @field #table _ATIS +_ATIS={} + +--- ATIS class version. +-- @field #string version +ATIS.version="0.7.1" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add new Normany airfields. +-- TODO: Zulu time --> Zulu in output. +-- TODO: Correct fog for elevation. +-- DONE: Add text report for output. +-- DONE: Add stop FMS functions. +-- NOGO: Use local time. Not realisitc! +-- DONE: Dew point. Approx. done. +-- DONE: Metric units. +-- DONE: Set UTC correction. +-- DONE: Set magnetic variation. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new ATIS class object for a specific aircraft carrier unit. +-- @param #ATIS self +-- @param #string airbasename Name of the airbase. +-- @param #number frequency Radio frequency in MHz. Default 143.00 MHz. +-- @param #number modulation Radio modulation: 0=AM, 1=FM. Default 0=AM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators +-- @return #ATIS self +function ATIS:New(airbasename, frequency, modulation) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #ATIS + + self.airbasename=airbasename + self.airbase=AIRBASE:FindByName(airbasename) + + if self.airbase==nil then + self:E("ERROR: Airbase %s for ATIS could not be found!", tostring(airbasename)) + return nil + end + + -- Default freq and modulation. + self.frequency=frequency or 143.00 + self.modulation=modulation or 0 + + -- Get map. + self.theatre=env.mission.theatre + + -- Set some string id for output to DCS.log file. + self.lid=string.format("ATIS %s | ", self.airbasename) + + -- This is just to hinder the garbage collector deallocating the ATIS object. + _ATIS[#_ATIS+1]=self + + -- Defaults: + self:SetSoundfilesPath() + self:SetSubtitleDuration() + self:SetMagneticDeclination() + self:SetRunwayCorrectionMagnetic2True() + self:SetRadioPower() + self:SetAltimeterQNH(true) + self:SetMapMarks(false) + self:SetRelativeHumidity() + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start FSM. + self:AddTransition("*", "Status", "*") -- Update status. + self:AddTransition("*", "Broadcast", "*") -- Broadcast ATIS message. + self:AddTransition("*", "CheckQueue", "*") -- Check if radio queue is empty. + self:AddTransition("*", "Report", "*") -- Report ATIS text. + self:AddTransition("*", "Stop", "Stopped") -- Stop. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the ATIS. + -- @function [parent=#ATIS] Start + -- @param #ATIS self + + --- Triggers the FSM event "Start" after a delay. + -- @function [parent=#ATIS] __Start + -- @param #ATIS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop". Stops the ATIS. + -- @function [parent=#ATIS] Stop + -- @param #ATIS self + + --- Triggers the FSM event "Stop" after a delay. + -- @function [parent=#ATIS] __Stop + -- @param #ATIS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status". + -- @function [parent=#ATIS] Status + -- @param #ATIS self + + --- Triggers the FSM event "Status" after a delay. + -- @function [parent=#ATIS] __Status + -- @param #ATIS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Broadcast". + -- @function [parent=#ATIS] Broadcast + -- @param #ATIS self + + --- Triggers the FSM event "Broadcast" after a delay. + -- @function [parent=#ATIS] __Broadcast + -- @param #ATIS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "CheckQueue". + -- @function [parent=#ATIS] CheckQueue + -- @param #ATIS self + + --- Triggers the FSM event "CheckQueue" after a delay. + -- @function [parent=#ATIS] __CheckQueue + -- @param #ATIS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Report". + -- @function [parent=#ATIS] Report + -- @param #ATIS self + -- @param #string Text Report text. + + --- Triggers the FSM event "Report" after a delay. + -- @function [parent=#ATIS] __Report + -- @param #ATIS self + -- @param #number delay Delay in seconds. + -- @param #string Text Report text. + + --- On after "Report" event user function. + -- @function [parent=#ATIS] OnAfterReport + -- @param #ATIS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string Text Report text. + + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set sound files folder within miz file. +-- @param #ATIS self +-- @param #string path Path for sound files. Default "ATIS Soundfiles/". Mind the slash "/" at the end! +-- @return #ATIS self +function ATIS:SetSoundfilesPath(path) + self.soundpath=tostring(path or "ATIS Soundfiles/") + self:I(self.lid..string.format("Setting sound files path to %s", self.soundpath)) + return self +end + +--- Set airborne unit (airplane or helicopter), used to transmit radio messages including subtitles. +-- Best is to place the unit on a parking spot of the airbase and set it to *uncontrolled* in the mission editor. +-- @param #ATIS self +-- @param #string unitname Name of the unit. +-- @return #ATIS self +function ATIS:SetRadioRelayUnitName(unitname) + self.relayunitname=unitname + self:I(self.lid..string.format("Setting radio relay unit to %s", self.relayunitname)) + return self +end + +--- Set tower frequencies. +-- @param #ATIS self +-- @param #table freqs Table of frequencies in MHz. A single frequency can be given as a plain number (*i.e.* must not be table). +-- @return #ATIS self +function ATIS:SetTowerFrequencies(freqs) + if type(freqs)=="table" then + -- nothing to do + else + freqs={freqs} + end + self.towerfrequency=freqs + return self +end + +--- Set active runway. This can be used if the automatic runway determination via the wind direction gives incorrect results. +-- For example, use this if there are two runways with the same directions. +-- @param #ATIS self +-- @param #string runway Active runway, *e.g.* "31L". +-- @return #ATIS self +function ATIS:SetActiveRunway(runway) + self.activerunway=tostring(runway) + return self +end + +--- Give information on runway length. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetRunwayLength() + self.rwylength=true + return self +end + +--- Give information on airfield elevation +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetElevation() + self.elevation=true + return self +end + +--- Set radio power. Note that this only applies if no relay unit is used. +-- @param #ATIS self +-- @param #number power Radio power in Watts. Default 100 W. +-- @return #ATIS self +function ATIS:SetRadioPower(power) + self.power=power or 100 + return self +end + +--- Use F10 map mark points. +-- @param #ATIS self +-- @param #boolean switch If *true* or *nil*, marks are placed on F10 map. If *false* this feature is set to off (default). +-- @return #ATIS self +function ATIS:SetMapMarks(switch) + if switch==nil or switch==true then + self.usemarker=true + else + self.usemarker=false + end + return self +end + +--- Set magnetic runway headings as depicted on the runway, *e.g.* "13" for 130° or "25L" for the left runway with magnetic heading 250°. +-- @param #ATIS self +-- @param #table headings Magnetic headings. Inverse (-180°) headings are added automatically. You only need to specify one heading per runway direction. "L"eft and "R" right can also be appended. +-- @return #ATIS self +function ATIS:SetRunwayHeadingsMagnetic(headings) + + -- First make sure, we have a table. + if type(headings)=="table" then + -- nothing to do + else + headings={headings} + end + + for _,heading in pairs(headings) do + + if type(heading)=="number" then + heading=string.format("%02d", heading) + end + + -- Add runway heading to table. + self:I(self.lid..string.format("Adding user specified magnetic runway heading %s", heading)) + table.insert(self.runwaymag, heading) + + local h=self:GetRunwayWithoutLR(heading) + + local head2=tonumber(h)-18 + if head2<0 then + head2=head2+36 + end + + -- Convert to string. + head2=string.format("%02d", head2) + + -- Append "L" or "R" if necessary. + local left=self:GetRunwayLR(heading) + if left==true then + head2=head2.."L" + elseif left==false then + head2=head2.."R" + end + + -- Add inverse runway heading to table. + self:I(self.lid..string.format("Adding user specified magnetic runway heading %s (inverse)", head2)) + table.insert(self.runwaymag, head2) + end + + return self +end + +--- Set duration how long subtitles are displayed. +-- @param #ATIS self +-- @param #number duration Duration in seconds. Default 10 seconds. +-- @return #ATIS self +function ATIS:SetSubtitleDuration(duration) + self.subduration=tonumber(duration or 10) + return self +end + +--- Set unit system to metric units. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetMetricUnits() + self.metric=true + return self +end + +--- Set unit system to imperial units. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetImperialUnits() + self.metric=false + return self +end + +--- Set pressure unit to millimeters of mercury (mmHg). +-- Default is inHg for imperial and hPa (=mBar) for metric units. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetPressureMillimetersMercury() + self.PmmHg=true + return self +end + +--- Set temperature to be given in degrees Fahrenheit. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetTemperatureFahrenheit() + self.TDegF=true + return self +end + +--- Set relative humidity. This is used to approximately calculate the dew point. +-- Note that the dew point is only an artificial information as DCS does not have an atmospheric model that includes humidity (yet). +-- @param #ATIS self +-- @param #number Humidity Relative Humidity, i.e. a number between 0 and 100 %. Default is 50 %. +-- @return #ATIS self +function ATIS:SetRelativeHumidity(Humidity) + self.relHumidity=Humidity or 50 + return self +end + +--- Report altimeter QNH. +-- @param #ATIS self +-- @param #boolean switch If true or nil, report altimeter QHN. If false, report QFF. +-- @return #ATIS self +function ATIS:SetAltimeterQNH(switch) + + if switch==true or switch==nil then + self.altimeterQNH=true + else + self.altimeterQNH=false + end + + return self +end + +--- Set magnetic declination/variation at the airport. +-- +-- Default is per map: +-- +-- * Caucasus +6 (East), year ~ 2011 +-- * NTTR +12 (East), year ~ 2011 +-- * Normandy -10 (West), year ~ 1944 +-- * Persian Gulf +2 (East), year ~ 2011 +-- +-- To get *true* from *magnetic* heading one has to add easterly or substract westerly variation, e.g +-- +-- A magnetic heading of 180° corresponds to a true heading of +-- +-- * 186° on the Caucaus map +-- * 192° on the Nevada map +-- * 170° on the Normany map +-- * 182° on the Persian Gulf map +-- +-- Likewise, to convert *magnetic* into *true* heading, one has to substract easterly and add westerly variation. +-- +-- @param #ATIS self +-- @param #number magvar Magnetic variation in degrees. Positive for easterly and negative for westerly variation. Default is magnatic declinaton of the used map, c.f. @{Utilities.UTils#UTILS.GetMagneticDeclination}. +-- @return #ATIS self +function ATIS:SetMagneticDeclination(magvar) + self.magvar=magvar or UTILS.GetMagneticDeclination() + return self +end + +--- Explicitly set correction of magnetic to true heading for runways. +-- @param #ATIS self +-- @param #number correction Correction of magnetic to true heading for runways in degrees. +-- @return #ATIS self +function ATIS:SetRunwayCorrectionMagnetic2True(correction) + self.runwaym2t=correction or ATIS.RunwayM2T[UTILS.GetDCSMap()] + return self +end + +--- Set wind direction (from) to be reported as *true* heading. Default is magnetic. +-- @param #ATIS self +-- @return #ATIS self +function ATIS:SetReportWindTrue() + self.windtrue=true + return self +end + +--- Set time local difference with respect to Zulu time. +-- Default is per map: +-- +-- * Caucasus +4 +-- * Nevada -7 +-- * Normandy +1 +-- * Persian Gulf +4 +-- +-- @param #ATIS self +-- @param #number delta Time difference in hours. +-- @return #ATIS self +function ATIS:SetZuluTimeDifference(delta) + self.zuludiff=delta + return self +end + +--- Add ILS station. Note that this can be runway specific. +-- @param #ATIS self +-- @param #number frequency ILS frequency in MHz. +-- @param #string runway (Optional) Runway for which the given ILS frequency applies. Default all (*nil*). +-- @return #ATIS self +function ATIS:AddILS(frequency, runway) + local ils={} --#ATIS.NavPoint + ils.frequency=tonumber(frequency) + ils.runway=runway and tostring(runway) or nil + table.insert(self.ils, ils) + return self +end + +--- Set VOR station. +-- @param #ATIS self +-- @param #number frequency VOR frequency. +-- @return #ATIS self +function ATIS:SetVOR(frequency) + self.vor=frequency + return self +end + +--- Add outer NDB. Note that this can be runway specific. +-- @param #ATIS self +-- @param #number frequency NDB frequency in MHz. +-- @param #string runway (Optional) Runway for which the given NDB frequency applies. Default all (*nil*). +-- @return #ATIS self +function ATIS:AddNDBouter(frequency, runway) + local ndb={} --#ATIS.NavPoint + ndb.frequency=tonumber(frequency) + ndb.runway=runway and tostring(runway) or nil + table.insert(self.ndbouter, ndb) + return self +end + +--- Add inner NDB. Note that this can be runway specific. +-- @param #ATIS self +-- @param #number frequency NDB frequency in MHz. +-- @param #string runway (Optional) Runway for which the given NDB frequency applies. Default all (*nil*). +-- @return #ATIS self +function ATIS:AddNDBinner(frequency, runway) + local ndb={} --#ATIS.NavPoint + ndb.frequency=tonumber(frequency) + ndb.runway=runway and tostring(runway) or nil + table.insert(self.ndbinner, ndb) + return self +end + +--- Set TACAN channel. +-- @param #ATIS self +-- @param #number channel TACAN channel. +-- @return #ATIS self +function ATIS:SetTACAN(channel) + self.tacan=channel + return self +end + +--- Set RSBN channel. +-- @param #ATIS self +-- @param #number channel RSBN channel. +-- @return #ATIS self +function ATIS:SetRSBN(channel) + self.rsbn=channel + return self +end + +--- Add PRMG channel. Note that this can be runway specific. +-- @param #ATIS self +-- @param #number channel PRMG channel. +-- @param #string runway (Optional) Runway for which the given PRMG channel applies. Default all (*nil*). +-- @return #ATIS self +function ATIS:AddPRMG(channel, runway) + local ndb={} --#ATIS.NavPoint + ndb.frequency=tonumber(channel) + ndb.runway=runway and tostring(runway) or nil + table.insert(self.prmg, ndb) + return self +end + + +--- Place marks with runway data on the F10 map. +-- @param #ATIS self +-- @param #boolean markall If true, mark all runways of the map. By default only the current ATIS runways are marked. +function ATIS:MarkRunways(markall) + local airbases=AIRBASE.GetAllAirbases() + for _,_airbase in pairs(airbases) do + local airbase=_airbase --Wrapper.Airbase#AIRBASE + if (not markall and airbase:GetName()==self.airbasename) or markall==true then + airbase:GetRunwayData(self.runwaym2t, true) + end + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Start & Status +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Start ATIS FSM. +-- @param #ATIS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ATIS:onafterStart(From, Event, To) + + -- Check that this is an airdrome. + if self.airbase:GetAirbaseCategory()~=Airbase.Category.AIRDROME then + self:E(self.lid..string.format("ERROR: Cannot start ATIS for airbase %s! Only AIRDROMES are supported but NOT FARPS or SHIPS.", self.airbasename)) + return + end + + -- Info. + self:I(self.lid..string.format("Starting ATIS v%s for airbase %s on %.3f MHz Modulation=%d", ATIS.version, self.airbasename, self.frequency, self.modulation)) + + -- Start radio queue. + self.radioqueue=RADIOQUEUE:New(self.frequency, self.modulation, string.format("ATIS %s", self.airbasename)) + + -- Send coordinate is airbase coord. + self.radioqueue:SetSenderCoordinate(self.airbase:GetCoordinate()) + + -- Set relay unit if we have one. + self.radioqueue:SetSenderUnitName(self.relayunitname) + + -- Set radio power. + self.radioqueue:SetRadioPower(self.power) + + -- Init numbers. + self.radioqueue:SetDigit(0, ATIS.Sound.N0.filename, ATIS.Sound.N0.duration, self.soundpath) + self.radioqueue:SetDigit(1, ATIS.Sound.N1.filename, ATIS.Sound.N1.duration, self.soundpath) + self.radioqueue:SetDigit(2, ATIS.Sound.N2.filename, ATIS.Sound.N2.duration, self.soundpath) + self.radioqueue:SetDigit(3, ATIS.Sound.N3.filename, ATIS.Sound.N3.duration, self.soundpath) + self.radioqueue:SetDigit(4, ATIS.Sound.N4.filename, ATIS.Sound.N4.duration, self.soundpath) + self.radioqueue:SetDigit(5, ATIS.Sound.N5.filename, ATIS.Sound.N5.duration, self.soundpath) + self.radioqueue:SetDigit(6, ATIS.Sound.N6.filename, ATIS.Sound.N6.duration, self.soundpath) + self.radioqueue:SetDigit(7, ATIS.Sound.N7.filename, ATIS.Sound.N7.duration, self.soundpath) + self.radioqueue:SetDigit(8, ATIS.Sound.N8.filename, ATIS.Sound.N8.duration, self.soundpath) + self.radioqueue:SetDigit(9, ATIS.Sound.N9.filename, ATIS.Sound.N9.duration, self.soundpath) + + -- Start radio queue. + self.radioqueue:Start(1, 0.1) + + -- Init status updates. + self:__Status(-2) + self:__CheckQueue(-3) +end + +--- Update status. +-- @param #ATIS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ATIS:onafterStatus(From, Event, To) + + -- Get FSM state. + local fsmstate=self:GetState() + + local relayunitstatus="N/A" + if self.relayunitname then + local ru=UNIT:FindByName(self.relayunitname) + if ru then + relayunitstatus=tostring(ru:IsAlive()) + end + end + + -- Info text. + local text=string.format("State %s: Freq=%.3f MHz %s, Relay unit=%s (alive=%s)", fsmstate, self.frequency, UTILS.GetModulationName(self.modulation), tostring(self.relayunitname), relayunitstatus) + self:I(self.lid..text) + + self:__Status(-60) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check if radio queue is empty. If so, start broadcasting the message again. +-- @param #ATIS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ATIS:onafterCheckQueue(From, Event, To) + + if #self.radioqueue.queue==0 then + self:T(self.lid..string.format("Radio queue empty. Repeating message.")) + self:Broadcast() + else + self:T2(self.lid..string.format("Radio queue %d transmissions queued.", #self.radioqueue.queue)) + end + + -- Check back in 5 seconds. + self:__CheckQueue(-5) +end + +--- Broadcast ATIS radio message. +-- @param #ATIS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ATIS:onafterBroadcast(From, Event, To) + + -- Get current coordinate. + local coord=self.airbase:GetCoordinate() + + -- Get elevation. + local height=coord:GetLandHeight() + + ---------------- + --- Pressure --- + ---------------- + + -- Pressure in hPa. + local qfe=coord:GetPressure(height) + local qnh=coord:GetPressure(0) + + if self.altimeterQNH then + + -- Some constants. + local L=-0.0065 --[K/m] + local R= 8.31446 --[J/mol/K] + local g= 9.80665 --[m/s^2] + local M= 0.0289644 --[kg/mol] + local T0=coord:GetTemperature(0)+273.15 --[K] Temp at sea level. + local TS=288.15 -- Standard Temperature assumed by Altimeter is 15°C + local q=qnh*100 + + -- Calculate Pressure. + local P=q*(1+L*height/T0)^(-g*M/(R*L)) -- Pressure at sea level + local Q=P/(1+L*height/TS)^(-g*M/(R*L)) -- Altimeter QNH + local A=(T0/L)*((P/q)^(((-R*L)/(g*M)))-1) -- Altitude check + + + -- Debug aoutput + self:T2(self.lid..string.format("height=%.1f, A=%.1f, T0=%.1f, QFE=%.1f, QNH=%.1f, P=%.1f, Q=%.1f hPa = %.2f", height, A, T0-273.15, qfe, qnh, P/100, Q/100, UTILS.hPa2inHg(Q/100))) + + -- Set QNH value in hPa. + qnh=Q/100 + + end + + + -- Convert to inHg. + if self.PmmHg then + qfe=UTILS.hPa2mmHg(qfe) + qnh=UTILS.hPa2mmHg(qnh) + else + if not self.metric then + qfe=UTILS.hPa2inHg(qfe) + qnh=UTILS.hPa2inHg(qnh) + end + end + + local QFE=UTILS.Split(string.format("%.2f", qfe), ".") + local QNH=UTILS.Split(string.format("%.2f", qnh), ".") + + if self.PmmHg then + QFE=UTILS.Split(string.format("%.1f", qfe), ".") + QNH=UTILS.Split(string.format("%.1f", qnh), ".") + else + if self.metric then + QFE=UTILS.Split(string.format("%.1f", qfe), ".") + QNH=UTILS.Split(string.format("%.1f", qnh), ".") + end + end + + ------------ + --- Wind --- + ------------ + + -- Get wind direction and speed in m/s. + local windFrom, windSpeed=coord:GetWind(height+10) + + -- Wind in magnetic or true. + local magvar=self.magvar + if self.windtrue then + magvar=0 + end + windFrom=windFrom-magvar + + -- Correct negative values. + if windFrom<0 then + windFrom=windFrom+360 + end + + local WINDFROM=string.format("%03d", windFrom) + local WINDSPEED=string.format("%d", UTILS.MpsToKnots(windSpeed)) + + -- Report North as 0. + if WINDFROM=="000" then + WINDFROM="360" + end + + env.info(string.format("FF WINDFROM = %s", tostring(WINDFROM))) + + if self.metric then + WINDSPEED=string.format("%d", windSpeed) + end + + -------------- + --- Runway --- + -------------- + + local runway, rwyLeft=self:GetActiveRunway() + + ------------ + --- Time --- + ------------ + local time=timer.getAbsTime() + + -- Conversion to Zulu time. + if self.zuludiff then + -- User specified. + time=time-self.zuludiff*60*60 + else + if self.theatre==DCSMAP.Caucasus then + time=time-4*60*60 -- Caucasus UTC+4 hours + elseif self.theatre==DCSMAP.PersianGulf then + time=time-4*60*60 -- Abu Dhabi UTC+4 hours + elseif self.theatre==DCSMAP.NTTR then + time=time+7*60*60 -- Las Vegas UTC-7 hours + elseif self.theatre==DCSMAP.Normandy then + time=time-1*60*60 -- Calais UTC+1 hour + end + end + + local clock=UTILS.SecondsToClock(time) + local zulu=UTILS.Split(clock, ":") + local ZULU=string.format("%s%s", zulu[1], zulu[2]) + + + -- NATO time stamp. 0=Alfa, 1=Bravo, 2=Charlie, etc. + local NATO=ATIS.Alphabet[tonumber(zulu[1])+1] + + -- Debug. + self:T3(string.format("clock=%s", tostring(clock))) + self:T3(string.format("zulu1=%s", tostring(zulu[1]))) + self:T3(string.format("zulu2=%s", tostring(zulu[2]))) + self:T3(string.format("ZULU =%s", tostring(ZULU))) + self:T3(string.format("NATO =%s", tostring(NATO))) + + --------------------------------- + --- Temperature and Dew Point --- + --------------------------------- + + -- Temperature in °C. + local temperature=coord:GetTemperature(height+5) + + -- Dew point in °C. + local dewpoint=temperature-(100-self.relHumidity)/5 + + -- Convert to °F. + if self.TDegF then + temperature=UTILS.CelciusToFarenheit(temperature) + dewpoint=UTILS.CelciusToFarenheit(dewpoint) + end + + local TEMPERATURE=string.format("%d", math.abs(temperature)) + local DEWPOINT=string.format("%d", math.abs(dewpoint)) + + --------------- + --- Weather --- + --------------- + + -- Get mission weather info. Most of this is static. + local clouds, visibility, turbulence, fog, dust, static=self:GetMissionWeather() + + -- Check that fog is actually "thick" enough to reach the airport. If an airport is in the mountains, fog might not affect it as it is measured from sea level. + if fog and fog.thicknessUTILS.FeetToMeters(1500) then + dust=nil + end + + ------------------ + --- Visibility --- + ------------------ + + -- Get min visibility. + local visibilitymin=visibility + + if fog then + if fog.visibility=9 then + -- Overcast 9,10 + CloudCover=ATIS.Sound.CloudsOvercast + CLOUDSsub="Overcast" + elseif clouddens>=7 then + -- Broken 7,8 + CloudCover=ATIS.Sound.CloudsBroken + CLOUDSsub="Broken clouds" + elseif clouddens>=4 then + -- Scattered 4,5,6 + CloudCover=ATIS.Sound.CloudsScattered + CLOUDSsub="Scattered clouds" + elseif clouddens>=1 then + -- Few 1,2,3 + CloudCover=ATIS.Sound.CloudsFew + CLOUDSsub="Few clouds" + else + -- No clouds + CLOUDBASE=nil + CLOUDCEIL=nil + CloudCover=ATIS.Sound.CloudsNo + CLOUDSsub="No clouds" + end + end + + -------------------- + --- Transmission --- + -------------------- + + -- Subtitle + local subtitle="" + + --Airbase name + subtitle=string.format("%s", self.airbasename) + if self.airbasename:find("AFB")==nil and self.airbasename:find("Airport")==nil and self.airbasename:find("Airstrip")==nil and self.airbasename:find("airfield")==nil and self.airbasename:find("AB")==nil then + subtitle=subtitle.." Airport" + end + self.radioqueue:NewTransmission(string.format("%s/%s.ogg", self.theatre, self.airbasename), 3.0, self.soundpath, nil, nil, subtitle, self.subduration) + local alltext=subtitle + + -- Information tag + subtitle=string.format("Information %s", NATO) + local _INFORMATION=subtitle + self:Transmission(ATIS.Sound.Information, 0.5, subtitle) + self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + alltext=alltext..";\n"..subtitle + + -- Zulu Time + subtitle=string.format("%s Zulu", ZULU) + self.radioqueue:Number2Transmission(ZULU, nil, 0.5) + self:Transmission(ATIS.Sound.TimeZulu, 0.2, subtitle) + alltext=alltext..";\n"..subtitle + + -- Visibility + if self.metric then + subtitle=string.format("Visibility %s km", VISIBILITY) + else + subtitle=string.format("Visibility %s NM", VISIBILITY) + end + self:Transmission(ATIS.Sound.Visibilty, 1.0, subtitle) + self.radioqueue:Number2Transmission(VISIBILITY) + if self.metric then + self:Transmission(ATIS.Sound.Kilometers, 0.2) + else + self:Transmission(ATIS.Sound.NauticalMiles, 0.2) + end + alltext=alltext..";\n"..subtitle + + -- Cloud base + self:Transmission(CloudCover, 1.0, CLOUDSsub) + if CLOUDBASE and static then + -- Base + if self.metric then + subtitle=string.format("Cloudbase %s, ceiling %s meters", CLOUDBASE, CLOUDCEIL) + else + subtitle=string.format("Cloudbase %s, ceiling %s ft", CLOUDBASE, CLOUDCEIL) + end + self:Transmission(ATIS.Sound.CloudBase, 1.0, subtitle) + if tonumber(CLOUDBASE1000)>0 then + self.radioqueue:Number2Transmission(CLOUDBASE1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(CLOUDBASE0100)>0 then + self.radioqueue:Number2Transmission(CLOUDBASE0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + -- Ceiling + self:Transmission(ATIS.Sound.CloudCeiling, 0.5) + if tonumber(CLOUDCEIL1000)>0 then + self.radioqueue:Number2Transmission(CLOUDCEIL1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(CLOUDCEIL0100)>0 then + self.radioqueue:Number2Transmission(CLOUDCEIL0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end + end + alltext=alltext..";\n"..subtitle + + -- Weather phenomena + local wp=false + local wpsub="" + if precepitation==1 then + wp=true + wpsub=wpsub.." rain" + elseif precepitation==2 then + if wp then + wpsub=wpsub.."," + end + wpsub=wpsub.." thunderstorm" + wp=true + elseif precepitation==3 then + wpsub=wpsub.." snow" + wp=true + elseif precepitation==4 then + wpsub=wpsub.." snowstorm" + wp=true + end + if fog then + if wp then + wpsub=wpsub.."," + end + wpsub=wpsub.." fog" + wp=true + end + if dust then + if wp then + wpsub=wpsub.."," + end + wpsub=wpsub.." dust" + wp=true + end + -- Actual output + if wp then + subtitle=string.format("Weather phenomena:%s", wpsub) + self:Transmission(ATIS.Sound.WeatherPhenomena, 1.0, subtitle) + if precepitation==1 then + self:Transmission(ATIS.Sound.Rain, 0.5) + elseif precepitation==2 then + self:Transmission(ATIS.Sound.ThunderStorm, 0.5) + elseif precepitation==3 then + self:Transmission(ATIS.Sound.Snow, 0.5) + elseif precepitation==4 then + self:Transmission(ATIS.Sound.SnowStorm, 0.5) + end + if fog then + self:Transmission(ATIS.Sound.Fog, 0.5) + end + if dust then + self:Transmission(ATIS.Sound.Dust, 0.5) + end + alltext=alltext..";\n"..subtitle + end + + -- Altimeter QNH/QFE. + if self.PmmHg then + subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s mmHg", QNH[1], QNH[2], QFE[1], QFE[2]) + else + if self.metric then + subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s hPa", QNH[1], QNH[2], QFE[1], QFE[2]) + else + subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s inHg", QNH[1], QNH[2], QFE[1], QFE[2]) + end + end + local _ALTIMETER=subtitle + self:Transmission(ATIS.Sound.Altimeter, 1.0, subtitle) + self:Transmission(ATIS.Sound.QNH, 0.5) + self.radioqueue:Number2Transmission(QNH[1]) + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(QNH[2]) + self:Transmission(ATIS.Sound.QFE, 0.75) + self.radioqueue:Number2Transmission(QFE[1]) + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(QFE[2]) + if self.PmmHg then + self:Transmission(ATIS.Sound.MillimetersOfMercury, 0.1) + else + if self.metric then + self:Transmission(ATIS.Sound.HectoPascal, 0.1) + else + self:Transmission(ATIS.Sound.InchesOfMercury, 0.1) + end + end + alltext=alltext..";\n"..subtitle + + -- Temperature + if self.TDegF then + if temperature<0 then + subtitle=string.format("Temperature -%s °F", TEMPERATURE) + else + subtitle=string.format("Temperature %s °F", TEMPERATURE) + end + else + if temperature<0 then + subtitle=string.format("Temperature -%s °C", TEMPERATURE) + else + subtitle=string.format("Temperature %s °C", TEMPERATURE) + end + end + local _TEMPERATURE=subtitle + self:Transmission(ATIS.Sound.Temperature, 1.0, subtitle) + if temperature<0 then + self:Transmission(ATIS.Sound.Minus, 0.2) + end + self.radioqueue:Number2Transmission(TEMPERATURE) + if self.TDegF then + self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) + else + self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + end + alltext=alltext..";\n"..subtitle + + -- Dew point + if self.TDegF then + if dewpoint<0 then + subtitle=string.format("Dew point -%s °F", DEWPOINT) + else + subtitle=string.format("Dew point %s °F", DEWPOINT) + end + else + if dewpoint<0 then + subtitle=string.format("Dew point -%s °C", DEWPOINT) + else + subtitle=string.format("Dew point %s °C", DEWPOINT) + end + end + local _DEWPOINT=subtitle + self:Transmission(ATIS.Sound.DewPoint, 1.0, subtitle) + if dewpoint<0 then + self:Transmission(ATIS.Sound.Minus, 0.2) + end + self.radioqueue:Number2Transmission(DEWPOINT) + if self.TDegF then + self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) + else + self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + end + alltext=alltext..";\n"..subtitle + + -- Wind + if self.metric then + subtitle=string.format("Wind from %s at %s m/s", WINDFROM, WINDSPEED) + else + subtitle=string.format("Wind from %s at %s knots", WINDFROM, WINDSPEED) + end + if turbulence>0 then + subtitle=subtitle..", gusting" + end + local _WIND=subtitle + self:Transmission(ATIS.Sound.WindFrom, 1.0, subtitle) + self.radioqueue:Number2Transmission(WINDFROM) + self:Transmission(ATIS.Sound.At, 0.2) + self.radioqueue:Number2Transmission(WINDSPEED) + if self.metric then + self:Transmission(ATIS.Sound.MetersPerSecond, 0.2) + else + self:Transmission(ATIS.Sound.Knots, 0.2) + end + if turbulence>0 then + self:Transmission(ATIS.Sound.Gusting, 0.2) + end + alltext=alltext..";\n"..subtitle + + -- Active runway. + local subtitle=string.format("Active runway %s", runway) + if rwyLeft==true then + subtitle=subtitle.." Left" + elseif rwyLeft==false then + subtitle=subtitle.." Right" + end + local _RUNACT=subtitle + self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) + self.radioqueue:Number2Transmission(runway) + if rwyLeft==true then + self:Transmission(ATIS.Sound.Left, 0.2) + elseif rwyLeft==false then + self:Transmission(ATIS.Sound.Right, 0.2) + end + alltext=alltext..";\n"..subtitle + + -- Runway length. + if self.rwylength then + + local runact=self.airbase:GetActiveRunway(self.runwaym2t) + local length=runact.length + if not self.metric then + length=UTILS.MetersToFeet(length) + end + + -- Length in thousands and hundrets of ft/meters. + local L1000, L0100=self:_GetThousandsAndHundreds(length) + + -- Subtitle. + local subtitle=string.format("Runway length %d", length) + if self.metric then + subtitle=subtitle.." meters" + else + subtitle=subtitle.." feet" + end + + -- Transmit. + self:Transmission(ATIS.Sound.RunwayLength, 1.0, subtitle) + if tonumber(L1000)>0 then + self.radioqueue:Number2Transmission(L1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(L0100)>0 then + self.radioqueue:Number2Transmission(L0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end + + alltext=alltext..";\n"..subtitle + end + + -- Airfield elevation + if self.elevation then + + local elevation=self.airbase:GetHeight() + if not self.metric then + elevation=UTILS.MetersToFeet(elevation) + end + + -- Length in thousands and hundrets of ft/meters. + local L1000, L0100=self:_GetThousandsAndHundreds(elevation) + + -- Subtitle. + local subtitle=string.format("Elevation %d", elevation) + if self.metric then + subtitle=subtitle.." meters" + else + subtitle=subtitle.." feet" + end + + -- Transmitt. + self:Transmission(ATIS.Sound.Elevation, 1.0, subtitle) + if tonumber(L1000)>0 then + self.radioqueue:Number2Transmission(L1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(L0100)>0 then + self.radioqueue:Number2Transmission(L0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end + + alltext=alltext..";\n"..subtitle + end + + -- Tower frequency. + if self.towerfrequency then + local freqs="" + for i,freq in pairs(self.towerfrequency) do + freqs=freqs..string.format("%.3f MHz", freq) + if i<#self.towerfrequency then + freqs=freqs..", " + end + end + subtitle=string.format("Tower frequency %s", freqs) + self:Transmission(ATIS.Sound.TowerFrequency, 1.0, subtitle) + for _,freq in pairs(self.towerfrequency) do + local f=string.format("%.3f", freq) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + end + + alltext=alltext..";\n"..subtitle + end + + -- ILS + local ils=self:GetNavPoint(self.ils, runway, rwyLeft) + if ils then + subtitle=string.format("ILS frequency %.2f MHz", ils.frequency) + self:Transmission(ATIS.Sound.ILSFrequency, 1.0, subtitle) + local f=string.format("%.2f", ils.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + + alltext=alltext..";\n"..subtitle + end + + -- Outer NDB + local ndb=self:GetNavPoint(self.ndbouter, runway, rwyLeft) + if ndb then + subtitle=string.format("Outer NDB frequency %.2f MHz", ndb.frequency) + self:Transmission(ATIS.Sound.OuterNDBFrequency, 1.0, subtitle) + local f=string.format("%.2f", ndb.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + + alltext=alltext..";\n"..subtitle + end + + -- Inner NDB + local ndb=self:GetNavPoint(self.ndbinner, runway, rwyLeft) + if ndb then + subtitle=string.format("Inner NDB frequency %.2f MHz", ndb.frequency) + self:Transmission(ATIS.Sound.InnerNDBFrequency, 1.0, subtitle) + local f=string.format("%.2f", ndb.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + + alltext=alltext..";\n"..subtitle + end + + -- VOR + if self.vor then + subtitle=string.format("VOR frequency %.2f MHz", self.vor) + self:Transmission(ATIS.Sound.VORFrequency, 1.0, subtitle) + local f=string.format("%.2f", self.vor) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + + alltext=alltext..";\n"..subtitle + end + + -- TACAN + if self.tacan then + subtitle=string.format("TACAN channel %dX", self.tacan) + self:Transmission(ATIS.Sound.TACANChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(tostring(self.tacan), nil, 0.2) + self.radioqueue:NewTransmission("NATO Alphabet/Xray.ogg", 0.75, self.soundpath, nil, 0.2) + + alltext=alltext..";\n"..subtitle + end + + -- RSBN + if self.rsbn then + subtitle=string.format("RSBN channel %d", self.rsbn) + self:Transmission(ATIS.Sound.RSBNChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(tostring(self.rsbn), nil, 0.2) + + alltext=alltext..";\n"..subtitle + end + + -- PRMG + local ndb=self:GetNavPoint(self.prmg, runway, rwyLeft) + if ndb then + subtitle=string.format("PRMG channel %d", ndb.frequency) + self:Transmission(ATIS.Sound.PRMGChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(tostring(ndb.frequency), nil, 0.5) + + alltext=alltext..";\n"..subtitle + end + + --[[ + -- End of Information Alpha, Bravo, ... + subtitle=string.format("End of information %s", NATO) + self:Transmission(ATIS.Sound.EndOfInformation, 0.5, subtitle) + self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + --]] + + -- Advice on initial... + subtitle=string.format("Advise on initial contact, you have information %s", NATO) + self:Transmission(ATIS.Sound.AdviceOnInitial, 0.5, subtitle) + self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + + alltext=alltext..";\n"..subtitle + + -- Report ATIS text. + self:Report(alltext) + + -- Update F10 marker. + if self.usemarker then + self:UpdateMarker(_INFORMATION, _RUNACT, _WIND, _ALTIMETER, _TEMPERATURE) + end + +end + +--- Text report of ATIS information. Information delimitor is a semicolon ";" and a line break "\n". +-- @param #ATIS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Text Report text. +function ATIS:onafterReport(From, Event, To, Text) + self:T(self.lid..string.format("Report:\n%s", Text)) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Update F10 map marker. +-- @param #ATIS self +-- @param #string information Information tag text. +-- @param #string runact Active runway text. +-- @param #string wind Wind text. +-- @param #string altimeter Altimeter text. +-- @param #string temperature Temperature text. +-- @return #number Marker ID. +function ATIS:UpdateMarker(information, runact, wind, altimeter, temperature) + + if self.markerid then + self.airbase:GetCoordinate():RemoveMark(self.markerid) + end + + local text=string.format("ATIS on %.3f %s, %s:\n", self.frequency, UTILS.GetModulationName(self.modulation), tostring(information)) + text=text..string.format("%s\n", tostring(runact)) + text=text..string.format("%s\n", tostring(wind)) + text=text..string.format("%s\n", tostring(altimeter)) + text=text..string.format("%s", tostring(temperature)) + -- More info is not displayed on the marker! + + -- Place new mark + self.markerid=self.airbase:GetCoordinate():MarkToAll(text, true) + + return self.markerid +end + +--- Get active runway runway. +-- @param #ATIS self +-- @return #string Active runway, e.g. "31" for 310 deg. +-- @return #boolean Use Left=true, Right=false, or nil. +function ATIS:GetActiveRunway() + + local coord=self.airbase:GetCoordinate() + local height=coord:GetLandHeight() + + -- Get wind direction and speed in m/s. + local windFrom, windSpeed=coord:GetWind(height+10) + + -- Get active runway data based on wind direction. + local runact=self.airbase:GetActiveRunway(self.runwaym2t) + + -- Active runway "31". + local runway=self:GetMagneticRunway(windFrom) or runact.idx + + -- Left or right in case there are two runways with the same heading. + local rwyLeft=nil + + -- Check if user explicitly specified a runway. + if self.activerunway then + + -- Get explicit runway heading if specified. + local runwayno=self:GetRunwayWithoutLR(self.activerunway) + if runwayno~="" then + runway=runwayno + end + + -- Was "L"eft or "R"ight given? + rwyLeft=self:GetRunwayLR(self.activerunway) + end + + return runway, rwyLeft +end + +--- Get runway from user supplied magnetic heading. +-- @param #ATIS self +-- @param #number windfrom Wind direction (from) in degrees. +-- @return #string Runway magnetic heading divided by ten (and rounded). Eg, "13" for 130°. +function ATIS:GetMagneticRunway(windfrom) + + local diffmin=nil + local runway=nil + for _,heading in pairs(self.runwaymag) do + + local hdg=self:GetRunwayWithoutLR(heading) + + local diff=UTILS.HdgDiff(windfrom, tonumber(hdg)*10) + if diffmin==nil or diff data is valid for all runways. + return nav + else + + local navy=tonumber(self:GetRunwayWithoutLR(nav.runway))*10 + local rwyy=tonumber(self:GetRunwayWithoutLR(runway))*10 + + local navL=self:GetRunwayLR(nav.runway) + local hdgD=UTILS.HdgDiff(navy,rwyy) + + if hdgD<=15 then --We allow an error of +-15° here. + if navL==nil or (navL==true and left==true) or (navL==false and left==false) then + return nav + end + end + end + end + + return nil +end + +--- Get runway heading without left or right info. +-- @param #ATIS self +-- @param #string runway Runway heading, *e.g.* "31L". +-- @return #string Runway heading without left or right, *e.g.* "31". +function ATIS:GetRunwayWithoutLR(runway) + local rwywo=runway:gsub("%D+", "") + --self:I(string.format("FF runway=%s ==> rwywo=%s", runway, rwywo)) + return rwywo +end + +--- Get info if left or right runway is active. +-- @param #ATIS self +-- @param #string runway Runway heading, *e.g.* "31L". +-- @return #boolean If *true*, left runway is active. If *false*, right runway. If *nil*, neither applies. +function ATIS:GetRunwayLR(runway) + + -- Get left/right if specified. + local rwyL=runway:lower():find("l") + local rwyR=runway:lower():find("r") + + if rwyL then + return true + elseif rwyR then + return false + else + return nil + end + +end + +--- Transmission via RADIOQUEUE. +-- @param #ATIS self +-- @param #ATIS.Soundfile sound ATIS sound object. +-- @param #number interval Interval in seconds after the last transmission finished. +-- @param #string subtitle Subtitle of the transmission. +-- @param #string path Path to sound file. Default self.soundpath. +function ATIS:Transmission(sound, interval, subtitle, path) + self.radioqueue:NewTransmission(sound.filename, sound.duration, path or self.soundpath, nil, interval, subtitle, self.subduration) +end + +--- Play all audio files. +-- @param #ATIS self +function ATIS:SoundCheck() + + for _,_sound in pairs(ATIS.Sound) do + local sound=_sound --#ATIS.Soundfile + local subtitle=string.format("Playing sound file %s, duration %.2f sec", sound.filename, sound.duration) + self:Transmission(sound, nil, subtitle) + MESSAGE:New(subtitle, 5, "ATIS"):ToAll() + end + +end + +--- Get weather of this mission from env.mission.weather variable. +-- @param #ATIS self +-- @return #table Clouds table which has entries "thickness", "density", "base", "iprecptns". +-- @return #number Visibility distance in meters. +-- @return #number Ground turbulence in m/s. +-- @return #table Fog table, which has entries "thickness", "visibility" or nil if fog is disabled in the mission. +-- @return #number Dust density or nil if dust is disabled in the mission. +-- @return #boolean static If true, static weather is used. If false, dynamic weather is used. +function ATIS:GetMissionWeather() + + -- Weather data from mission file. + local weather=env.mission.weather + + -- Clouds + --[[ + ["clouds"] = + { + ["thickness"] = 430, + ["density"] = 7, + ["base"] = 0, + ["iprecptns"] = 1, + }, -- end of ["clouds"] + ]] + local clouds=weather.clouds + + -- 0=static, 1=dynamic + local static=weather.atmosphere_type==0 + + -- Visibilty distance in meters. + local visibility=weather.visibility.distance + + -- Ground turbulence. + local turbulence=weather.groundTurbulence + + -- Dust + --[[ + ["enable_dust"] = false, + ["dust_density"] = 0, + ]] + local dust=nil + if weather.enable_dust==true then + dust=weather.dust_density + end + + -- Fog + --[[ + ["enable_fog"] = false, + ["fog"] = + { + ["thickness"] = 0, + ["visibility"] = 25, + }, -- end of ["fog"] + ]] + local fog=nil + if weather.enable_fog==true then + fog=weather.fog + end + + self:T("FF weather:") + self:T({clouds=clouds}) + self:T({visibility=visibility}) + self:T({turbulence=turbulence}) + self:T({fog=fog}) + self:T({dust=dust}) + self:T({static=static}) + return clouds, visibility, turbulence, fog, dust, static +end + + +--- Get thousands of a number. +-- @param #ATIS self +-- @param #number n Number, *e.g.* 4359. +-- @return #string Thousands of n, *e.g.* "4" for 4359. +-- @return #string Hundreds of n, *e.g.* "4" for 4359 because its rounded. +function ATIS:_GetThousandsAndHundreds(n) + + local N=UTILS.Round(n/1000, 1) + + local S=UTILS.Split(string.format("%.1f", N), ".") + + local t=S[1] + local h=S[2] + + return t, h +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua new file mode 100644 index 000000000..1ae6b24fa --- /dev/null +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -0,0 +1,17929 @@ +--- **Ops** - Manages aircraft CASE X recoveries for carrier operations (X=I, II, III). +-- +-- The AIRBOSS class manages recoveries of human pilots and AI aircraft on aircraft carriers. +-- +-- **Main Features:** +-- +-- * CASE I, II and III recoveries. +-- * Supports human pilots as well as AI flight groups. +-- * Automatic LSO grading including (optional) live grading while in the groove. +-- * Different skill levels from on-the-fly tips for flight students to *ziplip* for pros. Can be set for each player individually. +-- * Define recovery time windows with individual recovery cases in the same mission. +-- * Option to let the carrier steam into the wind automatically. +-- * Automatic TACAN and ICLS channel setting of carrier. +-- * Separate radio channels for LSO and Marshal transmissions. +-- * Voice over support for LSO and Marshal radio transmissions. +-- * Advanced F10 radio menu including carrier info, weather, radio frequencies, TACAN/ICLS channels, player LSO grades, marking of zones etc. +-- * Recovery tanker and refueling option via integration of @{Ops.RecoveryTanker} class. +-- * Rescue helicopter option via @{Ops.RescueHelo} class. +-- * Combine multiple human players to sections. +-- * Many parameters customizable by convenient user API functions. +-- * Multiple carrier support due to object oriented approach. +-- * Unlimited number of players. +-- * Persistence of player results (optional). LSO grading data is saved to csv file. +-- * Trap sheet (optional). +-- * Finite State Machine (FSM) implementation. +-- +-- **Supported Carriers:** +-- +-- * [USS John C. Stennis](https://en.wikipedia.org/wiki/USS_John_C._Stennis) (CVN-74) +-- * [USS Tarawa](https://en.wikipedia.org/wiki/USS_Tarawa_(LHA-1)) (LHA-1) [**WIP**] +-- +-- **Supported Aircraft:** +-- +-- * [F/A-18C Hornet Lot 20](https://forums.eagle.ru/forumdisplay.php?f=557) (Player & AI) +-- * [F-14B Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI) +-- * [A-4E Skyhawk Community Mod](https://forums.eagle.ru/showthread.php?t=224989) (Player & AI) +-- * [AV-8B N/A Harrier](https://forums.eagle.ru/forumdisplay.php?f=555) (Player & AI) [**WIP**] +-- * F/A-18C Hornet (AI) +-- * F-14A Tomcat (AI) +-- * E-2D Hawkeye (AI) +-- * S-3B Viking & tanker version (AI) +-- * [C-2A Greyhound](https://forums.eagle.ru/showthread.php?t=255641) (AI) +-- +-- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) and A-4E community mod as aircraft and the USS John C. Stennis as carrier. +-- +-- The AV-8B Harrier and the USS Tarawa are WIP. Those two can only be used together, i.e. the Tarawa is the only carrier the harrier is supposed to land on and +-- the no other fixed wing aircraft (human or AI controlled) are supposed to land on the Tarawa. Currently only Case I is supported. Case II/III take slightly steps from the CVN carrier. +-- However, the two Case II/III pattern are very similar so this is not a big drawback. +-- +-- Heatblur's mighty F-14B Tomcat has been added (March 13th 2019) as well. +-- +-- ## Discussion +-- +-- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-airboss channel. +-- There you also find an example mission and the necessary voice over sound files. Check out the **pinned messages**. +-- +-- ## Example Missions +-- +-- Example missions can be found [here](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Airboss). +-- They contain the latest development Moose.lua file. +-- +-- ## IMPORTANT +-- +-- Some important restrictions (of DCS) you should be aware of: +-- +-- * Each player slot (client) should be in a separate group as DCS does only allow for sending messages to groups and not individual units. +-- * Players are identified by their player name. Hence, ensure that no two player have the same name, e.g. "New Callsign", as this will lead to unexpected results. +-- * The modex (tail number) of an aircraft should **not** be changed dynamically in the mission by a player. Unfortunately, there is no way to get this information via scripting API functions. +-- * The A-4E-C mod needs *easy comms* activated to interact with the F10 radio menu. +-- +-- ## Youtube Videos +-- +-- ### AIRBOSS videos: +-- +-- * [[MOOSE] Airboss - Groove Testing (WIP)](https://www.youtube.com/watch?v=94KHQxxX3UI) +-- * [[MOOSE] Airboss - Groove Test A-4E Community Mod](https://www.youtube.com/watch?v=ZbjD7FHiaHo) +-- * [[MOOSE] Airboss - Groove Test: On-the-fly LSO Grading](https://www.youtube.com/watch?v=Xgs1hwDcPyM) +-- * [[MOOSE] Airboss - Carrier Auto Steam Into Wind](https://www.youtube.com/watch?v=IsU8dYgsp90) +-- * [[MOOSE] Airboss - CASE I Walkthrough in the F/A-18C by TG](https://www.youtube.com/watch?v=o1UrP4Q6PMM) +-- * [[MOOSE] Airboss - New LSO/Marshal Voice Overs by Raynor](https://www.youtube.com/watch?v=_Suo68bRu8k) +-- * [[MOOSE] Airboss - CASE I, "Until We Go Down" featuring the F-14B by Pikes](https://www.youtube.com/watch?v=ojgHDSw3Doc) +-- * [[MOOSE] Airboss - Skipper Menu](https://youtu.be/awnecCxRoNQ) +-- +-- ### Lex explaining Boat Ops: +-- +-- * [( DCS HORNET ) Some boat ops basics VID 1](https://www.youtube.com/watch?v=LvGQS-3AzMc) +-- * [( DCS HORNET ) Some boat ops basics VID 2](https://www.youtube.com/watch?v=bN44wvtRsw0) +-- +-- ### Jabbers Case I and III Recovery Tutorials: +-- +-- * [DCS World - F/A-18 - Case I Carrier Recovery Tutorial](https://www.youtube.com/watch?v=lm-M3VUy-_I) +-- * [DCS World - Case I Recovery Tutorial - Followup](https://www.youtube.com/watch?v=cW5R32Q6xC8) +-- * [DCS World - CASE III Recovery Tutorial](https://www.youtube.com/watch?v=Lnfug5CVAvo) +-- +-- ### Wags DCS Hornet Videos: +-- +-- * [DCS: F/A-18C Hornet - Episode 9: CASE I Carrier Landing](https://www.youtube.com/watch?v=TuigBLhtAH8) +-- * [DCS: F/A-18C Hornet – Episode 16: CASE III Introduction](https://www.youtube.com/watch?v=DvlMHnLjbDQ) +-- * [DCS: F/A-18C Hornet Case I Carrier Landing Training Lesson Recording](https://www.youtube.com/watch?v=D33uM9q4xgA) +-- +-- ### AV-8B Harrier at USS Tarawa +-- +-- * [Harrier Ship Landing Mission with Auto LSO!](https://www.youtube.com/watch?v=lqmVvpunk2c) +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Special Thanks To **Bankler** +-- For his great [Recovery Trainer](https://forums.eagle.ru/showthread.php?t=221412) mission and script! +-- His work was the initial inspiration for this class. Also note that this implementation uses some routines for determining the player position in Case I recoveries he developed. +-- Bankler was kind enough to allow me to add this to the class - thanks again! +-- +-- @module Ops.Airboss +-- @image Ops_Airboss.png + +--- AIRBOSS class. +-- @type AIRBOSS +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. Messages to all about status. +-- @field #string lid Class id string for output to DCS log file. +-- @field #string theatre The DCS map used in the mission. +-- @field Wrapper.Unit#UNIT carrier Aircraft carrier unit on which we want to practice. +-- @field #string carriertype Type name of aircraft carrier. +-- @field #AIRBOSS.CarrierParameters carrierparam Carrier specific parameters. +-- @field #string alias Alias of the carrier. +-- @field Wrapper.Airbase#AIRBASE airbase Carrier airbase object. +-- @field #table waypoints Waypoint coordinates of carrier. +-- @field #number currentwp Current waypoint, i.e. the one that has been passed last. +-- @field Core.Radio#BEACON beacon Carrier beacon for TACAN and ICLS. +-- @field #boolean TACANon Automatic TACAN is activated. +-- @field #number TACANchannel TACAN channel. +-- @field #string TACANmode TACAN mode, i.e. "X" or "Y". +-- @field #string TACANmorse TACAN morse code, e.g. "STN". +-- @field #boolean ICLSon Automatic ICLS is activated. +-- @field #number ICLSchannel ICLS channel. +-- @field #string ICLSmorse ICLS morse code, e.g. "STN". +-- @field #AIRBOSS.Radio LSORadio Radio for LSO calls. +-- @field #number LSOFreq LSO radio frequency in MHz. +-- @field #string LSOModu LSO radio modulation "AM" or "FM". +-- @field #AIRBOSS.Radio MarshalRadio Radio for carrier calls. +-- @field #number MarshalFreq Marshal radio frequency in MHz. +-- @field #string MarshalModu Marshal radio modulation "AM" or "FM". +-- @field #number TowerFreq Tower radio frequency in MHz. +-- @field Core.Scheduler#SCHEDULER radiotimer Radio queue scheduler. +-- @field Core.Zone#ZONE_UNIT zoneCCA Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier. +-- @field Core.Zone#ZONE_UNIT zoneCCZ Carrier controlled zone (CCZ), i.e. a zone of 5 NM radius around the carrier. +-- @field #table players Table of players. +-- @field #table menuadded Table of units where the F10 radio menu was added. +-- @field #AIRBOSS.Checkpoint BreakEntry Break entry checkpoint. +-- @field #AIRBOSS.Checkpoint BreakEarly Early break checkpoint. +-- @field #AIRBOSS.Checkpoint BreakLate Late break checkpoint. +-- @field #AIRBOSS.Checkpoint Abeam Abeam checkpoint. +-- @field #AIRBOSS.Checkpoint Ninety At the ninety checkpoint. +-- @field #AIRBOSS.Checkpoint Wake Checkpoint right behind the carrier. +-- @field #AIRBOSS.Checkpoint Final Checkpoint when turning to final. +-- @field #AIRBOSS.Checkpoint Groove In the groove checkpoint. +-- @field #AIRBOSS.Checkpoint Platform Case II/III descent at 2000 ft/min at 5000 ft platform. +-- @field #AIRBOSS.Checkpoint DirtyUp Case II/III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier. +-- @field #AIRBOSS.Checkpoint Bullseye Case III intercept glideslope and follow ICLS aka "bullseye". +-- @field #number defaultcase Default recovery case. This is the case used if not specified otherwise. +-- @field #number case Recovery case I, II or III currently in progress. +-- @field #table recoverytimes List of time windows when aircraft are recovered including the recovery case and holding offset. +-- @field #number defaultoffset Default holding pattern update if not specified otherwise. +-- @field #number holdingoffset Offset [degrees] of Case II/III holding pattern. +-- @field #table flights List of all flights in the CCA. +-- @field #table Qmarshal Queue of marshalling aircraft groups. +-- @field #table Qpattern Queue of aircraft groups in the landing pattern. +-- @field #table Qwaiting Queue of aircraft groups waiting outside 10 NM zone for the next free Marshal stack. +-- @field #table Qspinning Queue of aircraft currently spinning. +-- @field #table RQMarshal Radio queue of marshal. +-- @field #number TQMarshal Abs mission time, the last transmission ended. +-- @field #table RQLSO Radio queue of LSO. +-- @field #number TQLSO Abs mission time, the last transmission ended. +-- @field #number Nmaxpattern Max number of aircraft in landing pattern. +-- @field #number Nmaxmarshal Number of max Case I Marshal stacks available. Default 3, i.e. angels 2, 3 and 4. +-- @field #number NmaxSection Number of max section members (excluding the lead itself), i.e. NmaxSection=1 is a section of two. +-- @field #number NmaxStack Number of max flights per stack. Default 2. +-- @field #boolean handleai If true (default), handle AI aircraft. +-- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. +-- @field DCS#Vec3 Corientation Carrier orientation in space. +-- @field DCS#Vec3 Corientlast Last known carrier orientation. +-- @field Core.Point#COORDINATE Cposition Carrier position. +-- @field #string defaultskill Default player skill @{#AIRBOSS.Difficulty}. +-- @field #boolean adinfinitum If true, carrier patrols ad infinitum, i.e. when reaching its last waypoint it starts at waypoint one again. +-- @field #number magvar Magnetic declination in degrees. +-- @field #number Tcollapse Last time timer.gettime() the stack collapsed. +-- @field #AIRBOSS.Recovery recoverywindow Current or next recovery window opened. +-- @field #boolean usersoundradio Use user sound output instead of radio transmissions. +-- @field #number Tqueue Last time in seconds of timer.getTime() the queue was updated. +-- @field #number dTqueue Time interval in seconds for updating the queues etc. +-- @field #number dTstatus Time interval for call FSM status updates. +-- @field #boolean menumarkzones If false, disables the option to mark zones via smoke or flares. +-- @field #boolean menusmokezones If false, disables the option to mark zones via smoke. +-- @field #table playerscores Table holding all player scores and grades. +-- @field #boolean autosave If true, all player grades are automatically saved to a file on disk. +-- @field #string autosavepath Path where the player grades file is saved on auto save. +-- @field #string autosavefilename File name of the auto player grades save file. Default is auto generated from carrier name/alias. +-- @field #number marshalradius Radius of the Marshal stack zone. +-- @field #boolean airbossnice Airboss is a nice guy. +-- @field #boolean staticweather Mission uses static rather than dynamic weather. +-- @field #number windowcount Running number counting the recovery windows. +-- @field #number LSOdT Time interval in seconds before the LSO will make its next call. +-- @field #string senderac Name of the aircraft acting as sender for broadcasting radio messages from the carrier. DCS shortcoming workaround. +-- @field #string radiorelayLSO Name of the aircraft acting as sender for broadcasting LSO radio messages from the carrier. DCS shortcoming workaround. +-- @field #string radiorelayMSH Name of the aircraft acting as sender for broadcasting Marhsal radio messages from the carrier. DCS shortcoming workaround. +-- @field #boolean turnintowind If true, carrier is currently turning into the wind. +-- @field #boolean detour If true, carrier is currently making a detour from its path along the ME waypoints. +-- @field Core.Point#COORDINATE Creturnto Position to return to after turn into the wind leg is over. +-- @field Core.Set#SET_GROUP squadsetAI AI groups in this set will be handled by the airboss. +-- @field Core.Set#SET_GROUP excludesetAI AI groups in this set will be explicitly excluded from handling by the airboss and not forced into the Marshal pattern. +-- @field #boolean menusingle If true, menu is optimized for a single carrier. +-- @field #number collisiondist Distance up to which collision checks are done. +-- @field #number holdtimestamp Timestamp when the carrier first came to an unexpected hold. +-- @field #number Tmessage Default duration in seconds messages are displayed to players. +-- @field #string soundfolder Folder within the mission (miz) file where airboss sound files are located. +-- @field #string soundfolderLSO Folder withing the mission (miz) file where LSO sound files are stored. +-- @field #string soundfolderMSH Folder withing the mission (miz) file where Marshal sound files are stored. +-- @field #boolean despawnshutdown Despawn group after engine shutdown. +-- @field #number Tbeacon Last time the beacons were refeshed. +-- @field #number dTbeacon Time interval to refresh the beacons. Default 5 minutes. +-- @field #AIRBOSS.LSOCalls LSOCall Radio voice overs of the LSO. +-- @field #AIRBOSS.MarshalCalls MarshalCall Radio voice over of the Marshal/Airboss. +-- @field #AIRBOSS.PilotCalls PilotCall Radio voice over from AI pilots. +-- @field #number lowfuelAI Low fuel threshold for AI groups in percent. +-- @field #boolean emergency If true (default), allow emergency landings, i.e. bypass any pattern and go for final approach. +-- @field #boolean respawnAI If true, respawn AI flights as they enter the CCA to detach and airfields from the mission plan. Default false. +-- @field #boolean turning If true, carrier is currently turning. +-- @field #AIRBOSS.GLE gle Glidesope error thresholds. +-- @field #AIRBOSS.LUE lue Lineup error thresholds. +-- @field #boolean trapsheet If true, players can save their trap sheets. +-- @field #string trappath Path where to save the trap sheets. +-- @field #string trapprefix File prefix for trap sheet files. +-- @field #number initialmaxalt Max altitude in meters to register in the inital zone. +-- @field #boolean welcome If true, display welcome message to player. +-- @field #boolean skipperMenu If true, add skipper menu. +-- @field #number skipperSpeed Speed in knots for manual recovery start. +-- @field #number skipperCase Manual recovery case. +-- @field #boolean skipperUturn U-turn on/off via menu. +-- @field #number skipperOffset Holding offset angle in degrees for Case II/III manual recoveries. +-- @field #number skipperTime Recovery time in min for manual recovery. +-- @extends Core.Fsm#FSM + +--- Be the boss! +-- +-- === +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Main.png) +-- +-- # The AIRBOSS Concept +-- +-- On a carrier, the AIRBOSS is guy who is really in charge - don't mess with him! +-- +-- # Recovery Cases +-- +-- The AIRBOSS class supports all three commonly used recovery cases, i.e. +-- +-- * **CASE I** during daytime and good weather (ceiling > 3000 ft, visibility > 5 NM), +-- * **CASE II** during daytime but poor visibility conditions (ceiling > 1000 ft, visibility > 5NM), +-- * **CASE III** when below Case II conditions and during nighttime (ceiling < 1000 ft, visibility < 5 NM). +-- +-- That being said, this script allows you to use any of the three cases to be used at any time. Or, in other words, *you* need to specify when which case is safe and appropriate. +-- +-- This is a lot of responsability. *You* are the boss, but *you* need to make the right decisions or things will go terribly wrong! +-- +-- Recovery windows can be set up via the @{#AIRBOSS.AddRecoveryWindow} function as explained below. With this it is possible to seamlessly (within reason!) switch recovery cases in the same mission. +-- +-- ## CASE I +-- +-- As mentioned before, Case I recovery is the standard procedure during daytime and good visibility conditions. +-- +-- ### Holding Pattern +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Holding.png) +-- +-- The graphic depicts a the standard holding pattern during a Case I recovery. Incoming aircraft enter the holding pattern, which is a counter clockwise turn with a +-- diameter of 5 NM, at their assigned altitude. The holding altitude of the first stack is 2000 ft. The interval between stacks is 1000 ft. +-- +-- Once a recovery window opens, the aircraft of the lowest stack commence their landing approach and the rest of the Marshal stack collapses, i.e. aircraft switch from +-- their current stack to the next lower stack. +-- +-- The flight that transitions form the holding pattern to the landing approach, it should leave the Marshal stack at the 3 position and make a left hand turn to the *Initial* +-- position, which is 3 NM astern of the boat. Note that you need to be below 1300 feet to be registered in the initial zone. +-- The altitude can be set via the function @{AIRBOSS.SetInitialMaxAlt}(*altitude*) function. +-- As described below, the initial zone can be smoked or flared via the AIRBOSS F10 Help radio menu. +-- +-- ### Landing Pattern +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Landing.png) +-- +-- Once the aircraft reaches the Inital, the landing pattern begins. The important steps of the pattern are shown in the image above. +-- +-- +-- ## CASE III +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case3.png) +-- +-- A Case III recovery is conducted during nighttime. The holding position and the landing pattern are rather different from a Case I recovery as can be seen in the image above. +-- +-- The first holding zone starts 21 NM astern the carrier at angels 6. The separation between the stacks is 1000 ft just like in Case I. However, the distance to the boat +-- increases by 1 NM with each stack. The general form can be written as D=15+6+(N-1), where D is the distance to the boat in NM and N the number of the stack starting at N=1. +-- +-- Once the aircraft of the lowest stack is allowed to commence to the landing pattern, it starts a descent at 4000 ft/min until it reaches the "*Platform*" at 5000 ft and +-- ~19 NM DME. From there a shallower descent at 2000 ft/min should be performed. At an altitude of 1200 ft the aircraft should level out and "*Dirty Up*" (gear, flaps & hook down). +-- +-- At 3 NM distance to the carrier, the aircraft should intercept the 3.5 degrees glideslope at the "*Bullseye*". From there the pilot should "follow the needles" of the ICLS. +-- +-- ## CASE II +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case2.png) +-- +-- Case II is the common recovery procedure at daytime if visibility conditions are poor. It can be viewed as hybrid between Case I and III. +-- The holding pattern is very similar to that of the Case III recovery with the difference the the radial is the inverse of the BRC instead of the FB. +-- From the holding zone aircraft are follow the Case III path until they reach the Initial position 3 NM astern the boat. From there a standard Case I recovery procedure is +-- in place. +-- +-- Note that the image depicts the case, where the holding zone has an angle offset of 30 degrees with respect to the BRC. This is optional. Commonly used offset angels +-- are 0 (no offset), +-15 or +-30 degrees. The AIRBOSS class supports all these scenarios which are used during Case II and III recoveries. +-- +-- === +-- +-- # The F10 Radio Menu +-- +-- The F10 radio menu can be used to post requests to Marshal but also provides information about the player and carrier status. Additionally, helper functions +-- can be called. +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMain.png) +-- +-- By default, the script creates a submenu "Airboss" in the "F10 Other ..." menu and each @{#AIRBOSS} carrier gets its own submenu. +-- If you intend to have only one carrier, you can simplify the menu structure using the @{#AIRBOSS.SetMenuSingleCarrier} function, which will create all carrier specific menu entries directly +-- in the "Airboss" submenu. (Needless to say, that if you enable this and define multiple carriers, the menu structure will get completely screwed up.) +-- +-- ## Root Menu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuRoot.png) +-- +-- The general structure +-- +-- * **F1 Help...** (Help submenu, see below.) +-- * **F2 Kneeboard...** (Kneeboard submenu, see below. Carrier information, weather report, player status.) +-- * **F3 Request Marshal** +-- * **F4 Request Commence** +-- * **F5 Request Refueling** +-- * **F6 Spinning** +-- * **F7 Emergency Landing** +-- * **F8 [Reset My Status]** +-- +-- ### Request Marshal +-- +-- This radio command can be used to request a stack in the holding pattern from Marshal. Necessary conditions are that the flight is inside the Carrier Controlled Area (CCA) +-- (see @{#AIRBOSS.SetCarrierControlledArea}). +-- +-- Marshal will assign an individual stack for each player group depending on the current or next open recovery case window. +-- If multiple players have registered as a section, the section lead will be assigned a stack and is responsible to guide his section to the assigned holding position. +-- +-- ### Request Commence +-- +-- This command can be used to request commencing from the marshal stack to the landing pattern. Necessary condition is that the player is in the lowest marshal stack +-- and that the number of aircraft in the landing pattern is smaller than four (or the number set by the mission designer). +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1Pattern.png) +-- +-- The image displays the standard Case I Marshal pattern recovery. Pilots are supposed to fly a clockwise circle and descent between the **3** and **1** positions. +-- +-- Commence should be performed at around the **3** position. If the pilot is in the lowest Marshal stack, and flies through this area, he is automatically cleared for the +-- landing pattern. In other words, there is no need for the "Request Commence" radio command. The zone can be marked via smoke or flared using the player's F10 radio menu. +-- +-- A player can also request commencing if he is not registered in a marshal stack yet. If the pattern is free, Marshal will allow him to directly enter the landing pattern. +-- However, this is only possible when the Airboss has a nice day - see @{#AIRBOSS.SetAirbossNiceGuy}. +-- +-- ### Request Refueling +-- +-- If a recovery tanker has been set up via the @{#AIRBOSS.SetRecoveryTanker}, the player can request refueling at any time. If currently in the marshal stack, the stack above will collapse. +-- The player will be informed if the tanker is currently busy or going RTB to refuel itself at its home base. Once the re-fueling is complete, the player has to re-register to the marshal stack. +-- +-- ### Spinning +-- +-- If the pattern is full, players can go into the spinning pattern. This step is only allowed, if the player is in the pattern and his next step +-- is initial, break entry, early/late break. At this point, the player should climb to 1200 ft a fly on the port side of the boat to go back to the initial again. +-- +-- If a player is in the spin pattern, flights in the Marshal queue should hold their altitude and are not allowed into the pattern until the spinning aircraft +-- proceeds. +-- +-- Once the player reaches a point 100 meters behind the boat and at least 1 NM port, his step is set to "Initial" and he can resume the normal pattern approach. +-- +-- If necessary, the player can call "Spinning" again when in the above mentioned steps. +-- +-- ### Emergency Landing +-- +-- Request an emergency landing, i.e. bypass all pattern steps and go directly to the final approach. +-- +-- All section members are supposed to follow. Player (or section lead) is removed from all other queues and automatically added to the landing pattern queue. +-- +-- If this command is called while the player is currently on the carrier, he will be put in the bolter pattern. So the next expected step after take of +-- is the abeam position. This allows for quick landing training exercises without having to go through the whole pattern. +-- +-- The mission designer can forbid this option my setting @{#AIRBOSS.SetEmergencyLandings}(false) in the script. +-- +-- ### [Reset My Status] +-- +-- This will reset the current player status. If player is currently in a marshal stack, he will be removed from the marshal queue and the stack above will collapse. +-- The player needs to re-register later if desired. If player is currently in the landing pattern, he will be removed from the pattern queue. +-- +-- ## Help Menu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuHelp.png) +-- +-- This menu provides commands to help the player. +-- +-- ### Mark Zones Submenu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMarkZones.png) +-- +-- These commands can be used to mark marshal or landing pattern zones. +-- +-- * **Smoke Pattern Zones** Smoke is used to mark the landing pattern zone of the player depending on his recovery case. +-- For Case I this is the initial zone. For Case II/III and three these are the Platform, Arc turn, Dirty Up, Bullseye/Initial zones as well as the approach corridor. +-- * **Flare Pattern Zones** Similar to smoke but uses flares to mark the pattern zones. +-- * **Smoke Marshal Zone** This smokes the surrounding area of the currently assigned Marshal zone of the player. Player has to be registered in Marshal queue. +-- * **Flare Marshal Zone** Similar to smoke but uses flares to mark the Marshal zone. +-- +-- Note that the smoke lasts ~5 minutes but the zones are moving along with the carrier. So after some time, the smoke gives shows you a picture of the past. +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case3_FlarePattern.png) +-- +-- ### Skill Level Submenu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuSkill.png) +-- +-- The player can choose between three skill or difficulty levels. +-- +-- * **Flight Student**: The player receives tips at certain stages of the pattern, e.g. if he is at the right altitude, speed, etc. +-- * **Naval Aviator**: Less tips are show. Player should be familiar with the procedures and its aircraft parameters. +-- * **TOPGUN Graduate**: Only very few information is provided to the player. This is for the pros. +-- * **Hints On/Off**: Toggle displaying hints. +-- +-- ### My Status +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMyStatus.png) +-- +-- This command provides information about the current player status. For example, his current step in the pattern. +-- +-- ### Attitude Monitor +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuAttitudeMonitor.png) +-- +-- This command displays the current aircraft attitude of the player aircraft in short intervals as message on the screen. +-- It provides information about current pitch, roll, yaw, orientation of the plane with respect to the carrier's orientation (*Gamma*) etc. +-- +-- If you are in the groove, current lineup and glideslope errors are displayed and you get an on-the-fly LSO grade. +-- +-- ### LSO Radio Check +-- +-- LSO will transmit a short message on his radio frequency. See @{#AIRBOSS.SetLSORadio}. Note that in the A-4E you will not hear the message unless you are in the pattern. +-- +-- ### Marshal Radio Check +-- +-- Marshal will transmit a short message on his radio frequency. See @{#AIRBOSS.SetMarshalRadio}. +-- +-- ### Subtitles On/Off +-- +-- This command toggles the display of radio message subtitles if no radio relay unit is used. By default subtitles are on. +-- Note that subtitles for radio messages which do not have a complete voice over are always displayed. +-- +-- ### Trapsheet On/Off +-- +-- Each player can activated or deactivate the recording of his flight data (AoA, glideslope, lineup, etc.) during his landing approaches. +-- Note that this feature also has to be enabled by the mission designer. +-- +-- ## Kneeboard Menu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuKneeboard.png) +-- +-- The Kneeboard menu provides information about the carrier, weather and player results. +-- +-- ### Results Submenu +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuResults.png) +-- +-- Here you find your LSO grading results as well as scores of other players. +-- +-- * **Greenie Board** lists average scores of all players obtained during landing approaches. +-- * **My LSO Grades** lists all grades the player has received for his approaches in this mission. +-- * **Last Debrief** shows the detailed debriefing of the player's last approach. +-- +-- ### Carrier Info +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuCarrierInfo.png) +-- +-- Information about the current carrier status is displayed. This includes current BRC, FB, LSO and Marshal frequencies, list of next recovery windows. +-- +-- ### Weather Report +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuWeatherReport.png) +-- +-- Displays information about the current weather at the carrier such as QFE, wind and temperature. +-- +-- For missions using static weather, more information such as cloud base, thickness, precipitation, visibility distance, fog and dust are displayed. +-- If your mission uses dynamic weather, you can disable this output via the @{#AIRBOSS.SetStaticWeather}(**false**) function. +-- +-- ### Set Section +-- +-- With this command, you can define a section of human flights. The player who issues the command becomes the section lead and all other human players +-- within a radius of 100 meters become members of the section. +-- +-- The responsibilities of the section leader are: +-- +-- * To request Marshal. The section members are not allowed to do this and have to follow the lead to his assigned stack. +-- * To lead the right way to the pattern if the flight is allowed to commence. +-- * The lead is also the only one who can request commence if the flight wants to bypass the Marshal stack. +-- +-- Each time the command is issued by the lead, the complete section is set up from scratch. Members which are not inside the 100 m radius any more are +-- removed and/or new members which are now in range are added. +-- +-- If a section member issues this command, it is removed from the section of his lead. All flights which are not yet in another section will become members. +-- +-- The default maximum size of a section is two human players. This can be adjusted by the @{#AIRBOSS.SetMaxSectionSize}(*size*) function. The maximum allowed size +-- is four. +-- +-- ### Marshal Queue +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMarshalQueue.png) +-- +-- Lists all flights currently in the Marshal queue including their assigned stack, recovery case and Charlie time estimate. +-- By default, the number of available Case I stacks is three, i.e. at angels 2, 3 and 4. Usually, the recovery thanker orbits at angels 6. +-- The number of available stacks can be set by the @{#AIRBOSS.SetMaxMarshalStack} function. +-- +-- The default number of human players per stack is two. This can be set via the @{#AIRBOSS.SetMaxFlightsPerStack} function but has to be between one and four. +-- +-- Due to technical reasons, each AI group always gets its own stack. DCS does not allow to control the AI in a manner that more than one group per stack would make sense unfortunately. +-- +-- ### Pattern Queue +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuPatternQueue.png) +-- +-- Lists all flights currently in the landing pattern queue showing the time since they entered the pattern. +-- By default, a maximum of four flights is allowed to enter the pattern. This can be set via the @{#AIRBOSS.SetMaxLandingPattern} function. +-- +-- ### Waiting Queue +-- +-- Lists all flights currently waiting for a free Case I Marshal stack. Note, stacks are limited only for Case I recovery ops but not for Case II or III. +-- If the carrier is switches recovery ops form Case I to Case II or III, all waiting flights will be assigned a stack. +-- +-- # Landing Signal Officer (LSO) +-- +-- The LSO will first contact you on his radio channel when you are at the the abeam position (Case I) with the phrase "Paddles, contact.". +-- Once you are in the groove the LSO will ask you to "Call the ball." and then acknowledge your ball call by "Roger Ball." +-- +-- During the groove the LSO will give you advice if you deviate from the correct landing path. These advices will be given when you are +-- +-- * too low or too high with respect to the glideslope, +-- * too fast or too slow with respect to the optimal AoA, +-- * too far left or too far right with respect to the lineup of the (angled) runway. +-- +-- ## LSO Grading +-- +-- LSO grading starts when the player enters the groove. The flight path and aircraft attitude is evaluated at certain steps (distances measured from rundown): +-- +-- * **X** At the Start (0.75 NM = 1390 m). +-- * **IM** In the Middle (0.5 NM = 926 m), middle one third of the glideslope. +-- * **IC** In Close (0.25 NM = 463 m), last one third of the glideslope. +-- * **AR** At the Ramp (0.027 NM = 50 m). +-- * **IW** In the Wires (at the landing position). +-- +-- Grading at each step includes the above calls, i.e. +-- +-- * **L**ined **U**p **L**eft or **R**ight: LUL, LUR +-- * Too **H**igh or too **LO**w: H, LO +-- * Too **F**ast or too **SLO**w: F, SLO +-- * **Fly through** glideslope **down** or **up**: \\ , / +-- +-- Each grading, x, is subdivided by +-- +-- * (x): parenthesis, indicating "a little" for a minor deviation and +-- * \_x\_: underline, indicating "a lot" for major deviations. +-- +-- The position at the landing event is analyzed and the corresponding trapped wire calculated. If no wire was caught, the LSO will give the bolter call. +-- +-- If a player is significantly off from the ideal parameters from IC to AR, the LSO will wave the player off. Thresholds for wave off are +-- +-- * Line up error > 3.0 degrees left or right and/or +-- * Glideslope error < -1.2 degrees or > 1.8 degrees and/or +-- * AOA depending on aircraft type and only applied if skill level is "TOPGUN graduate". +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_LSOPlatcam.png) +-- +-- Line up and glideslope error thresholds were tested extensively using [VFA-113 Stingers LSO Mod](https://forums.eagle.ru/showthread.php?t=211557), +-- if the aircraft is outside the red box. In the picture above, **blue** numbers denote the line up thresholds while the **blacks** refer to the glideslope. +-- +-- A wave off is called, when the aircraft is outside the red rectangle. The measurement stops already ~50 m before the rundown, since the error in the calculation +-- increases the closer the aircraft gets to the origin/reference point. +-- +-- The optimal glideslope is assumed to be 3.5 degrees leading to a touch down point between the second and third wire. +-- The height of the carrier deck and the exact wire locations are taken into account in the calculations. +-- +-- ## Pattern Waveoff +-- +-- The player's aircraft position is evaluated at certain critical locations in the landing pattern. If the player is far off from the ideal approach, the LSO will +-- issue a pattern wave off. Currently, this is only implemented for Case I recoveries and the Case I part in the Case II recovery, i.e. +-- +-- * Break Entry +-- * Early Break +-- * Late Break +-- * Abeam +-- * Ninety +-- * Wake +-- * Groove +-- +-- At these points it is also checked if a player comes too close to another aircraft ahead of him in the pattern. +-- +-- ## Grading Points +-- +-- Currently grades are given by as follows +-- +-- * 5.0 Points **\_OK\_**: "Okay underline", given only for a perfect pass, i.e. when no deviations at all were observed by the LSO. The unicorn! +-- * 4.0 Points **OK**: "Okay pass" when only minor () deviations happened. +-- * 3.0 Points **(OK)**: "Fair pass", when only "normal" deviations were detected. +-- * 2.0 Points **--**: "No grade", for larger deviations. +-- +-- Furthermore, we have the cases: +-- +-- * 2.5 Points **B**: "Bolder", when the player landed but did not catch a wire. +-- * 2.0 Points **WOP**: "Pattern Wave-Off", when pilot was far away from where he should be in the pattern. +-- * 2.0 Points **OWO**: "Own Wave-Off**, when pilot flies past the deck without touching it. +-- * 1.0 Points **WO**: "Technique Wave-Off": Player got waved off in the final parts of the groove. +-- * 1.0 Points **LIG**: "Long In the Groove", when pilot extents the downwind leg too far and screws up the timing for the following aircraft. +-- * 0.0 Points **CUT**: "Cut pass", when player was waved off but landed anyway. +-- +-- ## Foul Deck Waveoff +-- +-- A foul deck waveoff is called by the LSO if an aircraft is detected within the landing area when an approaching aircraft is crossing the ship's wake during Case I/II operations, +-- or with an aircraft approaching the 3/4 NM during Case III operations. +-- +-- The approaching aircraft will be notified via LSO radio comms and is supposed to overfly the landing area to enter the Bolter pattern. **This pass is not graded**. +-- +-- === +-- +-- # Scripting +-- +-- Writing a basic script is easy and can be done in two lines. +-- +-- local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis") +-- airbossStennis:Start() +-- +-- The **first line** creates and AIRBOSS object via the @{#AIRBOSS.New}(*carriername*, *alias*) constructor. The first parameter *carriername* is name of the carrier unit as +-- defined in the mission editor. The second parameter *alias* is optional. This name will, e.g., be used for the F10 radio menu entry. If not given, the alias is identical +-- to the *carriername* of the first parameter. +-- +-- This simple script initializes a lot of parameters with default values: +-- +-- * TACAN channel is set to 74X, see @{#AIRBOSS.SetTACAN}, +-- * ICSL channel is set to 1, see @{#AIRBOSS.SetICLS}, +-- * LSO radio is set to 264 MHz FM, see @{#AIRBOSS.SetLSORadio}, +-- * Marshal radio is set to 305 MHz FM, see @{#AIRBOSS.SetMarshalRadio}, +-- * Default recovery case is set to 1, see @{#AIRBOSS.SetRecoveryCase}, +-- * Carrier Controlled Area (CCA) is set to 50 NM, see @{#AIRBOSS.SetCarrierControlledArea}, +-- * Default player skill "Flight Student" (easy), see @{#AIRBOSS.SetDefaultPlayerSkill}, +-- * Once the carrier reaches its final waypoint, it will restart its route, see @{#AIRBOSS.SetPatrolAdInfinitum}. +-- +-- The **second line** starts the AIRBOSS class. If you set options this should happen after the @{#AIRBOSS.New} and before @{#AIRBOSS.Start} command. +-- +-- However, good mission planning involves also planning when aircraft are supposed to be launched or recovered. The definition of *case specific* recovery ops within the same mission is described in +-- the next section. +-- +-- ## Recovery Windows +-- +-- Recovery of aircraft is only allowed during defined time slots. You can define these slots via the @{#AIRBOSS.AddRecoveryWindow}(*start*, *stop*, *case*, *holdingoffset*) function. +-- The parameters are: +-- +-- * *start*: The start time as a string. For example "8:00" for a window opening at 8 am. Or "13:30+1" for half past one on the next day. Default (nil) is ASAP. +-- * *stop*: Time when the window closes as a string. Same format as *start*. Default is 90 minutes after start time. +-- * *case*: The recovery case during that window (1, 2 or 3). Default 1. +-- * *holdingoffset*: Holding offset angle in degrees. Only for Case II or III recoveries. Default 0 deg. Common +-15 deg or +-30 deg. +-- +-- If recovery is closed, AI flights will be send to marshal stacks and orbit there until the next window opens. +-- Players can request marshal via the F10 menu and will also be given a marshal stack. Currently, human players can request commence via the F10 radio regardless of +-- whether a window is open or not and will be allowed to enter the pattern (if not already full). This will probably change in the future. +-- +-- At the moment there is no automatic recovery case set depending on weather or daytime. So it is the AIRBOSS (i.e. you as mission designer) who needs to make that decision. +-- It is probably a good idea to synchronize the timing with the waypoints of the carrier. For example, setting up the waypoints such that the carrier +-- already has turning into the wind, when a recovery window opens. +-- +-- The code for setting up multiple recovery windows could look like this +-- local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis") +-- airbossStennis:AddRecoveryWindow("8:30", "9:30", 1) +-- airbossStennis:AddRecoveryWindow("12:00", "13:15", 2, 15) +-- airbossStennis:AddRecoveryWindow("23:30", "00:30+1", 3, -30) +-- airbossStennis:Start() +-- +-- This will open a Case I recovery window from 8:30 to 9:30. Then a Case II recovery from 12:00 to 13:15, where the holing offset is +15 degrees wrt BRC. +-- Finally, a Case III window opens 23:30 on the day the mission starts and closes 0:30 on the following day. The holding offset is -30 degrees wrt FB. +-- +-- Note that incoming flights will be assigned a holding pattern for the next opening window case if no window is open at the moment. So in the above example, +-- all flights incoming after 13:15 will be assigned to a Case III marshal stack. Therefore, you should make sure that no flights are incoming long before the +-- next window opens or adjust the recovery planning accordingly. +-- +-- The following example shows how you set up a recovery window for the next week: +-- +-- for i=0,7 do +-- airbossStennis:AddRecoveryWindow(string.format("08:05:00+%d", i), string.format("08:50:00+%d", i)) +-- end +-- +-- ### Turning into the Wind +-- +-- For each recovery window, you can define if the carrier should automatically turn into the wind. This is done by passing one or two additional arguments to the @{#AIRBOSS.AddRecoveryWindow} function: +-- +-- airbossStennis:AddRecoveryWindow("8:30", "9:30", 1, nil, true, 20) +-- +-- Setting the fifth parameter to *true* enables the automatic turning into the wind. The sixth parameter (here 20) specifies the speed in knots the carrier will go so that to total wind above the deck +-- corresponds to this wind speed. For example, if the is blowing with 5 knots, the carrier will go 15 knots so that the total velocity adds up to the specified 20 knots for the pilot. +-- +-- The carrier will steam into the wind for as long as the recovery window is open. The distance up to which possible collisions are detected can be set by the @{#AIRBOSS.SetCollisionDistance} function. +-- +-- However, the AIRBOSS scans the type of the surface up to 5 NM in the direction of movement of the carrier. If he detects anything but deep water, he will stop the current course and head back to +-- the point where he initially turned into the wind. +-- +-- The same holds true after the recovery window closes. The carrier will head back to the place where he left its assigned route and resume the path to the next waypoint defined in the mission editor. +-- +-- Note that the carrier will only head into the wind, if the wind direction is different by more than 5° from the current heading of the carrier (the angled runway, if any, fis taken into account here). +-- +-- === +-- +-- # Persistence of Player Results +-- +-- LSO grades of players can be saved to disk and later reloaded when a new mission is started. +-- +-- ## Prerequisites +-- +-- **Important** By default, DCS does not allow for writing data to files. Therefore, one first has to comment out the line "sanitizeModule('io')" and "sanitizeModule('lfs')", i.e. +-- +-- do +-- sanitizeModule('os') +-- --sanitizeModule('io') -- required for saving files +-- --sanitizeModule('lfs') -- optional for setting the default path to your "Saved Games\DCS" folder +-- require = nil +-- loadlib = nil +-- end +-- +-- in the file "MissionScripting.lua", which is located in the subdirectory "Scripts" of your DCS installation root directory. +-- +-- **WARNING** Desanitizing the "io" and "lfs" modules makes your machine or server vulnerable to attacks from the outside! Use this at your own risk. +-- +-- ## Save Results +-- +-- Saving asset data to file is achieved by the @{AIRBOSS.Save}(*path*, *filename*) function. +-- +-- The parameter *path* specifies the path on the file system where the +-- player grades are saved. If you do not specify a path, the file is saved your the DCS installation root directory if the **lfs** module is *not* desanizied or +-- your "Saved Games\\DCS" folder in case you did desanitize the **lfs** module. +-- +-- The parameter *filename* is optional and defines the name of the saved file. By default this is automatically created from the AIRBOSS carrier name/alias, i.e. +-- "Airboss-USS Stennis_LSOgrades.csv", if the alias is "USS Stennis". +-- +-- In the easiest case, you desanitize the **io** and **lfs** modules and just add the line +-- +-- airbossStennis:Save() +-- +-- If you want to specify an explicit path you can do this by +-- +-- airbossStennis:Save("D:\\My Airboss Data\\") +-- +-- This will save all player grades to in "D:\\My Airboss Data\\Airboss-USS Stennis_LSOgrades.csv". +-- +-- ### Automatic Saving +-- +-- The player grades can be saved automatically after each graded player pass via the @{AIRBOSS.SetAutoSave}(*path*, *filename*) function. Again the parameters *path* and *filename* are optional. +-- In the simplest case, you desanitize the **lfs** module and just add +-- +-- airbossStennis:SetAutoSave() +-- +-- Note that the the stats are saved after the *final* grade has been given, i.e. the player has landed on the carrier. After intermediate results such as bolters or waveoffs the stats are not automatically saved. +-- +-- In case you want to specify an explicit path, you can write +-- +-- airbossStennis:SetAutoSave("D:\\My Airboss Data\\") +-- +-- ## Results Output +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_PersistenceResultsTable.png) +-- +-- The results file is stored as comma separated file. The columns are +-- +-- * *Name*: The player name. +-- * *Pass*: A running number counting the passes of the player +-- * *Points Final*: The final points (i.e. when the player has landed). This is the average over all previous bolters or waveoffs, if any. +-- * *Points Pass*: The points of each pass including bolters and waveoffs. +-- * *Grade*: LSO grade. +-- * *Details*: Detailed analysis of deviations within the groove. +-- * *Wire*: Trapped wire, if any. +-- * *Tgroove*: Time in the groove in seconds (not applicable during Case III). +-- * *Case*: The recovery case operations in progress during the pass. +-- * *Wind*: Wind on deck in knots during approach. +-- * *Modex*: Tail number of the player. +-- * *Airframe*: Aircraft type used in the recovery. +-- * *Carrier Type*: Type name of the carrier. +-- * *Carrier Name*: Name/alias of the carrier. +-- * *Theatre*: DCS map. +-- * *Mission Time*: Mission time at the end of the approach. +-- * *Mission Date*: Mission date in yyyy/mm/dd format. +-- * *OS Date*: Real life date from os.date(). Needs **os** to be desanitized. +-- +-- ## Load Results +-- +-- Loading player grades from file is achieved by the @{AIRBOSS.Load}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the +-- data is loaded from. If you do not specify a path, the file is loaded from your the DCS installation root directory or, if **lfs** was desanitized from you "Saved Games\DCS" directory. +-- The parameter *filename* is optional and defines the name of the file to load. By default this is automatically generated from the AIBOSS carrier name/alias, for example +-- "Airboss-USS Stennis_LSOgrades.csv". +-- +-- Note that the AIRBOSS FSM **must not be started** in order to load the data. In other words, loading should happen **after** the +-- @{#AIRBOSS.New} command is specified in the code but **before** the @{#AIRBOSS.Start} command is given. +-- +-- The easiest was to load player results is +-- +-- airbossStennis:New("USS Stennis") +-- airbossStennis:Load() +-- airbossStennis:SetAutoSave() +-- -- Additional specification of parameters such as recovery windows etc, if required. +-- airbossStennis:Start() +-- +-- This sequence loads all available player grades from the default file and automatically saved them when a player received a (final) grade. Again, if **lfs** was desanitized, the files are save to and loaded +-- from the "Saved Games\DCS" directory. If **lfs** was *not* desanitized, the DCS root installation folder is the default path. +-- +-- # Trap Sheet +-- +-- Important aircraft attitude parameters during the Groove can be saved to file for later analysis. This also requires the **io** and optionally **lfs** modules to be desanitized. +-- +-- In the script you have to add the @{#AIRBOSS.SetTrapSheet}(*path*) function to activate this feature. +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetTable.png) +-- +-- Data the is written to a file in csv format and contains the following information: +-- +-- * *Time*: time in seconds since start. +-- * *Rho*: distance from rundown to player aircraft in NM. +-- * *X*: distance parallel to the carrier in meters. +-- * *Z*: distance perpendicular to the carrier in meters. +-- * *Alt*: altitude of player aircraft in feet. +-- * *AoA*: angle of attack in degrees. +-- * *GSE*: glideslope error in degrees. +-- * *LUE*: lineup error in degrees. +-- * *Vtot*: total velocity of player aircraft in knots. +-- * *Vy*: vertical (descent) velocity in ft/min. +-- * *Gamma*: angle between vector of aircraft nose and vector point in the direction of the carrier runway in degrees. +-- * *Pitch*: pitch angle of player aircraft in degrees. +-- * *Roll*: roll angle of player aircraft in degrees. +-- * *Yaw*: yaw angle of player aircraft in degrees. +-- * *Step*: Step in the groove. +-- * *Grade*: Current LSO grade. +-- * *Points*: Current points for the pass. +-- * *Details*: Detailed grading analysis. +-- +--## Lineup Error +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetLUE.png) +-- +-- The graph displays the lineup error (LUE) as a function of the distance to the carrier. +-- +-- The pilot approaches the carrier from the port side, LUE>0°, at a distance of ~1 NM. +-- At the beginning of the groove (X), he significantly overshoots to the starboard side (LUE<5°). +-- In the middle (IM), he performs good corrections and smoothly reduces the lineup error. +-- Finally, at a distance of ~0.3 NM (IC) he has corrected his lineup with the runway to a reasonable level, |LUE|<0.5°. +-- +-- ## Glideslope Error +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetGLE.png) +-- +-- The graph displays the glideslope error (GSE) as a function of the distance to the carrier. +-- +-- In this case the pilot already enters the groove (X) below the optimal glideslope. He is not able to correct his height in the IM part and +-- stays significantly too low. In close, he performs a harsh correction to gain altitude and ends up even slightly too high (GSE>0.5°). +-- At his point further corrections are necessary. +-- +-- ## Angle of Attack +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetAoA.png) +-- +-- The graph displays the angle of attack (AoA) as a function of the distance to the carrier. +-- +-- The pilot starts off being on speed after the ball call. Then he get way to fast troughout the most part of the groove. He manages to correct +-- this somewhat short before touchdown. +-- +-- === +-- +-- # Sound Files +-- +-- An important aspect of the AIRBOSS is that it uses voice overs for greater immersion. The necessary sound files can be obtained from the +-- MOOSE Discord in the [#ops-airboss](https://discordapp.com/channels/378590350614462464/527363141185830915) channel. Check out the **pinned messages**. +-- +-- However, including sound files into a new mission is tedious as these usually need to be included into the mission **miz** file via (unused) triggers. +-- +-- The default location inside the miz file is "l10n/DEFAULT/". But simply opening the *miz* file with e.g. [7-zip](https://www.7-zip.org/) and copying the files into that folder does not work. +-- The next time the mission is saved, files not included via trigger are automatically removed by DCS. +-- +-- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used. The location of the sound files can be specified +-- via the @{#AIRBOSS.SetSoundfilesFolder}(*folderpath*) function. The parameter *folderpath* defines the location of the sound files folder within the mission *miz* file. +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_SoundfilesFolder.png) +-- +-- For example as +-- +-- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles/") +-- +-- ## Carrier Specific Voice Overs +-- +-- It is possible to use different sound files for different carriers. If you have set up two (or more) AIRBOSS objects at different carriers - say Stennis and Tarawa - each +-- carrier would use the files in the specified directory, e.g. +-- +-- airbossStennis:SetSoundfilesFolder("Airboss Soundfiles Stennis/") +-- airbossTarawa:SetSoundfilesFolder("Airboss Soundfiles Tarawa/") +-- +-- ## Sound Packs +-- +-- The AIRBOSS currently has two different "sound packs" for both LSO and Marshal radios. These contain voice overs by different actors. +-- These can be set by @{#AIRBOSS.SetVoiceOversLSOByRaynor}() and @{#AIRBOSS.SetVoiceOversMarshalByRaynor}(). These are the default settings. +-- The other sound files can be set by @{#AIRBOSS.SetVoiceOversLSOByFF}() and @{#AIRBOSS.SetVoiceOversMarshalByFF}(). +-- Also combinations can be used, e.g. +-- +-- airbossStennis:SetVoiceOversLSOByFF() +-- airbossStennis:SetVoiceOversMarshalByRaynor() +-- +-- In this example LSO voice overs by FF and Marshal voice overs by Raynor are used. +-- +-- **Note** that this only initializes the correct parameters parameters of sound files, i.e. the duration. The correct files have to be in the directory set by the +-- @{#AIRBOSS.SetSoundfilesFolder}(*folder*) function. +-- +-- ## How To Use Your Own Voice Overs +-- +-- If you have a set of AIRBOSS sound files recorded or got it from elsewhere it is possible to use those instead of the default ones. +-- I recommend to use exactly the same file names as the original sound files have. +-- +-- However, the **timing is critical**! As sometimes sounds are played directly after one another, e.g. by saying the modex but also on other occations, the airboss +-- script has a radio queue implemented (actually two - one for the LSO and one for the Marshal/Airboss radio). +-- By this it is automatically taken care that played messages are not overlapping and played over each other. The disadvantage is, that the script needs to know +-- the exact duration of *each* voice over. For the default sounds this is hard coded in the source code. For your own files, you need to give that bit of information +-- to the script via the @{#AIRBOSS.SetVoiceOver}(**radiocall**, **duration**, **subtitle**, **subduration**, **filename**, **suffix**) function. Only the first two +-- parameters **radiocall** and **duration** are usually important to adjust here. +-- +-- For example, if you want to change the LSO "Call the Ball" and "Roger Ball" calls: +-- +-- airbossStennis:SetVoiceOver(airbossStennis.LSOCall.CALLTHEBALL, 0.6) +-- airbossStennis:SetVoiceOver(airbossStennis.LSOCall.ROGERBALL, 0.7) +-- +-- Again, changing the file name, subtitle, subtitle duration is not required if you name the file exactly like the original one, which is this case would be "LSO-RogerBall.ogg". +-- +-- +-- +-- ## The Radio Dilemma +-- +-- DCS offers two (actually three) ways to send radio messages. Each one has its advantages and disadvantages and it is important to understand the differences. +-- +-- ### Transmission via Command +-- +-- *In principle*, the best way to transmit messages is via the [TransmitMessage](https://wiki.hoggitworld.com/view/DCS_command_transmitMessage) command. +-- This method has the advantage that subtitles can be used and these subtitles are only displayed to the players who dialed in the same radio frequency as +-- used for the transmission. +-- However, this method unfortunately only works if the sending unit is an **aircraft**. Therefore, it is not usable by the AIRBOSS per se as the transmission comes from +-- a naval unit (i.e. the carrier). +-- +-- As a workaround, you can put an aircraft, e.g. a Helicopter on the deck of the carrier or another ship of the strike group. The aircraft should be set to +-- uncontrolled and maybe even to immortal. With the @{#AIRBOSS.SetRadioUnitName}(*unitname*) function you can use this unit as "radio repeater" for both Marshal and LSO +-- radio channels. However, this might lead to interruptions in the transmission if both channels transmit simultaniously. Therefore, it is better to assign a unit for +-- each radio via the @{#AIRBOSS.SetRadioRelayLSO}(unitname) and @{#AIRBOSS.SetRadioRelayMarshal}(unitname) functions. +-- +-- Of course you can also use any other aircraft in the vicinity of the carrier, e.g. a rescue helo or a recovery tanker. It is just important that this +-- unit is and stays close the the boat as the distance from the sender to the receiver is modeled in DCS. So messages from too far away might not reach the players. +-- +-- **Note** that not all radio messages the airboss sends have voice overs. Therefore, if you use a radio relay unit, users should *not* disable the +-- subtitles in the DCS game menu. +-- +-- ### Transmission via Trigger +-- +-- Another way to broadcast messages is via the [radio transmission trigger](https://wiki.hoggitworld.com/view/DCS_func_radioTransmission). This method can be used for all +-- units (land, air, naval). However, messages cannot be subtitled. Therefore, subtitles are displayed to the players via normal textout messages. +-- The disadvantage is that is is impossible to know which players have the right radio frequencies dialed in. Therefore, subtitles of the Marshal radio calls are displayed to all players +-- inside the CCA. Subtitles on the LSO radio frequency are displayed to all players in the pattern. +-- +-- ### Sound to User +-- +-- The third way to play sounds to the user via the [outsound trigger](https://wiki.hoggitworld.com/view/DCS_func_outSound). +-- These sounds are not coming from a radio station and therefore can be heard by players independent of their actual radio frequency setting. +-- The AIRBOSS class uses this method to play sounds to players which are of a more "private" nature - for example when a player has left his assigned altitude +-- in the Marshal stack. Often this is the modex of the player in combination with a textout messaged displayed on screen. +-- +-- If you want to use this method for all radio messages you can enable it via the @{#AIRBOSS.SetUserSoundRadio}() function. This is the analogue of activating easy comms in DCS. +-- +-- Note that this method is used for all players who are in the A-4E community mod as this mod does not have the ability to use radios due to current DCS restrictions. +-- Therefore, A-4E drivers will hear all radio transmissions from the Marshal/Airboss and all LSO messages as soon as their commence the pattern. +-- +-- === +-- +-- # AI Handling +-- +-- The @{#AIRBOSS} class allows to handle incoming AI units and integrate them into the marshal and landing pattern. +-- +-- By default, incoming carrier capable aircraft which are detecting inside the Carrier Controlled Area (CCA) and approach the carrier by more than 5 NM are automatically guided to the holding zone. +-- Each AI group gets its own marshal stack in the holding pattern. Once a recovery window opens, the AI group of the lowest stack is transitioning to the landing pattern +-- and the Marshal stack collapses. +-- +-- If no AI handling is desired, this can be turned off via the @{#AIRBOSS.SetHandleAIOFF} function. +-- +-- In case only specifc AI groups shall be excluded, it can be done by adding the groups to a set, e.g. +-- +-- -- AI groups explicitly excluded from handling by the Airboss +-- local CarrierExcludeSet=SET_GROUP:New():FilterPrefixes("E-2D Wizard Group"):FilterStart() +-- AirbossStennis:SetExcludeAI(CarrierExcludeSet) +-- +-- Similarly, to the @{#AIRBOSS.SetExcludeAI} function, AI groups can be explicitly *included* via the @{#AIRBOSS.SetSquadronAI} function. If this is used, only the *included* groups are handled +-- by the AIRBOSS. +-- +-- ## Keep the Deck Clean +-- +-- Once the AI groups have landed on the carrier, they can be despawned automatically after they shut down their engines. This is achieved by the @{#AIRBOSS.SetDespawnOnEngineShutdown}() function. +-- +-- ## Refueling +-- +-- AI groups in the marshal pattern can be send to refuel at the recovery tanker or if none is defined to the nearest divert airfield. This can be enabled by the @{#AIRBOSS.SetRefuelAI}(*lowfuelthreshold*). +-- The parameter *lowfuelthreshold* is the threshold of fuel in percent. If the fuel drops below this value, the group will go for refueling. If refueling is performed at the recovery tanker, +-- the group will return to the marshal stack when done. The aircraft will not return from the divert airfield however. +-- +-- Note that this feature is not enabled by default as there might be bugs in DCS that prevent a smooth refueling of the AI. Enable at your own risk. +-- +-- ## Respawning - DCS Landing Bug +-- +-- AI groups that enter the CCA are usually guided to Marshal stack. However, due to DCS limitations they might not obey the landing task if they have another airfield as departure and/or destination in +-- their mission task. Therefore, AI groups can be respawned when detected in the CCA. This should clear all other airfields and allow the aircraft to land on the carrier. +-- This is achieved by the @{AIRBOSS.SetRespawnAI}() function. +-- +-- ## Known Issues +-- +-- Dealing with the DCS AI is a big challenge and there is only so much one can do. Please bear this in mind! +-- +-- ### Pattern Updates +-- +-- The holding position of the AI is updated regularly when the carrier has changed its position by more then 2.5 NM or changed its course significantly. +-- The patterns are realized by orbit or racetrack patterns of the DCS scripting API. +-- However, when the position is updated or the marshal stack collapses, it comes to disruptions of the regular orbit because a new waypoint with a new +-- orbit task needs to be created. +-- +-- ### Recovery Cases +-- +-- The AI performs a very realistic Case I recovery. Therefore, we already have a good Case I and II recovery simulation since the final part of Case II is a +-- Case I recovery. However, I don't think the AI can do a proper Case III recovery. If you give the AI the landing command, it is out of our hands and will +-- always go for a Case I in the final pattern part. Maybe this will improve in future DCS version but right now, there is not much we can do about it. +-- +-- === +-- +-- # Finite State Machine (FSM) +-- +-- The AIRBOSS class has a Finite State Machine (FSM) implementation for the carrier. This allows mission designers to hook into certain events and helps +-- simulate complex behaviour easier. +-- +-- FSM events are: +-- +-- * @{#AIRBOSS.Start}: Starts the AIRBOSS FSM. +-- * @{#AIRBOSS.Stop}: Stops the AIRBOSS FSM. +-- * @{#AIRBOSS.Idle}: Carrier is set to idle and not recovering. +-- * @{#AIRBOSS.RecoveryStart}: Starts the recovery ops. +-- * @{#AIRBOSS.RecoveryStop}: Stops the recovery ops. +-- * @{#AIRBOSS.RecoveryPause}: Pauses the recovery ops. +-- * @{#AIRBOSS.RecoveryUnpause}: Unpauses the recovery ops. +-- * @{#AIRBOSS.RecoveryCase}: Sets/switches the recovery case. +-- * @{#AIRBOSS.PassingWaypoint}: Carrier passes a waypoint defined in the mission editor. +-- +-- These events can be used in the user script. When the event is triggered, it is automatically a function OnAfter*Eventname* called. For example +-- +-- --- Carrier just passed waypoint *n*. +-- function AirbossStennis:OnAfterPassingWaypoint(From, Event, To, n) +-- -- Launch green flare. +-- self.carrier:FlareGreen() +-- end +-- +-- In this example, we only launch a green flare every time the carrier passes a waypoint defined in the mission editor. But, of course, you can also use it to add new +-- recovery windows each time a carrier passes a waypoint. Therefore, you can create an "infinite" number of windows easily. +-- +-- === +-- +-- # Examples +-- +-- In this section a few simple examples are given to illustrate the scripting part. +-- +-- ## Simple Case +-- +-- -- Create AIRBOSS object. +-- local AirbossStennis=AIRBOSS:New("USS Stennis") +-- +-- -- Add recovery windows: +-- -- Case I from 9 to 10 am. Carrier will turn into the wind 5 min before window opens and go at a speed so that wind over the deck is 25 knots. +-- local window1=AirbossStennis:AddRecoveryWindow("9:00", "10:00", 1, nil, true, 25) +-- -- Case II with +15 degrees holding offset from 15:00 for 60 min. +-- local window2=AirbossStennis:AddRecoveryWindow("15:00", "16:00", 2, 15) +-- -- Case III with +30 degrees holding offset from 21:00 to 23:30. +-- local window3=AirbossStennis:AddRecoveryWindow("21:00", "23:30", 3, 30) +-- +-- -- Load all saved player grades from your "Saved Games\DCS" folder (if lfs was desanitized). +-- AirbossStennis:Load() +-- +-- -- Automatically save player results to your "Saved Games\DCS" folder each time a player get a final grade from the LSO. +-- AirbossStennis:SetAutoSave() +-- +-- -- Start airboss class. +-- AirbossStennis:Start() +-- +-- === +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the @{#AIRBOSS} class should have the string "AIRBOSS" in the corresponding line. +-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("AIRBOSS") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- +-- ### Debug Mode +-- +-- You have the option to enable the debug mode for this class via the @{#AIRBOSS.SetDebugModeON} function. +-- If enabled, status and debug text messages will be displayed on the screen. Also informative marks on the F10 map are created. +-- +-- @field #AIRBOSS +AIRBOSS = { + ClassName = "AIRBOSS", + Debug = false, + lid = nil, + theatre = nil, + carrier = nil, + carriertype = nil, + carrierparam = {}, + alias = nil, + airbase = nil, + waypoints = {}, + currentwp = nil, + beacon = nil, + TACANon = nil, + TACANchannel = nil, + TACANmode = nil, + TACANmorse = nil, + ICLSon = nil, + ICLSchannel = nil, + ICLSmorse = nil, + LSORadio = nil, + LSOFreq = nil, + LSOModu = nil, + MarshalRadio = nil, + MarshalFreq = nil, + MarshalModu = nil, + TowerFreq = nil, + radiotimer = nil, + zoneCCA = nil, + zoneCCZ = nil, + players = {}, + menuadded = {}, + BreakEntry = {}, + BreakEarly = {}, + BreakLate = {}, + Abeam = {}, + Ninety = {}, + Wake = {}, + Final = {}, + Groove = {}, + Platform = {}, + DirtyUp = {}, + Bullseye = {}, + defaultcase = nil, + case = nil, + defaultoffset = nil, + holdingoffset = nil, + recoverytimes = {}, + flights = {}, + Qpattern = {}, + Qmarshal = {}, + Qwaiting = {}, + Qspinning = {}, + RQMarshal = {}, + RQLSO = {}, + TQMarshal = 0, + TQLSO = 0, + Nmaxpattern = nil, + Nmaxmarshal = nil, + NmaxSection = nil, + NmaxStack = nil, + handleai = nil, + tanker = nil, + Corientation = nil, + Corientlast = nil, + Cposition = nil, + defaultskill = nil, + adinfinitum = nil, + magvar = nil, + Tcollapse = nil, + recoverywindow = nil, + usersoundradio = nil, + Tqueue = nil, + dTqueue = nil, + dTstatus = nil, + menumarkzones = nil, + menusmokezones = nil, + playerscores = nil, + autosave = nil, + autosavefile = nil, + autosavepath = nil, + marshalradius = nil, + airbossnice = nil, + staticweather = nil, + windowcount = 0, + LSOdT = nil, + senderac = nil, + radiorelayLSO = nil, + radiorelayMSH = nil, + turnintowind = nil, + detour = nil, + squadsetAI = nil, + excludesetAI = nil, + menusingle = nil, + collisiondist = nil, + holdtimestamp = nil, + Tmessage = nil, + soundfolder = nil, + soundfolderLSO = nil, + soundfolderMSH = nil, + despawnshutdown= nil, + dTbeacon = nil, + Tbeacon = nil, + LSOCall = nil, + MarshalCall = nil, + lowfuelAI = nil, + emergency = nil, + respawnAI = nil, + gle = {}, + lue = {}, + trapsheet = nil, + trappath = nil, + trapprefix = nil, + initialmaxalt = nil, + welcome = nil, + skipperMenu = nil, + skipperSpeed = nil, + skipperTime = nil, + skipperOffset = nil, + skipperUturn = nil, +} + +--- Aircraft types capable of landing on carrier (human+AI). +-- @type AIRBOSS.AircraftCarrier +-- @field #string AV8B AV-8B Night Harrier. Works only with the USS Tarawa. +-- @field #string A4EC A-4E Community mod. +-- @field #string HORNET F/A-18C Lot 20 Hornet by Eagle Dynamics. +-- @field #string F14A F-14A by Heatblur. +-- @field #string F14B F-14B by Heatblur. +-- @field #string F14A_AI F-14A Tomcat (AI). +-- @field #string FA18C F/A-18C Hornet (AI). +-- @field #string S3B Lockheed S-3B Viking. +-- @field #string S3BTANKER Lockheed S-3B Viking tanker. +-- @field #string E2D Grumman E-2D Hawkeye AWACS. +-- @field #string C2A Grumman C-2A Greyhound from Military Aircraft Mod. +AIRBOSS.AircraftCarrier={ + AV8B="AV8BNA", + HORNET="FA-18C_hornet", + A4EC="A-4E-C", + F14A="F-14A_tomcat", + F14B="F-14B", + F14A_AI="F-14A", + FA18C="F/A-18C", + S3B="S-3B", + S3BTANKER="S-3B Tanker", + E2D="E-2C", + C2A="C2A_Greyhound", +} + +--- Carrier types. +-- @type AIRBOSS.CarrierType +-- @field #string ROOSEVELT USS Theodore Roosevelt (CVN-71) +-- @field #string LINCOLN USS Abraham Lincoln (CVN-72) +-- @field #string WASHINGTON USS George Washington (CVN-73) +-- @field #string STENNIS USS John C. Stennis (CVN-74) +-- @field #string VINSON USS Carl Vinson (CVN-70) +-- @field #string TARAWA USS Tarawa (LHA-1) +-- @field #string KUZNETSOV Admiral Kuznetsov (CV 1143.5) +AIRBOSS.CarrierType={ + ROOSEVELT="CVN_71", + LINCOLN="CVN_72", + WASHINGTON="CVN_73", + STENNIS="Stennis", + VINSON="VINSON", + TARAWA="LHA_Tarawa", + KUZNETSOV="KUZNECOW", +} + +--- Carrier specific parameters. +-- @type AIRBOSS.CarrierParameters +-- @field #number rwyangle Runway angle in degrees. for carriers with angled deck. For USS Stennis -9 degrees. +-- @field #number sterndist Distance in meters from carrier position to stern of carrier. For USS Stennis -150 meters. +-- @field #number deckheight Height of deck in meters. For USS Stennis ~63 ft = 19 meters. +-- @field #number wire1 Distance in meters from carrier position to first wire. +-- @field #number wire2 Distance in meters from carrier position to second wire. +-- @field #number wire3 Distance in meters from carrier position to third wire. +-- @field #number wire4 Distance in meters from carrier position to fourth wire. +-- @field #number rwylength Length of the landing runway in meters. +-- @field #number rwywidth Width of the landing runway in meters. +-- @field #number totlength Total length of carrier. +-- @field #number totwidthstarboard Total with of the carrier from stern position to starboard side (asymmetric carriers). +-- @field #number totwidthport Total with of the carrier from stern position to port side (asymmetric carriers). + +--- Aircraft specific Angle of Attack (AoA) (or alpha) parameters. +-- @type AIRBOSS.AircraftAoA +-- @field #number OnSpeedMin Minimum on speed AoA. Values below are fast +-- @field #number OnSpeedMax Maximum on speed AoA. Values above are slow. +-- @field #number OnSpeed Optimal on-speed AoA. +-- @field #number Fast Fast AoA threshold. Smaller means faster. +-- @field #number Slow Slow AoA threshold. Larger means slower. +-- @field #number FAST Really fast AoA threshold. +-- @field #number SLOW Really slow AoA threshold. + +--- Glideslope error thresholds in degrees. +-- @type AIRBOSS.GLE +-- @field #number _max Max _OK_ value. Default 0.4 deg. +-- @field #number _min Min _OK_ value. Default -0.3 deg. +-- @field #number High (H) threshold. Default 0.8 deg. +-- @field #number Low (L) threshold. Default -0.6 deg. +-- @field #number HIGH H threshold. Default 1.5 deg. +-- @field #number LOW L threshold. Default -0.9 deg. + +--- Lineup error thresholds in degrees. +-- @type AIRBOSS.LUE +-- @field #number _max Max _OK_ value. Default 0.5 deg. +-- @field #number _min Min _OK_ value. Default -0.5 deg. +-- @field #number Left (LUR) threshold. Default -1.0 deg. +-- @field #number Right (LUL) threshold. Default 1.0 deg. +-- @field #number LEFT LUR threshold. Default -3.0 deg. +-- @field #number RIGHT LUL threshold. Default 3.0 deg. + + +--- Pattern steps. +-- @type AIRBOSS.PatternStep +-- @field #string UNDEFINED "Undefined". +-- @field #string REFUELING "Refueling". +-- @field #string SPINNING "Spinning". +-- @field #string COMMENCING "Commencing". +-- @field #string HOLDING "Holding". +-- @field #string WAITING "Waiting for free Marshal stack". +-- @field #string PLATFORM "Platform". +-- @field #string ARCIN "Arc Turn In". +-- @field #string ARCOUT "Arc Turn Out". +-- @field #string DIRTYUP "Dirty Up". +-- @field #string BULLSEYE "Bullseye". +-- @field #string INITIAL "Initial". +-- @field #string BREAKENTRY "Break Entry". +-- @field #string EARLYBREAK "Early Break". +-- @field #string LATEBREAK "Late Break". +-- @field #string ABEAM "Abeam". +-- @field #string NINETY "Ninety". +-- @field #string WAKE "Wake". +-- @field #string FINAL "Final". +-- @field #string GROOVE_XX "Groove X". +-- @field #string GROOVE_IM "Groove In the Middle". +-- @field #string GROOVE_IC "Groove In Close". +-- @field #string GROOVE_AR "Groove At the Ramp". +-- @field #string GROOVE_AL "Groove Abeam Landing Spot". +-- @field #string GROOVE_LC "Groove Level Cross". +-- @field #string GROOVE_IW "Groove In the Wires". +-- @field #string BOLTER "Bolter Pattern". +-- @field #string EMERGENCY "Emergency Landing". +-- @field #string DEBRIEF "Debrief". +AIRBOSS.PatternStep={ + UNDEFINED="Undefined", + REFUELING="Refueling", + SPINNING="Spinning", + COMMENCING="Commencing", + HOLDING="Holding", + WAITING="Waiting for free Marshal stack", + PLATFORM="Platform", + ARCIN="Arc Turn In", + ARCOUT="Arc Turn Out", + DIRTYUP="Dirty Up", + BULLSEYE="Bullseye", + INITIAL="Initial", + BREAKENTRY="Break Entry", + EARLYBREAK="Early Break", + LATEBREAK="Late Break", + ABEAM="Abeam", + NINETY="Ninety", + WAKE="Wake", + FINAL="Turn Final", + GROOVE_XX="Groove X", + GROOVE_IM="Groove In the Middle", + GROOVE_IC="Groove In Close", + GROOVE_AR="Groove At the Ramp", + GROOVE_IW="Groove In the Wires", + GROOVE_AL="Groove Abeam Landing Spot", + GROOVE_LC="Groove Level Cross", + BOLTER="Bolter Pattern", + EMERGENCY="Emergency Landing", + DEBRIEF="Debrief", +} + +--- Groove position. +-- @type AIRBOSS.GroovePos +-- @field #string X0 "X0": Entering the groove. +-- @field #string XX "XX": At the start, i.e. 3/4 from the run down. +-- @field #string IM "IM": In the middle. +-- @field #string IC "IC": In close. +-- @field #string AR "AR": At the ramp. +-- @field #string AL "AL": Abeam landing position (Tarawa). +-- @field #string LC "LC": Level crossing (Tarawa). +-- @field #string IW "IW": In the wires. +AIRBOSS.GroovePos={ + X0="X0", + XX="XX", + IM="IM", + IC="IC", + AR="AR", + AL="AL", + LC="LC", + IW="IW", +} + +--- Radio. +-- @type AIRBOSS.Radio +-- @field #number frequency Frequency in Hz. +-- @field #number modulation Band modulation. +-- @field #string alias Radio alias. + +--- Radio sound file and subtitle. +-- @type AIRBOSS.RadioCall +-- @field #string file Sound file name without suffix. +-- @field #string suffix File suffix/extension, e.g. "ogg". +-- @field #boolean loud Loud version of sound file available. +-- @field #string subtitle Subtitle displayed during transmission. +-- @field #number duration Duration of the sound in seconds. This is also the duration the subtitle is displayed. +-- @field #number subduration Duration in seconds the subtitle is displayed. +-- @field #string modexsender Onboard number of the sender (optional). +-- @field #string modexreceiver Onboard number of the receiver (optional). +-- @field #string sender Sender of the message (optional). Default radio alias. + +--- Pilot radio calls. +-- type AIRBOSS.PilotCalls +-- @field #AIRBOSS.RadioCall N0 "Zero" call. +-- @field #AIRBOSS.RadioCall N1 "One" call. +-- @field #AIRBOSS.RadioCall N2 "Two" call. +-- @field #AIRBOSS.RadioCall N3 "Three" call. +-- @field #AIRBOSS.RadioCall N4 "Four" call. +-- @field #AIRBOSS.RadioCall N5 "Five" call. +-- @field #AIRBOSS.RadioCall N6 "Six" call. +-- @field #AIRBOSS.RadioCall N7 "Seven" call. +-- @field #AIRBOSS.RadioCall N8 "Eight" call. +-- @field #AIRBOSS.RadioCall N9 "Nine" call. +-- @field #AIRBOSS.RadioCall POINT "Point" call. +-- @field #AIRBOSS.RadioCall BALL "Ball" call. +-- @field #AIRBOSS.RadioCall HARRIER "Harrier" call. +-- @field #AIRBOSS.RadioCall HAWKEYE "Hawkeye" call. +-- @field #AIRBOSS.RadioCall HORNET "Hornet" call. +-- @field #AIRBOSS.RadioCall SKYHAWK "Skyhawk" call. +-- @field #AIRBOSS.RadioCall TOMCAT "Tomcat" call. +-- @field #AIRBOSS.RadioCall VIKING "Viking" call. +-- @field #AIRBOSS.RadioCall BINGOFUEL "Bingo Fuel" call. +-- @field #AIRBOSS.RadioCall GASATDIVERT "Going for gas at the divert field" call. +-- @field #AIRBOSS.RadioCall GASATTANKER "Going for gas at the recovery tanker" call. + +--- LSO radio calls. +-- @type AIRBOSS.LSOCalls +-- @field #AIRBOSS.RadioCall BOLTER "Bolter, Bolter" call. +-- @field #AIRBOSS.RadioCall CALLTHEBALL "Call the Ball" call. +-- @field #AIRBOSS.RadioCall CHECK "CHECK" call. +-- @field #AIRBOSS.RadioCall CLEAREDTOLAND "Cleared to land" call. +-- @field #AIRBOSS.RadioCall COMELEFT "Come left" call. +-- @field #AIRBOSS.RadioCall DEPARTANDREENTER "Depart and re-enter" call. +-- @field #AIRBOSS.RadioCall EXPECTHEAVYWAVEOFF "Expect heavy wavoff" call. +-- @field #AIRBOSS.RadioCall EXPECTSPOT75 "Expect spot 7.5" call. +-- @field #AIRBOSS.RadioCall FAST "You're fast" call. +-- @field #AIRBOSS.RadioCall FOULDECK "Foul Deck" call. +-- @field #AIRBOSS.RadioCall HIGH "You're high" call. +-- @field #AIRBOSS.RadioCall IDLE "Idle" call. +-- @field #AIRBOSS.RadioCall LONGINGROOVE "You're long in the groove" call. +-- @field #AIRBOSS.RadioCall LOW "You're low" call. +-- @field #AIRBOSS.RadioCall N0 "Zero" call. +-- @field #AIRBOSS.RadioCall N1 "One" call. +-- @field #AIRBOSS.RadioCall N2 "Two" call. +-- @field #AIRBOSS.RadioCall N3 "Three" call. +-- @field #AIRBOSS.RadioCall N4 "Four" call. +-- @field #AIRBOSS.RadioCall N5 "Five" call. +-- @field #AIRBOSS.RadioCall N6 "Six" call. +-- @field #AIRBOSS.RadioCall N7 "Seven" call. +-- @field #AIRBOSS.RadioCall N8 "Eight" call. +-- @field #AIRBOSS.RadioCall N9 "Nine" call. +-- @field #AIRBOSS.RadioCall PADDLESCONTACT "Paddles, contact" call. +-- @field #AIRBOSS.RadioCall POWER "Power" call. +-- @field #AIRBOSS.RadioCall RADIOCHECK "Paddles, radio check" call. +-- @field #AIRBOSS.RadioCall RIGHTFORLINEUP "Right for line up" call. +-- @field #AIRBOSS.RadioCall ROGERBALL "Roger ball" call. +-- @field #AIRBOSS.RadioCall SLOW "You're slow" call. +-- @field #AIRBOSS.RadioCall STABILIZED "Stabilized" call. +-- @field #AIRBOSS.RadioCall WAVEOFF "Wave off" call. +-- @field #AIRBOSS.RadioCall WELCOMEABOARD "Welcome aboard" call. +-- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. +-- @field #AIRBOSS.RadioCall NOISE Static noise sound. +-- @field #AIRBOSS.RadioCall SPINIT "Spin it" call. + +--- Marshal radio calls. +-- @type AIRBOSS.MarshalCalls +-- @field #AIRBOSS.RadioCall AFFIRMATIVE "Affirmative" call. +-- @field #AIRBOSS.RadioCall ALTIMETER "Altimeter" call. +-- @field #AIRBOSS.RadioCall BRC "BRC" call. +-- @field #AIRBOSS.RadioCall CARRIERTURNTOHEADING "Turn to heading" call. +-- @field #AIRBOSS.RadioCall CASE "Case" call. +-- @field #AIRBOSS.RadioCall CHARLIETIME "Charlie Time" call. +-- @field #AIRBOSS.RadioCall CLEAREDFORRECOVERY "You're cleared for case" call. +-- @field #AIRBOSS.RadioCall DECKCLOSED "Deck closed" sound. +-- @field #AIRBOSS.RadioCall DEGREES "Degrees" call. +-- @field #AIRBOSS.RadioCall EXPECTED "Expected" call. +-- @field #AIRBOSS.RadioCall FLYNEEDLES "Fly your needles" call. +-- @field #AIRBOSS.RadioCall HOLDATANGELS "Hold at angels" call. +-- @field #AIRBOSS.RadioCall HOURS "Hours" sound. +-- @field #AIRBOSS.RadioCall MARSHALRADIAL "Marshal radial" call. +-- @field #AIRBOSS.RadioCall N0 "Zero" call. +-- @field #AIRBOSS.RadioCall N1 "One" call. +-- @field #AIRBOSS.RadioCall N2 "Two" call. +-- @field #AIRBOSS.RadioCall N3 "Three" call. +-- @field #AIRBOSS.RadioCall N4 "Four" call. +-- @field #AIRBOSS.RadioCall N5 "Five" call. +-- @field #AIRBOSS.RadioCall N6 "Six" call. +-- @field #AIRBOSS.RadioCall N7 "Seven" call. +-- @field #AIRBOSS.RadioCall N8 "Eight" call. +-- @field #AIRBOSS.RadioCall N9 "Nine" call. +-- @field #AIRBOSS.RadioCall NEGATIVE "Negative" sound. +-- @field #AIRBOSS.RadioCall NEWFB "New final bearing" call. +-- @field #AIRBOSS.RadioCall OBS "Obs" call. +-- @field #AIRBOSS.RadioCall POINT "Point" call. +-- @field #AIRBOSS.RadioCall RADIOCHECK "Radio check" call. +-- @field #AIRBOSS.RadioCall RECOVERY "Recovery" call. +-- @field #AIRBOSS.RadioCall RECOVERYOPSSTOPPED "Recovery ops stopped" sound. +-- @field #AIRBOSS.RadioCall RECOVERYPAUSEDNOTICE "Recovery paused until further notice" call. +-- @field #AIRBOSS.RadioCall RECOVERYPAUSEDRESUMED "Recovery paused and will be resumed at" call. +-- @field #AIRBOSS.RadioCall RESUMERECOVERY "Resuming aircraft recovery" call. +-- @field #AIRBOSS.RadioCall REPORTSEEME "Report see me" call. +-- @field #AIRBOSS.RadioCall ROGER "Roger" call. +-- @field #AIRBOSS.RadioCall SAYNEEDLES "Say needles" call. +-- @field #AIRBOSS.RadioCall STACKFULL "Marshal stack is currently full. Hold outside 10 NM zone and wait for further instructions" call. +-- @field #AIRBOSS.RadioCall STARTINGRECOVERY "Starting aircraft recovery" call. +-- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound. +-- @field #AIRBOSS.RadioCall NOISE Static noise sound. + + +--- Difficulty level. +-- @type AIRBOSS.Difficulty +-- @field #string EASY Flight Student. Shows tips and hints in important phases of the approach. +-- @field #string NORMAL Naval aviator. Moderate number of hints but not really zip lip. +-- @field #string HARD TOPGUN graduate. For people who know what they are doing. Nearly *ziplip*. +AIRBOSS.Difficulty={ + EASY="Flight Student", + NORMAL="Naval Aviator", + HARD="TOPGUN Graduate", +} + +--- Recovery window parameters. +-- @type AIRBOSS.Recovery +-- @field #number START Start of recovery in seconds of abs mission time. +-- @field #number STOP End of recovery in seconds of abs mission time. +-- @field #number CASE Recovery case (1-3) of that time slot. +-- @field #number OFFSET Angle offset of the holding pattern in degrees. Usually 0, +-15, or +-30 degrees. +-- @field #boolean OPEN Recovery window is currently open. +-- @field #boolean OVER Recovery window is over and closed. +-- @field #boolean WIND Carrier will turn into the wind. +-- @field #number SPEED The speed in knots the carrier has during the recovery. +-- @field #boolean UTURN If true, carrier makes a U-turn to the point it came from before resuming its route to the next waypoint. +-- @field #number ID Recovery window ID. + +--- Groove data. +-- @type AIRBOSS.GrooveData +-- @field #number Step Current step. +-- @field #number Time Time in seconds. +-- @field #number Rho Distance in meters. +-- @field #number X Distance in meters. +-- @field #number Z Distance in meters. +-- @field #number AoA Angle of Attack. +-- @field #number Alt Altitude in meters. +-- @field #number GSE Glideslope error in degrees. +-- @field #number LUE Lineup error in degrees. +-- @field #number Pitch Pitch angle in degrees. +-- @field #number Roll Roll angle in degrees. +-- @field #number Yaw Yaw angle in degrees. +-- @field #number Vel Total velocity in m/s. +-- @field #number Vy Vertical velocity in m/s. +-- @field #number Gamma Relative heading player to carrier's runway. 0=parallel, +-90=perpendicular. +-- @field #string Grade LSO grade. +-- @field #number GradePoints LSO grade points +-- @field #string GradeDetail LSO grade details. +-- @field #string FlyThrough Fly through up "/" or fly through down "\\". + +--- LSO grade data. +-- @type AIRBOSS.LSOgrade +-- @field #string grade LSO grade, i.e. _OK_, OK, (OK), --, CUT +-- @field #number points Points received. +-- @field #number finalscore Points received after player has finally landed. This is the average over all incomplete passes (bolter, waveoff) before. +-- @field #string details Detailed flight analysis. +-- @field #number wire Wire caught. +-- @field #number Tgroove Time in the groove in seconds. +-- @field #number case Recovery case. +-- @field #string wind Wind speed on deck in knots. +-- @field #string modex Onboard number. +-- @field #string airframe Aircraft type name of player. +-- @field #string carriertype Carrier type name. +-- @field #string carriername Carrier name/alias. +-- @field #string theatre DCS map. +-- @field #string mitime Mission time in hh:mm:ss+d format +-- @field #string midate Mission date in yyyy/mm/dd format. +-- @field #string osdate Real live date. Needs **os** to be desanitized. + +--- Checkpoint parameters triggering the next step in the pattern. +-- @type AIRBOSS.Checkpoint +-- @field #string name Name of checkpoint. +-- @field #number Xmin Minimum allowed longitual distance to carrier. +-- @field #number Xmax Maximum allowed longitual distance to carrier. +-- @field #number Zmin Minimum allowed latitudal distance to carrier. +-- @field #number Zmax Maximum allowed latitudal distance to carrier. +-- @field #number LimitXmin Latitudal threshold for triggering the next step if XXmax. +-- @field #number LimitZmin Latitudal threshold for triggering the next step if ZZmax. + +--- Parameters of a flight group. +-- @type AIRBOSS.FlightGroup +-- @field Wrapper.Group#GROUP group Flight group. +-- @field #string groupname Name of the group. +-- @field #number nunits Number of units in group. +-- @field #number dist0 Distance to carrier in meters when the group was first detected inside the CCA. +-- @field #number time Timestamp in seconds of timer.getAbsTime() of the last important event, e.g. added to the queue. +-- @field #number flag Flag value describing the current stack. +-- @field #boolean ai If true, flight is purly AI. +-- @field #string actype Aircraft type name. +-- @field #table onboardnumbers Onboard numbers of aircraft in the group. +-- @field #string onboard Onboard number of player or first unit in group. +-- @field #number case Recovery case of flight. +-- @field #string seclead Name of section lead. +-- @field #table section Other human flight groups belonging to this flight. This flight is the lead. +-- @field #boolean holding If true, flight is in holding zone. +-- @field #boolean ballcall If true, flight called the ball in the groove. +-- @field #table elements Flight group elements. +-- @field #number Tcharlie Charlie (abs) time in seconds. +-- @field #string name Player name or name of first AI unit. +-- @field #boolean refueling Flight is refueling. + +--- Parameters of an element in a flight group. +-- @type AIRBOSS.FlightElement +-- @field Wrapper.Unit#UNIT unit Aircraft unit. +-- @field #string unitname Name of the unit. +-- @field #boolean ai If true, AI sits inside. If false, human player is flying. +-- @field #string onboard Onboard number of the aircraft. +-- @field #boolean ballcall If true, flight called the ball in the groove. +-- @field #boolean recovered If true, element was successfully recovered. + +--- Player data table holding all important parameters of each player. +-- @type AIRBOSS.PlayerData +-- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field #string unitname Name of the unit. +-- @field Wrapper.Client#CLIENT client Client object of player. +-- @field #string callsign Callsign of player. +-- @field #string difficulty Difficulty level. +-- @field #string step Current/next pattern step. +-- @field #boolean warning Set true once the player got a warning. +-- @field #number passes Number of passes. +-- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly. +-- @field #table debrief Debrief analysis of the current step of this pass. +-- @field #table lastdebrief Debrief of player performance of last completed pass. +-- @field #boolean landed If true, player landed or attempted to land. +-- @field #boolean boltered If true, player boltered. +-- @field #boolean waveoff If true, player was waved off during final approach. +-- @field #boolean wop If true, player was waved off during the pattern. +-- @field #boolean lig If true, player was long in the groove. +-- @field #boolean owo If true, own waveoff by player. +-- @field #boolean wofd If true, player was waved off because of a foul deck. +-- @field #number Tlso Last time the LSO gave an advice. +-- @field #number Tgroove Time in the groove in seconds. +-- @field #number TIG0 Time in groove start timer.getTime(). +-- @field #number wire Wire caught by player when trapped. +-- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elements are of type @{#AIRBOSS.GrooveData}. +-- @field #table points Points of passes until finally landed. +-- @field #number finalscore Final score if points are averaged over multiple passes. +-- @field #boolean valid If true, player made a valid approach. Is set true on start of Groove X. +-- @field #boolean subtitles If true, display subtitles of radio messages. +-- @field #boolean showhints If true, show step hints. +-- @field #table trapsheet Groove data table recorded every 0.5 seconds. +-- @field #boolean trapon If true, save trap sheets. +-- @field #string debriefschedulerID Debrief scheduler ID. +-- @extends #AIRBOSS.FlightGroup + +--- Main group level radio menu: F10 Other/Airboss. +-- @field #table MenuF10 +AIRBOSS.MenuF10={} + +--- Airboss mission level F10 root menu. +-- @field #table MenuF10Root +AIRBOSS.MenuF10Root=nil + +--- Airboss class version. +-- @field #string version +AIRBOSS.version="1.1.3" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Handle tanker and AWACS. Put them into pattern. +-- TODO: Handle cases where AI crashes on carrier deck ==> Clean up deck. +-- TODO: Player eject and crash debrief "gradings". +-- TODO: PWO during case 2/3. +-- TODO: PWO when player comes too close to other flight. +-- DONE: Spin pattern. Add radio menu entry. Not sure what to add though?! +-- DONE: Despawn AI after engine shutdown option. +-- DONE: What happens when section lead or member dies? +-- DONE: Do not remove recovered elements but only set switch. Remove only groups which are completely recovered. +-- DONE: Option to filter AI groups for recovery. +-- DONE: Rework radio messages. Better control over player board numbers. +-- DONE: Case I & II/III zone so that player gets into pattern automatically. Case I 3 position on the circle. Case II/III when the player enters the approach corridor maybe? +-- DONE: Add static weather information. +-- DONE: Allow up to two flights per Case I marshal stack. +-- DONE: Add max stack for Case I and define waiting queue outside CCZ. +-- DONE: Maybe do an additional step at the initial (Case II) or bullseye (Case III) and register player in case he missed some steps. +-- DONE: Subtitles off options on player level. +-- DONE: Persistence of results. +-- DONE: Foul deck waveoff. +-- DONE: Get Charlie time estimate function. +-- DONE: Average player grades until landing. +-- DONE: Check player heading at zones, e.g. initial. +-- DONE: Fix bug that player leaves the approach zone if he boltered or was waved off during Case II or III. NOTE: Partly due to increasing approach zone size. +-- DONE: Fix bug that player gets an altitude warning if stack collapses. NOTE: Would not work if two stacks Case I and II/III are used. +-- DONE: Improve radio messages. Maybe usersound for messages which are only meant for players? +-- DONE: Add voice over fly needs and welcome aboard. +-- DONE: Improve trapped wire calculation. +-- DONE: Carrier zone with dimensions of carrier. to check if landing happened on deck. +-- DONE: Carrier runway zone for fould deck check. +-- DONE: More Hints for Case II/III. +-- DONE: Set magnetic declination function. +-- DONE: First send AI to marshal and then allow them into the landing pattern ==> task function when reaching the waypoint. +-- DONE: Extract (static) weather from mission for cloud cover etc. +-- DONE: Check distance to players during approach. +-- DONE: Option to turn AI handling off. +-- DONE: Add user functions. +-- DONE: Update AI holding pattern wrt to moving carrier. +-- DONE: Generalize parameters for other carriers. +-- DONE: Generalize parameters for other aircraft. +-- DONE: Add radio check (LSO, AIRBOSS) to F10 radio menu. +-- DONE: Right pattern step after bolter/wo/patternWO? Guess so. +-- DONE: Set case II and III times (via recovery time). +-- DONE: Get correct wire when trapped. DONE but might need further tweaking. +-- DONE: Add radio transmission queue for LSO and airboss. +-- DONE: CASE II. +-- DONE: CASE III. +-- NOPE: Strike group with helo bringing cargo etc. Not yet. +-- DONE: Handle crash event. Delete A/C from queue, send rescue helo. +-- DONE: Get fuel state in pounds. (working for the hornet, did not check others) +-- DONE: Add aircraft numbers in queue to carrier info F10 radio output. +-- DONE: Monitor holding of players/AI in zoneHolding. +-- DONE: Transmission via radio. +-- DONE: Get board numbers. +-- DONE: Get an _OK_ pass if long in groove. Possible other pattern wave offs as well?! +-- DONE: Add scoring to radio menu. +-- DONE: Optimized debrief. +-- DONE: Add automatic grading. +-- DONE: Fix radio menu. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new AIRBOSS class object for a specific aircraft carrier unit. +-- @param #AIRBOSS self +-- @param carriername Name of the aircraft carrier unit as defined in the mission editor. +-- @param alias (Optional) Alias for the carrier. This will be used for radio messages and the F10 radius menu. Default is the carrier name as defined in the mission editor. +-- @return #AIRBOSS self or nil if carrier unit does not exist. +function AIRBOSS:New(carriername, alias) + + -- Inherit everthing from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #AIRBOSS + + -- Debug. + self:F2({carriername=carriername, alias=alias}) + + -- Set carrier unit. + self.carrier=UNIT:FindByName(carriername) + + -- Check if carrier unit exists. + if self.carrier==nil then + -- Error message. + local text=string.format("ERROR: Carrier unit %s could not be found! Make sure this UNIT is defined in the mission editor and check the spelling of the unit name carefully.", carriername) + MESSAGE:New(text, 120):ToAll() + self:E(text) + return nil + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("AIRBOSS %s | ", carriername) + + -- Current map. + self.theatre=env.mission.theatre + self:T2(self.lid..string.format("Theatre = %s.", tostring(self.theatre))) + + -- Get carrier type. + self.carriertype=self.carrier:GetTypeName() + + -- Set alias. + self.alias=alias or carriername + + -- Set carrier airbase object. + self.airbase=AIRBASE:FindByName(carriername) + + -- Create carrier beacon. + self.beacon=BEACON:New(self.carrier) + + -- Set Tower Frequency of carrier. + self:_GetTowerFrequency() + + -- Init player scores table. + self.playerscores={} + + -- Initialize ME waypoints. + self:_InitWaypoints() + + -- Current waypoint. + self.currentwp=1 + + -- Patrol route. + self:_PatrolRoute() + + ------------- + --- Defaults: + ------------- + + -- Set up Airboss radio. + self:SetMarshalRadio() + + -- Set up LSO radio. + self:SetLSORadio() + + -- Set LSO call interval. Default 4 sec. + self:SetLSOCallInterval() + + -- Radio scheduler. + self.radiotimer=SCHEDULER:New() + + -- Set magnetic declination. + self:SetMagneticDeclination() + + -- Set ICSL to channel 1. + self:SetICLS() + + -- Set TACAN to channel 74X. + self:SetTACAN() + + -- Becons are reactivated very 5 min. + self:SetBeaconRefresh() + + -- Set max aircraft in landing pattern. Default 4. + self:SetMaxLandingPattern() + + -- Set max Case I Marshal stacks. Default 3. + self:SetMaxMarshalStacks() + + -- Set max section members. Default 2. + self:SetMaxSectionSize() + + -- Set max flights per stack. Default is 2. + self:SetMaxFlightsPerStack() + + -- Set AI handling On. + self:SetHandleAION() + + -- Airboss is a nice guy. + self:SetAirbossNiceGuy() + + -- Allow emergency landings. + self:SetEmergencyLandings() + + -- No despawn after engine shutdown by default. + self:SetDespawnOnEngineShutdown(false) + + -- No respawning of AI groups when entering the CCA. + self:SetRespawnAI(false) + + -- Mission uses static weather by default. + self:SetStaticWeather() + + -- Default recovery case. This sets self.defaultcase and self.case. Default Case I. + self:SetRecoveryCase() + + -- Set time the turn starts before the window opens. + self:SetRecoveryTurnTime() + + -- Set holding offset to 0 degrees. This set self.defaultoffset and self.holdingoffset. + self:SetHoldingOffsetAngle() + + -- Set Marshal stack radius. Default 2.75 NM, which gives a diameter of 5.5 NM. + self:SetMarshalRadius() + + -- Set max alt at initial. Default 1300 ft. + self:SetInitialMaxAlt() + + -- Default player skill EASY. + self:SetDefaultPlayerSkill(AIRBOSS.Difficulty.EASY) + + -- Default glideslope error thresholds. + self:SetGlideslopeErrorThresholds() + + -- Default lineup error thresholds. + self:SetLineupErrorThresholds() + + -- CCA 50 NM radius zone around the carrier. + self:SetCarrierControlledArea() + + -- CCZ 5 NM radius zone around the carrier. + self:SetCarrierControlledZone() + + -- Carrier patrols its waypoints until the end of time. + self:SetPatrolAdInfinitum(true) + + -- Collision check distance. Default 5 NM. + self:SetCollisionDistance() + + -- Set update time intervals. + self:SetQueueUpdateTime() + self:SetStatusUpdateTime() + self:SetDefaultMessageDuration() + + -- Menu options. + self:SetMenuMarkZones() + self:SetMenuSmokeZones() + self:SetMenuSingleCarrier(false) + + -- Welcome players. + self:SetWelcomePlayers(true) + + -- Init carrier parameters. + if self.carriertype==AIRBOSS.CarrierType.STENNIS then + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.ROOSEVELT then + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.LINCOLN then + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.WASHINGTON then + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.VINSON then + -- TODO: Carl Vinson parameters. + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.TARAWA then + -- Tarawa parameters. + self:_InitTarawa() + elseif self.carriertype==AIRBOSS.CarrierType.KUZNETSOV then + -- Kusnetsov parameters - maybe... + self:_InitStennis() + else + self:E(self.lid..string.format("ERROR: Unknown carrier type %s!", tostring(self.carriertype))) + return nil + end + + -- Init voice over files. + self:_InitVoiceOvers() + + ------------------- + -- Debug Section -- + ------------------- + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(3) + --self.dTstatus=0.1 + end + + -- Smoke zones. + if false then + local case=3 + self.holdingoffset=30 + self:_GetZoneGroove():SmokeZone(SMOKECOLOR.Red, 5) + self:_GetZoneLineup():SmokeZone(SMOKECOLOR.Green, 5) + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White, 45) + self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) + self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) + self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) + self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Blue, 45) + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + self:_GetZoneHolding(case, 1):SmokeZone(SMOKECOLOR.White, 45) + self:_GetZoneHolding(case, 2):SmokeZone(SMOKECOLOR.White, 45) + self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Orange, 45) + self:_GetZoneCommence(case, 1):SmokeZone(SMOKECOLOR.Red, 45) + self:_GetZoneCommence(case, 2):SmokeZone(SMOKECOLOR.Red, 45) + self:_GetZoneAbeamLandingSpot():SmokeZone(SMOKECOLOR.Red, 5) + self:_GetZoneLandingSpot():SmokeZone(SMOKECOLOR.Red, 5) + end + + -- Carrier parameter debug tests. + if false then + -- Stern coordinate. + local FB=self:GetFinalBearing(false) + local hdg=self:GetHeading(false) + + -- Stern pos. + local stern=self:_GetSternCoord() + + -- Bow pos. + local bow=stern:Translate(self.carrierparam.totlength, hdg) + + -- End of rwy. + local rwy=stern:Translate(self.carrierparam.rwylength, FB, true) + + --- Flare points and zones. + local function flareme() + + -- Carrier pos. + self:GetCoordinate():FlareYellow() + + -- Stern + stern:FlareYellow() + + -- Bow + bow:FlareYellow() + + -- Runway half width = 10 m. + local r1=stern:Translate(self.carrierparam.rwywidth*0.5, FB+90) + local r2=stern:Translate(self.carrierparam.rwywidth*0.5, FB-90) + r1:FlareWhite() + r2:FlareWhite() + + -- End of runway. + rwy:FlareRed() + + -- Right 30 meters from stern. + local cR=stern:Translate(self.carrierparam.totwidthstarboard, hdg+90) + cR:FlareYellow() + + -- Left 40 meters from stern. + local cL=stern:Translate(self.carrierparam.totwidthport, hdg-90) + cL:FlareYellow() + + + -- Carrier specific. + if self.carrier:GetTypeName()~=AIRBOSS.CarrierType.TARAWA then + + -- Flare wires. + local w1=stern:Translate(self.carrierparam.wire1, FB) + local w2=stern:Translate(self.carrierparam.wire2, FB) + local w3=stern:Translate(self.carrierparam.wire3, FB) + local w4=stern:Translate(self.carrierparam.wire4, FB) + w1:FlareWhite() + w2:FlareYellow() + w3:FlareWhite() + w4:FlareYellow() + + else + + -- Abeam landing spot zone. + local ALSPT=self:_GetZoneAbeamLandingSpot() + ALSPT:FlareZone(FLARECOLOR.Red, 5, nil, UTILS.FeetToMeters(120)) + + -- Primary landing spot zone. + local LSPT=self:_GetZoneLandingSpot() + LSPT:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) + + -- Landing spot coordinate. + local PLSC=self:_GetLandingSpotCoordinate() + PLSC:FlareWhite() + end + + -- Flare carrier and landing runway. + local cbox=self:_GetZoneCarrierBox() + local rbox=self:_GetZoneRunwayBox() + cbox:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) + rbox:FlareZone(FLARECOLOR.White, 5, nil, self.carrierparam.deckheight) + end + + -- Flare points every 3 seconds for 3 minutes. + SCHEDULER:New(nil, flareme, {}, 1, 3, nil, 180) + end + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Load", "Stopped") -- Load player scores from file. + self:AddTransition("Stopped", "Start", "Idle") -- Start AIRBOSS script. + self:AddTransition("*", "Idle", "Idle") -- Carrier is idling. + self:AddTransition("Idle", "RecoveryStart", "Recovering") -- Start recovering aircraft. + self:AddTransition("Recovering", "RecoveryStop", "Idle") -- Stop recovering aircraft. + self:AddTransition("Recovering", "RecoveryPause", "Paused") -- Pause recovering aircraft. + self:AddTransition("Paused", "RecoveryUnpause", "Recovering") -- Unpause recovering aircraft. + self:AddTransition("*", "Status", "*") -- Update status of players and queues. + self:AddTransition("*", "RecoveryCase", "*") -- Switch to another case recovery. + self:AddTransition("*", "PassingWaypoint", "*") -- Carrier is passing a waypoint. + self:AddTransition("*", "LSOGrade", "*") -- LSO grade. + self:AddTransition("*", "Marshal", "*") -- A flight was send into the marshal stack. + self:AddTransition("*", "Save", "*") -- Save player scores to file. + self:AddTransition("*", "Stop", "Stopped") -- Stop AIRBOSS FMS. + + + --- Triggers the FSM event "Start" that starts the airboss. Initializes parameters and starts event handlers. + -- @function [parent=#AIRBOSS] Start + -- @param #AIRBOSS self + + --- Triggers the FSM event "Start" that starts the airboss after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#AIRBOSS] __Start + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + --- On after "Start" user function. Called when the AIRBOSS FSM is started. + -- @function [parent=#AIRBOSS] OnAfterStart + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Idle" that puts the carrier into state "Idle" where no recoveries are carried out. + -- @function [parent=#AIRBOSS] Idle + -- @param #AIRBOSS self + + --- Triggers the FSM delayed event "Idle" that puts the carrier into state "Idle" where no recoveries are carried out. + -- @function [parent=#AIRBOSS] __Idle + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RecoveryStart" that starts the recovery of aircraft. Marshalling aircraft are send to the landing pattern. + -- @function [parent=#AIRBOSS] RecoveryStart + -- @param #AIRBOSS self + -- @param #number Case Recovery case (1, 2 or 3) that is started. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + --- Triggers the FSM delayed event "RecoveryStart" that starts the recovery of aircraft. Marshalling aircraft are send to the landing pattern. + -- @function [parent=#AIRBOSS] __RecoveryStart + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #number Case Recovery case (1, 2 or 3) that is started. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + --- On after "RecoveryStart" user function. Called when recovery of aircraft is started and carrier switches to state "Recovering". + -- @function [parent=#AIRBOSS] OnAfterRecoveryStart + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number Case The recovery case (1, 2 or 3) to start. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + + --- Triggers the FSM event "RecoveryStop" that stops the recovery of aircraft. + -- @function [parent=#AIRBOSS] RecoveryStop + -- @param #AIRBOSS self + + --- Triggers the FSM delayed event "RecoveryStop" that stops the recovery of aircraft. + -- @function [parent=#AIRBOSS] __RecoveryStop + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + --- On after "RecoveryStop" user function. Called when recovery of aircraft is stopped. + -- @function [parent=#AIRBOSS] OnAfterRecoveryStop + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "RecoveryPause" that pauses the recovery of aircraft. + -- @function [parent=#AIRBOSS] RecoveryPause + -- @param #AIRBOSS self + -- @param #number duration Duration of pause in seconds. After that recovery is automatically resumed. + + --- Triggers the FSM delayed event "RecoveryPause" that pauses the recovery of aircraft. + -- @function [parent=#AIRBOSS] __RecoveryPause + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #number duration Duration of pause in seconds. After that recovery is automatically resumed. + + --- Triggers the FSM event "RecoveryUnpause" that resumes the recovery of aircraft if it was paused. + -- @function [parent=#AIRBOSS] RecoveryUnpause + -- @param #AIRBOSS self + + --- Triggers the FSM delayed event "RecoveryUnpause" that resumes the recovery of aircraft if it was paused. + -- @function [parent=#AIRBOSS] __RecoveryUnpause + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RecoveryCase" that switches the aircraft recovery case. + -- @function [parent=#AIRBOSS] RecoveryCase + -- @param #AIRBOSS self + -- @param #number Case The new recovery case (1, 2 or 3). + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + --- Triggers the delayed FSM event "RecoveryCase" that sets the used aircraft recovery case. + -- @function [parent=#AIRBOSS] __RecoveryCase + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #number Case The new recovery case (1, 2 or 3). + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + + --- Triggers the FSM event "PassingWaypoint". Called when the carrier passes a waypoint. + -- @function [parent=#AIRBOSS] PassingWaypoint + -- @param #AIRBOSS self + -- @param #number waypoint Number of waypoint. + + --- Triggers the FSM delayed event "PassingWaypoint". Called when the carrier passes a waypoint. + -- @function [parent=#AIRBOSS] __PassingWaypoint + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #number Case Recovery case (1, 2 or 3) that is started. + -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. + + --- On after "PassingWaypoint" user function. Called when the carrier passes a waypoint of its route. + -- @function [parent=#AIRBOSS] OnAfterPassingWaypoint + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #number waypoint Number of waypoint. + + + --- Triggers the FSM event "Save" that saved the player scores to a file. + -- @function [parent=#AIRBOSS] Save + -- @param #AIRBOSS self + -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + + --- Triggers the FSM delayed event "Save" that saved the player scores to a file. + -- @function [parent=#AIRBOSS] __Save + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + + --- On after "Save" event user function. Called when the player scores are saved to disk. + -- @function [parent=#AIRBOSS] OnAfterSave + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + + + --- Triggers the FSM event "Load" that loads the player scores from a file. AIRBOSS FSM must **not** be started at this point. + -- @function [parent=#AIRBOSS] Load + -- @param #AIRBOSS self + -- @param #string path Path where the file is located. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is AIRBOSS-_LSOgrades.csv. + + --- Triggers the FSM delayed event "Load" that loads the player scores from a file. AIRBOSS FSM must **not** be started at this point. + -- @function [parent=#AIRBOSS] __Load + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #string path Path where the file is located. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + + --- On after "Load" event user function. Called when the player scores are loaded from disk. + -- @function [parent=#AIRBOSS] OnAfterLoad + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path Path where the file is located. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized. + -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv. + + + --- Triggers the FSM event "LSOGrade". Called when the LSO grades a player + -- @function [parent=#AIRBOSS] LSOGrade + -- @param #AIRBOSS self + -- @param #AIRBOSS.PlayerData playerData Player Data. + -- @param #AIRBOSS.LSOgrade grade LSO grade. + + --- Triggers the FSM event "LSOGrade". Delayed called when the LSO grades a player. + -- @function [parent=#AIRBOSS] __LSOGrade + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #AIRBOSS.PlayerData playerData Player Data. + -- @param #AIRBOSS.LSOgrade grade LSO grade. + + --- On after "LSOGrade" user function. Called when the carrier passes a waypoint of its route. + -- @function [parent=#AIRBOSS] OnAfterLSOGrade + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #AIRBOSS.PlayerData playerData Player Data. + -- @param #AIRBOSS.LSOgrade grade LSO grade. + + + --- Triggers the FSM event "Marshal". Called when a flight is send to the Marshal stack. + -- @function [parent=#AIRBOSS] Marshal + -- @param #AIRBOSS self + -- @param #AIRBOSS.FlightGroup flight The flight group data. + + --- Triggers the FSM event "Marshal". Delayed call when a flight is send to the Marshal stack. + -- @function [parent=#AIRBOSS] __Marshal + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + -- @param #AIRBOSS.FlightGroup flight The flight group data. + + --- On after "Marshal" user function. Called when a flight is send to the Marshal stack. + -- @function [parent=#AIRBOSS] OnAfterMarshal + -- @param #AIRBOSS self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #AIRBOSS.FlightGroup flight The flight group data. + + + --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped. + -- @function [parent=#AIRBOSS] Stop + -- @param #AIRBOSS self + + --- Triggers the FSM event "Stop" that stops the airboss after a delay. Event handlers are stopped. + -- @function [parent=#AIRBOSS] __Stop + -- @param #AIRBOSS self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- USER API Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set welcome messages for players. +-- @param #AIRBOSS self +-- @param #boolean switch If true, display welcome message to player. +-- @return #AIRBOSS self +function AIRBOSS:SetWelcomePlayers(switch) + + self.welcome=switch + + return self +end + + +--- Set carrier controlled area (CCA). +-- This is a large zone around the carrier, which is constantly updated wrt the carrier position. +-- @param #AIRBOSS self +-- @param #number radius Radius of zone in nautical miles (NM). Default 50 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetCarrierControlledArea(radius) + + radius=UTILS.NMToMeters(radius or 50) + + self.zoneCCA=ZONE_UNIT:New("Carrier Controlled Area", self.carrier, radius) + + return self +end + +--- Set carrier controlled zone (CCZ). +-- This is a small zone (usually 5 NM radius) around the carrier, which is constantly updated wrt the carrier position. +-- @param #AIRBOSS self +-- @param #number radius Radius of zone in nautical miles (NM). Default 5 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetCarrierControlledZone(radius) + + radius=UTILS.NMToMeters(radius or 5) + + self.zoneCCZ=ZONE_UNIT:New("Carrier Controlled Zone", self.carrier, radius) + + return self +end + +--- Set distance up to which water ahead is scanned for collisions. +-- @param #AIRBOSS self +-- @param #number dist Distance in NM. Default 5 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetCollisionDistance(distance) + self.collisiondist=UTILS.NMToMeters(distance or 5) + return self +end + +--- Set the default recovery case. +-- @param #AIRBOSS self +-- @param #number case Case of recovery. Either 1, 2 or 3. Default 1. +-- @return #AIRBOSS self +function AIRBOSS:SetRecoveryCase(case) + + -- Set default case or 1. + self.defaultcase=case or 1 + + -- Current case init. + self.case=self.defaultcase + + return self +end + +--- Set holding pattern offset from final bearing for Case II/III recoveries. +-- Usually, this is +-15 or +-30 degrees. You should not use and offset angle >= 90 degrees, because this will cause a devision by zero in some of the equations used to calculate the approach corridor. +-- So best stick to the defaults up to 30 degrees. +-- @param #AIRBOSS self +-- @param #number offset Offset angle in degrees. Default 0. +-- @return #AIRBOSS self +function AIRBOSS:SetHoldingOffsetAngle(offset) + + -- Set default angle or 0. + self.defaultoffset=offset or 0 + + -- Current offset init. + self.holdingoffset=self.defaultoffset + + return self +end + +--- Enable F10 menu to manually start recoveries. +-- @param #AIRBOSS self +-- @param #number duration Default duration of the recovery in minutes. Default 30 min. +-- @param #number windondeck Default wind on deck in knots. Default 25 knots. +-- @param #boolean uturn U-turn after recovery window closes on=true or off=false/nil. Default off. +-- @param #number offset Relative Marshal radial in degrees for Case II/III recoveries. Default 30°. +-- @return #AIRBOSS self +function AIRBOSS:SetMenuRecovery(duration, windondeck, uturn, offset) + + self.skipperMenu=true + self.skipperTime=duration or 30 + self.skipperSpeed=windondeck or 25 + self.skipperOffset=offset or 30 + + if uturn then + self.skipperUturn=true + else + self.skipperUturn=false + end + + return self +end + +--- Add aircraft recovery time window and recovery case. +-- @param #AIRBOSS self +-- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now. +-- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time. +-- @param #number case Recovery case for that time slot. Number between one and three. +-- @param #number holdingoffset Only for CASE II/III: Angle in degrees the holding pattern is offset. +-- @param #boolean turnintowind If true, carrier will turn into the wind 5 minutes before the recovery window opens. +-- @param #number speed Speed in knots during turn into wind leg. +-- @param #boolean uturn If true (or nil), carrier wil perform a U-turn and go back to where it came from before resuming its route to the next waypoint. If false, it will go directly to the next waypoint. +-- @return #AIRBOSS.Recovery Recovery window. +function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset, turnintowind, speed, uturn) + + -- Absolute mission time in seconds. + local Tnow=timer.getAbsTime() + + if starttime and type(starttime)=="number" then + starttime=UTILS.SecondsToClock(Tnow+starttime) + end + + if stoptime and type(stoptime)=="number" then + stoptime=UTILS.SecondsToClock(Tnow+stoptime) + end + + + -- Input or now. + starttime=starttime or UTILS.SecondsToClock(Tnow) + + -- Set start time. + local Tstart=UTILS.ClockToSeconds(starttime) + + -- Set stop time. + local Tstop=UTILS.ClockToSeconds(stoptime or Tstart+90*60) + + -- Consistancy check for timing. + if Tstart>Tstop then + self:E(string.format("ERROR: Recovery stop time %s lies before recovery start time %s! Recovery window rejected.", UTILS.SecondsToClock(Tstart), UTILS.SecondsToClock(Tstop))) + return self + end + if Tstop<=Tnow then + self:I(string.format("WARNING: Recovery stop time %s already over. Tnow=%s! Recovery window rejected.", UTILS.SecondsToClock(Tstop), UTILS.SecondsToClock(Tnow))) + return self + end + + -- Case or default value. + case=case or self.defaultcase + + -- Holding offset or default value. + holdingoffset=holdingoffset or self.defaultoffset + + -- Offset zero for case I. + if case==1 then + holdingoffset=0 + end + + -- Increase counter. + self.windowcount=self.windowcount+1 + + -- Recovery window. + local recovery={} --#AIRBOSS.Recovery + recovery.START=Tstart + recovery.STOP=Tstop + recovery.CASE=case + recovery.OFFSET=holdingoffset + recovery.OPEN=false + recovery.OVER=false + recovery.WIND=turnintowind + recovery.SPEED=speed or 20 + recovery.ID=self.windowcount + + if uturn==nil or uturn==true then + recovery.UTURN=true + else + recovery.UTURN=false + end + + -- Add to table + table.insert(self.recoverytimes, recovery) + + return recovery +end + +--- Define a set of AI groups that are handled by the airboss. +-- @param #AIRBOSS self +-- @param Core.Set#SET_GROUP setgroup The set of AI groups which are handled by the airboss. +-- @return #AIRBOSS self +function AIRBOSS:SetSquadronAI(setgroup) + self.squadsetAI=setgroup + return self +end + +--- Define a set of AI groups that excluded from AI handling. Members of this set will be left allone by the airboss and not forced into the Marshal pattern. +-- @param #AIRBOSS self +-- @param Core.Set#SET_GROUP setgroup The set of AI groups which are excluded. +-- @return #AIRBOSS self +function AIRBOSS:SetExcludeAI(setgroup) + self.excludesetAI=setgroup + return self +end + +--- Add a group to the exclude set. If no set exists, it is created. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group The group to be excluded. +-- @return #AIRBOSS self +function AIRBOSS:AddExcludeAI(group) + + self.excludesetAI=self.excludesetAI or SET_GROUP:New() + + self.excludesetAI:AddGroup(group) + + return self +end + +--- Close currently running recovery window and stop recovery ops. Recovery window is deleted. +-- @param #AIRBOSS self +-- @param #number delay (Optional) Delay in seconds before the window is deleted. +function AIRBOSS:CloseCurrentRecoveryWindow(delay) + + if delay and delay>0 then + --SCHEDULER:New(nil, self.CloseCurrentRecoveryWindow, {self}, delay) + self:ScheduleOnce(delay, self.CloseCurrentRecoveryWindow, self) + else + if self:IsRecovering() and self.recoverywindow and self.recoverywindow.OPEN then + self:RecoveryStop() + self.recoverywindow.OPEN=false + self.recoverywindow.OVER=true + self:DeleteRecoveryWindow(self.recoverywindow) + end + end +end + +--- Delete all recovery windows. +-- @param #AIRBOSS self +-- @param #number delay (Optional) Delay in seconds before the windows are deleted. +-- @return #AIRBOSS self +function AIRBOSS:DeleteAllRecoveryWindows(delay) + + -- Loop over all recovery windows. + for _,recovery in pairs(self.recoverytimes) do + self:I(self.lid..string.format("Deleting recovery window ID %s", tostring(recovery.ID))) + self:DeleteRecoveryWindow(recovery, delay) + end + + return self +end + +--- Return the recovery window of the given ID. +-- @param #AIRBOSS self +-- @param #number id The ID of the recovery window. +-- @return #AIRBOSS.Recovery Recovery window with the right ID or nil if no such window exists. +function AIRBOSS:GetRecoveryWindowByID(id) + if id then + for _,_window in pairs(self.recoverytimes) do + local window=_window --#AIRBOSS.Recovery + if window and window.ID==id then + return window + end + end + end + return nil +end + +--- Delete a recovery window. If the window is currently open, it is closed and the recovery stopped. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Recovery window Recovery window. +-- @param #number delay Delay in seconds, before the window is deleted. +function AIRBOSS:DeleteRecoveryWindow(window, delay) + + if delay and delay>0 then + -- Delayed call. + --SCHEDULER:New(nil, self.DeleteRecoveryWindow, {self, window}, delay) + self:ScheduleOnce(delay, self.DeleteRecoveryWindow, self, window) + else + + for i,_recovery in pairs(self.recoverytimes) do + local recovery=_recovery --#AIRBOSS.Recovery + + if window and window.ID==recovery.ID then + if window.OPEN then + -- Window is currently open. + self:RecoveryStop() + else + table.remove(self.recoverytimes, i) + end + + end + end + end +end + +--- Set time before carrier turns and recovery window opens. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds. Default 600 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetRecoveryTurnTime(interval) + self.dTturn=interval or 600 + return self +end + +--- Set time interval for updating queues and other stuff. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds. Default 30 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetQueueUpdateTime(interval) + self.dTqueue=interval or 30 + return self +end + +--- Set time interval between LSO calls. Optimal time in the groove is ~16 seconds. So the default of 4 seconds gives around 3-4 correction calls in the groove. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds between LSO calls. Default 4 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetLSOCallInterval(timeinterval) + self.LSOdT=timeinterval or 4 + return self +end + +--- Airboss is a rather nice guy and not strictly following the rules. Fore example, he does allow you into the landing pattern if you are not coming from the Marshal stack. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, Airboss bends the rules a bit. +-- @return #AIRBOSS self +function AIRBOSS:SetAirbossNiceGuy(switch) + if switch==true or switch==nil then + self.airbossnice=true + else + self.airbossnice=false + end + return self +end + +--- Allow emergency landings, i.e. bypassing any pattern and go directly to final approach. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, emergency landings are okay. +-- @return #AIRBOSS self +function AIRBOSS:SetEmergencyLandings(switch) + if switch==true or switch==nil then + self.emergency=true + else + self.emergency=false + end + return self +end + + +--- Despawn AI groups after they they shut down their engines. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, AI groups are despawned. +-- @return #AIRBOSS self +function AIRBOSS:SetDespawnOnEngineShutdown(switch) + if switch==true or switch==nil then + self.despawnshutdown=true + else + self.despawnshutdown=false + end + return self +end + +--- Respawn AI groups once they reach the CCA. Clears any attached airbases and allows making them land on the carrier via script. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, AI groups are respawned. +-- @return #AIRBOSS self +function AIRBOSS:SetRespawnAI(switch) + if switch==true or switch==nil then + self.respawnAI=true + else + self.respawnAI=false + end + return self +end + +--- Give AI aircraft the refueling task if a recovery tanker is present or send them to the nearest divert airfield. +-- @param #AIRBOSS self +-- @param #number lowfuelthreshold Low fuel threshold in percent. AI will go refueling if their fuel level drops below this value. Default 10 %. +-- @return #AIRBOSS self +function AIRBOSS:SetRefuelAI(lowfuelthreshold) + self.lowfuelAI=lowfuelthreshold or 10 + return self +end + +--- Set max alitude to register flights in the initial zone. Aircraft above this altitude will not be registerered. +-- @param #AIRBOSS self +-- @param #number altitude Max alitude in feet. Default 1300 ft. +-- @return #AIRBOSS self +function AIRBOSS:SetInitialMaxAlt(altitude) + self.initialmaxalt=UTILS.FeetToMeters(altitude or 1300) + return self +end + + +--- Set folder where the airboss sound files are located **within you mission (miz) file**. +-- The default path is "l10n/DEFAULT/" but sound files simply copied there will be removed by DCS the next time you save the mission. +-- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used. +-- @param #AIRBOSS self +-- @param #string folderpath The path to the sound files, e.g. "Airboss Soundfiles/". +-- @return #AIRBOSS self +function AIRBOSS:SetSoundfilesFolder(folderpath) + + -- Check that it ends with / + if folderpath then + local lastchar=string.sub(folderpath, -1) + if lastchar~="/" then + folderpath=folderpath.."/" + end + end + + -- Folderpath. + self.soundfolder=folderpath + + -- Info message. + self:I(self.lid..string.format("Setting sound files folder to: %s", self.soundfolder)) + + return self +end + +--- Set time interval for updating player status and other things. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds. Default 0.5 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetStatusUpdateTime(interval) + self.dTstatus=interval or 0.5 + return self +end + +--- Set duration how long messages are displayed to players. +-- @param #AIRBOSS self +-- @param #number duration Duration in seconds. Default 10 sec. +-- @return #AIRBOSS self +function AIRBOSS:SetDefaultMessageDuration(duration) + self.Tmessage=duration or 10 + return self +end + + +--- Set glideslope error thresholds. +-- @param #AIRBOSS self +-- @param #number _max +-- @param #number _min +-- @param #number High +-- @param #number HIGH +-- @param #number Low +-- @param #number LOW +-- @return #AIRBOSS self +function AIRBOSS:SetGlideslopeErrorThresholds(_max,_min, High, HIGH, Low, LOW) + self.gle._max=_max or 0.4 + self.gle.High=High or 0.8 + self.gle.HIGH=HIGH or 1.5 + self.gle._min=_min or -0.3 + self.gle.Low=Low or -0.6 + self.gle.LOW=LOW or -0.9 + return self +end + +--- Set lineup error thresholds. +-- @param #AIRBOSS self +-- @param #number _max +-- @param #number _min +-- @param #number Left +-- @param #number LEFT +-- @param #number Right +-- @param #number RIGHT +-- @return #AIRBOSS self +function AIRBOSS:SetLineupErrorThresholds(_max,_min, Left, LEFT, Right, RIGHT) + self.lue._max=_max or 0.5 + self.lue._min=_min or -0.5 + self.lue.Left=Left or -1.0 + self.lue.LEFT=LEFT or -3.0 + self.lue.Right=Right or 1.0 + self.lue.RIGHT=RIGHT or 3.0 + return self +end + +--- Set Case I Marshal radius. This is the radius of the valid zone around "the post" aircraft are supposed to be holding in the Case I Marshal stack. +-- The post is 2.5 NM port of the carrier. +-- @param #AIRBOSS self +-- @param #number Radius in NM. Default 2.8 NM, which gives a diameter of 5.6 NM. +-- @return #AIRBOSS self +function AIRBOSS:SetMarshalRadius(radius) + self.marshalradius=UTILS.NMToMeters(radius or 2.8) + return self +end + +--- Optimized F10 radio menu for a single carrier. The menu entries will be stored directly under F10 Other/Airboss/ and not F10 Other/Airboss/"Carrier Alias"/. +-- **WARNING**: If you use this with two airboss objects/carriers, the radio menu will be screwed up! +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil single menu is enabled. If false, menu is for multiple carriers in the mission. +-- @return #AIRBOSS self +function AIRBOSS:SetMenuSingleCarrier(switch) + if switch==true or switch==nil then + self.menusingle=true + else + self.menusingle=false + end + return self +end + +--- Enable or disable F10 radio menu for marking zones via smoke or flares. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, menu is enabled. If false, menu is not available to players. +-- @return #AIRBOSS self +function AIRBOSS:SetMenuMarkZones(switch) + if switch==nil or switch==true then + self.menumarkzones=true + else + self.menumarkzones=false + end + return self +end + +--- Enable or disable F10 radio menu for marking zones via smoke. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, menu is enabled. If false, menu is not available to players. +-- @return #AIRBOSS self +function AIRBOSS:SetMenuSmokeZones(switch) + if switch==nil or switch==true then + self.menusmokezones=true + else + self.menusmokezones=false + end + return self +end + +--- Enable saving of player's trap sheets and specify an optional directory path. +-- @param #AIRBOSS self +-- @param #string path (Optional) Path where to save the trap sheets. +-- @param #string prefix (Optional) Prefix for trap sheet files. File name will be saved as *prefix_aircrafttype-0001.csv*, *prefix_aircrafttype-0002.csv*, etc. +-- @return #AIRBOSS self +function AIRBOSS:SetTrapSheet(path, prefix) + if io then + self.trapsheet=true + self.trappath=path + self.trapprefix=prefix + else + self:E(self.lid.."ERROR: io is not desanitized. Cannot save trap sheet.") + end + return self +end + +--- Specify weather the mission has set static or dynamic weather. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, mission uses static weather. If false, dynamic weather is used in this mission. +-- @return #AIRBOSS self +function AIRBOSS:SetStaticWeather(switch) + if switch==nil or switch==true then + self.staticweather=true + else + self.staticweather=false + end + return self +end + + +--- Disable automatic TACAN activation +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetTACANoff() + self.TACANon=false + return self +end + +--- Set TACAN channel of carrier. +-- @param #AIRBOSS self +-- @param #number channel TACAN channel. Default 74. +-- @param #string mode TACAN mode, i.e. "X" or "Y". Default "X". +-- @param #string morsecode Morse code identifier. Three letters, e.g. "STN". +-- @return #AIRBOSS self +function AIRBOSS:SetTACAN(channel, mode, morsecode) + + self.TACANchannel=channel or 74 + self.TACANmode=mode or "X" + self.TACANmorse=morsecode or "STN" + self.TACANon=true + + return self +end + +--- Disable automatic ICLS activation. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetICLSoff() + self.ICLSon=false + return self +end + +--- Set ICLS channel of carrier. +-- @param #AIRBOSS self +-- @param #number channel ICLS channel. Default 1. +-- @param #string morsecode Morse code identifier. Three letters, e.g. "STN". Default "STN". +-- @return #AIRBOSS self +function AIRBOSS:SetICLS(channel, morsecode) + + self.ICLSchannel=channel or 1 + self.ICLSmorse=morsecode or "STN" + self.ICLSon=true + + return self +end + + +--- Set beacon (TACAN/ICLS) time refresh interfal in case the beacons die. +-- @param #AIRBOSS self +-- @param #number interval Time interval in seconds. Default 1200 sec = 20 min. +-- @return #AIRBOSS self +function AIRBOSS:SetBeaconRefresh(interval) + self.dTbeacon=interval or 20*60 + return self +end + + +--- Set LSO radio frequency and modulation. Default frequency is 264 MHz AM. +-- @param #AIRBOSS self +-- @param #number frequency Frequency in MHz. Default 264 MHz. +-- @param #string modulation Modulation, i.e. "AM" (default) or "FM". +-- @return #AIRBOSS self +function AIRBOSS:SetLSORadio(frequency, modulation) + + self.LSOFreq=(frequency or 264) + modulation=modulation or "AM" + + if modulation=="FM" then + self.LSOModu=radio.modulation.FM + else + self.LSOModu=radio.modulation.AM + end + + self.LSORadio={} --#AIRBOSS.Radio + self.LSORadio.frequency=self.LSOFreq + self.LSORadio.modulation=self.LSOModu + self.LSORadio.alias="LSO" + + return self +end + +--- Set carrier radio frequency and modulation. Default frequency is 305 MHz AM. +-- @param #AIRBOSS self +-- @param #number frequency Frequency in MHz. Default 305 MHz. +-- @param #string modulation Modulation, i.e. "AM" (default) or "FM". +-- @return #AIRBOSS self +function AIRBOSS:SetMarshalRadio(frequency, modulation) + + self.MarshalFreq=frequency or 305 + modulation=modulation or "AM" + + if modulation=="FM" then + self.MarshalModu=radio.modulation.FM + else + self.MarshalModu=radio.modulation.AM + end + + self.MarshalRadio={} --#AIRBOSS.Radio + self.MarshalRadio.frequency=self.MarshalFreq + self.MarshalRadio.modulation=self.MarshalModu + self.MarshalRadio.alias="MARSHAL" + + return self +end + +--- Set unit name for sending radio messages. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @return #AIRBOSS self +function AIRBOSS:SetRadioUnitName(unitname) + self.senderac=unitname + return self +end + +--- Set unit acting as radio relay for the LSO radio. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @return #AIRBOSS self +function AIRBOSS:SetRadioRelayLSO(unitname) + self.radiorelayLSO=unitname + return self +end + +--- Set unit acting as radio relay for the Marshal radio. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @return #AIRBOSS self +function AIRBOSS:SetRadioRelayMarshal(unitname) + self.radiorelayMSH=unitname + return self +end + + +--- Use user sound output instead of radio transmission for messages. Might be handy if radio transmissions are broken. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetUserSoundRadio() + self.usersoundradio=true + return self +end + +--- Test LSO radio sounds. +-- @param #AIRBOSS self +-- @param #number delay Delay in seconds be sound check starts. +-- @return #AIRBOSS self +function AIRBOSS:SoundCheckLSO(delay) + + if delay and delay>0 then + -- Delayed call. + --SCHEDULER:New(nil, AIRBOSS.SoundCheckLSO, {self}, delay) + self:ScheduleOnce(delay, AIRBOSS.SoundCheckLSO, self) + else + + + local text="Playing LSO sound files:" + + for _name,_call in pairs(self.LSOCall) do + local call=_call --#AIRBOSS.RadioCall + + -- Debug text. + text=text..string.format("\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".", call.file, call.suffix, call.duration, tostring(call.loud), call.subtitle) + + -- Radio transmission to queue. + self:RadioTransmission(self.LSORadio, call, false) + + -- Also play the loud version. + if call.loud then + self:RadioTransmission(self.LSORadio, call, true) + end + end + + -- Debug message. + self:I(self.lid..text) + + end +end + +--- Test Marshal radio sounds. +-- @param #AIRBOSS self +-- @param #number delay Delay in seconds be sound check starts. +-- @return #AIRBOSS self +function AIRBOSS:SoundCheckMarshal(delay) + + if delay and delay>0 then + -- Delayed call. + --SCHEDULER:New(nil, AIRBOSS.SoundCheckMarshal, {self}, delay) + self:ScheduleOnce(delay, AIRBOSS.SoundCheckMarshal, self) + else + + + local text="Playing Marshal sound files:" + + for _name,_call in pairs(self.MarshalCall) do + local call=_call --#AIRBOSS.RadioCall + + -- Debug text. + text=text..string.format("\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".", call.file, call.suffix, call.duration, tostring(call.loud), call.subtitle) + + -- Radio transmission to queue. + self:RadioTransmission(self.MarshalRadio, call, false) + + -- Also play the loud version. + if call.loud then + self:RadioTransmission(self.MarshalRadio, call, true) + end + end + + -- Debug message. + self:I(self.lid..text) + + end +end + +--- Set number of aircraft units, which can be in the landing pattern before the pattern is full. +-- @param #AIRBOSS self +-- @param #number nmax Max number. Default 4. Minimum is 1, maximum is 6. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxLandingPattern(nmax) + nmax=nmax or 4 + nmax=math.max(nmax,1) + nmax=math.min(nmax,6) + self.Nmaxpattern=nmax + return self +end + +--- Set number available Case I Marshal stacks. If Marshal stacks are full, flights requesting Marshal will be told to hold outside 10 NM zone until a stack becomes available again. +-- Marshal stacks for Case II/III are unlimited. +-- @param #AIRBOSS self +-- @param #number nmax Max number of stacks available to players and AI flights. Default 3, i.e. angels 2, 3, 4. Minimum is 1. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxMarshalStacks(nmax) + self.Nmaxmarshal=nmax or 3 + self.Nmaxmarshal=math.max(self.Nmaxmarshal, 1) + return self +end + +--- Set max number of section members. Minimum is one, i.e. the section lead itself. Maximum number is four. Default is two, i.e. the lead and one other human flight. +-- @param #AIRBOSS self +-- @param #number nmax Number of max allowed members including the lead itself. For example, Nmax=2 means a section lead plus one member. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxSectionSize(nmax) + nmax=nmax or 2 + nmax=math.max(nmax,1) + nmax=math.min(nmax,4) + self.NmaxSection=nmax-1 -- We substract one because internally the section lead is not counted! + return self +end + +--- Set max number of flights per stack. All members of a section count as one "flight". +-- @param #AIRBOSS self +-- @param #number nmax Number of max allowed flights per stack. Default is two. Minimum is one, maximum is 4. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxFlightsPerStack(nmax) + nmax=nmax or 2 + nmax=math.max(nmax,1) + nmax=math.min(nmax,4) + self.NmaxStack=nmax + return self +end + + +--- Handle AI aircraft. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetHandleAION() + self.handleai=true + return self +end + +--- Do not handle AI aircraft. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetHandleAIOFF() + self.handleai=false + return self +end + + +--- Define recovery tanker associated with the carrier. +-- @param #AIRBOSS self +-- @param Ops.RecoveryTanker#RECOVERYTANKER recoverytanker Recovery tanker object. +-- @return #AIRBOSS self +function AIRBOSS:SetRecoveryTanker(recoverytanker) + self.tanker=recoverytanker + return self +end + +--- Define an AWACS associated with the carrier. +-- @param #AIRBOSS self +-- @param Ops.RecoveryTanker#RECOVERYTANKER awacs AWACS (recovery tanker) object. +-- @return #AIRBOSS self +function AIRBOSS:SetAWACS(awacs) + self.awacs=awacs + return self +end + +--- Set default player skill. New players will be initialized with this skill. +-- +-- * "Flight Student" = @{#AIRBOSS.Difficulty.Easy} +-- * "Naval Aviator" = @{#AIRBOSS.Difficulty.Normal} +-- * "TOPGUN Graduate" = @{#AIRBOSS.Difficulty.Hard} +-- @param #AIRBOSS self +-- @param #string skill Player skill. Default "Naval Aviator". +-- @return #AIRBOSS self +function AIRBOSS:SetDefaultPlayerSkill(skill) + + -- Set skill or normal. + self.defaultskill=skill or AIRBOSS.Difficulty.NORMAL + + -- Check that defualt skill is valid. + local gotit=false + for _,_skill in pairs(AIRBOSS.Difficulty) do + if _skill==self.defaultskill then + gotit=true + end + end + + -- If invalid user input, fall back to normal. + if not gotit then + self.defaultskill=AIRBOSS.Difficulty.NORMAL + self:E(self.lid..string.format("ERROR: Invalid default skill = %s. Resetting to Naval Aviator.", tostring(skill))) + end + + return self +end + +--- Enable auto save of player results each time a player is *finally* graded. *Finally* means after the player landed on the carrier! After intermediate passes (bolter or waveoff) the stats are *not* saved. +-- @param #AIRBOSS self +-- @param #string path Path where to save the asset data file. Default is the DCS root installation directory or your "Saved Games\\DCS" folder if lfs was desanitized. +-- @param #string filename File name. Default is generated automatically from airboss carrier name/alias. +-- @return #AIRBOSS self +function AIRBOSS:SetAutoSave(path, filename) + self.autosave=true + self.autosavepath=path + self.autosavefile=filename + return self +end + +--- Activate debug mode. Display debug messages on screen. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetDebugModeON() + self.Debug=true + return self +end + +--- Carrier patrols ad inifintum. If the last waypoint is reached, it will go to waypoint one and repeat its route. +-- @param #AIRBOSS self +-- @param #boolean switch If true or nil, patrol until the end of time. If false, go along the waypoints once and stop. +-- @return #AIRBOSS self +function AIRBOSS:SetPatrolAdInfinitum(switch) + if switch==false then + self.adinfinitum=false + else + self.adinfinitum=true + end + return self +end + +--- Set the magnetic declination (or variation). By default this is set to the standard declination of the map. +-- @param #AIRBOSS self +-- @param #number declination Declination in degrees or nil for default declination of the map. +-- @return #AIRBOSS self +function AIRBOSS:SetMagneticDeclination(declination) + self.magvar=declination or UTILS.GetMagneticDeclination() + return self +end + +--- Deactivate debug mode. This is also the default setting. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetDebugModeOFF() + self.Debug=false + return self +end + +--- Get next time the carrier will start recovering aircraft. +-- @param #AIRBOSS self +-- @param #boolean InSeconds If true, abs. mission time seconds is returned. Default is a clock #string. +-- @return #string Clock start (or start time in abs. seconds). +-- @return #string Clock stop (or stop time in abs. seconds). +function AIRBOSS:GetNextRecoveryTime(InSeconds) + if self.recoverywindow then + if InSeconds then + return self.recoverywindow.START, self.recoverywindow.STOP + else + return UTILS.SecondsToClock(self.recoverywindow.START), UTILS.SecondsToClock(self.recoverywindow.STOP) + end + else + if InSeconds then + return -1, -1 + else + return "?", "?" + end + end +end + +--- Check if carrier is recovering aircraft. +-- @param #AIRBOSS self +-- @return #boolean If true, time slot for recovery is open. +function AIRBOSS:IsRecovering() + return self:is("Recovering") +end + +--- Check if carrier is idle, i.e. no operations are carried out. +-- @param #AIRBOSS self +-- @return #boolean If true, carrier is in idle state. +function AIRBOSS:IsIdle() + return self:is("Idle") +end + +--- Check if recovery of aircraft is paused. +-- @param #AIRBOSS self +-- @return #boolean If true, recovery is paused +function AIRBOSS:IsPaused() + return self:is("Paused") +end + +--- Activate TACAN and ICLS beacons. +-- @param #AIRBOSS self +function AIRBOSS:_ActivateBeacons() + self:T(self.lid..string.format("Activating Beacons (TACAN=%s, ICLS=%s)", tostring(self.TACANon), tostring(self.ICLSon))) + + -- Activate TACAN. + if self.TACANon then + self:I(self.lid..string.format("Activating TACAN Channel %d%s (%s)", self.TACANchannel, self.TACANmode, self.TACANmorse)) + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) + end + + -- Activate ICLS. + if self.ICLSon then + self:I(self.lid..string.format("Activating ICLS Channel %d (%s)", self.ICLSchannel, self.ICLSmorse)) + self.beacon:ActivateICLS(self.ICLSchannel, self.ICLSmorse) + end + + -- Set time stamp. + self.Tbeacon=timer.getTime() +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM event functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the AIRBOSS. Adds event handlers and schedules status updates of requests and queue. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterStart(From, Event, To) + + -- Events are handled my MOOSE. + self:I(self.lid..string.format("Starting AIRBOSS v%s for carrier unit %s of type %s on map %s", AIRBOSS.version, self.carrier:GetName(), self.carriertype, self.theatre)) + + -- Activate TACAN and ICLS if desired. + self:_ActivateBeacons() + + -- Schedule radio queue checks. + --self.RQLid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQLSO, "LSO"}, 1, 0.1) + --self.RQMid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQMarshal, "MARSHAL"}, 1, 0.1) + + --self:I("FF: starting timer.scheduleFunction") + --timer.scheduleFunction(AIRBOSS._CheckRadioQueueT, {airboss=self, radioqueue=self.RQLSO, name="LSO"}, timer.getTime()+1) + --timer.scheduleFunction(AIRBOSS._CheckRadioQueueT, {airboss=self, radioqueue=self.RQMarshal, name="MARSHAL"}, timer.getTime()+1) + + -- Initial carrier position and orientation. + self.Cposition=self:GetCoordinate() + self.Corientation=self.carrier:GetOrientationX() + self.Corientlast=self.Corientation + self.Tpupdate=timer.getTime() + + -- Check if no recovery window is set. DISABLED! + if #self.recoverytimes==0 and false then + + -- Open window in 15 minutes for 3 hours. + local Topen=timer.getAbsTime()+15*60 + local Tclose=Topen+3*60*60 + + -- Add window. + self:AddRecoveryWindow(UTILS.SecondsToClock(Topen), UTILS.SecondsToClock(Tclose)) + end + + -- Check Recovery time.s + self:_CheckRecoveryTimes() + + -- Time stamp for checking queues. We substract 60 seconds so the routine is called right after status is called the first time. + self.Tqueue=timer.getTime()-60 + + -- Handle events. + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.EngineShutdown) + self:HandleEvent(EVENTS.Takeoff) + self:HandleEvent(EVENTS.Crash) + self:HandleEvent(EVENTS.Ejection) + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) + self:HandleEvent(EVENTS.MissionEnd) + + -- Start status check in 1 second. + self:__Status(1) +end + +--- On after Status event. Checks for new flights, updates queue and checks player status. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterStatus(From, Event, To) + + if true then + --env.info("FF Status ==> return") + --return + end + + -- Get current time. + local time=timer.getTime() + + -- Update marshal and pattern queue every 30 seconds. + if time-self.Tqueue>self.dTqueue then + + --collectgarbage() + + -- Get time. + local clock=UTILS.SecondsToClock(timer.getAbsTime()) + local eta=UTILS.SecondsToClock(self:_GetETAatNextWP()) + + -- Current heading and position of the carrier. + local hdg=self:GetHeading() + local pos=self:GetCoordinate() + local speed=self.carrier:GetVelocityKNOTS() + + -- Check water is ahead. + local collision=self:_CheckCollisionCoord(pos:Translate(self.collisiondist, hdg)) + + local holdtime=0 + if self.holdtimestamp then + holdtime=timer.getTime()-self.holdtimestamp + end + + -- Check if carrier is stationary. + local NextWP=self:_GetNextWaypoint() + local ExpectedSpeed=UTILS.MpsToKnots(NextWP:GetVelocity()) + if speed<0.5 and ExpectedSpeed>0 and not (self.detour or self.turnintowind) then + if not self.holdtimestamp then + self:E(self.lid..string.format("Carrier came to an unexpected standstill. Trying to re-route in 3 min. Speed=%.1f knots, expected=%.1f knots", speed, ExpectedSpeed)) + self.holdtimestamp=timer.getTime() + else + if holdtime>3*60 then + local coord=self:GetCoordinate():Translate(500, hdg+10) + --coord:MarkToAll("Re-route after standstill.") + self:CarrierResumeRoute(coord) + self.holdtimestamp=nil + end + end + end + + -- Debug info. + local text=string.format("Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s - Turning=%s - Collision Warning=%s - Detour=%s - Turn Into Wind=%s - Holdtime=%d sec", + clock, self:GetState(), self.case, speed, hdg, self.currentwp, eta, tostring(self.turning), tostring(collision), tostring(self.detour), tostring(self.turnintowind), holdtime) + self:T(self.lid..text) + + -- Players online: + text="Players:" + local i=0 + for _name,_player in pairs(self.players) do + i=i+1 + local player=_player --#AIRBOSS.FlightGroup + text=text..string.format("\n%d.) %s: Step=%s, Unit=%s, Airframe=%s", i, tostring(player.name), tostring(player.step), tostring(player.unitname), tostring(player.actype)) + end + if i==0 then + text=text.." none" + end + self:I(self.lid..text) + + -- Check for collision. + if collision then + + -- We are currently turning into the wind. + if self.turnintowind then + + -- Carrier resumes its initial route. This disables turnintowind switch. + self:CarrierResumeRoute(self.Creturnto) + + -- Since current window would stay open, we disable the WIND switch. + if self:IsRecovering() and self.recoverywindow and self.recoverywindow.WIND then + -- Disable turn into the wind for this window so that we do not do this all over again. + self.recoverywindow.WIND=false + end + + else + + -- Find path around the obstacle. + if not self.detour then + --self:_Pathfinder() + end + + end + end + + + -- Check recovery times and start/stop recovery mode if necessary. + self:_CheckRecoveryTimes() + + -- Remove dead/zombie flight groups. Player leaving the server whilst in pattern etc. + --self:_RemoveDeadFlightGroups() + + -- Scan carrier zone for new aircraft. + self:_ScanCarrierZone() + + -- Check marshal and pattern queues. + self:_CheckQueue() + + -- Check if carrier is currently turning. + self:_CheckCarrierTurning() + + -- Check if marshal pattern of AI needs an update. + self:_CheckPatternUpdate() + + -- Time stamp. + self.Tqueue=time + end + + -- (Re-)activate TACAN and ICLS channels. + if time-self.Tbeacon>self.dTbeacon then + self:_ActivateBeacons() + end + + -- Check player status. + self:_CheckPlayerStatus() + + -- Check AI landing pattern status + self:_CheckAIStatus() + + -- Call status every ~0.5 seconds. + self:__Status(-self.dTstatus) +end + +--- Check AI status. Pattern queue AI in the groove? Marshal queue AI arrived in holding zone? +-- @param #AIRBOSS self +function AIRBOSS:_CheckAIStatus() + + -- Loop over all flights in Marshal stack. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Only AI! + if flight.ai then + + -- Get fuel amount in %. + local fuel=flight.group:GetFuelMin()*100 + + -- Debug text. + local text=string.format("Group %s fuel=%.1f %%", flight.groupname, fuel) + self:T3(self.lid..text) + + -- Check if flight is low on fuel and not yet refueling. + if self.lowfuelAI and fuel=recovery.START then + -- Start time has passed. + + if time0 then + + -- Extend recovery time. 5 min per flight. + local extmin=5*npattern + recovery.STOP=recovery.STOP+extmin*60 + + local text=string.format("We still got flights in the pattern.\nRecovery time prolonged by %d minutes.\nNow get your act together and no more bolters!", extmin) + self:MessageToPattern(text, "AIRBOSS", "99", 10, false, nil) + + else + + -- Set carrier to idle. + self:RecoveryStop() + state="closing now" + + -- Closed. + recovery.OPEN=false + + -- Window just closed. + recovery.OVER=true + + end + else + + -- Carrier is already idle. + state="closed" + end + + end + + else + -- This recovery is in the future. + state="in the future" + + -- This is the next to come as we sorted by start time. + if nextwindow==nil then + nextwindow=recovery + state="next in line" + end + end + + -- Debug text. + text=text..string.format("\n- Start=%s Stop=%s Case=%d Offset=%d Open=%s Closed=%s Status=\"%s\"", Cstart, Cstop, recovery.CASE, recovery.OFFSET, tostring(recovery.OPEN), tostring(recovery.OVER), state) + end + + -- Debug output. + self:T(self.lid..text) + + -- Current recovery window. + self.recoverywindow=nil + + + if self:IsIdle() then + ----------------------------------------------------------------------------------------------------------------- + -- Carrier is idle: We need to make sure that incoming flights get the correct recovery info of the next window. + ----------------------------------------------------------------------------------------------------------------- + + -- Check if there is a next windows defined. + if nextwindow then + + -- Set case and offset of the next window. + self:RecoveryCase(nextwindow.CASE, nextwindow.OFFSET) + + -- Check if time is less than 5 minutes. + if nextwindow.WIND and nextwindow.START-time 5° different from the current heading. + local hdg=self:GetHeading() + local wind=self:GetHeadingIntoWind() + local delta=self:_GetDeltaHeading(hdg, wind) + local uturn=delta>5 + + -- Check if wind is actually blowing (0.1 m/s = 0.36 km/h = 0.2 knots) + local _,vwind=self:GetWind() + if vwind<0.1 then + uturn=false + end + + -- U-turn disabled by user input. + if not nextwindow.UTURN then + uturn=false + end + + --Debug info + self:T(self.lid..string.format("Heading=%03d°, Wind=%03d° %.1f kts, Delta=%03d° ==> U-turn=%s", hdg, wind,UTILS.MpsToKnots(vwind), delta, tostring(uturn))) + + -- Time into the wind 1 day or if longer recovery time + the 5 min early. + local t=math.max(nextwindow.STOP-nextwindow.START+self.dTturn, 60*60*24) + + -- Recovery wind on deck in knots. + local v=UTILS.KnotsToMps(nextwindow.SPEED) + + -- Check that we do not go above max possible speed. + local vmax=self.carrier:GetSpeedMax()/3.6 -- convert to m/s + v=math.min(v,vmax) + + -- Route carrier into the wind. Sets self.turnintowind=true + self:CarrierTurnIntoWind(t, v, uturn) + + end + + -- Set current recovery window. + self.recoverywindow=nextwindow + + else + -- No next window. Set default values. + self:RecoveryCase() + end + + else + ------------------------------------------------------------------------------------- + -- Carrier is recovering: We set the recovery window to the current one or next one. + ------------------------------------------------------------------------------------- + + if currwindow then + self.recoverywindow=currwindow + else + self.recoverywindow=nextwindow + end + end + + self:T2({"FF", recoverywindow=self.recoverywindow}) +end + +--- Get section lead of a flight. +--@param #AIRBOSS self +--@param #AIRBOSS.FlightGroup flight +--@return #AIRBOSS.FlightGroup The leader of the section. Could be the flight itself. +--@return #boolean If true, flight is lead. +function AIRBOSS:_GetFlightLead(flight) + + if flight.name~=flight.seclead then + -- Section lead of flight. + local lead=self.players[flight.seclead] + return lead,false + else + -- Flight without section or section lead. + return flight,true + end + +end + +--- On before "RecoveryCase" event. Check if case or holding offset did change. If not transition is denied. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Case The recovery case (1, 2 or 3) to switch to. +-- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. +function AIRBOSS:onbeforeRecoveryCase(From, Event, To, Case, Offset) + + -- Input or default value. + Case=Case or self.defaultcase + + -- Input or default value + Offset=Offset or self.defaultoffset + + if Case==self.case and Offset==self.holdingoffset then + return false + end + + return true +end + +--- On after "RecoveryCase" event. Sets new aircraft recovery case. Updates +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Case The recovery case (1, 2 or 3) to switch to. +-- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. +function AIRBOSS:onafterRecoveryCase(From, Event, To, Case, Offset) + + -- Input or default value. + Case=Case or self.defaultcase + + -- Input or default value + Offset=Offset or self.defaultoffset + + -- Debug output. + local text=string.format("Switching recovery case %d ==> %d", self.case, Case) + if Case>1 then + text=text..string.format(" Holding offset angle %d degrees.", Offset) + end + MESSAGE:New(text, 20, self.alias):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Set new recovery case. + self.case=Case + + -- Set holding offset. + self.holdingoffset=Offset + + -- Update case of all flights not in Marshal or Pattern queue. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + if not (self:_InQueue(self.Qmarshal, flight.group) or self:_InQueue(self.Qpattern, flight.group)) then + + -- Also not for section members. These are not in the marshal or pattern queue if the lead is. + if flight.name~=flight.seclead then + local lead=self.players[flight.seclead] + + if lead and not (self:_InQueue(self.Qmarshal, lead.group) or self:_InQueue(self.Qpattern, lead.group)) then + -- This is section member and the lead is not in the Marshal or Pattern queue. + flight.case=self.case + end + + else + + -- This is a flight without section or the section lead. + flight.case=self.case + + end + + end + end +end + +--- On after "RecoveryStart" event. Recovery of aircraft is started and carrier switches to state "Recovering". +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number Case The recovery case (1, 2 or 3) to start. +-- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries. +function AIRBOSS:onafterRecoveryStart(From, Event, To, Case, Offset) + + -- Input or default value. + Case=Case or self.defaultcase + + -- Input or default value. + Offset=Offset or self.defaultoffset + + -- Radio message: "99, starting aircraft recovery case X ops. (Marshal radial XYZ degrees)" + self:_MarshalCallRecoveryStart(Case) + + -- Switch to case. + self:RecoveryCase(Case, Offset) +end + +--- On after "RecoveryStop" event. Recovery of aircraft is stopped and carrier switches to state "Idle". Running recovery window is deleted. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterRecoveryStop(From, Event, To) + -- Debug output. + self:T(self.lid..string.format("Stopping aircraft recovery.")) + + -- Recovery ops stopped message. + self:_MarshalCallRecoveryStopped(self.case) + + -- If carrier is currently heading into the wind, we resume the original route. + if self.turnintowind then + + -- Coordinate to return to. + local coord=self.Creturnto + + -- No U-turn. + if self.recoverywindow and self.recoverywindow.UTURN==false then + coord=nil + end + + -- Carrier resumes route. + self:CarrierResumeRoute(coord) + end + + -- Delete current recovery window if open. + if self.recoverywindow and self.recoverywindow.OPEN==true then + self.recoverywindow.OPEN=false + self.recoverywindow.OVER=true + self:DeleteRecoveryWindow(self.recoverywindow) + end + + -- Check recovery windows. This sets self.recoverywindow to the next window. + self:_CheckRecoveryTimes() +end + + +--- On after "RecoveryPause" event. Recovery of aircraft is paused. Marshal queue stays intact. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number duration Duration of pause in seconds. After that recovery is resumed automatically. +function AIRBOSS:onafterRecoveryPause(From, Event, To, duration) + -- Debug output. + self:T(self.lid..string.format("Pausing aircraft recovery.")) + + -- Message text + + if duration then + + -- Auto resume. + self:__RecoveryUnpause(duration) + + -- Time to resume. + local clock=UTILS.SecondsToClock(timer.getAbsTime()+duration) + + -- Marshal call: "99, aircraft recovery paused and will be resume at XX:YY." + self:_MarshalCallRecoveryPausedResumedAt(clock) + else + + local text=string.format("aircraft recovery is paused until further notice.") + + -- Marshal call: "99, aircraft recovery paused until further notice." + self:_MarshalCallRecoveryPausedNotice() + + end + +end + +--- On after "RecoveryUnpause" event. Recovery of aircraft is resumed. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterRecoveryUnpause(From, Event, To) + -- Debug output. + self:T(self.lid..string.format("Unpausing aircraft recovery.")) + + -- Resume recovery. + self:_MarshalCallRecoveryResume() + +end + +--- On after "PassingWaypoint" event. Carrier has just passed a waypoint +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #number n Number of waypoint that was passed. +function AIRBOSS:onafterPassingWaypoint(From, Event, To, n) + -- Debug output. + self:I(self.lid..string.format("Carrier passed waypoint %d.", n)) +end + +--- On after "Idle" event. Carrier goes to state "Idle". +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterIdle(From, Event, To) + -- Debug output. + self:T(self.lid..string.format("Carrier goes to idle.")) +end + +--- On after Stop event. Unhandle events. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterStop(From, Event, To) + self:I(self.lid..string.format("Stopping airboss script.")) + + -- Unhandle events. + self:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.EngineShutdown) + self:UnHandleEvent(EVENTS.Takeoff) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.Ejection) + self:UnHandleEvent(EVENTS.PlayerLeaveUnit) + self:UnHandleEvent(EVENTS.MissionEnd) + + self.CallScheduler:Clear() +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Parameter initialization +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Init parameters for USS Stennis carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitStennis() + + -- Carrier Parameters. + self.carrierparam.sterndist =-153 + self.carrierparam.deckheight = 19 + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlength=310 -- Wiki says 332.8 meters overall length. + self.carrierparam.totwidthport=40 -- Wiki says 76.8 meters overall beam. + self.carrierparam.totwidthstarboard=30 + + -- Landing runway. + self.carrierparam.rwyangle = -9 + self.carrierparam.rwylength = 225 + self.carrierparam.rwywidth = 20 + + -- Wires. + self.carrierparam.wire1 = 46 -- Distance from stern to first wire. + self.carrierparam.wire2 = 46+12 + self.carrierparam.wire3 = 46+24 + self.carrierparam.wire4 = 46+35 -- Last wire is strangely one meter closer. + + + -- Platform at 5k. Reduce descent rate to 2000 ft/min to 1200 dirty up level flight. + self.Platform.name="Platform 5k" + self.Platform.Xmin=-UTILS.NMToMeters(22) -- Not more than 22 NM behind the boat. Last check was at 21 NM. + self.Platform.Xmax =nil + self.Platform.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port of boat. + self.Platform.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard of boat. + self.Platform.LimitXmin=nil -- Limits via zone + self.Platform.LimitXmax=nil + self.Platform.LimitZmin=nil + self.Platform.LimitZmax=nil + + -- Level out at 1200 ft and dirty up. + self.DirtyUp.name="Dirty Up" + self.DirtyUp.Xmin=-UTILS.NMToMeters(21) -- Not more than 21 NM behind the boat. + self.DirtyUp.Xmax= nil + self.DirtyUp.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port of boat. + self.DirtyUp.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard of boat. + self.DirtyUp.LimitXmin=nil -- Limits via zone + self.DirtyUp.LimitXmax=nil + self.DirtyUp.LimitZmin=nil + self.DirtyUp.LimitZmax=nil + + -- Intercept glide slope and follow bullseye. + self.Bullseye.name="Bullseye" + self.Bullseye.Xmin=-UTILS.NMToMeters(11) -- Not more than 11 NM behind the boat. Last check was at 10 NM. + self.Bullseye.Xmax= nil + self.Bullseye.Zmin=-UTILS.NMToMeters(30) -- Not more than 30 NM port. + self.Bullseye.Zmax= UTILS.NMToMeters(30) -- Not more than 30 NM starboard. + self.Bullseye.LimitXmin=nil -- Limits via zone. + self.Bullseye.LimitXmax=nil + self.Bullseye.LimitZmin=nil + self.Bullseye.LimitZmax=nil + + -- Break entry. + self.BreakEntry.name="Break Entry" + self.BreakEntry.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of 500 m and 100 m starboard. + self.BreakEntry.Xmax= nil + self.BreakEntry.Zmin=-UTILS.NMToMeters(0.5) -- Not more than 0.5 NM port of boat. + self.BreakEntry.Zmax= UTILS.NMToMeters(1.5) -- Not more than 1.5 NM starboard. + self.BreakEntry.LimitXmin=0 -- Check and next step when at carrier and starboard of carrier. + self.BreakEntry.LimitXmax=nil + self.BreakEntry.LimitZmin=nil + self.BreakEntry.LimitZmax=nil + + -- Early break. + self.BreakEarly.name="Early Break" + self.BreakEarly.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakEarly.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakEarly.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.BreakEarly.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakEarly.LimitXmin= 0 -- Check and next step 0.2 NM port and in front of boat. + self.BreakEarly.LimitXmax= nil + self.BreakEarly.LimitZmin=-UTILS.NMToMeters(0.2) -- -370 m port + self.BreakEarly.LimitZmax= nil + + -- Late break. + self.BreakLate.name="Late Break" + self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.BreakLate.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakLate.LimitXmin= 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax= nil + self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.8) -- -1470 m port + self.BreakLate.LimitZmax= nil + + -- Abeam position. + self.Abeam.name="Abeam Position" + self.Abeam.Xmin=-UTILS.NMToMeters(5) -- Not more then 5 NM astern of boat. Should be LIG call anyway. + self.Abeam.Xmax= UTILS.NMToMeters(5) -- Not more then 5 NM ahead of boat. + self.Abeam.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.Abeam.Zmax= 500 -- Not more than 500 m starboard. Must be port! + self.Abeam.LimitXmin=-200 -- Check and next step 200 meters behind the ship. + self.Abeam.LimitXmax= nil + self.Abeam.LimitZmin= nil + self.Abeam.LimitZmax= nil + + -- At the Ninety. + self.Ninety.name="Ninety" + self.Ninety.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. LIG check anyway. + self.Ninety.Xmax= 0 -- Must be behind the boat. + self.Ninety.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port of boat. + self.Ninety.Zmax= nil + self.Ninety.LimitXmin=nil + self.Ninety.LimitXmax=nil + self.Ninety.LimitZmin=nil + self.Ninety.LimitZmax=-UTILS.NMToMeters(0.6) -- Check and next step when 0.6 NM port. + + -- At the Wake. + self.Wake.name="Wake" + self.Wake.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Wake.Xmax= 0 -- Must be behind the boat. + self.Wake.Zmin=-2000 -- Not more than 2 km port of boat. + self.Wake.Zmax= nil + self.Wake.LimitXmin=nil + self.Wake.LimitXmax=nil + self.Wake.LimitZmin=0 -- Check and next step when directly behind the boat. + self.Wake.LimitZmax=nil + + -- Turn to final. + self.Final.name="Final" + self.Final.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Final.Xmax= 0 -- Must be behind the boat. + self.Final.Zmin=-2000 -- Not more than 2 km port. + self.Final.Zmax= nil + self.Final.LimitXmin=nil -- No limits. Check is carried out differently. + self.Final.LimitXmax=nil + self.Final.LimitZmin=nil + self.Final.LimitZmax=nil + + -- In the Groove. + self.Groove.name="Groove" + self.Groove.Xmin=-UTILS.NMToMeters(4) -- Not more than 4 NM behind the boat. + self.Groove.Xmax= nil + self.Groove.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port + self.Groove.Zmax= UTILS.NMToMeters(2) -- Not more than 2 NM starboard. + self.Groove.LimitXmin=nil -- No limits. Check is carried out differently. + self.Groove.LimitXmax=nil + self.Groove.LimitZmin=nil + self.Groove.LimitZmax=nil + +end + +--- Init parameters for USS Stennis carrier. +-- @param #AIRBOSS self +function AIRBOSS:_InitTarawa() + + -- Init Stennis as default. + self:_InitStennis() + + -- Carrier Parameters. + self.carrierparam.sterndist =-125 + self.carrierparam.deckheight = 21 --69 ft + + -- Total size of the carrier (approx as rectangle). + self.carrierparam.totlength=245 + self.carrierparam.totwidthport=10 + self.carrierparam.totwidthstarboard=25 + + -- Landing runway. + self.carrierparam.rwyangle = 0 + self.carrierparam.rwylength = 225 + self.carrierparam.rwywidth = 15 + + -- Wires. + self.carrierparam.wire1=nil + self.carrierparam.wire2=nil + self.carrierparam.wire3=nil + self.carrierparam.wire4=nil + + -- Late break. + self.BreakLate.name="Late Break" + self.BreakLate.Xmin=-UTILS.NMToMeters(1) -- Not more than 1 NM behind the boat. Last check was at 0. + self.BreakLate.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. Enough for late breaks? + self.BreakLate.Zmin=-UTILS.NMToMeters(1.6) -- Not more than 1.6 NM port. + self.BreakLate.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + self.BreakLate.LimitXmin= 0 -- Check and next step 0.8 NM port and in front of boat. + self.BreakLate.LimitXmax= nil + self.BreakLate.LimitZmin=-UTILS.NMToMeters(0.5) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2 + self.BreakLate.LimitZmax= nil + +end + +--- Init parameters for Marshal Voice overs *Gabriella* by HighwaymanEd. +-- @param #AIRBOSS self +-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. +function AIRBOSS:SetVoiceOversMarshalByGabriella(mizfolder) + + -- Set sound files folder. + if mizfolder then + local lastchar=string.sub(mizfolder, -1) + if lastchar~="/" then + mizfolder=mizfolder.."/" + end + self.soundfolderMSH=mizfolder + else + -- Default is the general folder. + self.soundfolderMSH=self.soundfolder + end + + -- Report for duty. + self:I(self.lid..string.format("Marshal Gabriella reporting for duty! Soundfolder=%s", tostring(self.soundfolderMSH))) + + self.MarshalCall.AFFIRMATIVE.duration=0.65 + self.MarshalCall.ALTIMETER.duration=0.60 + self.MarshalCall.BRC.duration=0.67 + self.MarshalCall.CARRIERTURNTOHEADING.duration=1.62 + self.MarshalCall.CASE.duration=0.30 + self.MarshalCall.CHARLIETIME.duration=0.77 + self.MarshalCall.CLEAREDFORRECOVERY.duration=0.93 + self.MarshalCall.DECKCLOSED.duration=0.73 + self.MarshalCall.DEGREES.duration=0.48 + self.MarshalCall.EXPECTED.duration=0.50 + self.MarshalCall.FLYNEEDLES.duration=0.89 + self.MarshalCall.HOLDATANGELS.duration=0.81 + self.MarshalCall.HOURS.duration=0.41 + self.MarshalCall.MARSHALRADIAL.duration=0.95 + self.MarshalCall.N0.duration=0.41 + self.MarshalCall.N1.duration=0.30 + self.MarshalCall.N2.duration=0.34 + self.MarshalCall.N3.duration=0.31 + self.MarshalCall.N4.duration=0.34 + self.MarshalCall.N5.duration=0.30 + self.MarshalCall.N6.duration=0.33 + self.MarshalCall.N7.duration=0.38 + self.MarshalCall.N8.duration=0.35 + self.MarshalCall.N9.duration=0.35 + self.MarshalCall.NEGATIVE.duration=0.60 + self.MarshalCall.NEWFB.duration=0.95 + self.MarshalCall.OPS.duration=0.23 + self.MarshalCall.POINT.duration=0.38 + self.MarshalCall.RADIOCHECK.duration=1.27 + self.MarshalCall.RECOVERY.duration=0.60 + self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.25 + self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.55 + self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=2.55 + self.MarshalCall.REPORTSEEME.duration=0.87 + self.MarshalCall.RESUMERECOVERY.duration=1.55 + self.MarshalCall.ROGER.duration=0.50 + self.MarshalCall.SAYNEEDLES.duration=0.82 + self.MarshalCall.STACKFULL.duration=5.70 + self.MarshalCall.STARTINGRECOVERY.duration=1.61 + +end + + + +--- Init parameters for Marshal Voice overs by *Raynor*. +-- @param #AIRBOSS self +-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. +function AIRBOSS:SetVoiceOversMarshalByRaynor(mizfolder) + + -- Set sound files folder. + if mizfolder then + local lastchar=string.sub(mizfolder, -1) + if lastchar~="/" then + mizfolder=mizfolder.."/" + end + self.soundfolderMSH=mizfolder + else + -- Default is the general folder. + self.soundfolderMSH=self.soundfolder + end + + -- Report for duty. + self:I(self.lid..string.format("Marshal Raynor reporting for duty! Soundfolder=%s", tostring(self.soundfolderMSH))) + + self.MarshalCall.AFFIRMATIVE.duration=0.70 + self.MarshalCall.ALTIMETER.duration=0.60 + self.MarshalCall.BRC.duration=0.60 + self.MarshalCall.CARRIERTURNTOHEADING.duration=1.87 + self.MarshalCall.CASE.duration=0.60 + self.MarshalCall.CHARLIETIME.duration=0.81 + self.MarshalCall.CLEAREDFORRECOVERY.duration=1.21 + self.MarshalCall.DECKCLOSED.duration=0.86 + self.MarshalCall.DEGREES.duration=0.55 + self.MarshalCall.EXPECTED.duration=0.61 + self.MarshalCall.FLYNEEDLES.duration=0.90 + self.MarshalCall.HOLDATANGELS.duration=0.91 + self.MarshalCall.HOURS.duration=0.54 + self.MarshalCall.MARSHALRADIAL.duration=0.80 + self.MarshalCall.N0.duration=0.38 + self.MarshalCall.N1.duration=0.30 + self.MarshalCall.N2.duration=0.30 + self.MarshalCall.N3.duration=0.30 + self.MarshalCall.N4.duration=0.32 + self.MarshalCall.N5.duration=0.41 + self.MarshalCall.N6.duration=0.48 + self.MarshalCall.N7.duration=0.51 + self.MarshalCall.N8.duration=0.38 + self.MarshalCall.N9.duration=0.34 + self.MarshalCall.NEGATIVE.duration=0.60 + self.MarshalCall.NEWFB.duration=1.10 + self.MarshalCall.OPS.duration=0.46 + self.MarshalCall.POINT.duration=0.21 + self.MarshalCall.RADIOCHECK.duration=0.95 + self.MarshalCall.RECOVERY.duration=0.63 + self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.36 + self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.8 -- Strangely the file is actually a shorter ~2.4 sec. + self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=2.75 + self.MarshalCall.REPORTSEEME.duration=1.06 --0.96 + self.MarshalCall.RESUMERECOVERY.duration=1.41 + self.MarshalCall.ROGER.duration=0.41 + self.MarshalCall.SAYNEEDLES.duration=0.79 + self.MarshalCall.STACKFULL.duration=4.70 + self.MarshalCall.STARTINGRECOVERY.duration=2.06 + +end + +--- Set parameters for LSO Voice overs by *Raynor*. +-- @param #AIRBOSS self +-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. +function AIRBOSS:SetVoiceOversLSOByRaynor(mizfolder) + + -- Set sound files folder. + if mizfolder then + local lastchar=string.sub(mizfolder, -1) + if lastchar~="/" then + mizfolder=mizfolder.."/" + end + self.soundfolderLSO=mizfolder + else + -- Default is the general folder. + self.soundfolderLSO=self.soundfolder + end + + -- Report for duty. + self:I(self.lid..string.format("LSO Raynor reporting for duty! Soundfolder=%s", tostring(self.soundfolderLSO))) + + self.LSOCall.BOLTER.duration=0.75 + self.LSOCall.CALLTHEBALL.duration=0.625 + self.LSOCall.CHECK.duration=0.40 + self.LSOCall.CLEAREDTOLAND.duration=0.85 + self.LSOCall.COMELEFT.duration=0.60 + self.LSOCall.DEPARTANDREENTER.duration=1.10 + self.LSOCall.EXPECTHEAVYWAVEOFF.duration=1.30 + self.LSOCall.EXPECTSPOT75.duration=1.85 + self.LSOCall.FAST.duration=0.75 + self.LSOCall.FOULDECK.duration=0.75 + self.LSOCall.HIGH.duration=0.65 + self.LSOCall.IDLE.duration=0.40 + self.LSOCall.LONGINGROOVE.duration=1.25 + self.LSOCall.LOW.duration=0.60 + self.LSOCall.N0.duration=0.38 + self.LSOCall.N1.duration=0.30 + self.LSOCall.N2.duration=0.30 + self.LSOCall.N3.duration=0.30 + self.LSOCall.N4.duration=0.32 + self.LSOCall.N5.duration=0.41 + self.LSOCall.N6.duration=0.48 + self.LSOCall.N7.duration=0.51 + self.LSOCall.N8.duration=0.38 + self.LSOCall.N9.duration=0.34 + self.LSOCall.PADDLESCONTACT.duration=0.91 + self.LSOCall.POWER.duration=0.45 + self.LSOCall.RADIOCHECK.duration=0.90 + self.LSOCall.RIGHTFORLINEUP.duration=0.70 + self.LSOCall.ROGERBALL.duration=0.72 + self.LSOCall.SLOW.duration=0.63 + --self.LSOCall.SLOW.duration=0.59 --TODO + self.LSOCall.STABILIZED.duration=0.75 + self.LSOCall.WAVEOFF.duration=0.55 + self.LSOCall.WELCOMEABOARD.duration=0.80 +end + + + +--- Set parameters for LSO Voice overs by *funkyfranky*. +-- @param #AIRBOSS self +-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. +function AIRBOSS:SetVoiceOversLSOByFF(mizfolder) + + -- Set sound files folder. + if mizfolder then + local lastchar=string.sub(mizfolder, -1) + if lastchar~="/" then + mizfolder=mizfolder.."/" + end + self.soundfolderLSO=mizfolder + else + -- Default is the general folder. + self.soundfolderLSO=self.soundfolder + end + + -- Report for duty. + self:I(self.lid..string.format("LSO FF reporting for duty! Soundfolder=%s", tostring(self.soundfolderLSO))) + + self.LSOCall.BOLTER.duration=0.75 + self.LSOCall.CALLTHEBALL.duration=0.60 + self.LSOCall.CHECK.duration=0.45 + self.LSOCall.CLEAREDTOLAND.duration=1.00 + self.LSOCall.COMELEFT.duration=0.60 + self.LSOCall.DEPARTANDREENTER.duration=1.10 + self.LSOCall.EXPECTHEAVYWAVEOFF.duration=1.20 + self.LSOCall.EXPECTSPOT75.duration=2.00 + self.LSOCall.FAST.duration=0.70 + self.LSOCall.FOULDECK.duration=0.62 + self.LSOCall.HIGH.duration=0.65 + self.LSOCall.IDLE.duration=0.45 + self.LSOCall.LONGINGROOVE.duration=1.20 + self.LSOCall.LOW.duration=0.50 + self.LSOCall.N0.duration=0.40 + self.LSOCall.N1.duration=0.25 + self.LSOCall.N2.duration=0.37 + self.LSOCall.N3.duration=0.37 + self.LSOCall.N4.duration=0.39 + self.LSOCall.N5.duration=0.39 + self.LSOCall.N6.duration=0.40 + self.LSOCall.N7.duration=0.40 + self.LSOCall.N8.duration=0.37 + self.LSOCall.N9.duration=0.40 + self.LSOCall.PADDLESCONTACT.duration=1.00 + self.LSOCall.POWER.duration=0.50 + self.LSOCall.RADIOCHECK.duration=1.10 + self.LSOCall.RIGHTFORLINEUP.duration=0.80 + self.LSOCall.ROGERBALL.duration=1.00 + self.LSOCall.SLOW.duration=0.65 + self.LSOCall.SLOW.duration=0.59 + self.LSOCall.STABILIZED.duration=0.90 + self.LSOCall.WAVEOFF.duration=0.60 + self.LSOCall.WELCOMEABOARD.duration=1.00 +end + +--- Intit parameters for Marshal Voice overs by *funkyfranky*. +-- @param #AIRBOSS self +-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located. +function AIRBOSS:SetVoiceOversMarshalByFF(mizfolder) + + -- Set sound files folder. + if mizfolder then + local lastchar=string.sub(mizfolder, -1) + if lastchar~="/" then + mizfolder=mizfolder.."/" + end + self.soundfolderMSH=mizfolder + else + -- Default is the general folder. + self.soundfolderMSH=self.soundfolder + end + + -- Report for duty. + self:I(self.lid..string.format("Marshal FF reporting for duty! Soundfolder=%s", tostring(self.soundfolderMSH))) + + self.MarshalCall.AFFIRMATIVE.duration=0.90 + self.MarshalCall.ALTIMETER.duration=0.85 + self.MarshalCall.BRC.duration=0.80 + self.MarshalCall.CARRIERTURNTOHEADING.duration=2.48 + self.MarshalCall.CASE.duration=0.40 + self.MarshalCall.CHARLIETIME.duration=0.90 + self.MarshalCall.CLEAREDFORRECOVERY.duration=1.25 + self.MarshalCall.DECKCLOSED.duration=1.10 + self.MarshalCall.DEGREES.duration=0.60 + self.MarshalCall.EXPECTED.duration=0.55 + self.MarshalCall.FLYNEEDLES.duration=0.90 + self.MarshalCall.HOLDATANGELS.duration=1.10 + self.MarshalCall.HOURS.duration=0.60 + self.MarshalCall.MARSHALRADIAL.duration=1.10 + self.MarshalCall.N0.duration=0.40 + self.MarshalCall.N1.duration=0.25 + self.MarshalCall.N2.duration=0.37 + self.MarshalCall.N3.duration=0.37 + self.MarshalCall.N4.duration=0.39 + self.MarshalCall.N5.duration=0.39 + self.MarshalCall.N6.duration=0.40 + self.MarshalCall.N7.duration=0.40 + self.MarshalCall.N8.duration=0.37 + self.MarshalCall.N9.duration=0.40 + self.MarshalCall.NEGATIVE.duration=0.80 + self.MarshalCall.NEWFB.duration=1.35 + self.MarshalCall.OPS.duration=0.48 + self.MarshalCall.POINT.duration=0.33 + self.MarshalCall.RADIOCHECK.duration=1.20 + self.MarshalCall.RECOVERY.duration=0.70 + self.MarshalCall.RECOVERYOPSSTOPPED.duration=1.65 + self.MarshalCall.RECOVERYPAUSEDNOTICE.duration=2.9 -- Strangely the file is actually a shorter ~2.4 sec. + self.MarshalCall.RECOVERYPAUSEDRESUMED.duration=3.40 + self.MarshalCall.REPORTSEEME.duration=0.95 + self.MarshalCall.RESUMERECOVERY.duration=1.75 + self.MarshalCall.ROGER.duration=0.53 + self.MarshalCall.SAYNEEDLES.duration=0.90 + self.MarshalCall.STACKFULL.duration=6.35 + self.MarshalCall.STARTINGRECOVERY.duration=2.65 + +end + +--- Init voice over radio transmission call. +-- @param #AIRBOSS self +function AIRBOSS:_InitVoiceOvers() + + --------------- + -- LSO Radio -- + --------------- + + -- LSO Radio Calls. + self.LSOCall={ + BOLTER={ + file="LSO-BolterBolter", + suffix="ogg", + loud=false, + subtitle="Bolter, Bolter", + duration=0.75, + subduration=5, + }, + CALLTHEBALL={ + file="LSO-CallTheBall", + suffix="ogg", + loud=false, + subtitle="Call the ball", + duration=0.6, + subduration=2, + }, + CHECK={ + file="LSO-Check", + suffix="ogg", + loud=false, + subtitle="Check", + duration=0.45, + subduration=2.5, + }, + CLEAREDTOLAND={ + file="LSO-ClearedToLand", + suffix="ogg", + loud=false, + subtitle="Cleared to land", + duration=1.0, + subduration=5, + }, + COMELEFT={ + file="LSO-ComeLeft", + suffix="ogg", + loud=true, + subtitle="Come left", + duration=0.60, + subduration=1, + }, + RADIOCHECK={ + file="LSO-RadioCheck", + suffix="ogg", + loud=false, + subtitle="Paddles, radio check", + duration=1.1, + subduration=5, + }, + RIGHTFORLINEUP={ + file="LSO-RightForLineup", + suffix="ogg", + loud=true, + subtitle="Right for line up", + duration=0.80, + subduration=1, + }, + HIGH={ + file="LSO-High", + suffix="ogg", + loud=true, + subtitle="You're high", + duration=0.65, + subduration=1, + }, + LOW={ + file="LSO-Low", + suffix="ogg", + loud=true, + subtitle="You're low", + duration=0.50, + subduration=1, + }, + POWER={ + file="LSO-Power", + suffix="ogg", + loud=true, + subtitle="Power", + duration=0.50, --0.45 was too short + subduration=1, + }, + SLOW={ + file="LSO-Slow", + suffix="ogg", + loud=true, + subtitle="You're slow", + duration=0.65, + subduration=1, + }, + FAST={ + file="LSO-Fast", + suffix="ogg", + loud=true, + subtitle="You're fast", + duration=0.70, + subduration=1, + }, + ROGERBALL={ + file="LSO-RogerBall", + suffix="ogg", + loud=false, + subtitle="Roger ball", + duration=1.00, + subduration=2, + }, + WAVEOFF={ + file="LSO-WaveOff", + suffix="ogg", + loud=false, + subtitle="Wave off", + duration=0.6, + subduration=5, + }, + LONGINGROOVE={ + file="LSO-LongInTheGroove", + suffix="ogg", + loud=false, + subtitle="You're long in the groove", + duration=1.2, + subduration=5, + }, + FOULDECK={ + file="LSO-FoulDeck", + suffix="ogg", + loud=false, + subtitle="Foul deck", + duration=0.62, + subduration=5, + }, + DEPARTANDREENTER={ + file="LSO-DepartAndReenter", + suffix="ogg", + loud=false, + subtitle="Depart and re-enter", + duration=1.1, + subduration=5, + }, + PADDLESCONTACT={ + file="LSO-PaddlesContact", + suffix="ogg", + loud=false, + subtitle="Paddles, contact", + duration=1.0, + subduration=5, + }, + WELCOMEABOARD={ + file="LSO-WelcomeAboard", + suffix="ogg", + loud=false, + subtitle="Welcome aboard", + duration=1.0, + subduration=5, + }, + EXPECTHEAVYWAVEOFF={ + file="LSO-ExpectHeavyWaveoff", + suffix="ogg", + loud=false, + subtitle="Expect heavy waveoff", + duration=1.2, + subduration=5, + }, + EXPECTSPOT75={ + file="LSO-ExpectSpot75", + suffix="ogg", + loud=false, + subtitle="Expect spot 7.5", + duration=2.0, + subduration=5, + }, + STABILIZED={ + file="LSO-Stabilized", + suffix="ogg", + loud=false, + subtitle="Stabilized", + duration=0.9, + subduration=5, + }, + IDLE={ + file="LSO-Idle", + suffix="ogg", + loud=false, + subtitle="Idle", + duration=0.45, + subduration=5, + }, + N0={ + file="LSO-N0", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N1={ + file="LSO-N1", + suffix="ogg", + loud=false, + subtitle="", + duration=0.25, + }, + N2={ + file="LSO-N2", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N3={ + file="LSO-N3", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N4={ + file="LSO-N4", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N5={ + file="LSO-N5", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N6={ + file="LSO-N6", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N7={ + file="LSO-N7", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N8={ + file="LSO-N8", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N9={ + file="LSO-N9", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + CLICK={ + file="AIRBOSS-RadioClick", + suffix="ogg", + loud=false, + subtitle="", + duration=0.35, + }, + NOISE={ + file="AIRBOSS-Noise", + suffix="ogg", + loud=false, + subtitle="", + duration=3.6, + }, + SPINIT={ + file="AIRBOSS-SpinIt", + suffix="ogg", + loud=false, + subtitle="", + duration=0.73, + subduration=5, + }, + } + + ----------------- + -- Pilot Calls -- + ----------------- + + -- Pilot Radio Calls. + self.PilotCall={ + N0={ + file="PILOT-N0", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N1={ + file="PILOT-N1", + suffix="ogg", + loud=false, + subtitle="", + duration=0.25, + }, + N2={ + file="PILOT-N2", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N3={ + file="PILOT-N3", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N4={ + file="PILOT-N4", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N5={ + file="PILOT-N5", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N6={ + file="PILOT-N6", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N7={ + file="PILOT-N7", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N8={ + file="PILOT-N8", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N9={ + file="PILOT-N9", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + POINT={ + file="PILOT-Point", + suffix="ogg", + loud=false, + subtitle="", + duration=0.33, + }, + SKYHAWK={ + file="PILOT-Skyhawk", + suffix="ogg", + loud=false, + subtitle="", + duration=0.95, + subduration=5, + }, + HARRIER={ + file="PILOT-Harrier", + suffix="ogg", + loud=false, + subtitle="", + duration=0.58, + subduration=5, + }, + HAWKEYE={ + file="PILOT-Hawkeye", + suffix="ogg", + loud=false, + subtitle="", + duration=0.63, + subduration=5, + }, + TOMCAT={ + file="PILOT-Tomcat", + suffix="ogg", + loud=false, + subtitle="", + duration=0.66, + subduration=5, + }, + HORNET={ + file="PILOT-Hornet", + suffix="ogg", + loud=false, + subtitle="", + duration=0.56, + subduration=5, + }, + VIKING={ + file="PILOT-Viking", + suffix="ogg", + loud=false, + subtitle="", + duration=0.61, + subduration=5, + }, + BALL={ + file="PILOT-Ball", + suffix="ogg", + loud=false, + subtitle="", + duration=0.50, + subduration=5, + }, + BINGOFUEL={ + file="PILOT-BingoFuel", + suffix="ogg", + loud=false, + subtitle="", + duration=0.80, + }, + GASATDIVERT={ + file="PILOT-GasAtDivert", + suffix="ogg", + loud=false, + subtitle="", + duration=1.80, + }, + GASATTANKER={ + file="PILOT-GasAtTanker", + suffix="ogg", + loud=false, + subtitle="", + duration=1.95, + }, + } + + ------------------- + -- MARSHAL Radio -- + ------------------- + + -- MARSHAL Radio Calls. + self.MarshalCall={ + AFFIRMATIVE={ + file="MARSHAL-Affirmative", + suffix="ogg", + loud=false, + subtitle="", + duration=0.90, + }, + ALTIMETER={ + file="MARSHAL-Altimeter", + suffix="ogg", + loud=false, + subtitle="", + duration=0.85, + }, + BRC={ + file="MARSHAL-BRC", + suffix="ogg", + loud=false, + subtitle="", + duration=0.80, + }, + CARRIERTURNTOHEADING={ + file="MARSHAL-CarrierTurnToHeading", + suffix="ogg", + loud=false, + subtitle="", + duration=2.48, + subduration=5, + }, + CASE={ + file="MARSHAL-Case", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + CHARLIETIME={ + file="MARSHAL-CharlieTime", + suffix="ogg", + loud=false, + subtitle="", + duration=0.90, + }, + CLEAREDFORRECOVERY={ + file="MARSHAL-ClearedForRecovery", + suffix="ogg", + loud=false, + subtitle="", + duration=1.25, + }, + DECKCLOSED={ + file="MARSHAL-DeckClosed", + suffix="ogg", + loud=false, + subtitle="", + duration=1.10, + subduration=5, + }, + DEGREES={ + file="MARSHAL-Degrees", + suffix="ogg", + loud=false, + subtitle="", + duration=0.60, + }, + EXPECTED={ + file="MARSHAL-Expected", + suffix="ogg", + loud=false, + subtitle="", + duration=0.55, + }, + FLYNEEDLES={ + file="MARSHAL-FlyYourNeedles", + suffix="ogg", + loud=false, + subtitle="Fly your needles", + duration=0.9, + subduration=5, + }, + HOLDATANGELS={ + file="MARSHAL-HoldAtAngels", + suffix="ogg", + loud=false, + subtitle="", + duration=1.10, + }, + HOURS={ + file="MARSHAL-Hours", + suffix="ogg", + loud=false, + subtitle="", + duration=0.60, + subduration=5, + }, + MARSHALRADIAL={ + file="MARSHAL-MarshalRadial", + suffix="ogg", + loud=false, + subtitle="", + duration=1.10, + }, + N0={ + file="MARSHAL-N0", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N1={ + file="MARSHAL-N1", + suffix="ogg", + loud=false, + subtitle="", + duration=0.25, + }, + N2={ + file="MARSHAL-N2", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N3={ + file="MARSHAL-N3", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N4={ + file="MARSHAL-N4", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N5={ + file="MARSHAL-N5", + suffix="ogg", + loud=false, + subtitle="", + duration=0.39, + }, + N6={ + file="MARSHAL-N6", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N7={ + file="MARSHAL-N7", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + N8={ + file="MARSHAL-N8", + suffix="ogg", + loud=false, + subtitle="", + duration=0.37, + }, + N9={ + file="MARSHAL-N9", + suffix="ogg", + loud=false, + subtitle="", + duration=0.40, + }, + NEGATIVE={ + file="MARSHAL-Negative", + suffix="ogg", + loud=false, + subtitle="", + duration=0.80, + subduration=5, + }, + NEWFB={ + file="MARSHAL-NewFB", + suffix="ogg", + loud=false, + subtitle="", + duration=1.35, + }, + OPS={ + file="MARSHAL-Ops", + suffix="ogg", + loud=false, + subtitle="", + duration=0.48, + }, + POINT={ + file="MARSHAL-Point", + suffix="ogg", + loud=false, + subtitle="", + duration=0.33, + }, + RADIOCHECK={ + file="MARSHAL-RadioCheck", + suffix="ogg", + loud=false, + subtitle="Radio check", + duration=1.20, + subduration=5, + }, + RECOVERY={ + file="MARSHAL-Recovery", + suffix="ogg", + loud=false, + subtitle="", + duration=0.70, + subduration=5, + }, + RECOVERYOPSSTOPPED={ + file="MARSHAL-RecoveryOpsStopped", + suffix="ogg", + loud=false, + subtitle="", + duration=1.65, + subduration=5, + }, + RECOVERYPAUSEDNOTICE={ + file="MARSHAL-RecoveryPausedNotice", + suffix="ogg", + loud=false, + subtitle="aircraft recovery paused until further notice", + duration=2.90, + subduration=5, + }, + RECOVERYPAUSEDRESUMED={ + file="MARSHAL-RecoveryPausedResumed", + suffix="ogg", + loud=false, + subtitle="", + duration=3.40, + subduration=5, + }, + REPORTSEEME={ + file="MARSHAL-ReportSeeMe", + suffix="ogg", + loud=false, + subtitle="", + duration=0.95, + }, + RESUMERECOVERY={ + file="MARSHAL-ResumeRecovery", + suffix="ogg", + loud=false, + subtitle="resuming aircraft recovery", + duration=1.75, + subduraction=5, + }, + ROGER={ + file="MARSHAL-Roger", + suffix="ogg", + loud=false, + subtitle="", + duration=0.53, + subduration=5, + }, + SAYNEEDLES={ + file="MARSHAL-SayNeedles", + suffix="ogg", + loud=false, + subtitle="Say needles", + duration=0.90, + subduration=5, + }, + STACKFULL={ + file="MARSHAL-StackFull", + suffix="ogg", + loud=false, + subtitle="Marshal Stack is currently full. Hold outside 10 NM zone and wait for further instructions", + duration=6.35, + subduration=10, + }, + STARTINGRECOVERY={ + file="MARSHAL-StartingRecovery", + suffix="ogg", + loud=false, + subtitle="", + duration=2.65, + subduration=5, + }, + CLICK={ + file="AIRBOSS-RadioClick", + suffix="ogg", + loud=false, + subtitle="", + duration=0.35, + }, + NOISE={ + file="AIRBOSS-Noise", + suffix="ogg", + loud=false, + subtitle="", + duration=3.6, + }, + } + + -- Default timings by Raynor + self:SetVoiceOversLSOByRaynor() + self:SetVoiceOversMarshalByRaynor() + +end + +--- Init voice over radio transmission call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.RadioCall radiocall LSO or Marshal radio call object. +-- @param #number duration Duration of the voice over in seconds. +-- @param #string subtitle (Optional) Subtitle to be displayed along with voice over. +-- @param #number subduration (Optional) Duration how long the subtitle is displayed. +-- @param #string filename (Optional) Name of the voice over sound file. +-- @param #string suffix (Optional) Extention of file. Default ".ogg". +function AIRBOSS:SetVoiceOver(radiocall, duration, subtitle, subduration, filename, suffix) + radiocall.duration=duration + radiocall.subtitle=subtitle or radiocall.subtitle + radiocall.file=filename + radiocall.suffix=suffix or ".ogg" +end + +--- Get optimal aircraft AoA parameters.. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #AIRBOSS.AircraftAoA AoA parameters for the given aircraft type. +function AIRBOSS:_GetAircraftAoA(playerData) + + -- Get AC type. + local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET + local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC + local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B + local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B + + -- Table with AoA values. + local aoa={} -- #AIRBOSS.AircraftAoA + + if hornet then + -- F/A-18C Hornet parameters. + aoa.SLOW = 9.8 + aoa.Slow = 9.3 + aoa.OnSpeedMax = 8.8 + aoa.OnSpeed = 8.1 + aoa.OnSpeedMin = 7.4 + aoa.Fast = 6.9 + aoa.FAST = 6.3 + elseif tomcat then + -- F-14A/B Tomcat parameters (taken from NATOPS). Converted from units 0-30 to degrees. + -- Currently assuming a linear relationship with 0=-10 degrees and 30=+40 degrees as stated in NATOPS. + aoa.SLOW = self:_AoAUnit2Deg(playerData, 17.0) --18.33 --17.0 units + aoa.Slow = self:_AoAUnit2Deg(playerData, 16.0) --16.67 --16.0 units + aoa.OnSpeedMax = self:_AoAUnit2Deg(playerData, 15.5) --15.83 --15.5 units + aoa.OnSpeed = self:_AoAUnit2Deg(playerData, 15.0) --15.0 --15.0 units + aoa.OnSpeedMin = self:_AoAUnit2Deg(playerData, 14.5) --14.17 --14.5 units + aoa.Fast = self:_AoAUnit2Deg(playerData, 14.0) --13.33 --14.0 units + aoa.FAST = self:_AoAUnit2Deg(playerData, 13.0) --11.67 --13.0 units + elseif skyhawk then + -- A-4E-C Skyhawk parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390 + -- Note that these are arbitrary UNITS and not degrees. We need a conversion formula! + -- Github repo suggests they simply use a factor of two to get from degrees to units. + aoa.SLOW = 9.50 --=19.0/2 + aoa.Slow = 9.25 --=18.5/2 + aoa.OnSpeedMax = 9.00 --=18.0/2 + aoa.OnSpeed = 8.75 --=17.5/2 8.1 + aoa.OnSpeedMin = 8.50 --=17.0/2 + aoa.Fast = 8.25 --=17.5/2 + aoa.FAST = 8.00 --=16.5/2 + elseif harrier then + -- AV-8B Harrier parameters. This might need further tuning. + aoa.SLOW = 14.0 + aoa.Slow = 13.0 + aoa.OnSpeedMax = 12.0 + aoa.OnSpeed = 11.0 + aoa.OnSpeedMin = 10.0 + aoa.Fast = 9.0 + aoa.FAST = 8.0 + end + + return aoa +end + +--- Convert AoA from arbitrary units to degrees. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number aoaunits AoA in arbitrary units. +-- @return #number AoA in degrees. +function AIRBOSS:_AoAUnit2Deg(playerData, aoaunits) + + -- Init. + local degrees=aoaunits + + -- Check aircraft type of player. + if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + + ------------- + -- F-14A/B -- + ------------- + + -- NATOPS: + -- unit=0 ==> alpha=-10 degrees. + -- unit=30 ==> alpha=+40 degrees. + + -- Assuming a linear relationship between these to points of the graph. + -- However: AoA=15 Units ==> 15 degrees, which is too much. + degrees=-10+50/30*aoaunits + + -- HB Facebook page https://www.facebook.com/heatblur/photos/a.683612385159716/754368278084126 + -- AoA=15 Units <==> AoA=10.359 degrees. + degrees=0.918*aoaunits-3.411 + + elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + + ---------- + -- A-4E -- + ---------- + + -- A-4E-C source code suggests a simple factor of 1/2 for conversion. + degrees=0.5*aoaunits + + end + + return degrees +end + +--- Convert AoA from degrees to arbitrary units. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number degrees AoA in degrees. +-- @return #number AoA in arbitrary units. +function AIRBOSS:_AoADeg2Units(playerData, degrees) + + -- Init. + local aoaunits=degrees + + -- Check aircraft type of player. + if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + + ------------- + -- F-14A/B -- + ------------- + + -- NATOPS: + -- unit=0 ==> alpha=-10 degrees. + -- unit=30 ==> alpha=+40 degrees. + + -- Assuming a linear relationship between these to points of the graph. + aoaunits=(degrees+10)*30/50 + + -- HB Facebook page https://www.facebook.com/heatblur/photos/a.683612385159716/754368278084126 + -- AoA=15 Units <==> AoA=10.359 degrees. + aoaunits=1.089*degrees+3.715 + + elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + + ---------- + -- A-4E -- + ---------- + + -- A-4E source code suggests a simple factor of two as conversion. + aoaunits=2*degrees + + end + + return aoaunits +end + +--- Get optimal aircraft flight parameters at checkpoint. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #string step Pattern step. +-- @return #number Altitude in meters or nil. +-- @return #number Angle of Attack or nil. +-- @return #number Distance to carrier in meters or nil. +-- @return #number Speed in m/s or nil. +function AIRBOSS:_GetAircraftParameters(playerData, step) + + -- Get parameters depended on step. + step=step or playerData.step + + -- Get AC type. + local hornet=playerData.actype==AIRBOSS.AircraftCarrier.HORNET + local skyhawk=playerData.actype==AIRBOSS.AircraftCarrier.A4EC + local tomcat=playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B + local harrier=playerData.actype==AIRBOSS.AircraftCarrier.AV8B + + -- Return values. + local alt + local aoa + local dist + local speed + + -- Aircraft specific AoA. + local aoaac=self:_GetAircraftAoA(playerData) + + if step==AIRBOSS.PatternStep.PLATFORM then + + alt=UTILS.FeetToMeters(5000) + + --dist=UTILS.NMToMeters(20) + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.ARCIN then + + if tomcat then + speed=UTILS.KnotsToMps(150) + else + speed=UTILS.KnotsToMps(250) + end + + elseif step==AIRBOSS.PatternStep.ARCOUT then + + if tomcat then + speed=UTILS.KnotsToMps(150) + else + speed=UTILS.KnotsToMps(250) + end + + elseif step==AIRBOSS.PatternStep.DIRTYUP then + + alt=UTILS.FeetToMeters(1200) + + --speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.BULLSEYE then + + alt=UTILS.FeetToMeters(1200) + + dist=-UTILS.NMToMeters(3) + + aoa=aoaac.OnSpeed + + elseif step==AIRBOSS.PatternStep.INITIAL then + + if hornet or tomcat or harrier then + alt=UTILS.FeetToMeters(800) + speed=UTILS.KnotsToMps(350) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + speed=UTILS.KnotsToMps(250) + end + + elseif step==AIRBOSS.PatternStep.BREAKENTRY then + + if hornet or tomcat or harrier then + alt=UTILS.FeetToMeters(800) + speed=UTILS.KnotsToMps(350) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + speed=UTILS.KnotsToMps(250) + end + + elseif step==AIRBOSS.PatternStep.EARLYBREAK then + + if hornet or tomcat or harrier then + alt=UTILS.FeetToMeters(800) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + end + + elseif step==AIRBOSS.PatternStep.LATEBREAK then + + if hornet or tomcat or harrier then + alt=UTILS.FeetToMeters(800) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + end + + elseif step==AIRBOSS.PatternStep.ABEAM then + + if hornet or tomcat or harrier then + alt=UTILS.FeetToMeters(600) + elseif skyhawk then + alt=UTILS.FeetToMeters(500) + end + + aoa=aoaac.OnSpeed + + if harrier then + -- 0.8 to 1.0 NM + dist=UTILS.NMToMeters(0.9) + else + dist=UTILS.NMToMeters(1.2) + end + + elseif step==AIRBOSS.PatternStep.NINETY then + + if hornet or tomcat then + alt=UTILS.FeetToMeters(500) + elseif skyhawk then + alt=UTILS.FeetToMeters(500) + elseif harrier then + alt=UTILS.FeetToMeters(425) + end + + aoa=aoaac.OnSpeed + + elseif step==AIRBOSS.PatternStep.WAKE then + + if hornet then + alt=UTILS.FeetToMeters(370) + elseif tomcat then + alt=UTILS.FeetToMeters(430) -- Tomcat should be a bit higher as it intercepts the GS a bit higher. + elseif skyhawk then + alt=UTILS.FeetToMeters(370) --? + end + -- Harrier wont get into wake pos. Runway is not angled and it stays port. + + aoa=aoaac.OnSpeed + + elseif step==AIRBOSS.PatternStep.FINAL then + + if hornet then + alt=UTILS.FeetToMeters(300) + elseif tomcat then + alt=UTILS.FeetToMeters(360) + elseif skyhawk then + alt=UTILS.FeetToMeters(300) --? + elseif harrier then + -- 300-325 ft + alt=UTILS.FeetToMeters(300) + end + + aoa=aoaac.OnSpeed + + end + + return alt, aoa, dist, speed +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- QUEUE Functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get next marshal flight which is ready to enter the landing pattern. +-- @param #AIRBOSS self +-- @return #AIRBOSS.FlightGroup Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready. +function AIRBOSS:_GetNextMarshalFight() + + -- Loop over all marshal flights. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Current stack. + local stack=flight.flag + + -- Total marshal time in seconds. + local Tmarshal=timer.getAbsTime()-flight.time + + -- Min time in marshal stack. + local TmarshalMin=2*60 --Two minutes for human players. + if flight.ai then + TmarshalMin=3*60 -- Three minutes for AI. + end + + -- Check if conditions are right. + if flight.holding~=nil and Tmarshal>=TmarshalMin then + if flight.case==1 and stack==1 or flight.case>1 then + if flight.ai then + -- Return AI flight. + return flight + else + -- Check for human player if they are already commencing. + if flight.step~=AIRBOSS.PatternStep.COMMENCING then + return flight + end + end + end + end + end + + return nil +end + +--- Check marshal and pattern queues. +-- @param #AIRBOSS self +function AIRBOSS:_CheckQueue() + + -- Print queues. + if self.Debug then + self:_PrintQueue(self.flights, "All Flights") + end + self:_PrintQueue(self.Qmarshal, "Marshal") + self:_PrintQueue(self.Qpattern, "Pattern") + self:_PrintQueue(self.Qwaiting, "Waiting") + self:_PrintQueue(self.Qspinning, "Spinning") + + -- If flights are waiting outside 10 NM zone and carrier switches from Case I to Case II/III, they should be added to the Marshal stack as now there is no stack limit any more. + if self.case>1 then + for _,_flight in pairs(self.Qwaiting) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Remove flight from waiting queue. + local removed=self:_RemoveFlightFromQueue(self.Qwaiting, flight) + + if removed then + + -- Get free stack + local stack=self:_GetFreeStack(flight.ai) + + -- Debug info. + self:T(self.lid..string.format("Moving flight %s onboard %s from Waiting queue to Case %d Marshal stack %d", flight.groupname, flight.onboard, self.case, stack)) + + -- Send flight to marshal stack. + if flight.ai then + self:_MarshalAI(flight, stack) + else + self:_MarshalPlayer(flight, stack) + end + + -- Break the loop so that only one flight per 30 seconds is removed. + break + end + + end + end + + -- Check if carrier is currently in recovery mode. + if not self:IsRecovering() then + + ----------------------------- + -- Switching Recovery Case -- + ----------------------------- + + -- Loop over all flights currently in the marshal queue. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- TODO: In principle this should be done/necessary only if case 1-->2/3 or 2/3-->1, right? + -- When recovery switches from 2->3 or 3-->2 nothing changes in the marshal stack. + + -- Check if a change of stack is necessary. + if (flight.case==1 and self.case>1) or (flight.case>1 and self.case==1) then + + -- Remove flight from marshal queue. + local removed=self:_RemoveFlightFromQueue(self.Qmarshal, flight) + + if removed then + + -- Get free stack + local stack=self:_GetFreeStack(flight.ai) + + -- Debug output. + self:T(self.lid..string.format("Moving flight %s onboard %s from Marshal Case %d ==> %d Marshal stack %d", flight.groupname, flight.onboard, flight.case, self.case, stack)) + + -- Send flight to marshal queue. + if flight.ai then + self:_MarshalAI(flight, stack) + else + self:_MarshalPlayer(flight, stack) + end + + -- Break the loop so that only one flight per 30 seconds is removed. No spam of messages, no conflict with the loop over queue entries. + break + + elseif flight.case~=self.case then + + -- This should handle 2-->3 or 3-->2 + flight.case=self.case + + end + + end + end + + -- Not recovering ==> skip the rest! + return + end + + -- Get number of airborne aircraft units(!) currently in pattern. + local _,npattern=self:_GetQueueInfo(self.Qpattern) + + -- Get number of aircraft units spinning. + local _,nspinning=self:_GetQueueInfo(self.Qspinning) + + -- Get next marshal flight. + local marshalflight=self:_GetNextMarshalFight() + + -- Check if there are flights waiting in the Marshal stack and if the pattern is free. No one should be spinning. + if marshalflight and npattern0 then + + -- Last flight group send to pattern. + local patternflight=self.Qpattern[#self.Qpattern] --#AIRBOSS.FlightGroup + + -- Recovery case of pattern flight. + pcase=patternflight.case + + -- Number of airborne aircraft in this group. Count includes section members. + local npunits=self:_GetFlightUnits(patternflight, false) + + -- Get time in pattern. + Tpattern=timer.getAbsTime()-patternflight.time + self:T(self.lid..string.format("Pattern time of last group %s = %d seconds. # of units=%d.", patternflight.groupname, Tpattern, npunits)) + end + + -- Min time in pattern before next aircraft is allowed. + local TpatternMin + if pcase==1 then + TpatternMin=2*60*npunits --45*npunits -- 45 seconds interval per plane! + else + TpatternMin=2*60*npunits --120*npunits -- 120 seconds interval per plane! + end + + -- Check interval to last pattern flight. + if Tpattern>TpatternMin then + self:T(self.lid..string.format("Sending marshal flight %s to pattern.", marshalflight.groupname)) + self:_ClearForLanding(marshalflight) + end + + end +end + + +--- Clear flight for landing. AI are removed from Marshal queue and the Marshal stack is collapsed. +-- If next in line is an AI flight, this is done. If human player is next, we wait for "Commence" via F10 radio menu command. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight to go to pattern. +function AIRBOSS:_ClearForLanding(flight) + + -- Check if flight is AI or human. If AI, we collapse the stack and commence. If human, we suggest to commence. + if flight.ai then + + -- Collapse stack and send AI to pattern. + self:_RemoveFlightFromMarshalQueue(flight, false) + self:_LandAI(flight) + + -- Cleared for Case X recovery. + self:_MarshalCallClearedForRecovery(flight.onboard, flight.case) + + else + + -- Cleared for Case X recovery. + if flight.step~=AIRBOSS.PatternStep.COMMENCING then + self:_MarshalCallClearedForRecovery(flight.onboard, flight.case) + flight.time=timer.getAbsTime() + end + + -- Set step to commencing. This will trigger the zone check until the player is in the right place. + self:_SetPlayerStep(flight, AIRBOSS.PatternStep.COMMENCING, 3) + + end + +end + +--- Set player step. Any warning is erased and next step hint shown. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string step Next step. +-- @param #number delay (Optional) Set set after a delay in seconds. +function AIRBOSS:_SetPlayerStep(playerData, step, delay) + + if delay and delay>0 then + -- Delayed call. + --SCHEDULER:New(nil, self._SetPlayerStep, {self, playerData, step}, delay) + self:ScheduleOnce(delay, self._SetPlayerStep, self, playerData, step) + else + + -- Check if player still exists after possible delay. + if playerData then + + -- Set player step. + playerData.step=step + + -- Erase warning. + playerData.warning=nil + + -- Next step hint. + self:_StepHint(playerData) + end + + end + +end + +--- Scan carrier zone for (new) units. +-- @param #AIRBOSS self +function AIRBOSS:_ScanCarrierZone() + + -- Carrier position. + local coord=self:GetCoordinate() + + -- Scan radius = radius of the CCA. + local RCCZ=self.zoneCCA:GetRadius() + + -- Debug info. + self:T(self.lid..string.format("Scanning Carrier Controlled Area. Radius=%.1f NM.", UTILS.MetersToNM(RCCZ))) + + -- Scan units in carrier zone. + local _,_,_,unitscan=coord:ScanObjects(RCCZ, true, false, false) + + + -- Make a table with all groups currently in the CCA zone. + local insideCCA={} + for _,_unit in pairs(unitscan) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Necessary conditions to be met: + local airborne=unit:IsAir() --and unit:InAir() + local inzone=unit:IsInZone(self.zoneCCA) + local friendly=self:GetCoalition()==unit:GetCoalition() + local carrierac=self:_IsCarrierAircraft(unit) + + -- Check if this an aircraft and that it is airborne and closing in. + if airborne and inzone and friendly and carrierac then + + local group=unit:GetGroup() + local groupname=group:GetName() + + if insideCCA[groupname]==nil then + insideCCA[groupname]=group + end + + end + end + + -- Find new flights that are inside CCA. + for groupname,_group in pairs(insideCCA) do + local group=_group --Wrapper.Group#GROUP + + -- Get flight group if possible. + local knownflight=self:_GetFlightFromGroupInQueue(group, self.flights) + + -- Get aircraft type name. + local actype=group:GetTypeName() + + -- Create a new flight group + if knownflight then + + -- Debug output. + self:T2(self.lid..string.format("Known flight group %s of type %s in CCA.", groupname, actype)) + + -- Check if flight is AI and if we want to handle it at all. + if knownflight.ai and self.handleai then + + -- Defines if AI group should be handled by the airboss. + local iscarriersquad=true + + -- Check if AI group is part of the group set if a set was defined. + if self.squadsetAI then + local group=self.squadsetAI:FindGroup(groupname) + if group then + iscarriersquad=true + else + iscarriersquad=false + end + end + + -- Check if group was explicitly excluded. + if self.excludesetAI then + local group=self.excludesetAI:FindGroup(groupname) + if group then + iscarriersquad=false + end + end + + + -- Get distance to carrier. + local dist=knownflight.group:GetCoordinate():Get2DDistance(self:GetCoordinate()) + + -- Close in distance. Is >0 if AC comes closer wrt to first detected distance d0. + local closein=knownflight.dist0-dist + + -- Debug info. + self:T3(self.lid..string.format("Known AI flight group %s closed in by %.1f NM", knownflight.groupname, UTILS.MetersToNM(closein))) + + -- Is this group the tanker? + local istanker=self.tanker and self.tanker.tanker:GetName()==groupname + + -- Is this group the AWACS? + local isawacs=self.awacs and self.awacs.tanker:GetName()==groupname + + -- Send tanker to marshal stack? + local tanker2marshal = istanker and self.tanker:IsReturning() and self.tanker.airbase:GetName()==self.airbase:GetName() and knownflight.flag==-100 and self.tanker.recovery==true + + -- Send AWACS to marhsal stack? + local awacs2marshal = isawacs and self.awacs:IsReturning() and self.awacs.airbase:GetName()==self.airbase:GetName() and knownflight.flag==-100 and self.awacs.recovery==true + + -- Put flight into Marshal. + local putintomarshal=closein>UTILS.NMToMeters(5) and knownflight.flag==-100 and iscarriersquad and (not istanker) and (not isawacs) + + -- Send AI flight to marshal stack if group closes in more than 5 and has initial flag value. + if putintomarshal or tanker2marshal or awacs2marshal then + + -- Get the next free stack for current recovery case. + local stack=self:_GetFreeStack(knownflight.ai) + + -- Repawn. + local respawn=self.respawnAI --or tanker2marshal + + if stack then + + -- Send AI to marshal stack. We respawn the group to clean possible departure and destination airbases. + self:_MarshalAI(knownflight, stack, respawn) + + else + + -- Send AI to orbit outside 10 NM zone and wait until the next Marshal stack is available. + if not self:_InQueue(self.Qwaiting, knownflight.group) then + self:_WaitAI(knownflight, respawn) -- Group is respawned to clear any attached airfields. + end + + end + + -- Break the loop to not have all flights at once! Spams the message screen. + break + + end -- Closed in or tanker/AWACS + end -- AI + + else + + -- Unknown new AI flight. Create a new flight group. + if not self:_IsHuman(group) then + self:_CreateFlightGroup(group) + end + + end + + end + + -- Find flights that are not in CCA. + local remove={} + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + if insideCCA[flight.groupname]==nil then + -- Do not remove flights in marshal pattern. At least for case 2 & 3. If zone is set small, they might be outside in the holding pattern. + if flight.ai and not (self:_InQueue(self.Qmarshal, flight.group) or self:_InQueue(self.Qpattern, flight.group)) then + table.insert(remove, flight) + end + end + end + + -- Remove flight groups outside CCA. + for _,flight in pairs(remove) do + self:_RemoveFlightFromQueue(self.flights, flight) + end + +end + +--- Tell player to wait outside the 10 NM zone until a Marshal stack is available. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_WaitPlayer(playerData) + + -- Check if flight is known to the airboss already. + if playerData then + + -- Number of waiting flights + local nwaiting=#self.Qwaiting + + -- Radio message: Stack is full. + self:_MarshalCallStackFull(playerData.onboard, nwaiting) + + -- Add player flight to waiting queue. + table.insert(self.Qwaiting, playerData) + + -- Set time stamp. + playerData.time=timer.getAbsTime() + + -- Set step to waiting. + playerData.step=AIRBOSS.PatternStep.WAITING + playerData.warning=nil + + -- Set all flights in section to waiting. + for _,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + flight.step=AIRBOSS.PatternStep.WAITING + flight.time=timer.getAbsTime() + flight.warning=nil + end + + end + +end + + +--- Orbit at a specified position at a specified altitude with a specified speed. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #number stack The Marshal stack the player gets. +function AIRBOSS:_MarshalPlayer(playerData, stack) + + -- Check if flight is known to the airboss already. + if playerData then + + -- Add group to marshal stack. + self:_AddMarshalGroup(playerData, stack) + + -- Set step to holding. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.HOLDING) + + -- Holding switch to nil until player arrives in the holding zone. + playerData.holding=nil + + -- Set same stack for all flights in section. + for _,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + + -- XXX: Inform player? Should be done by lead via radio? + + -- Set step. + self:_SetPlayerStep(flight, AIRBOSS.PatternStep.HOLDING) + + -- Holding to nil, until arrived. + flight.holding=nil + + -- Set case to that of lead. + flight.case=playerData.case + + -- Set stack flag. + flight.flag=stack + + -- Trigger Marshal event. + self:Marshal(flight) + end + + else + self:E(self.lid.."ERROR: Could not add player to Marshal stack! playerData=nil") + end + +end + +--- Command AI flight to orbit outside the 10 NM zone and wait for a free Marshal stack. +-- If the flight is not already holding in the Marshal stack, it is guided there first. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +-- @param #boolean respawn If true respawn the group. Otherwise reset the mission task with new waypoints. +function AIRBOSS:_WaitAI(flight, respawn) + + -- Set flag to something other than -100 and <0 + flight.flag=-99 + + -- Add AI flight to waiting queue. + table.insert(self.Qwaiting, flight) + + -- Flight group name. + local group=flight.group + local groupname=flight.groupname + + -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.) + local speedOrbitMps=UTILS.KnotsToMps(274) + + -- Orbit speed in km/h for waypoints. + local speedOrbitKmh=UTILS.KnotsToKmph(274) + + -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.) + local speedTransit=UTILS.KnotsToKmph(370) + + -- Carrier coordinate + local cv=self:GetCoordinate() + + -- Coordinate of flight group + local fc=group:GetCoordinate() + + -- Carrier heading + local hdg=self:GetHeading(false) + + -- Heading from carrier to flight group + local hdgto=cv:HeadingTo(fc) + + -- Holding alitude between angels 6 and 10 (random). + local angels=math.random(6,10) + local altitude=UTILS.FeetToMeters(angels*1000) + + -- Point outsize 10 NM zone of the carrier. + local p0=cv:Translate(UTILS.NMToMeters(11), hdgto):Translate(UTILS.NMToMeters(5), hdg):SetAltitude(altitude) + + -- Waypoints array to be filled depending on case etc. + local wp={} + + -- Current position. Always good for as the first waypoint. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedTransit, {}, "Current Position") + + -- Set orbit task. + local taskorbit=group:TaskOrbit(p0, altitude, speedOrbitMps) + + -- Orbit at waypoint. + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedOrbitKmh, {taskorbit}, string.format("Waiting Orbit at Angels %d", angels)) + + -- Debug markers. + if self.Debug then + p0:MarkToAll(string.format("Waiting Orbit of flight %s at Angels %s", groupname, angels)) + end + + if respawn then + + -- This should clear the landing waypoints. + -- Note: This resets the weapons and the fuel state. But not the units fortunately. + + -- Get group template. + local Template=group:GetTemplate() + + -- Set route points. + Template.route.points=wp + + -- Respawn the group. + group=group:Respawn(Template, true) + + end + + -- Reinit waypoints. + group:WayPointInitialize(wp) + + -- Route group. + group:Route(wp, 1) + +end + +--- Command AI flight to orbit at a specified position at a specified altitude with a specified speed. If flight is not in the Marshal queue yet, it is added. This fixes the recovery case. +-- If the flight is not already holding in the Marshal stack, it is guided there first. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +-- @param #number nstack Stack number of group. Can also be the current stack if AI position needs to be updated wrt to changed carrier position. +-- @param #boolean respawn If true, respawn the flight otherwise update mission task with new waypoints. +function AIRBOSS:_MarshalAI(flight, nstack, respawn) + self:F2({flight=flight, nstack=nstack, respawn=respawn}) + + -- Nil check. + if flight==nil or flight.group==nil then + self:E(self.lid.."ERROR: flight or flight.group is nil.") + return + end + + -- Nil check. + if flight.group:GetCoordinate()==nil then + self:E(self.lid.."ERROR: cannot get coordinate of flight group.") + return + end + + -- Check if flight is already in Marshal queue. + if not self:_InQueue(self.Qmarshal,flight.group) then + -- Add group to marshal stack queue. + self:_AddMarshalGroup(flight, nstack) + end + + -- Explode unit for testing. Worked! + --local u1=flight.group:GetUnit(1) --Wrapper.Unit#UNIT + --u1:Explode(500, 10) + + -- Recovery case. + local case=flight.case + + -- Get old/current stack. + local ostack=flight.flag + + -- Flight group name. + local group=flight.group + local groupname=flight.groupname + + -- Set new stack. + flight.flag=nstack + + -- Current carrier position. + local Carrier=self:GetCoordinate() + + -- Carrier heading. + local hdg=self:GetHeading() + + -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.) + local speedOrbitMps=UTILS.KnotsToMps(274) + + -- Orbit speed in km/h for waypoints. + local speedOrbitKmh=UTILS.KnotsToKmph(274) + + -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.) + local speedTransit=UTILS.KnotsToKmph(370) + + local altitude + local p0 --Core.Point#COORDINATE + local p1 --Core.Point#COORDINATE + local p2 --Core.Point#COORDINATE + + -- Get altitude and positions. + altitude, p1, p2=self:_GetMarshalAltitude(nstack, case) + + -- Waypoints array to be filled depending on case etc. + local wp={} + + -- If flight has not arrived in the holding zone, we guide it there. + if not flight.holding then + + ---------------------- + -- Route to Holding -- + ---------------------- + + -- Debug info. + self:T(self.lid..string.format("Guiding AI flight %s to marshal stack %d-->%d.", groupname, ostack, nstack)) + + -- Current position. Always good for as the first waypoint. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedTransit, {}, "Current Position") + + -- Task function when arriving at the holding zone. This will set flight.holding=true. + local TaskArrivedHolding=flight.group:TaskFunction("AIRBOSS._ReachedHoldingZone", self, flight) + + -- Select case. + if case==1 then + + -- Initial point 7 NM and a bit port of carrier. + local pE=Carrier:Translate(UTILS.NMToMeters(7), hdg-30):SetAltitude(altitude) + + -- Entry point 5 NM port and slightly astern the boat. + p0=Carrier:Translate(UTILS.NMToMeters(5), hdg-135):SetAltitude(altitude) + + -- Waypoint ahead of carrier's holding zone. + wp[#wp+1]=pE:WaypointAirTurningPoint(nil, speedTransit, {TaskArrivedHolding}, "Entering Case I Marshal Pattern") + + else + + -- Get correct radial depending on recovery case including offset. + local radial=self:GetRadial(case, false, true) + + -- Point in the middle of the race track and a 5 NM more port perpendicular. + p0=p2:Translate(UTILS.NMToMeters(5), radial+90):Translate(UTILS.NMToMeters(5), radial, true) + + -- Entering Case II/III marshal pattern waypoint. + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedTransit, {TaskArrivedHolding}, "Entering Case II/III Marshal Pattern") + + end + + else + + ------------------------ + -- In Marshal Pattern -- + ------------------------ + + -- Debug info. + self:T(self.lid..string.format("Updating AI flight %s at marshal stack %d-->%d.", groupname, ostack, nstack)) + + -- Current position. Speed expected in km/h. + wp[1]=group:GetCoordinate():WaypointAirTurningPoint(nil, speedOrbitKmh, {}, "Current Position") + + -- Create new waypoint 0.2 Nm ahead of current positon. + p0=group:GetCoordinate():Translate(UTILS.NMToMeters(0.2), group:GetHeading(), true) + + end + + -- Set orbit task. + local taskorbit=group:TaskOrbit(p1, altitude, speedOrbitMps, p2) + + -- Orbit at waypoint. + wp[#wp+1]=p0:WaypointAirTurningPoint(nil, speedOrbitKmh, {taskorbit}, string.format("Marshal Orbit Stack %d", nstack)) + + -- Debug markers. + if self.Debug then + p0:MarkToAll("WP P0 "..groupname) + p1:MarkToAll("RT P1 "..groupname) + p2:MarkToAll("RT P2 "..groupname) + end + + if respawn then + + -- This should clear the landing waypoints. + -- Note: This resets the weapons and the fuel state. But not the units fortunately. + + -- Get group template. + local Template=group:GetTemplate() + + -- Set route points. + Template.route.points=wp + + -- Respawn the group. + flight.group=group:Respawn(Template, true) + + end + + -- Reinit waypoints. + flight.group:WayPointInitialize(wp) + + -- Route group. + flight.group:Route(wp, 1) + + -- Trigger Marshal event. + self:Marshal(flight) + +end + +--- Tell AI to refuel. Either at the recovery tanker or at the nearest divert airfield. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +function AIRBOSS:_RefuelAI(flight) + + -- Waypoints array. + local wp={} + + -- Current speed. + local CurrentSpeed=flight.group:GetVelocityKMH() + + -- Current positon. + wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, CurrentSpeed, {}, "Current position") + + -- Check if aircraft can be refueled. + -- TODO: This should also depend on the tanker type AC. + local refuelac=false + local actype=flight.group:GetTypeName() + if actype==AIRBOSS.AircraftCarrier.AV8B or + actype==AIRBOSS.AircraftCarrier.F14A or + actype==AIRBOSS.AircraftCarrier.F14B or + actype==AIRBOSS.AircraftCarrier.F14A_AI or + actype==AIRBOSS.AircraftCarrier.HORNET or + actype==AIRBOSS.AircraftCarrier.FA18C or + actype==AIRBOSS.AircraftCarrier.S3B or + actype==AIRBOSS.AircraftCarrier.S3BTANKER then + refuelac=true + end + + -- Message. + local text="" + + -- Refuel or divert? + if self.tanker and refuelac then + + -- Current Tanker position. + local tankerpos=self.tanker.tanker:GetCoordinate() + + -- Task refueling. + local TaskRefuel=flight.group:TaskRefueling() + + -- Task to go back to Marshal. + local TaskMarshal=flight.group:TaskFunction("AIRBOSS._TaskFunctionMarshalAI", self, flight) + + -- Waypoint with tasks. + wp[#wp+1]=tankerpos:WaypointAirTurningPoint(nil, CurrentSpeed, {TaskRefuel, TaskMarshal}, "Refueling") + + -- Marshal Message. + self:_MarshalCallGasAtTanker(flight.onboard) + + else + + ------------------------------ + -- Guide AI to divert field -- + ------------------------------ + + -- Closest Airfield of the coaliton. + local divertfield=self:GetCoordinate():GetClosestAirbase(Airbase.Category.AIRDROME, self:GetCoalition()) + + -- Handle case where there is no divert field of the own coalition and try neutral instead. + if divertfield==nil then + divertfield=self:GetCoordinate():GetClosestAirbase(Airbase.Category.AIRDROME, 0) + end + + if divertfield then + + -- Coordinate. + local divertcoord=divertfield:GetCoordinate() + + -- Landing waypoint. + wp[#wp+1]=divertcoord:WaypointAirLanding(UTILS.KnotsToKmph(300), divertfield, {}, "Divert Field") + + -- Marshal Message. + self:_MarshalCallGasAtDivert(flight.onboard, divertfield:GetName()) + + -- Respawn! + + -- Get group template. + local Template=flight.group:GetTemplate() + + -- Set route points. + Template.route.points=wp + + -- Respawn the group. + flight.group=flight.group:Respawn(Template, true) + + else + -- Set flight to refueling so this is not called again. + self:E(self.lid..string.format("WARNING: No recovery tanker or divert field available for group %s.", flight.groupname)) + flight.refueling=true + return + end + + end + + -- Reinit waypoints. + flight.group:WayPointInitialize(wp) + + -- Route group. + flight.group:Route(wp, 1) + + -- Set refueling switch. + flight.refueling=true + +end + +--- Tell AI to land on the carrier. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +function AIRBOSS:_LandAI(flight) + + -- Debug info. + self:T(self.lid..string.format("Landing AI flight %s.", flight.groupname)) + + -- NOTE: Looks like the AI needs to approach at the "correct" speed. If they are too fast, they fly an unnecessary circle to bleed of speed first. + -- Unfortunately, the correct speed depends on the aircraft type! + + -- Aircraft speed when flying the pattern. + local Speed=UTILS.KnotsToKmph(200) + + if flight.actype==AIRBOSS.AircraftCarrier.HORNET or flight.actype==AIRBOSS.AircraftCarrier.FA18C then + Speed=UTILS.KnotsToKmph(200) + elseif flight.actype==AIRBOSS.AircraftCarrier.E2D then + Speed=UTILS.KnotsToKmph(150) + elseif flight.actype==AIRBOSS.AircraftCarrier.F14A_AI or flight.actype==AIRBOSS.AircraftCarrier.F14A or flight.actype==AIRBOSS.AircraftCarrier.F14B then + Speed=UTILS.KnotsToKmph(175) + elseif flight.actype==AIRBOSS.AircraftCarrier.S3B or flight.actype==AIRBOSS.AircraftCarrier.S3BTANKER then + Speed=UTILS.KnotsToKmph(140) + end + + -- Carrier position. + local Carrier=self:GetCoordinate() + + -- Carrier heading. + local hdg=self:GetHeading() + + -- Waypoints array. + local wp={} + + local CurrentSpeed=flight.group:GetVelocityKMH() + + -- Current positon. + wp[#wp+1]=flight.group:GetCoordinate():WaypointAirTurningPoint(nil, CurrentSpeed, {}, "Current position") + + -- Altitude 800 ft. Looks like this works best. + local alt=UTILS.FeetToMeters(800) + + -- Landing waypoint 5 NM behind carrier at 2000 ft = 610 meters ASL. + wp[#wp+1]=Carrier:Translate(UTILS.NMToMeters(4), hdg-160):SetAltitude(alt):WaypointAirLanding(Speed, self.airbase, nil, "Landing") + --wp[#wp+1]=Carrier:Translate(UTILS.NMToMeters(4), hdg-160):SetAltitude(alt):WaypointAirLandingReFu(Speed, self.airbase, nil, "Landing") + + --wp[#wp+1]=self:GetCoordinate():Translate(UTILS.NMToMeters(3), hdg-160):SetAltitude(alt):WaypointAirTurningPoint(nil,Speed, {}, "Before Initial") ---WaypointAirLanding(Speed, self.airbase, nil, "Landing") + --wp[#wp+1]=self:GetCoordinate():WaypointAirLanding(Speed, self.airbase, nil, "Landing") + + -- Reinit waypoints. + flight.group:WayPointInitialize(wp) + + -- Route group. + flight.group:Route(wp, 0) +end + +--- Get marshal altitude and two positions of a counter-clockwise race track pattern. +-- @param #AIRBOSS self +-- @param #number stack Assigned stack number. Counting starts at one, i.e. stack=1 is the first stack. +-- @param #number case Recovery case. Default is self.case. +-- @return #number Holding altitude in meters. +-- @return Core.Point#COORDINATE First race track coordinate. +-- @return Core.Point#COORDINATE Second race track coordinate. +function AIRBOSS:_GetMarshalAltitude(stack, case) + + -- Stack <= 0. + if stack<=0 then + return 0,nil,nil + end + + -- Recovery case. + case=case or self.case + + -- Carrier position. + local Carrier=self:GetCoordinate() + + -- Altitude of first stack. Depends on recovery case. + local angels0 + local Dist + local p1=nil --Core.Point#COORDINATE + local p2=nil --Core.Point#COORDINATE + + -- Stack number. + local nstack=stack-1 + + if case==1 then + + -- CASE I: Holding at 2000 ft on a circular pattern port of the carrier. Interval +1000 ft for next stack. + angels0=2 + + -- Get true heading of carrier. + local hdg=self.carrier:GetHeading() + + -- For CCW pattern: First point astern, second ahead of the carrier. + + -- First point over carrier. + p1=Carrier + + -- Second point 1.5 NM ahead. + p2=Carrier:Translate(UTILS.NMToMeters(1.5), hdg) + + -- Tarawa Delta pattern. + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + + -- Pattern is directly overhead the carrier. + p1=Carrier:Translate(UTILS.NMToMeters(1.0), hdg+90) + p2=p1:Translate(2.5, hdg) + + end + + else + + -- CASE II/III: Holding at 6000 ft on a racetrack pattern astern the carrier. + angels0=6 + + -- Distance: d=n*angels0+15 NM, so first stack is at 15+6=21 NM + Dist=UTILS.NMToMeters(nstack+angels0+15) + + -- Get correct radial depending on recovery case including offset. + local radial=self:GetRadial(case, false, true) + + -- For CCW pattern: p1 further astern than p2. + + -- Length of the race track pattern. + local l=UTILS.NMToMeters(10) + + -- First point of race track pattern. + p1=Carrier:Translate(Dist+l, radial) + + -- Second point. + p2=Carrier:Translate(Dist, radial) + + end + + -- Pattern altitude. + local altitude=UTILS.FeetToMeters((nstack+angels0)*1000) + + -- Set altitude of coordinate. + p1:SetAltitude(altitude, true) + p2:SetAltitude(altitude, true) + + return altitude, p1, p2 +end + +--- Calculate an estimate of the charlie time of the player based on how many other aircraft are in the marshal or pattern queue before him. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flightgroup Flight data. +-- @return #number Charlie (abs) time in seconds. Or nil, if stack<0 or no recovery window will open. +function AIRBOSS:_GetCharlieTime(flightgroup) + + -- Get current stack of player. + local stack=flightgroup.flag + + -- Flight is not in marshal stack. + if stack<=0 then + return nil + end + + -- Current abs time. + local Tnow=timer.getAbsTime() + + -- Time the player has to spend in marshal stack until all lower stacks are emptied. + local Tcharlie=0 + + local Trecovery=0 + if self.recoverywindow then + -- Time in seconds until the next recovery starts or 0 if window is already open. + Trecovery=math.max(self.recoverywindow.START-Tnow, 0) + else + -- Set ~7 min if no future recovery window is defined. Otherwise radio call function crashes. + Trecovery=7*60 + end + + -- Loop over flights currently in the marshal queue. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Stack of marshal flight. + local mstack=flight.flag + + -- Time to get to the marshal stack if not holding already. + local Tarrive=0 + + -- Minimum holding time per stack. + local Tholding=3*60 + + if stack>0 and mstack>0 and mstack<=stack then + + -- Check if flight is already holding or just on its way. + if flight.holding==nil then + -- Flight is on its way to the marshal stack. + + -- Coordinate of the holding zone. + local holdingzone=self:_GetZoneHolding(flight.case, 1):GetCoordinate() + + -- Distance to holding zone. + local d0=holdingzone:Get2DDistance(flight.group:GetCoordinate()) + + -- Current velocity. + local v0=flight.group:GetVelocityMPS() + + -- Time to get to the carrier. + Tarrive=d0/v0 + + self:T3(self.lid..string.format("Tarrive=%.1f seconds, Clock %s", Tarrive, UTILS.SecondsToClock(Tnow+Tarrive))) + + else + -- Flight is already holding. + + -- Next in line. + if mstack==1 then + + -- Current holding time. flight.time stamp should be when entering holding or last time the stack collapsed. + local tholding=timer.getAbsTime()-flight.time + + -- Deduce current holding time. Ensure that is >=0. + Tholding=math.max(3*60-tholding, 0) + end + + end + + -- This is the approx time needed to get to the pattern. If we are already there, it is the time until the recovery window opens or 0 if it is already open. + local Tmin=math.max(Tarrive, Trecovery) + + -- Charlie time + 2 min holding in stack 1. + Tcharlie=math.max(Tmin, Tcharlie)+Tholding + end + + end + + -- Convert to abs time. + Tcharlie=Tcharlie+Tnow + + -- Debug info. + local text=string.format("Charlie time for flight %s (%s) %s", flightgroup.onboard, flightgroup.groupname, UTILS.SecondsToClock(Tcharlie)) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + return Tcharlie +end + +--- Add a flight group to the Marshal queue at a specific stack. Flight is informed via message. This fixes the recovery case to the current case ops in progress self.case). +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group. +-- @param #number stack Marshal stack. This (re-)sets the flag value. +function AIRBOSS:_AddMarshalGroup(flight, stack) + + -- Set flag value. This corresponds to the stack number which starts at 1. + flight.flag=stack + + -- Set recovery case. + flight.case=self.case + + -- Add to marshal queue. + table.insert(self.Qmarshal, flight) + + -- Pressure. + local P=UTILS.hPa2inHg(self:GetCoordinate():GetPressure()) + + -- Stack altitude. + --local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, flight.case)) + local alt=self:_GetMarshalAltitude(stack, flight.case) + + -- Current BRC. + local brc=self:GetBRC() + + -- If the carrier is supposed to turn into the wind, we take the wind coordinate. + if self.recoverywindow and self.recoverywindow.WIND then + brc=self:GetBRCintoWind() + end + + -- Get charlie time estimate. + flight.Tcharlie=self:_GetCharlieTime(flight) + + -- Convert to clock string. + local Ccharlie=UTILS.SecondsToClock(flight.Tcharlie) + + -- Combined marshal call. + self:_MarshalCallArrived(flight.onboard, flight.case, brc, alt, Ccharlie, P) + + -- Hint about TACAN bearing. + if self.TACANon and (not flight.ai) and flight.difficulty==AIRBOSS.Difficulty.EASY then + -- Get inverse magnetic radial potential offset. + local radial=self:GetRadial(flight.case, true, true, true) + if flight.case==1 then + -- For case 1 we want the BRC but above routine return FB. + radial=self:GetBRC() + end + local text=string.format("Select TACAN %03d°, channel %d%s (%s)", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) + self:MessageToPlayer(flight, text, nil, "") + end + +end + +--- Collapse marshal stack. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight that left the marshal stack. +-- @param #boolean nopattern If true, flight does not go to pattern. +function AIRBOSS:_CollapseMarshalStack(flight, nopattern) + self:F2({flight=flight, nopattern=nopattern}) + + -- Recovery case of flight. + local case=flight.case + + -- Stack of flight. + local stack=flight.flag + + -- Check that stack > 0. + if stack<=0 then + self:E(self.lid..string.format("ERROR: Flight %s is has stack value %d<0. Cannot collapse stack!", flight.groupname, stack)) + return + end + + -- Memorize time when stack collapsed. Should better depend on case but for now we assume there are no two different stacks Case I or II/III. + self.Tcollapse=timer.getTime() + + -- Decrease flag values of all flight groups in marshal stack. + for _,_flight in pairs(self.Qmarshal) do + local mflight=_flight --#AIRBOSS.PlayerData + + -- Only collapse stack of which the flight left. CASE II/III stacks are not collapsed. + if (case==1 and mflight.case==1) then --or (case>1 and mflight.case>1) then + + -- Get current flag/stack value. + local mstack=mflight.flag + + -- Only collapse stacks above the new pattern flight. + if mstack>stack then + + -- TODO: Is this now right as we allow more flights per stack? + -- Question is, does the stack collapse if the lower stack is completely empty or do aircraft descent if just one flight leaves. + -- For now, assuming that the stack must be completely empty before the next higher AC are allowed to descent. + local newstack=self:_GetFreeStack(mflight.ai, mflight.case, true) + + -- Free stack has to be below. + if newstack and newstack %d.", mflight.groupname, mflight.case, mstack, newstack)) + + if mflight.ai then + + -- Command AI to decrease stack. Flag is set in the routine. + self:_MarshalAI(mflight, newstack) + + else + + -- Decrease stack/flag. Human player needs to take care himself. + mflight.flag=newstack + + -- Angels of new stack. + local angels=self:_GetAngels(self:_GetMarshalAltitude(newstack, case)) + + -- Inform players. + if mflight.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Send message to all non-pros that they can descent. + local text=string.format("descent to stack at Angels %d.", angels) + self:MessageToPlayer(mflight, text, "MARSHAL") + + end + + -- Set time stamp. + mflight.time=timer.getAbsTime() + + -- Loop over section members. + for _,_sec in pairs(mflight.section) do + local sec=_sec --#AIRBOSS.PlayerData + + -- Also decrease flag for section members of flight. + sec.flag=newstack + + -- Set new time stamp. + sec.time=timer.getAbsTime() + + -- Inform section member. + if sec.difficulty~=AIRBOSS.Difficulty.HARD then + local text=string.format("descent to stack at Angels %d.", angels) + self:MessageToPlayer(sec, text, "MARSHAL") + end + + end + + end + + end + end + end + end + + + if nopattern then + + -- Debug message. + self:T(self.lid..string.format("Flight %s is leaving stack but not going to pattern.", flight.groupname)) + + else + + -- Debug message. + local Tmarshal=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) + self:T(self.lid..string.format("Flight %s is leaving marshal after %s and going pattern.", flight.groupname, Tmarshal)) + + -- Add flight to pattern queue. + self:_AddFlightToPatternQueue(flight) + + end + + -- Set flag to -1 (-1 is rather arbitrary but it should not be positive or -100 or -42). + flight.flag=-1 + + -- New time stamp for time in pattern. + flight.time=timer.getAbsTime() + +end + +--- Get next free Marshal stack. Depending on AI/human and recovery case. +-- @param #AIRBOSS self +-- @param #boolean ai If true, get a free stack for an AI flight group. +-- @param #number case Recovery case. Default current (self) case in progress. +-- @param #boolean empty Return lowest stack that is completely empty. +-- @return #number Lowest free stack available for the given case or nil if all Case I stacks are taken. +function AIRBOSS:_GetFreeStack(ai, case, empty) + + -- Recovery case. + case=case or self.case + + if case==1 then + return self:_GetFreeStack_Old(ai, case, empty) + end + + -- Max number of stacks available. + local nmaxstacks=100 + if case==1 then + nmaxstacks=self.Nmaxmarshal + end + + -- Assume up to two (human) flights per stack. All are free. + local stack={} + for i=1,nmaxstacks do + stack[i]=self.NmaxStack -- Number of human flights per stack. + end + + local nmax=1 + + -- Loop over all flights in marshal stack. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Check that the case is right. + if flight.case==case then + + -- Get stack of flight. + local n=flight.flag + + if n>nmax then + nmax=n + end + + if n>0 then + if flight.ai or flight.case>1 then + stack[n]=0 -- AI get one stack on their own. Also CASE II/III get one stack each. + else + stack[n]=stack[n]-1 + end + else + self:E(string.format("ERROR: Flight %s in marshal stack has stack value <= 0. Stack value is %d.", flight.groupname, n)) + end + + end + end + + local nfree=nil + if stack[nmax]==0 then + -- Max occupied stack is completely full! + if case==1 then + if nmax>=nmaxstacks then + -- Already all Case I stacks are occupied ==> wait outside 10 NM zone. + nfree=nil + else + -- Return next free stack. + nfree=nmax+1 + end + else + -- Case II/III return next stack + nfree=nmax+1 + end + + elseif stack[nmax]==self.NmaxStack then + -- Max occupied stack is completely empty! This should happen only when there is no other flight in the marshal queue. + self:E(self.lid..string.format("ERROR: Max occupied stack is empty. Should not happen! Nmax=%d, stack[nmax]=%d", nmax, stack[nmax])) + nfree=nmax + else + -- Max occupied stack is partly full. + if ai or empty or case>1 then + nfree=nmax+1 + else + nfree=nmax + end + + end + + self:I(self.lid..string.format("Returning free stack %s", tostring(nfree))) + return nfree +end + +--- Get next free Marshal stack. Depending on AI/human and recovery case. +-- @param #AIRBOSS self +-- @param #boolean ai If true, get a free stack for an AI flight group. +-- @param #number case Recovery case. Default current (self) case in progress. +-- @param #boolean empty Return lowest stack that is completely empty. +-- @return #number Lowest free stack available for the given case or nil if all Case I stacks are taken. +function AIRBOSS:_GetFreeStack_Old(ai, case, empty) + + -- Recovery case. + case=case or self.case + + -- Max number of stacks available. + local nmaxstacks=100 + if case==1 then + nmaxstacks=self.Nmaxmarshal + end + + -- Assume up to two (human) flights per stack. All are free. + local stack={} + for i=1,nmaxstacks do + stack[i]=self.NmaxStack -- Number of human flights per stack. + end + + -- Loop over all flights in marshal stack. + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Check that the case is right. + if flight.case==case then + + -- Get stack of flight. + local n=flight.flag + + if n>0 then + if flight.ai or flight.case>1 then + stack[n]=0 -- AI get one stack on their own. Also CASE II/III get one stack each. + else + stack[n]=stack[n]-1 + end + else + self:E(string.format("ERROR: Flight %s in marshal stack has stack value <= 0. Stack value is %d.", flight.groupname, n)) + end + + end + end + + -- Loop over stacks and check which one has a place left. + local nfree=nil + for i=1,nmaxstacks do + self:T2(self.lid..string.format("FF Stack[%d]=%d", i, stack[i])) + if ai or empty or case>1 then + -- AI need the whole stack. + if stack[i]==self.NmaxStack then + nfree=i + return i + end + else + -- Human players only need one free spot. + if stack[i]>0 then + nfree=i + return i + end + end + end + + return nfree +end + +--- Get number of (airborne) units in a flight. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight The flight group. +-- @param #boolean onground If true, include units on the ground. By default only airborne units are counted. +-- @return #number Number of units in flight including section members. +-- @return #number Number of units in flight excluding section members. +-- @return #number Number of section members. +function AIRBOSS:_GetFlightUnits(flight, onground) + + -- Default is only airborne. + local inair=true + if onground==true then + inair=false + end + + --- Count units of a group which are alive and in the air. + local function countunits(_group, inair) + local group=_group --Wrapper.Group#GROUP + local units=group:GetUnits() + local n=0 + if units then + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + if inair then + -- Only count units in air. + if unit:InAir() then + self:T2(self.lid..string.format("Unit %s is in AIR", unit:GetName())) + n=n+1 + end + else + -- Count units in air or on the ground. + n=n+1 + end + end + end + end + return n + end + + + -- Count units of the group itself (alive units in air). + local nunits=countunits(flight.group, inair) + + -- Count section members. + local nsection=0 + for _,sec in pairs(flight.section) do + local secflight=sec --#AIRBOSS.PlayerData + -- Count alive units in air. + nsection=nsection+countunits(secflight.group, inair) + end + + return nunits+nsection, nunits, nsection +end + +--- Get number of groups and units in queue, which are alive and airborne. In units we count the section members as well. +-- @param #AIRBOSS self +-- @param #table queue The queue. Can be self.flights, self.Qmarshal or self.Qpattern. +-- @param #number case (Optional) Only count flights, which are in a specific recovery case. Note that you can use case=23 for flights that are either in Case II or III. By default all groups/units regardless of case are counted. +-- @return #number Total number of flight groups in queue. +-- @return #number Total number of aircraft in queue since each flight group can contain multiple aircraft. +function AIRBOSS:_GetQueueInfo(queue, case) + + local ngroup=0 + local Nunits=0 + + -- Loop over flight groups. + for _,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Check if a specific case was requested. + if case then + + ------------------------------------------------------------------------ + -- Only count specific case with special 23 = CASE II and III combined. + ------------------------------------------------------------------------ + + if (flight.case==case) or (case==23 and (flight.case==2 or flight.case==3)) then + + -- Number of total units, units in flight and section members ALIVE and AIRBORNE. + local ntot,nunits,nsection=self:_GetFlightUnits(flight) + + -- Add up total unit number. + Nunits=Nunits+ntot + + -- Increase group count. + if ntot>0 then + ngroup=ngroup+1 + end + + end + + else + + --------------------------------------------------------------------------- + -- No specific case requested. Count all groups & units in selected queue. + --------------------------------------------------------------------------- + + -- Number of total units, units in flight and section members ALIVE and AIRBORNE. + local ntot,nunits,nsection=self:_GetFlightUnits(flight) + + -- Add up total unit number. + Nunits=Nunits+ntot + + -- Increase group count. + if ntot>0 then + ngroup=ngroup+1 + end + + end + + end + + return ngroup, Nunits +end + +--- Print holding queue. +-- @param #AIRBOSS self +-- @param #table queue Queue to print. +-- @param #string name Queue name. +function AIRBOSS:_PrintQueue(queue, name) + + --local nqueue=#queue + local Nqueue, nqueue=self:_GetQueueInfo(queue) + + local text=string.format("%s Queue N=%d (#%d), n=%d:", name, Nqueue, #queue, nqueue) + if #queue==0 then + text=text.." empty." + else + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + + local clock=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) + local case=flight.case + local stack=flight.flag + local fuel=flight.group:GetFuelMin()*100 + local ai=tostring(flight.ai) + local lead=flight.seclead + local Nsec=#flight.section + local actype=self:_GetACNickname(flight.actype) + local onboard=flight.onboard + local holding=tostring(flight.holding) + + -- Airborne units. + local _, nunits, nsec=self:_GetFlightUnits(flight, false) + + -- Text. + text=text..string.format("\n[%d] %s*%d (%s): lead=%s (%d/%d), onboard=%s, flag=%d, case=%d, time=%s, fuel=%d, ai=%s, holding=%s", + i, flight.groupname, nunits, actype, lead, nsec, Nsec, onboard, stack, case, clock, fuel, ai, holding) + if stack>0 then + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, case)) + text=text..string.format(" stackalt=%d ft", alt) + end + for j,_element in pairs(flight.elements) do + local element=_element --#AIRBOSS.FlightElement + text=text..string.format("\n (%d) %s (%s): ai=%s, ballcall=%s, recovered=%s", + j, element.onboard, element.unitname, tostring(element.ai), tostring(element.ballcall), tostring(element.recovered)) + end + end + end + self:T(self.lid..text) +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FLIGHT & PLAYER functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new flight group. Usually when a flight appears in the CCA. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #AIRBOSS.FlightGroup Flight group. +function AIRBOSS:_CreateFlightGroup(group) + + -- Debug info. + self:T(self.lid..string.format("Creating new flight for group %s of aircraft type %s.", group:GetName(), group:GetTypeName())) + + -- New flight. + local flight={} --#AIRBOSS.FlightGroup + + -- Check if not already in flights + if not self:_InQueue(self.flights, group) then + + -- Flight group name + local groupname=group:GetName() + local human, playername=self:_IsHuman(group) + + -- Queue table item. + flight.group=group + flight.groupname=group:GetName() + flight.nunits=#group:GetUnits() + flight.time=timer.getAbsTime() + flight.dist0=group:GetCoordinate():Get2DDistance(self:GetCoordinate()) + flight.flag=-100 + flight.ai=not human + flight.actype=group:GetTypeName() + flight.onboardnumbers=self:_GetOnboardNumbers(group) + flight.seclead=flight.group:GetUnit(1):GetName() -- Sec lead is first unitname of group but player name for players. + flight.section={} + flight.ballcall=false + flight.refueling=false + flight.holding=nil + flight.name=flight.group:GetUnit(1):GetName() --Will be overwritten in _Newplayer with player name if human player in the group. + + -- Note, this should be re-set elsewhere! + flight.case=self.case + + -- Flight elements. + local text=string.format("Flight elements of group %s:", flight.groupname) + flight.elements={} + local units=group:GetUnits() + for i,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + local element={} --#AIRBOSS.FlightElement + element.unit=unit + element.unitname=unit:GetName() + element.onboard=flight.onboardnumbers[element.unitname] + element.ballcall=false + element.ai=not self:_IsHumanUnit(unit) + element.recovered=nil + text=text..string.format("\n[%d] %s onboard #%s, AI=%s", i, element.unitname, tostring(element.onboard), tostring(element.ai)) + table.insert(flight.elements, element) + end + self:T(self.lid..text) + + -- Onboard + if flight.ai then + local onboard=flight.onboardnumbers[flight.seclead] + flight.onboard=onboard + else + flight.onboard=self:_GetOnboardNumberPlayer(group) + end + + -- Add to known flights. + table.insert(self.flights, flight) + + else + self:E(self.lid..string.format("ERROR: Flight group %s already exists in self.flights!", group:GetName())) + return nil + end + + return flight +end + + +--- Initialize player data after birth event of player unit. +-- @param #AIRBOSS self +-- @param #string unitname Name of the player unit. +-- @return #AIRBOSS.PlayerData Player data. +function AIRBOSS:_NewPlayer(unitname) + + -- Get player unit and name. + local playerunit, playername=self:_GetPlayerUnitAndName(unitname) + + if playerunit and playername then + + -- Get group. + local group=playerunit:GetGroup() + + -- Player data. + local playerData --#AIRBOSS.PlayerData + + -- Create a flight group for the player. + playerData=self:_CreateFlightGroup(group) + + -- Nil check. + if playerData then + + -- Player unit, client and callsign. + playerData.unit = playerunit + playerData.unitname = unitname + playerData.name = playername + playerData.callsign = playerData.unit:GetCallsign() + playerData.client = CLIENT:FindByName(unitname, nil, true) + playerData.seclead = playername + + -- Number of passes done by player in this slot. + playerData.passes=0 --playerData.passes or 0 + + -- Messages for player. + playerData.messages={} + + -- Debriefing tables. + playerData.lastdebrief=playerData.lastdebrief or {} + + -- Attitude monitor. + playerData.attitudemonitor=false + + -- Trap sheet save. + if playerData.trapon==nil then + playerData.trapon=self.trapsheet + end + + -- Set difficulty level. + playerData.difficulty=playerData.difficulty or self.defaultskill + + -- Subtitles of player. + if playerData.subtitles==nil then + playerData.subtitles=true + end + + -- Show step hints. + if playerData.showhints==nil then + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + playerData.showhints=false + else + playerData.showhints=true + end + end + + -- Points rewarded. + playerData.points={} + + -- Init stuff for this round. + playerData=self:_InitPlayer(playerData) + + -- Init player data. + self.players[playername]=playerData + + -- Init player grades table if necessary. + self.playerscores[playername]=self.playerscores[playername] or {} + + -- Welcome player message. + if self.welcome then + self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), string.format("AIRBOSS %s", self.alias), "", 5) + end + + end + + -- Return player data table. + return playerData + end + + return nil +end + +--- Initialize player data by (re-)setting parmeters to initial values. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string step (Optional) New player step. Default UNDEFINED. +-- @return #AIRBOSS.PlayerData Initialized player data. +function AIRBOSS:_InitPlayer(playerData, step) + self:T(self.lid..string.format("Initializing player data for %s callsign %s.", playerData.name, playerData.callsign)) + + playerData.step=step or AIRBOSS.PatternStep.UNDEFINED + playerData.groove={} + playerData.debrief={} + playerData.trapsheet={} + playerData.warning=nil + playerData.holding=nil + playerData.refueling=false + playerData.valid=false + playerData.lig=false + playerData.wop=false + playerData.waveoff=false + playerData.wofd=false + playerData.owo=false + playerData.boltered=false + playerData.landed=false + playerData.Tlso=timer.getTime() + playerData.Tgroove=nil + playerData.TIG0=nil + playerData.wire=nil + playerData.flag=-100 + playerData.debriefschedulerID=nil + + -- Set us up on final if group name contains "Groove". But only for the first pass. + if playerData.group:GetName():match("Groove") and playerData.passes==0 then + self:MessageToPlayer(playerData, "Group name contains \"Groove\". Happy groove testing.") + playerData.attitudemonitor=true + playerData.step=AIRBOSS.PatternStep.FINAL + self:_AddFlightToPatternQueue(playerData) + self.dTstatus=0.1 + end + + return playerData +end + + +--- Get flight from group in a queue. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Group that will be removed from queue. +-- @param #table queue The queue from which the group will be removed. +-- @return #AIRBOSS.FlightGroup Flight group or nil. +-- @return #number Queue index or nil. +function AIRBOSS:_GetFlightFromGroupInQueue(group, queue) + + if group then + + -- Group name + local name=group:GetName() + + -- Loop over all flight groups in queue + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + + if flight.groupname==name then + return flight, i + end + end + + self:T2(self.lid..string.format("WARNING: Flight group %s could not be found in queue.", name)) + end + + self:T2(self.lid..string.format("WARNING: Flight group could not be found in queue. Group is nil!")) + return nil, nil +end + +--- Get element in flight. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @return #AIRBOSS.FlightElement Element of the flight or nil. +-- @return #number Element index or nil. +-- @return #AIRBOSS.FlightGroup The Flight group or nil +function AIRBOSS:_GetFlightElement(unitname) + + -- Get the unit. + local unit=UNIT:FindByName(unitname) + + -- Check if unit exists. + if unit then + + -- Get flight element from all flights. + local flight=self:_GetFlightFromGroupInQueue(unit:GetGroup(), self.flights) + + -- Check if fight exists. + if flight then + + -- Loop over all elements in flight group. + for i,_element in pairs(flight.elements) do + local element=_element --#AIRBOSS.FlightElement + + if element.unit:GetName()==unitname then + return element, i, flight + end + end + + self:T2(self.lid..string.format("WARNING: Flight element %s could not be found in flight group.", unitname, flight.groupname)) + end + end + + return nil, nil, nil +end + +--- Get element in flight. +-- @param #AIRBOSS self +-- @param #string unitname Name of the unit. +-- @return #boolean If true, element could be removed or nil otherwise. +function AIRBOSS:_RemoveFlightElement(unitname) + + -- Get table index. + local element,idx, flight=self:_GetFlightElement(unitname) + + if idx then + table.remove(flight.elements, idx) + return true + else + self:T("WARNING: Flight element could not be removed from flight group. Index=nil!") + return nil + end +end + +--- Check if a group is in a queue. +-- @param #AIRBOSS self +-- @param #table queue The queue to check. +-- @param Wrapper.Group#GROUP group The group to be checked. +-- @return #boolean If true, group is in the queue. False otherwise. +function AIRBOSS:_InQueue(queue, group) + local name=group:GetName() + for _,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + if name==flight.groupname then + return true + end + end + return false +end + +--- Remove dead flight groups from all queues. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #AIRBOSS.FlightGroup Flight group. +function AIRBOSS:_RemoveDeadFlightGroups() + + -- Remove dead flights from all flights table. + for i=#self.flight,1,-1 do + local flight=self.flights[i] --#AIRBOSS.FlightGroup + if not flight.group:IsAlive() then + self:T(string.format("Removing dead flight group %s from ALL flights table.", flight.groupname)) + table.remove(self.flights, i) + end + end + + -- Remove dead flights from Marhal queue table. + for i=#self.Qmarshal,1,-1 do + local flight=self.Qmarshal[i] --#AIRBOSS.FlightGroup + if not flight.group:IsAlive() then + self:T(string.format("Removing dead flight group %s from Marshal Queue table.", flight.groupname)) + table.remove(self.Qmarshal, i) + end + end + + -- Remove dead flights from Pattern queue table. + for i=#self.Qpattern,1,-1 do + local flight=self.Qpattern[i] --#AIRBOSS.FlightGroup + if not flight.group:IsAlive() then + self:T(string.format("Removing dead flight group %s from Pattern Queue table.", flight.groupname)) + table.remove(self.Qpattern, i) + end + end + +end + +--- Get the lead flight group of a flight group. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group to check. +-- @return #AIRBOSS.FlightGroup Flight group of the leader or flight itself if no other leader. +function AIRBOSS:_GetLeadFlight(flight) + + -- Init. + local lead=flight + + -- Only human players can be section leads of other players. + if flight.name~=flight.seclead then + lead=self.players[flight.seclead] + end + + return lead +end + +--- Check if all elements of a flight were recovered. This also checks potential section members. +-- If so, flight is removed from the queue. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group to check. +-- @return #boolean If true, all elements landed. +function AIRBOSS:_CheckSectionRecovered(flight) + + -- Nil check. + if flight==nil then + return true + end + + -- Get the lead flight first, so that we can also check all section members. + local lead=self:_GetLeadFlight(flight) + + -- Check all elements of the lead flight group. + for _,_element in pairs(lead.elements) do + local element=_element --#AIRBOSS.FlightElement + if not element.recovered then + return false + end + end + + -- Now check all section members, if any. + for _,_section in pairs(lead.section) do + local sectionmember=_section --#AIRBOSS.FlightGroup + + -- Check all elements of the secmember flight group. + for _,_element in pairs(sectionmember.elements) do + local element=_element --#AIRBOSS.FlightElement + if not element.recovered then + return false + end + end + end + + -- Remove lead flight from pattern queue. It is this flight who is added to the queue. + self:_RemoveFlightFromQueue(self.Qpattern, lead) + + -- Just for now, check if it is in other queues as well. + if self:_InQueue(self.Qmarshal, lead.group) then + self:E(self.lid..string.format("ERROR: lead flight group %s should not be in marshal queue", lead.groupname)) + self:_RemoveFlightFromMarshalQueue(lead, true) + end + -- Just for now, check if it is in other queues as well. + if self:_InQueue(self.Qwaiting, lead.group) then + self:E(self.lid..string.format("ERROR: lead flight group %s should not be in pattern queue", lead.groupname)) + self:_RemoveFlightFromQueue(self.Qwaiting, lead) + end + + return true +end + +--- Add flight to pattern queue and set recoverd to false for all elements of the flight and its section members. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup Flight group of element. +function AIRBOSS:_AddFlightToPatternQueue(flight) + + -- Add flight to table. + table.insert(self.Qpattern, flight) + + -- Set flag to -1 (-1 is rather arbitrary but it should not be positive or -100 or -42). + flight.flag=-1 + -- New time stamp for time in pattern. + flight.time=timer.getAbsTime() + + -- Init recovered switch. + flight.recovered=false + for _,elem in pairs(flight.elements) do + elem.recoverd=false + end + + -- Set recovered for all section members. + for _,sec in pairs(flight.section) do + -- Set flag and timestamp for section members + sec.flag=-1 + sec.time=timer.getAbsTime() + for _,elem in pairs(sec.elements) do + elem.recoverd=false + end + end +end + +--- Sets flag recovered=true for a flight element, which was successfully recovered (landed). +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The aircraft unit that was recovered. +-- @return #AIRBOSS.FlightGroup Flight group of element. +function AIRBOSS:_RecoveredElement(unit) + + -- Get element of flight. + local element, idx, flight=self:_GetFlightElement(unit:GetName()) --#AIRBOSS.FlightElement + + -- Nil check. Could be if a helo landed or something else we dont know! + if element then + element.recovered=true + end + + return flight +end + +--- Remove a flight group from the Marshal queue. Marshal stack is collapsed, too, if flight was in the queue. Waiting flights are send to marshal. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight Flight group that will be removed from queue. +-- @param #boolean nopattern If true, flight is NOT going to landing pattern. +-- @return #boolean True, flight was removed or false otherwise. +-- @return #number Table index of the flight in the Marshal queue. +function AIRBOSS:_RemoveFlightFromMarshalQueue(flight, nopattern) + + -- Remove flight from marshal queue if it is in. + local removed, idx=self:_RemoveFlightFromQueue(self.Qmarshal, flight) + + -- Collapse marshal stack if flight was removed. + if removed then + + -- Flight is not holding any more. + flight.holding=nil + + -- Collapse marshal stack if flight was removed. + self:_CollapseMarshalStack(flight, nopattern) + + -- Stacks are only limited for Case I. + if flight.case==1 and #self.Qwaiting>0 then + + -- Next flight in line waiting. + local nextflight=self.Qwaiting[1] --#AIRBOSS.FlightGroup + + -- Get free stack. + local freestack=self:_GetFreeStack(nextflight.ai) + + -- Send next flight to marshal stack. + if nextflight.ai then + + -- Send AI to Marshal Stack. + self:_MarshalAI(nextflight, freestack) + + else + + -- Send player to Marshal stack. + self:_MarshalPlayer(nextflight, freestack) + + end + + -- Remove flight from waiting queue. + self:_RemoveFlightFromQueue(self.Qwaiting, nextflight) + + end + end + + return removed, idx +end + +--- Remove a flight group from a queue. +-- @param #AIRBOSS self +-- @param #table queue The queue from which the group will be removed. +-- @param #AIRBOSS.FlightGroup flight Flight group that will be removed from queue. +-- @return #boolean True, flight was in Queue and removed. False otherwise. +-- @return #number Table index of removed queue element or nil. +function AIRBOSS:_RemoveFlightFromQueue(queue, flight) + + -- Loop over all flights in group. + for i,_flight in pairs(queue) do + local qflight=_flight --#AIRBOSS.FlightGroup + + -- Check for name. + if qflight.groupname==flight.groupname then + self:T(self.lid..string.format("Removing flight group %s from queue.", flight.groupname)) + table.remove(queue, i) + return true, i + end + end + + return false, nil +end + +--- Remove a unit and its element from a flight group (e.g. when landed) and update all queues if the whole flight group is gone. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit to be removed. +function AIRBOSS:_RemoveUnitFromFlight(unit) + + -- Check if unit exists. + if unit then + + -- Get group. + local group=unit:GetGroup() + + -- Check if group exists. + if group then + + -- Get flight. + local flight=self:_GetFlightFromGroupInQueue(group, self.flights) + + -- Check if flight exists. + if flight then + + -- Remove element from flight group. + local removed=self:_RemoveFlightElement(unit:GetName()) + + if removed then + + -- Get number of units (excluding section members). For AI only those that are still in air as we assume once they landed, they are out of the game. + local _,nunits=self:_GetFlightUnits(flight, not flight.ai) + + -- Number of flight elements still left. + local nelements=#flight.elements + + -- Debug info. + self:T(self.lid..string.format("Removed unit %s: nunits=%d, nelements=%d", unit:GetName(), nunits, nelements)) + + -- Check if no units are left. + if nunits==0 or nelements==0 then + -- Remove flight from all queues. + self:_RemoveFlight(flight) + end + + end + end + end + end + +end + +--- Remove a flight, which is a member of a section, from this section. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight The flight to be removed from the section +function AIRBOSS:_RemoveFlightFromSection(flight) + + -- First check if player is not the lead. + if flight.name~=flight.seclead then + + -- Remove this flight group from the section of the leader. + local lead=self.players[flight.seclead] --#AIRBOSS.FlightGroup + if lead then + for i,sec in pairs(lead.section) do + local sectionmember=sec --#AIRBOSS.FlightGroup + if sectionmember.name==flight.name then + table.remove(lead.section, i) + break + end + end + end + end + +end + +--- Update section if a flight is removed. +-- If removed flight is member of a section, he is removed for the leaders section. +-- If removed flight is the section lead, we try to find a new leader. +-- @param #AIRBOSS self +-- @param #AIRBOSS.FlightGroup flight The flight to be removed. +function AIRBOSS:_UpdateFlightSection(flight) + + -- Check if this player is the leader of a section. + if flight.seclead==flight.name then + + -------------------- + -- Section Leader -- + -------------------- + + -- This player is the leader ==> We need a new one. + if #flight.section>=1 then + + -- New leader. + local newlead=flight.section[1] --#AIRBOSS.FlightGroup + newlead.seclead=newlead.name + + -- Adjust new section members. + for i=2,#flight.section do + local member=flight.section[i] --#AIRBOSS.FlightGroup + + -- Add remaining members new leaders table. + table.insert(newlead.section, member) + + -- Set new section lead of member. + member.seclead=newlead.name + end + + end + + -- Flight section empty + flight.section={} + + else + + -------------------- + -- Section Member -- + -------------------- + + -- Remove flight from its leaders section. + self:_RemoveFlightFromSection(flight) + + end + +end + +--- Remove a flight from Marshal, Pattern and Waiting queues. If flight is in Marhal queue, the above stack is collapsed. +-- Also set player step to undefined if applicable or remove human flight if option *completely* is true. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData flight The flight to be removed. +-- @param #boolean completely If true, also remove human flight from all flights table. +function AIRBOSS:_RemoveFlight(flight, completely) + self:F(self.lid.. string.format("Removing flight %s, ai=%s completely=%s.", tostring(flight.groupname), tostring(flight.ai), tostring(completely))) + + -- Remove flight from all queues. + self:_RemoveFlightFromMarshalQueue(flight, true) + self:_RemoveFlightFromQueue(self.Qpattern, flight) + self:_RemoveFlightFromQueue(self.Qwaiting, flight) + self:_RemoveFlightFromQueue(self.Qspinning, flight) + + -- Check if player or AI + if flight.ai then + + -- Remove AI flight completely. Pure AI flights have no sections and cannot be members. + self:_RemoveFlightFromQueue(self.flights, flight) + + else + + -- Remove all grades until a final grade is reached. + local grades=self.playerscores[flight.name] + if grades and #grades>0 then + while #grades>0 and grades[#grades].finalscore==nil do + table.remove(grades, #grades) + end + end + + -- Check if flight should be completely removed, e.g. after the player died or simply left the slot. + if completely then + + -- Update flight section. Remove flight from section or find new section leader if flight was the lead. + self:_UpdateFlightSection(flight) + + -- Remove completely. + self:_RemoveFlightFromQueue(self.flights, flight) + + -- Remove player from players table. + local playerdata=self.players[flight.name] + if playerdata then + self:I(self.lid..string.format("Removing player %s completely.", flight.name)) + self.players[flight.name]=nil + end + + -- Remove flight. + flight=nil + + else + + -- Set player step to undefined. + self:_SetPlayerStep(flight, AIRBOSS.PatternStep.UNDEFINED) + + -- Also set this for the section members as they are in the same boat. + for _,sectionmember in pairs(flight.section) do + self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.UNDEFINED) + -- Also remove section member in case they are in the spinning queue. + self:_RemoveFlightFromQueue(self.Qspinning, sectionmember) + end + + -- What if flight is member of a section. His status is now undefined. Should he be removed from the section? + -- I think yes, if he pulls the trigger. + self:_RemoveFlightFromSection(flight) + + end + end + +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Player Status +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check current player status. +-- @param #AIRBOSS self +function AIRBOSS:_CheckPlayerStatus() + + -- Loop over all players. + for _playerName,_playerData in pairs(self.players) do + local playerData=_playerData --#AIRBOSS.PlayerData + + if playerData then + + -- Player unit. + local unit=playerData.unit + + -- Check if unit is alive. + if unit and unit:IsAlive() then + + -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier). + -- TODO: This might cause problems if the CCA is set to be very small! + if unit:IsInZone(self.zoneCCA) then + + -- Display aircraft attitude and other parameters as message text. + if playerData.attitudemonitor then + self:_AttitudeMonitor(playerData) + end + + -- Check distance to other flights. + self:_CheckPlayerPatternDistance(playerData) + + -- Foul deck check. + self:_CheckFoulDeck(playerData) + + -- Check current step. + if playerData.step==AIRBOSS.PatternStep.UNDEFINED then + + -- Status undefined. + --local time=timer.getAbsTime() + --local clock=UTILS.SecondsToClock(time) + --self:T3(string.format("Player status undefined. Waiting for next step. Time %s", clock)) + + elseif playerData.step==AIRBOSS.PatternStep.REFUELING then + + -- Nothing to do here at the moment. + + elseif playerData.step==AIRBOSS.PatternStep.SPINNING then + + -- Player is spinning. + self:_Spinning(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.HOLDING then + + -- CASE I/II/III: In holding pattern. + self:_Holding(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.WAITING then + + -- CASE I: Waiting outside 10 NM zone for next free Marshal stack. + self:_Waiting(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.COMMENCING then + + -- CASE I/II/III: New approach. + self:_Commencing(playerData, true) + + elseif playerData.step==AIRBOSS.PatternStep.BOLTER then + + -- CASE I/II/III: Bolter pattern. + self:_BolterPattern(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then + + -- CASE II/III: Player has reached 5k "Platform". + self:_Platform(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.ARCIN then + + -- Case II/III if offset. + self:_ArcInTurn(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.ARCOUT then + + -- Case II/III if offset. + self:_ArcOutTurn(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.DIRTYUP then + + -- CASE III: Player has descended to 1200 ft and is going level from now on. + self:_DirtyUp(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.BULLSEYE then + + -- CASE III: Player has intercepted the glide slope and should follow "Bullseye" (ICLS). + self:_Bullseye(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.INITIAL then + + -- CASE I/II: Player is at the initial position entering the landing pattern. + self:_Initial(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.BREAKENTRY then + + -- CASE I/II: Break entry. + self:_BreakEntry(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.EARLYBREAK then + + -- CASE I/II: Early break. + self:_Break(playerData, AIRBOSS.PatternStep.EARLYBREAK) + + elseif playerData.step==AIRBOSS.PatternStep.LATEBREAK then + + -- CASE I/II: Late break. + self:_Break(playerData, AIRBOSS.PatternStep.LATEBREAK) + + elseif playerData.step==AIRBOSS.PatternStep.ABEAM then + + -- CASE I/II: Abeam position. + self:_Abeam(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.NINETY then + + -- CASE:I/II: Check long down wind leg. + self:_CheckForLongDownwind(playerData) + + -- At the ninety. + self:_Ninety(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.WAKE then + + -- CASE I/II: In the wake. + self:_Wake(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.EMERGENCY then + + -- Emergency landing. Player pos is not checked. + self:_Final(playerData, true) + + elseif playerData.step==AIRBOSS.PatternStep.FINAL then + + -- CASE I/II: Turn to final and enter the groove. + self:_Final(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC or + playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_AL or + playerData.step==AIRBOSS.PatternStep.GROOVE_LC or + playerData.step==AIRBOSS.PatternStep.GROOVE_IW then + + -- CASE I/II: In the groove. + self:_Groove(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then + + -- Debriefing in 5 seconds. + --SCHEDULER:New(nil, self._Debrief, {self, playerData}, 5) + playerData.debriefschedulerID=self:ScheduleOnce(5, self._Debrief, self, playerData) + + -- Undefined status. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + else + + -- Error, unknown step! + self:E(self.lid..string.format("ERROR: Unknown player step %s. Please report!", tostring(playerData.step))) + + end + + -- Check if player missed a step during Case II/III and allow him to enter the landing pattern. + self:_CheckMissedStepOnEntry(playerData) + + else + self:T2(self.lid.."WARNING: Player unit not inside the CCA!") + end + + else + -- Unit not alive. + self:T(self.lid.."WARNING: Player unit is not alive!") + end + end + end + +end + + +--- Checks if a player is in the pattern queue and has missed a step in Case II/III approach. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_CheckMissedStepOnEntry(playerData) + + -- Conditions to be met: Case II/III, in pattern queue, flag!=42 (will be set to 42 at the end if player missed a step). + local rightcase=playerData.case>1 + local rightqueue=self:_InQueue(self.Qpattern, playerData.group) + local rightflag=playerData.flag~=-42 + + -- Steps that the player could have missed during Case II/III. + local step=playerData.step + local missedstep=step==AIRBOSS.PatternStep.PLATFORM or step==AIRBOSS.PatternStep.ARCIN or step==AIRBOSS.PatternStep.ARCOUT or step==AIRBOSS.PatternStep.DIRTYUP + + -- Check if player is about to enter the initial or bullseye zones and maybe has missed a step in the pattern. + if rightcase and rightqueue and rightflag then + + -- Get right zone. + local zone=nil + if playerData.case==2 and missedstep then + + zone=self:_GetZoneInitial(playerData.case) + + elseif playerData.case==3 and missedstep then + + zone=self:_GetZoneBullseye(playerData.case) + + end + + -- Zone only exists if player is not at the initial or bullseye step. + if zone then + + -- Check if player is in initial or bullseye zone. + local inzone=playerData.unit:IsInZone(zone) + + -- Relative heading to carrier direction. + local relheading=self:_GetRelativeHeading(playerData.unit, false) + + -- Check if player is in zone and flying roughly in the right direction. + if inzone and math.abs(relheading)<60 then + + -- Player is in one of the initial zones short before the landing pattern. + local text=string.format("you missed an important step in the pattern!\nYour next step would have been %s.", playerData.step) + self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 5) + + if playerData.case==2 then + -- Set next step to initial. + playerData.step=AIRBOSS.PatternStep.INITIAL + elseif playerData.case==3 then + -- Set next step to bullseye. + playerData.step=AIRBOSS.PatternStep.BULLSEYE + end + + -- Set flag value to -42. This is the value to ensure that this routine is not called again! + playerData.flag=-42 + end + end + end +end + +--- Set time in the groove for player. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_SetTimeInGroove(playerData) + + -- Set time in the groove + if playerData.TIG0 then + playerData.Tgroove=timer.getTime()-playerData.TIG0 + else + playerData.Tgroove=999 + end + +end + +--- Get time in the groove of player. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @return #number Player's time in groove in seconds. +function AIRBOSS:_GetTimeInGroove(playerData) + + local Tgroove=999 + + -- Get time in the groove. + if playerData.TIG0 then + Tgroove=timer.getTime()-playerData.TIG0 + end + + return Tgroove +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- EVENT functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Airboss event handler for event birth. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventBirth(EventData) + self:F3({eventbirth = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event BIRTH!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!") + self:E(EventData) + return + end + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T(self.lid.."BIRTH: player = "..tostring(_playername)) + + if _unit and _playername then + + local _uid=_unit:GetID() + local _group=_unit:GetGroup() + local _callsign=_unit:GetCallsign() + + -- Debug output. + local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.", _playername, _callsign, _unitName, _group:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + -- Check if aircraft type the player occupies is carrier capable. + local rightaircraft=self:_IsCarrierAircraft(_unit) + if rightaircraft==false then + local text=string.format("Player aircraft type %s not supported by AIRBOSS class.", _unit:GetTypeName()) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + self:T2(self.lid..text) + return + end + + -- Check that coalition of the carrier and aircraft match. + if self:GetCoalition()~=_unit:GetCoalition() then + local text=string.format("Player entered aircraft of other coalition.") + MESSAGE:New(text, 30):ToAllIf(self.Debug) + self:T(self.lid..text) + return + end + + -- Add Menu commands. + self:_AddF10Commands(_unitName) + + -- Delaying the new player for a second, because AI units of the flight would not be registered correctly. + --SCHEDULER:New(nil, self._NewPlayer, {self, _unitName}, 1) + self:ScheduleOnce(1, self._NewPlayer, self, _unitName) + + end +end + +--- Airboss event handler for event land. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventLand(EventData) + self:F3({eventland = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event LAND!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event LAND!") + self:E(EventData) + return + end + + -- Get unit name that landed. + local _unitName=EventData.IniUnitName + + -- Check if this was a player. + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + -- Debug output. + self:T(self.lid.."LAND: unit = "..tostring(EventData.IniUnitName)) + self:T(self.lid.."LAND: group = "..tostring(EventData.IniGroupName)) + self:T(self.lid.."LAND: player = "..tostring(_playername)) + + -- This would be the closest airbase. + local airbase=EventData.Place + + -- Nil check for airbase. Crashed as player gave me no airbase. + if airbase==nil then + return + end + + -- Get airbase name. + local airbasename=tostring(airbase:GetName()) + + -- Check if aircraft landed on the right airbase. + if airbasename==self.airbase:GetName() then + + -- Stern coordinate at the rundown. + local stern=self:_GetSternCoord() + + -- Polygon zone close around the carrier. + local zoneCarrier=self:_GetZoneCarrierBox() + + -- Check if player or AI landed. + if _unit and _playername then + + ------------------------- + -- Human Player landed -- + ------------------------- + + -- Get info. + local _uid=_unit:GetID() + local _group=_unit:GetGroup() + local _callsign=_unit:GetCallsign() + + -- Debug output. + local text=string.format("Player %s, callsign %s unit %s (ID=%d) of group %s landed at airbase %s", _playername, _callsign, _unitName, _uid, _group:GetName(), airbasename) + self:T(self.lid..text) + MESSAGE:New(text, 5, "DEBUG"):ToAllIf(self.Debug) + + -- Player data. + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + -- Check if playerData is okay. + if playerData==nil then + self:E(self.lid..string.format("ERROR: playerData nil in landing event. unit=%s player=%s", tostring(_unitName), tostring(_playername))) + return + end + + -- Check that player landed on the carrier. + if _unit:IsInZone(zoneCarrier) then + + -- Check if this was a valid approach. + if not playerData.valid then + -- Player missed at least one step in the pattern. + local text=string.format("you missed at least one important step in the pattern!\nYour next step would have been %s.\nThis pass is INVALID.", playerData.step) + self:MessageToPlayer(playerData, text, "AIRBOSS", nil, 30, true, 5) + + -- Clear queues just in case. + self:_RemoveFlightFromMarshalQueue(playerData, true) + self:_RemoveFlightFromQueue(self.Qpattern, playerData) + self:_RemoveFlightFromQueue(self.Qwaiting, playerData) + self:_RemoveFlightFromQueue(self.Qspinning, playerData) + + -- Reinitialize player data. + self:_InitPlayer(playerData) + + return + end + + -- Check if player already landed. We dont need a second time. + if playerData.landed then + + self:E(self.lid..string.format("Player %s just landed a second time.", _playername)) + + else + + -- We did land. + playerData.landed=true + + -- Switch attitude monitor off if on. + playerData.attitudemonitor=false + + -- Coordinate at landing event. + local coord=playerData.unit:GetCoordinate() + + -- Get distances relative to + local X,Z,rho,phi=self:_GetDistances(_unit) + + -- Landing distance wrt to stern position. + local dist=coord:Get2DDistance(stern) + + -- Debug mark of player landing coord. + if self.Debug and false then + -- Debug mark of player landing coord. + local lp=coord:MarkToAll("Landing coord.") + coord:SmokeGreen() + end + + -- Set time in the groove of player. + self:_SetTimeInGroove(playerData) + + -- Debug text. + local text=string.format("Player %s AC type %s landed at dist=%.1f m. Tgroove=%.1f sec.", playerData.name, playerData.actype, dist, self:_GetTimeInGroove(playerData)) + text=text..string.format(" X=%.1f m, Z=%.1f m, rho=%.1f m.", X, Z, rho) + self:T(self.lid..text) + + -- Check carrier type. + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + + -- Power "Idle". + self:RadioTransmission(self.LSORadio, self.LSOCall.IDLE, false, 1, nil, true) + + -- Next step debrief. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) + + else + + -- Next step undefined until we know more. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.UNDEFINED) + + -- Call trapped function in 1 second to make sure we did not bolter. + --SCHEDULER:New(nil, self._Trapped, {self, playerData}, 1) + self:ScheduleOnce(1, self._Trapped, self, playerData) + + end + + end + + else + -- Handle case where player did not land on the carrier. + -- Well, I guess, he leaves the slot or ejects. Both should be handled. + if playerData then + self:E(self.lid..string.format("Player %s did not land in carrier box zone. Maybe in the water near the carrier?", playerData.name)) + end + end + + else + + -------------------- + -- AI unit landed -- + -------------------- + + if self.carriertype~=AIRBOSS.CarrierType.TARAWA then + + -- Coordinate at landing event + local coord=EventData.IniUnit:GetCoordinate() + + -- Debug mark of player landing coord. + local dist=coord:Get2DDistance(self:GetCoordinate()) + + -- Get wire + local wire=self:_GetWire(coord, 0) + + -- Aircraft type. + local _type=EventData.IniUnit:GetTypeName() + + -- Debug text. + local text=string.format("AI unit %s of type %s landed at dist=%.1f m. Trapped wire=%d.", _unitName, _type, dist, wire) + self:T(self.lid..text) + + end + + -- AI always lands ==> remove unit from flight group and queues. + local flight=self:_RecoveredElement(EventData.IniUnit) + + -- Check if all were recovered. If so update pattern queue. + self:_CheckSectionRecovered(flight) + + end + end +end + +--- Airboss event handler for event that a unit shuts down its engines. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventEngineShutdown(EventData) + self:F3({eventengineshutdown=EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event ENGINESHUTDOWN!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event ENGINESHUTDOWN!") + self:E(EventData) + return + end + + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."ENGINESHUTDOWN: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."ENGINESHUTDOWN: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."ENGINESHUTDOWN: player = "..tostring(_playername)) + + if _unit and _playername then + + -- Debug message. + self:T(self.lid..string.format("Player %s shut down its engines!",_playername)) + + else + + -- Debug message. + self:T(self.lid..string.format("AI unit %s shut down its engines!", _unitName)) + + -- Get flight. + local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) + + -- Only AI flights. + if flight and flight.ai then + + -- Check if all elements were recovered. + local recovered=self:_CheckSectionRecovered(flight) + + -- Despawn group and completely remove flight. + if recovered then + self:T(self.lid..string.format("AI group %s completely recovered. Despawning group after engine shutdown event as requested in 5 seconds.", tostring(EventData.IniGroupName))) + + -- Remove flight. + self:_RemoveFlight(flight) + + -- Check if this is a tanker or AWACS associated with the carrier. + local istanker=self.tanker and self.tanker.tanker:GetName()==EventData.IniGroupName + local isawacs=self.awacs and self.awacs.tanker:GetName()==EventData.IniGroupName + + -- Destroy group if desired. Recovery tankers have their own logic for despawning. + if self.despawnshutdown and not (istanker or isawacs) then + EventData.IniGroup:Destroy(nil, 5) + end + + end + + end + end +end + +--- Airboss event handler for event that a unit takes off. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventTakeoff(EventData) + self:F3({eventtakeoff=EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event TAKEOFF!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event TAKEOFF!") + self:E(EventData) + return + end + + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."TAKEOFF: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."TAKEOFF: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."TAKEOFF: player = "..tostring(_playername)) + + -- Airbase. + local airbase=EventData.Place + + -- Airbase name. + local airbasename="unknown" + if airbase then + airbasename=airbase:GetName() + end + + -- Check right airbase. + if airbasename==self.airbase:GetName() then + + if _unit and _playername then + + -- Debug message. + self:T(self.lid..string.format("Player %s took off at %s!",_playername, airbasename)) + + else + + -- Debug message. + self:T2(self.lid..string.format("AI unit %s took off at %s!", _unitName, airbasename)) + + -- Get flight. + local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) + + if flight then + + -- Set ballcall and recoverd status. + for _,elem in pairs(flight.elements) do + local element=elem --#AIRBOSS.FlightElement + element.ballcall=false + element.recovered=nil + end + end + end + + end +end + +--- Airboss event handler for event crash. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventCrash(EventData) + self:F3({eventcrash = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event CRASH!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event CRASH!") + self:E(EventData) + return + end + + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."CRASH: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."CRASH: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."CARSH: player = "..tostring(_playername)) + + if _unit and _playername then + -- Debug message. + self:T(self.lid..string.format("Player %s crashed!",_playername)) + + -- Get player flight. + local flight=self.players[_playername] + + -- Remove flight completely from all queues and collapse marshal if necessary. + -- This also updates the section, if any and removes any unfinished gradings of the player. + if flight then + self:_RemoveFlight(flight, true) + end + + else + -- Debug message. + self:T2(self.lid..string.format("AI unit %s crashed!", EventData.IniUnitName)) + + -- Remove unit from flight and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) + end + +end + +--- Airboss event handler for event Ejection. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventEjection(EventData) + self:F3({eventland = EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event EJECTION!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event EJECTION!") + self:E(EventData) + return + end + + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."EJECT: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."EJECT: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."EJECT: player = "..tostring(_playername)) + + if _unit and _playername then + self:T(self.lid..string.format("Player %s ejected!",_playername)) + + -- Get player flight. + local flight=self.players[_playername] + + -- Remove flight completely from all queues and collapse marshal if necessary. + if flight then + self:_RemoveFlight(flight, true) + end + + else + -- Debug message. + self:T(self.lid..string.format("AI unit %s ejected!", EventData.IniUnitName)) + + -- Remove element/unit from flight group and from all queues if no elements alive. + self:_RemoveUnitFromFlight(EventData.IniUnit) + + -- What could happen is, that another element has landed (recovered) already and this one crashes. + -- This would mean that the flight would not be deleted from the queue ==> Check if section recovered. + local flight=self:_GetFlightFromGroupInQueue(EventData.IniGroup, self.flights) + self:_CheckSectionRecovered(flight) + end + +end + +--- Airboss event handler for event player leave unit. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +--function AIRBOSS:OnEventPlayerLeaveUnit(EventData) +function AIRBOSS:_PlayerLeft(EventData) + self:F3({eventleave=EventData}) + + -- Nil checks. + if EventData==nil then + self:E(self.lid.."ERROR: EventData=nil in event PLAYERLEFTUNIT!") + self:E(EventData) + return + end + if EventData.IniUnit==nil then + self:E(self.lid.."ERROR: EventData.IniUnit=nil in event PLAYERLEFTUNIT!") + self:E(EventData) + return + end + + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."PLAYERLEAVEUNIT: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."PLAYERLEAVEUNIT: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."PLAYERLEAVEUNIT: player = "..tostring(_playername)) + + if _unit and _playername then + + -- Debug info. + self:T(self.lid..string.format("Player %s left unit %s!",_playername, _unitName)) + + -- Get player flight. + local flight=self.players[_playername] + + -- Remove flight completely from all queues and collapse marshal if necessary. + if flight then + self:_RemoveFlight(flight, true) + end + + end + +end + +--- Airboss event function handling the mission end event. +-- Handles the case when the mission is ended. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData Event data. +function AIRBOSS:OnEventMissionEnd(EventData) + self:T3(self.lid.."Mission Ended") +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- PATTERN functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Spinning +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Spinning(playerData) + + -- Early break. + local SpinIt={} + SpinIt.name="Spinning" + SpinIt.Xmin=-UTILS.NMToMeters(6) -- Not more than 5 NM behind the boat. + SpinIt.Xmax= UTILS.NMToMeters(5) -- Not more than 5 NM in front of the boat. + SpinIt.Zmin=-UTILS.NMToMeters(6) -- Not more than 5 NM port. + SpinIt.Zmax= UTILS.NMToMeters(2) -- Not more than 3 NM starboard. + SpinIt.LimitXmin=-100 -- 100 meters behind the boat + SpinIt.LimitXmax=nil + SpinIt.LimitZmin=-UTILS.NMToMeters(1) -- 1 NM port + SpinIt.LimitZmax=nil + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, SpinIt) then + + -- Player is "de-spinned". Should go to initial again. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.INITIAL) + + -- Remove player from spinning queue. + self:_RemoveFlightFromQueue(self.Qspinning, playerData) + + end + +end + +--- Waiting outside 10 NM zone for free Marshal stack. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Waiting(playerData) + + -- Create 10 NM zone around the carrier. + local radius=UTILS.NMToMeters(10) + local zone=ZONE_RADIUS:New("Carrier 10 NM Zone", self.carrier:GetVec2(), radius) + + -- Check if player is inside 10 NM radius of the carrier. + local inzone=playerData.unit:IsInZone(zone) + + -- Time player is waiting. + local Twaiting=timer.getAbsTime()-playerData.time + + -- Warning if player is inside the zone. + if inzone and Twaiting>3*60 and not playerData.warning then + local text=string.format("You are supposed to wait outside the 10 NM zone.") + self:MessageToPlayer(playerData, text, "AIRBOSS") + playerData.warning=true + end + + -- Reset warning. + if inzone==false and playerData.warning==true then + playerData.warning=nil + end + +end + +--- Holding. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Holding(playerData) + + -- Player unit and flight. + local unit=playerData.unit + + -- Current stack. + local stack=playerData.flag + + -- Check for reported error. + if stack<=0 then + local text=string.format("ERROR: player %s in step %s is holding but has stack=%s (<=0)", playerData.name, playerData.step, tostring(stack)) + self:E(self.lid..text) + end + + --------------------------- + -- Holding Pattern Check -- + --------------------------- + + -- Pattern altitude. + local patternalt=self:_GetMarshalAltitude(stack, playerData.case) + + -- Player altitude. + local playeralt=unit:GetAltitude() + + -- Get holding zone of player. + local zoneHolding=self:_GetZoneHolding(playerData.case, stack) + + -- Nil check. + if zoneHolding==nil then + self:E(self.lid.."ERROR: zoneHolding is nil!") + self:E({playerData=playerData}) + return + end + + -- Check if player is in holding zone. + local inholdingzone=unit:IsInZone(zoneHolding) + + -- Altitude difference between player and assigned stack. + local altdiff=playeralt-patternalt + + -- Acceptable altitude depending on player skill. + local altgood=UTILS.FeetToMeters(500) + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- Pros can be expected to be within +-200 ft. + altgood=UTILS.FeetToMeters(200) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- Normal guys should be within +-350 ft. + altgood=UTILS.FeetToMeters(350) + elseif playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Students should be within +-500 ft. + altgood=UTILS.FeetToMeters(500) + end + + -- When back to good altitude = 50%. + local altback=altgood*0.5 + + -- Check if stack just collapsed and give the player one minute to change the altitude. + local justcollapsed=false + if self.Tcollapse then + -- Time since last stack change. + local dT=timer.getTime()-self.Tcollapse + + -- TODO: check if this works. + --local dT=timer.getAbsTime()-playerData.time + + -- Check if less then 90 seconds. + if dT<=90 then + justcollapsed=true + end + end + + -- Check if altitude is acceptable. + local goodalt=math.abs(altdiff)altgood then + + -- Issue warning for being too high. + if not playerData.warning then + text=text..string.format("You left your assigned altitude. Descent to angels %d.", angels) + playerData.warning=true + end + + elseif altdiff<-altgood then + + -- Issue warning for being too low. + if not playerData.warning then + text=text..string.format("You left your assigned altitude. Climb to angels %d.", angels) + playerData.warning=true + end + + end + + end + + -- Back to assigned altitude. + if playerData.warning and math.abs(altdiff)<=altback then + text=text..string.format("Altitude is looking good again.") + playerData.warning=nil + end + + elseif playerData.holding==false then + + -- Player left holding zone + if inholdingzone then + -- Player is back in the holding zone. + text=text..string.format("You are back in the holding zone. Now stay there!") + playerData.holding=true + else + -- Player is still outside the holding zone. + self:T3("Player still outside the holding zone. What are you doing man?!") + end + + elseif playerData.holding==nil then + -- Player did not entered the holding zone yet. + + if inholdingzone then + + -- Player arrived in holding zone. + playerData.holding=true + + -- Inform player. + text=text..string.format("You arrived at the holding zone.") + + -- Feedback on altitude. + if goodalt then + text=text..string.format(" Altitude is good.") + else + if altdiff<0 then + text=text..string.format(" But you're too low.") + else + text=text..string.format(" But you're too high.") + end + text=text..string.format("\nCurrently assigned altitude is %d ft.", UTILS.MetersToFeet(patternalt)) + playerData.warning=true + end + + else + -- Player did not yet arrive in holding zone. + self:T3("Waiting for player to arrive in the holding zone.") + end + + end + + -- Send message. + if playerData.showhints then + self:MessageToPlayer(playerData, text, "MARSHAL") + end + +end + + +--- Commence approach. This step initializes the player data. Section members are also set to commence. Next step depends on recovery case: +-- +-- * Case 1: Initial +-- * Case 2/3: Platform +-- +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #boolean zonecheck If true, zone is checked before player is released. +function AIRBOSS:_Commencing(playerData, zonecheck) + + -- Check for auto commence + if zonecheck then + + -- Get auto commence zone. + local zoneCommence=self:_GetZoneCommence(playerData.case, playerData.flag) + + -- Check if unit is in the zone. + local inzone=playerData.unit:IsInZone(zoneCommence) + + -- Skip the rest if not in the zone yet. + if not inzone then + + -- Friendly reminder. + if timer.getAbsTime()-playerData.time>180 then + self:_MarshalCallClearedForRecovery(playerData.onboard, playerData.case) + playerData.time=timer.getAbsTime() + end + + -- Skip the rest. + return + end + + end + + -- Remove flight from Marshal queue. If flight was in queue, stack is collapsed and flight added to the pattern queue. + self:_RemoveFlightFromMarshalQueue(playerData) + + -- Initialize player data for new approach. + self:_InitPlayer(playerData) + + -- Commencing message to player only. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Text + local text="" + + -- Positive response. + if playerData.case==1 then + text=text.."Proceed to initial." + else + text=text.."Descent to platform." + if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.showhints then + text=text.." VSI 4000 ft/min until you reach 5000 ft." + end + end + + -- Message to player. + self:MessageToPlayer(playerData, text, "MARSHAL") + end + + -- Next step: depends on case recovery. + local nextstep + if playerData.case==1 then + -- CASE I: Player has to fly to the initial which is 3 NM DME astern of the boat. + nextstep=AIRBOSS.PatternStep.INITIAL + else + -- CASE II/III: Player has to start the descent at 4000 ft/min to the platform at 5k ft. + nextstep=AIRBOSS.PatternStep.PLATFORM + end + + -- Next step hint. + self:_SetPlayerStep(playerData, nextstep) + + -- Commence section members as well but dont check the zone. + for i,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + self:_Commencing(flight, false) + end + +end + +--- Start pattern when player enters the initial zone in case I/II recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #boolean True if player is in the initial zone. +function AIRBOSS:_Initial(playerData) + + -- Check if player is in initial zone and entering the CASE I pattern. + local inzone=playerData.unit:IsInZone(self:_GetZoneInitial(playerData.case)) + + -- Relative heading to carrier direction. + local relheading=self:_GetRelativeHeading(playerData.unit, false) + + -- Alitude of player in feet. + local altitude=playerData.unit:GetAltitude() + + -- Check if player is in zone and flying roughly in the right direction. + if inzone and math.abs(relheading)<60 and altitude<=self.initialmaxalt then + + -- Send message for normal and easy difficulty. + if playerData.showhints then + + -- Inform player. + local hint=string.format("Initial") + + -- Hook down for students. + if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then + if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + hint=hint.." - Hook down, SAS on, Wing Sweep 68°!" + else + hint=hint.." - Hook down!" + end + end + + self:MessageToPlayer(playerData, hint, "MARSHAL") + end + + -- Next step: Break entry. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.BREAKENTRY) + + return true + end + + return false +end + +--- Check if player is in CASE II/III approach corridor. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_CheckCorridor(playerData) + + -- Check if player is in valid zone + local validzone=self:_GetZoneCorridor(playerData.case) + + -- Check if we are inside the moving zone. + local invalid=playerData.unit:IsNotInZone(validzone) + + -- Issue warning. + if invalid and (not playerData.warning) then + self:MessageToPlayer(playerData, "you left the approach corridor!", "AIRBOSS") + playerData.warning=true + end + + -- Back in zone. + if (not invalid) and playerData.warning then + self:MessageToPlayer(playerData, "you're back in the approach corridor.", "AIRBOSS") + playerData.warning=false + end + +end + +--- Platform at 5k ft for case II/III recoveries. Descent at 2000 ft/min. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Platform(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZonePlatform(playerData.case)) + + -- Check if we are in zone. + if inzone then + + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + + -- Next step: depends. + local nextstep + if math.abs(self.holdingoffset)>0 and playerData.case>1 then + -- Turn to BRC (case II) or FB (case III). + nextstep=AIRBOSS.PatternStep.ARCIN + else + if playerData.case==2 then + -- Case II: Initial zone then Case I recovery. + nextstep=AIRBOSS.PatternStep.INITIAL + elseif playerData.case==3 then + -- CASE III: Dirty up. + nextstep=AIRBOSS.PatternStep.DIRTYUP + end + end + + -- Next step hint. + self:_SetPlayerStep(playerData, nextstep) + + end +end + + +--- Arc in turn for case II/III recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_ArcInTurn(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneArcIn(playerData.case)) + + if inzone then + + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + + -- Next step: Arc Out Turn. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.ARCOUT) + + end +end + +--- Arc out turn for case II/III recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_ArcOutTurn(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneArcOut(playerData.case)) + + if inzone then + + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + + -- Next step: + local nextstep + if playerData.case==3 then + -- Case III: Dirty up. + nextstep=AIRBOSS.PatternStep.DIRTYUP + else + -- Case II: Initial. + nextstep=AIRBOSS.PatternStep.INITIAL + end + + -- Next step hint. + self:_SetPlayerStep(playerData, nextstep) + end +end + +--- Dirty up and level out at 1200 ft for case III recovery. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_DirtyUp(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneDirtyUp(playerData.case)) + + if inzone then + + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + + -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. + if playerData.actype==AIRBOSS.AircraftCarrier.HORNET or playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + local callsay=self:_NewRadioCall(self.MarshalCall.SAYNEEDLES, nil, nil, 5, playerData.onboard) + local callfly=self:_NewRadioCall(self.MarshalCall.FLYNEEDLES, nil, nil, 5, playerData.onboard) + self:RadioTransmission(self.MarshalRadio, callsay, false, 55, nil, true) + self:RadioTransmission(self.MarshalRadio, callfly, false, 60, nil, true) + end + + -- TODO: Make Fly Bullseye call if no automatic ICLS is active. + + -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.BULLSEYE) + + end +end + +--- Intercept glide slop and follow ICLS, aka Bullseye for case III recovery. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #boolean If true, player is in bullseye zone. +function AIRBOSS:_Bullseye(playerData) + + -- Check if player left or got back to the approach corridor. + self:_CheckCorridor(playerData) + + -- Check if we are inside the moving zone. + local inzone=playerData.unit:IsInZone(self:_GetZoneBullseye(playerData.case)) + + -- Relative heading to carrier direction of the runway. + local relheading=self:_GetRelativeHeading(playerData.unit, true) + + -- Check if player is in zone and flying roughly in the right direction. + if inzone and math.abs(relheading)<60 then + + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + + -- LSO expect spot 7.5 call + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + self:RadioTransmission(self.LSORadio, self.LSOCall.EXPECTSPOT75, nil, nil, nil, true) + end + + -- Next step: Groove Call the ball. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) + + end +end + +--- Bolter pattern. Sends player to abeam for Case I/II or Bullseye for Case III ops. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_BolterPattern(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z, rho, phi=self:_GetDistances(playerData.unit) + + -- Bolter Pattern thresholds. + local Bolter={} + Bolter.name="Bolter Pattern" + Bolter.Xmin=-UTILS.NMToMeters(5) -- Not more then 5 NM astern of boat. + Bolter.Xmax= UTILS.NMToMeters(3) -- Not more then 3 NM ahead of boat. + Bolter.Zmin=-UTILS.NMToMeters(5) -- Not more than 2 NM port. + Bolter.Zmax= UTILS.NMToMeters(1) -- Not more than 1 NM starboard. + Bolter.LimitXmin= 100 -- Check that 100 meter ahead and port + Bolter.LimitXmax= nil + Bolter.LimitZmin= nil + Bolter.LimitZmax= nil + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, Bolter) then + local nextstep + if playerData.case<3 then + nextstep=AIRBOSS.PatternStep.ABEAM + else + nextstep=AIRBOSS.PatternStep.BULLSEYE + end + self:_SetPlayerStep(playerData, nextstep) + end +end + + + +--- Break entry for case I/II recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_BreakEntry(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z=self:_GetDistances(playerData.unit) + + -- Abort condition check. + if self:_CheckAbort(X, Z, self.BreakEntry) then + self:_AbortPattern(playerData, X, Z, self.BreakEntry, true) + return + end + + -- Check if we are in front of the boat (diffX > 0). + if self:_CheckLimits(X, Z, self.BreakEntry) then + + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + + -- Next step: Early Break. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.EARLYBREAK) + + end +end + + +--- Break. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #string part Part of the break. +function AIRBOSS:_Break(playerData, part) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z=self:_GetDistances(playerData.unit) + + -- Early or late break. + local breakpoint = self.BreakEarly + if part==AIRBOSS.PatternStep.LATEBREAK then + breakpoint = self.BreakLate + end + + -- Check abort conditions. + if self:_CheckAbort(X, Z, breakpoint) then + self:_AbortPattern(playerData, X, Z, breakpoint, true) + return + end + + -- Player made a very tight turn and did not trigger the latebreak threshold at 0.8 NM. + local tooclose=false + if part==AIRBOSS.PatternStep.LATEBREAK then + local close=0.8 + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + close=0.5 + end + if X<0 and Z90 and self:_CheckLimits(X, Z, self.Wake) then + -- Message to player. + self:MessageToPlayer(playerData, "you are already at the wake and have not passed the 90. Turn faster next time!", "LSO") + self:RadioTransmission(self.LSORadio, self.LSOCall.DEPARTANDREENTER, nil, nil, nil, true) + playerData.wop=true + -- Debrief. + self:_AddToDebrief(playerData, "Overshoot at wake - Pattern Waveoff!") + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) + end +end + +--- At the Wake. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Wake(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z=self:_GetDistances(playerData.unit) + + -- Check abort conditions. + if self:_CheckAbort(X, Z, self.Wake) then + self:_AbortPattern(playerData, X, Z, self.Wake, true) + return + end + + -- Right behind the wake of the carrier dZ>0. + if self:_CheckLimits(X, Z, self.Wake) then + + -- Hint for player about altitude, AoA etc. + self:_PlayerHint(playerData) + + -- Next step: Final. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.FINAL) + + end +end + +--- Get groove data. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #AIRBOSS.GrooveData Groove data table. +function AIRBOSS:_GetGrooveData(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier). + local X, Z=self:_GetDistances(playerData.unit) + + -- Stern position at the rundown. + local stern=self:_GetSternCoord() + + -- Distance from rundown to player aircraft. + local rho=stern:Get2DDistance(playerData.unit:GetCoordinate()) + + -- Aircraft is behind the carrier. + local astern=X5. This would mean the player has not turned in correctly! + + -- Groove data. + playerData.groove.X0=UTILS.DeepCopy(groovedata) + + -- Set time stamp. Next call in 4 seconds. + playerData.Tlso=timer.getTime() + + -- Next step: X start. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_XX) + end + + -- Groovedata step. + groovedata.Step=playerData.step + +end + +--- In the groove. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Groove(playerData) + + -- Ranges in the groove. + local RX0=UTILS.NMToMeters(1.000) -- Everything before X 1.00 = 1852 m + local RXX=UTILS.NMToMeters(0.750) -- Start of groove. 0.75 = 1389 m + local RIM=UTILS.NMToMeters(0.500) -- In the Middle 0.50 = 926 m (middle one third of the glideslope) + local RIC=UTILS.NMToMeters(0.250) -- In Close 0.25 = 463 m (last one third of the glideslope) + local RAR=UTILS.NMToMeters(0.040) -- At the Ramp. 0.04 = 75 m + + -- Groove data. + local groovedata=self:_GetGrooveData(playerData) + + -- Add data to trapsheet. + table.insert(playerData.trapsheet, groovedata) + + -- Coords. + local X=groovedata.X + local Z=groovedata.Z + + -- Check abort conditions. + if self:_CheckAbort(groovedata.X, groovedata.Z, self.Groove) then + self:_AbortPattern(playerData, groovedata.X, groovedata.Z, self.Groove, true) + return + end + + -- Shortcuts. + local rho=groovedata.Rho + local lineupError=groovedata.LUE + local glideslopeError=groovedata.GSE + local AoA=groovedata.AoA + + + if rho<=RXX and playerData.step==AIRBOSS.PatternStep.GROOVE_XX and (math.abs(groovedata.Roll)<=4.0 or playerData.unit:IsInZone(self:_GetZoneLineup())) then + + -- Start time in groove + playerData.TIG0=timer.getTime() + + -- LSO "Call the ball" call. + self:RadioTransmission(self.LSORadio, self.LSOCall.CALLTHEBALL, nil, nil, nil, true) + playerData.Tlso=timer.getTime() + + -- Pilot "405, Hornet Ball, 3.2". + + -- LSO "Roger ball" call in three seconds. + self:RadioTransmission(self.LSORadio, self.LSOCall.ROGERBALL, false, nil, 2, true) + + -- Store data. + playerData.groove.XX=UTILS.DeepCopy(groovedata) + + -- This is a valid approach and player did not miss any important steps in the pattern. + playerData.valid=true + + -- Next step: in the middle. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IM) + + elseif rho<=RIM and playerData.step==AIRBOSS.PatternStep.GROOVE_IM then + + -- Store data. + playerData.groove.IM=UTILS.DeepCopy(groovedata) + + -- Next step: in close. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IC) + + elseif rho<=RIC and playerData.step==AIRBOSS.PatternStep.GROOVE_IC then + + -- Store data. + playerData.groove.IC=UTILS.DeepCopy(groovedata) + + -- Next step: AR at the ramp. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_AR) + + elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AR then + + -- Store data. + playerData.groove.AR=UTILS.DeepCopy(groovedata) + + -- Next step: in the wires. + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_AL) + else + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_IW) + end + + elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AL then + + -- Store data. + playerData.groove.AL=UTILS.DeepCopy(groovedata) + + -- Get zone abeam LDG spot. + local ZoneALS=self:_GetZoneAbeamLandingSpot() + + -- Get player velocity in km/h. + local vplayer=playerData.unit:GetVelocityKMH() + + -- Get carrier velocity in km/h. + local vcarrier=self.carrier:GetVelocityKMH() + + -- Speed difference. + local dv=math.abs(vplayer-vcarrier) + + -- Stable when speed difference < 10 km/h. + local stable=dv<10 + + -- Check if player is inside the zone. + if playerData.unit:IsInZone(ZoneALS) and stable then + + -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. + self:RadioTransmission(self.LSORadio, self.LSOCall.CLEAREDTOLAND, nil, nil, nil, true) + + -- Next step: Level cross. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.GROOVE_LC) + end + + elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_LC then + + -- Store data. + playerData.groove.LC=UTILS.DeepCopy(groovedata) + + -- Get zone primary LDG spot. + local ZoneLS=self:_GetZoneLandingSpot() + + -- Get player velocity in km/h. + local vplayer=playerData.unit:GetVelocityKMH() + + -- Get carrier velocity in km/h. + local vcarrier=self.carrier:GetVelocityKMH() + + -- Speed difference. + local dv=math.abs(vplayer-vcarrier) + + -- Stable when v<7.5 km/h. + local stable=dv<7.5 + + -- Radio Transmission "Cleared to land" once the aircraft is inside the zone. + if playerData.unit:IsInZone(ZoneLS) and stable and playerData.warning==false then + self:RadioTransmission(self.LSORadio, self.LSOCall.STABILIZED, nil, nil, nil, true) + playerData.warning=true + end + + -- We keep it in this step until landed. + + end + + -------------- + -- Wave Off -- + -------------- + + -- Between IC and AR check for wave off. + if rho>=RAR and rho<=RIC and not playerData.waveoff then + + -- Check if player should wave off. + local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) + + -- Let's see.. + if waveoff then + + -- Debug info. + self:T3(self.lid..string.format("Waveoff distance rho=%.1f m", rho)) + + -- LSO Wave off! + self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF, nil, nil, nil, true) + playerData.Tlso=timer.getTime() + + -- Player was waved off! + playerData.waveoff=true + + -- Nothing else necessary. + return + end + + end + + -- Groovedata step. + groovedata.Step=playerData.step + + ----------------- + -- Groove Data -- + ----------------- + + -- Check if we are beween 3/4 NM and end of ship. + if rho>=RAR and rhomath.abs(gd.LUE) then + self:T(self.lid..string.format("Got bigger LUE at step %s, d=%.3f: LUE %.3f>%.3f", gs, d, lineupError, gd.LUE)) + gd.LUE=lineupError + end + + -- Fly through good window of glideslope. + if gd.GSE>0.4 and glideslopeError<-0.3 then + -- Fly through down ==> "\" + gd.FlyThrough="\\" + self:T(self.lid..string.format("Got Fly through DOWN at step %s, d=%.3f: Max GSE=%.3f, lower GSE=%.3f", gs, d, gd.GSE, glideslopeError)) + elseif gd.GSE<-0.3 and glideslopeError>0.4 then + -- Fly through up ==> "/" + gd.FlyThrough="/" + self:E(self.lid..string.format("Got Fly through UP at step %s, d=%.3f: Min GSE=%.3f, lower GSE=%.3f", gs, d, gd.GSE, glideslopeError)) + end + + -- Update max deviation of glideslope error. + if math.abs(glideslopeError)>math.abs(gd.GSE) then + self:T(self.lid..string.format("Got bigger GSE at step %s, d=%.3f: GSE |%.3f|>|%.3f|", gs, d, glideslopeError, gd.GSE)) + gd.GSE=glideslopeError + end + + -- Get aircraft AoA parameters. + local aircraftaoa=self:_GetAircraftAoA(playerData) + + -- On Speed AoA. + local aoaopt=aircraftaoa.OnSpeed + + -- Compare AoAs wrt on speed AoA and update max deviation. + if math.abs(AoA-aoaopt)>math.abs(gd.AoA-aoaopt) then + self:T(self.lid..string.format("Got bigger AoA error at step %s, d=%.3f: AoA %.3f>%.3f.", gs, d, AoA, gd.AoA)) + gd.AoA=AoA + end + + --local gs2=self:_GS(groovedata.Step, -1) + --env.info(string.format("groovestep %s %s d=%.3f NM: GSE=%.3f %.3f, LUE=%.3f %.3f, AoA=%.3f %.3f", gs, gs2, d, groovedata.GSE, gd.GSE, groovedata.LUE, gd.LUE, groovedata.AoA, gd.AoA)) + + end + + --------------- + -- LSO Calls -- + --------------- + + -- Time since last LSO call. + local deltaT=timer.getTime()-playerData.Tlso + + -- Wait until player passed the 0.75 NM distance. + local _advice=true + if playerData.TIG0==nil and playerData.difficulty~=AIRBOSS.Difficulty.EASY then --rho>RXX + _advice=false + end + + -- LSO call if necessary. + if deltaT>=self.LSOdT and _advice then + self:_LSOadvice(playerData, glideslopeError, lineupError) + end + + end + + ---------------------------------------------------------- + --- Some time here the landing event MIGHT be triggered -- + ---------------------------------------------------------- + + -- Player infront of the carrier X>~77 m. + if X>self.carrierparam.totlength+self.carrierparam.sterndist then + + if playerData.waveoff then + + if playerData.landed then + -- This should not happen because landing event was triggered. + self:_AddToDebrief(playerData, "You were waved off but landed anyway. Airboss wants to talk to you!") + else + self:_AddToDebrief(playerData, "You were waved off.") + end + + elseif playerData.boltered then + + -- This should not happen because landing event was triggered. + self:_AddToDebrief(playerData, "You boltered.") + + else + + -- This should not happen. + self:T("Player was not waved off but flew past the carrier without landing ==> Own wave off!") + + -- We count this as OWO. + self:_AddToDebrief(playerData, "Own waveoff.") + + -- Set Owo + playerData.owo=true + + end + + -- Next step: debrief. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.DEBRIEF) + + end + +end + +--- LSO check if player needs to wave off. +-- Wave off conditions are: +-- +-- * Glideslope error <1.2 or >1.8 degrees. +-- * |Line up error| > 3 degrees. +-- * AoA check but only for TOPGUN graduates. +-- @param #AIRBOSS self +-- @param #number glideslopeError Glideslope error in degrees. +-- @param #number lineupError Line up error in degrees. +-- @param #number AoA Angle of attack of player aircraft. +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @return #boolean If true, player should wave off! +function AIRBOSS:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) + + -- Assume we're all good. + local waveoff=false + + -- Parameters + local glMax= 1.8 + local glMin=-1.2 + local luAbs= 3.0 + + -- For the harrier, we allow a bit more room. + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + glMax= 4.0 + glMin=-3.0 + luAbs= 5.0 + -- No waveoff for harrier pilots at the moment. + return false + end + + -- Too high or too low? + if glideslopeError>glMax then + local text=string.format("\n- Waveoff due to glideslope error %.2f > %.1f degrees!", glideslopeError, glMax) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + waveoff=true + elseif glideslopeErrorluAbs then + local text=string.format("\n- Waveoff due to line up error |%.1f| > %.1f degrees!", lineupError, luAbs) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + waveoff=true + end + + -- Too slow or too fast? Only for pros. + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- Get aircraft specific AoA values + local aoaac=self:_GetAircraftAoA(playerData) + -- Check too slow or too fast. + if AoAaoaac.SLOW then + local text=string.format("\n- Waveoff due to AoA %.1f > %.1f!", AoA, aoaac.SLOW) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + waveoff=true + end + end + + return waveoff +end + +--- Check if other aircraft are currently on the landing runway. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @return boolean If true, we have a foul deck. +function AIRBOSS:_CheckFoulDeck(playerData) + + -- Assume no check necessary. + local check=false + + -- CVN: Check at IM and IC. + if playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC then + check=true + end + + -- AV-8B check until + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + if playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_AL then + check=true + end + end + + -- Check if player was already waved off. Should not be necessary as player step is set to debrief afterwards! + if playerData.wofd==true or check==false then + -- Player was already waved off. + return + end + + -- Landing runway zone. + local runway=self:_GetZoneRunwayBox() + + -- For AB-8B we just check the primary landing spot. + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + runway=self:_GetZoneLandingSpot() + end + + -- Scan radius. + local R=250 + + -- Debug info. + self:T(self.lid..string.format("Foul deck check: Scanning Carrier Runway Area. Radius=%.1f m.", R)) + + -- Scan units in carrier zone. + local _,_,_,unitscan=self:GetCoordinate():ScanObjects(R, true, false, false) + + -- Loop over all scanned units and check if they are on the runway. + local fouldeck=false + local foulunit=nil --Wrapper.Unit#UNIT + for _,_unit in pairs(unitscan) do + local unit=_unit --Wrapper.Unit#UNIT + + -- Check if unit is in zone. + local inzone=unit:IsInZone(runway) + + -- Check if aircraft and in air. + local isaircraft=unit:IsAir() + local isairborn =unit:InAir() + + if inzone and isaircraft and not isairborn then + local text=string.format("Unit %s on landing runway ==> Foul deck!", unit:GetName()) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + if self.Debug then + runway:FlareZone(FLARECOLOR.Red, 30) + end + fouldeck=true + foulunit=unit + end + end + + + -- Add to debrief and + if playerData and fouldeck then + + -- Debrief text. + local text=string.format("Foul deck waveoff due to aircraft %s!", foulunit:GetName()) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + + -- Foul deck + wave off radio message. + self:RadioTransmission(self.LSORadio, self.LSOCall.FOULDECK, false, 1) + self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF, false, 1.2, nil, true) + + -- Player hint for flight students. + if playerData.showhints then + local text=string.format("overfly landing area and enter bolter pattern.") + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) + end + + -- Set player parameters for foul deck. + playerData.wofd=true + + -- Debrief. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil + + -- Pass would be invalid if the player lands. + playerData.valid=false + + -- Send a message to the player that blocks the runway. + if foulunit then + local foulflight=self:_GetFlightFromGroupInQueue(foulunit:GetGroup(), self.flights) + if foulflight and not foulflight.ai then + self:MessageToPlayer(foulflight, "move your ass from my runway. NOW!", "AIRBOSS") + end + end + end + + return fouldeck +end + +--- Get "stern" coordinate. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Coordinate at the rundown of the carrier. +function AIRBOSS:_GetSternCoord() + + -- Heading of carrier (true). + local hdg=self.carrier:GetHeading() + + -- Final bearing (true). + local FB=self:GetFinalBearing() + + -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. + local stern=self:GetCoordinate() + + -- Stern coordinate (sterndist<0). + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + -- Tarawa: Translate 8 meters port. + stern=stern:Translate(self.carrierparam.sterndist, hdg):Translate(8, FB-90) + else + -- Stennis: translate 7 meters starboard wrt Final bearing. + stern=stern:Translate(self.carrierparam.sterndist, hdg):Translate(7, FB+90) + end + + -- Set altitude. + stern:SetAltitude(self.carrierparam.deckheight) + + return stern +end + +--- Get wire from landing position. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE Lcoord Landing position. +-- @param #number dc Distance correction. Shift the landing coord back if dc>0 and forward if dc<0. +-- @return #number Trapped wire (1-4) or 99 if no wire was trapped. +function AIRBOSS:_GetWire(Lcoord, dc) + + -- Final bearing (true). + local FB=self:GetFinalBearing() + + -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing. + local Scoord=self:_GetSternCoord() + + -- Distance to landing coord. + local Ldist=Lcoord:Get2DDistance(Scoord) + + -- For human (not AI) the lading event is delayed unfortunately. Therefore, we need another correction factor. + dc= dc or 65 + + -- Corrected landing distance wrt to stern. Landing distance needs to be reduced due to delayed landing event for human players. + local d=Ldist-dc + + -- Shift wires from stern to their correct position. + local w1=self.carrierparam.wire1 + local w2=self.carrierparam.wire2 + local w3=self.carrierparam.wire3 + local w4=self.carrierparam.wire4 + + -- Which wire was caught? + local wire + if d wire=%d (dc=%.1f)", Ldist, Ldist-dc, wire, dc)) + + return wire +end + +--- Trapped? Check if in air or not after landing event. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Trapped(playerData) + + if playerData.unit:InAir()==false then + -- Seems we have successfully landed. + + -- Lets see if we can get a good wire. + local unit=playerData.unit + + -- Coordinate of player aircraft. + local coord=unit:GetCoordinate() + + -- Get velocity in km/h. We need to substrackt the carrier velocity. + local v=unit:GetVelocityKMH()-self.carrier:GetVelocityKMH() + + -- Stern coordinate. + local stern=self:_GetSternCoord() + + -- Distance to stern pos. + local s=stern:Get2DDistance(coord) + + -- Get current wire (estimate). This now based on the position where the player comes to a standstill which should reflect the trapped wire better. + local dcorr=100 + if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then + dcorr=100 + elseif playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + -- TODO: Check Tomcat. + dcorr=100 + elseif playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + -- A-4E gets slowed down much faster the the F/A-18C! + dcorr=56 + end + + -- Get wire. + local wire=self:_GetWire(coord, dcorr) + + -- Debug. + local text=string.format("Player %s _Trapped: v=%.1f km/h, s-dcorr=%.1f m ==> wire=%d (dcorr=%d)", playerData.name, v, s-dcorr, wire, dcorr) + self:T(self.lid..text) + + -- Call this function again until v < threshold. Player comes to a standstill ==> Get wire! + if v>5 then + + -- Check if we passed all wires. + if wire>4 and v>10 and not playerData.warning then + -- Looks like we missed the wires ==> Bolter! + self:RadioTransmission(self.LSORadio, self.LSOCall.BOLTER, nil, nil, nil, true) + playerData.warning=true + end + + -- Call function again and check if converged or back in air. + --SCHEDULER:New(nil, self._Trapped, {self, playerData}, 0.1) + self:ScheduleOnce(0.1, self._Trapped, self, playerData) + return + end + + ---------------------------------------- + --- Form this point on we have converged + ---------------------------------------- + + -- Put some smoke and a mark. + if self.Debug then + coord:SmokeBlue() + coord:MarkToAll(text) + stern:MarkToAll("Stern") + end + + -- Set player wire. + playerData.wire=wire + + -- Message to player. + local text=string.format("Trapped %d-wire.", wire) + if wire==3 then + text=text.." Well done!" + elseif wire==2 then + text=text.." Not bad, maybe you even get the 3rd next time." + elseif wire==4 then + text=text.." That was scary. You can do better than this!" + elseif wire==1 then + text=text.." Try harder next time!" + end + + -- Message to player. + self:MessageToPlayer(playerData, text, "LSO", "") + + -- Debrief. + local hint = string.format("Trapped %d-wire.", wire) + self:_AddToDebrief(playerData, hint, "Groove: IW") + + else + + --Again in air ==> Boltered! + local text=string.format("Player %s boltered in trapped function.", playerData.name) + self:T(self.lid..text) + MESSAGE:New(text, 5, "DEBUG"):ToAllIf(self.debug) + + -- Bolter switch on. + playerData.boltered=true + + end + + -- Next step: debriefing. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ZONE functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get Initial zone for Case I or II. +-- @param #AIRBOSS self +-- @param #number case Recovery Case. +-- @return Core.Zone#ZONE_POLYGON_BASE Initial zone. +function AIRBOSS:_GetZoneInitial(case) + + -- Get radial, i.e. inverse of BRC. + local radial=self:GetRadial(2, false, false) + + -- Carrier coordinate. + local cv=self:GetCoordinate() + + -- Vec2 array. + local vec2 + + if case==1 then + -- Case I + + local c1=cv:Translate(UTILS.NMToMeters(0.5), radial-90) -- 0.0 0.5 starboard + local c2=cv:Translate(UTILS.NMToMeters(1.3), radial-90):Translate(UTILS.NMToMeters(3), radial) -- -3.0 1.3 starboard, astern + local c3=cv:Translate(UTILS.NMToMeters(0.4), radial+90):Translate(UTILS.NMToMeters(3), radial) -- -3.0 -0.4 port, astern + local c4=cv:Translate(UTILS.NMToMeters(1.0), radial) + local c5=cv + + -- Vec2 array. + vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2()} + + else + -- Case II + + -- Funnel. + local c1=cv:Translate(UTILS.NMToMeters(0.5), radial-90) -- 0.0, 0.5 + local c2=c1:Translate(UTILS.NMToMeters(0.5), radial) -- 0.5, 0.5 + local c3=cv:Translate(UTILS.NMToMeters(1.2), radial-90):Translate(UTILS.NMToMeters(3), radial) -- 3.0, 1.2 + local c4=cv:Translate(UTILS.NMToMeters(1.2), radial+90):Translate(UTILS.NMToMeters(3), radial) -- 3.0,-1.2 + local c5=cv:Translate(UTILS.NMToMeters(0.5), radial) + local c6=cv + + -- Vec2 array. + vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2()} + + end + + -- Polygon zone. + local zone=ZONE_POLYGON_BASE:New("Zone CASE I/II Initial", vec2) + + return zone +end + +--- Get lineup groove zone. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE_POLYGON_BASE Lineup zone. +function AIRBOSS:_GetZoneLineup() + + -- Get radial, i.e. inverse of BRC. + local fbi=self:GetRadial(1, false, false) + + -- Stern coordinate. + local st=self:_GetOptLandingCoordinate() + + -- Zone points. + local c1=st + local c2=st:Translate(UTILS.NMToMeters(0.50), fbi+15) + local c3=st:Translate(UTILS.NMToMeters(0.50), fbi+self.lue._max-0.05) + local c4=st:Translate(UTILS.NMToMeters(0.77), fbi+self.lue._max-0.05) + local c5=c4:Translate(UTILS.NMToMeters(0.25), fbi-90) + + -- Vec2 array. + local vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2()} + + -- Polygon zone. + local zone=ZONE_POLYGON_BASE:New("Zone Lineup", vec2) + + return zone +end + + +--- Get groove zone. +-- @param #AIRBOSS self +-- @param #number l Length of the groove in NM. Default 1.5 NM. +-- @param #number w Width of the groove in NM. Default 0.25 NM. +-- @param #number b Width of the beginning in NM. Default 0.10 NM. +-- @return Core.Zone#ZONE_POLYGON_BASE Groove zone. +function AIRBOSS:_GetZoneGroove(l, w, b) + + l=l or 1.50 + w=w or 0.25 + b=b or 0.10 + + -- Get radial, i.e. inverse of BRC. + local fbi=self:GetRadial(1, false, false) + + -- Stern coordinate. + local st=self:_GetSternCoord() + + -- Zone points. + local c1=st:Translate(self.carrierparam.totwidthstarboard, fbi-90) + local c2=st:Translate(UTILS.NMToMeters(0.10), fbi-90):Translate(UTILS.NMToMeters(0.3), fbi) + local c3=st:Translate(UTILS.NMToMeters(0.25), fbi-90):Translate(UTILS.NMToMeters(l), fbi) + local c4=st:Translate(UTILS.NMToMeters(w/2), fbi+90):Translate(UTILS.NMToMeters(l), fbi) + local c5=st:Translate(UTILS.NMToMeters(b), fbi+90):Translate(UTILS.NMToMeters(0.3), fbi) + local c6=st:Translate(self.carrierparam.totwidthport, fbi+90) + + -- Vec2 array. + local vec2={c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2()} + + -- Polygon zone. + local zone=ZONE_POLYGON_BASE:New("Zone Groove", vec2) + + return zone +end + +--- Get Bullseye zone with radius 1 NM and DME 3 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneBullseye(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Distance = 3 NM + local distance=UTILS.NMToMeters(3) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, false) + + -- Get coordinate and vec2. + local coord=self:GetCoordinate():Translate(distance, radial) + local vec2=coord:GetVec2() + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Bullseye", vec2, radius) + + return zone +end + +--- Get dirty up zone with radius 1 NM and DME 9 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Dirty up zone. +function AIRBOSS:_GetZoneDirtyUp(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Distance = 9 NM + local distance=UTILS.NMToMeters(9) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, false) + + -- Get coordinate and vec2. + local coord=self:GetCoordinate():Translate(distance, radial) + local vec2=coord:GetVec2() + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Dirty Up", vec2, radius) + + return zone +end + +--- Get arc out zone with radius 1 NM and DME 12 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneArcOut(case) + + -- Radius = 1.25 NM. + local radius=UTILS.NMToMeters(1.25) + + -- Distance = 12 NM + local distance=UTILS.NMToMeters(11.75) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, false) + + -- Get coordinate of carrier and translate. + local coord=self:GetCoordinate():Translate(distance, radial) + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Arc Out", coord:GetVec2(), radius) + + return zone +end + +--- Get arc in zone with radius 1 NM and DME 14 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Arc in zone. +function AIRBOSS:_GetZoneArcIn(case) + + -- Radius = 1.25 NM. + local radius=UTILS.NMToMeters(1.25) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, true) + + -- Angle between FB/BRC and holding zone. + local alpha=math.rad(self.holdingoffset) + + -- 14+x NM from carrier + local x=14 --/math.cos(alpha) + + -- Distance = 14 NM + local distance=UTILS.NMToMeters(x) + + -- Get coordinate. + local coord=self:GetCoordinate():Translate(distance, radial) + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Arc In", coord:GetVec2(), radius) + + return zone +end + +--- Get platform zone with radius 1 NM and DME 19 NM from the carrier. Radial depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @return Core.Zone#ZONE_RADIUS Circular platform zone. +function AIRBOSS:_GetZonePlatform(case) + + -- Radius = 1 NM. + local radius=UTILS.NMToMeters(1) + + -- Zone depends on Case recovery. + local radial=self:GetRadial(case, false, true) + + -- Angle between FB/BRC and holding zone. + local alpha=math.rad(self.holdingoffset) + + -- Distance = 19 NM + local distance=UTILS.NMToMeters(19) --/math.cos(alpha) + + -- Get coordinate. + local coord=self:GetCoordinate():Translate(distance, radial) + + -- Create zone. + local zone=ZONE_RADIUS:New("Zone Platform", coord:GetVec2(), radius) + + return zone +end + + +--- Get approach corridor zone. Shape depends on recovery case. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @param #number l Length of the zone in NM. Default 31 (=21+10) NM. +-- @return Core.Zone#ZONE_POLYGON_BASE Box zone. +function AIRBOSS:_GetZoneCorridor(case, l) + + -- Total length. + l=l or 31 + + -- Radial and offset. + local radial=self:GetRadial(case, false, false) + local offset=self:GetRadial(case, false, true) + + -- Distance shift ahead of carrier to allow for some space to bolter. + local dx=5 + + -- Width of the box in NM. + local w=2 + local w2=w/2 + + -- Distance from carrier to arc out zone. + local d=12 + + -- Carrier position. + local cv=self:GetCoordinate() + + -- Polygon points. + local c={} + + -- First point. Carrier coordinate translated 5 NM in direction of travel to allow for bolter space. + c[1]=cv:Translate(-UTILS.NMToMeters(dx), radial) + + if math.abs(self.holdingoffset)>=5 then + + ----------------- + -- Angled Case -- + ----------------- + + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) -- 1 Right of carrier, dx ahead. + c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2), radial) -- 13 "south" @ 1 right + + c[4]=cv:Translate(UTILS.NMToMeters(15), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[5]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[6]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset+90) + c[7]=cv:Translate(UTILS.NMToMeters(13), offset):Translate(UTILS.NMToMeters(1), offset+90) + c[8]=cv:Translate(UTILS.NMToMeters(11), radial):Translate(UTILS.NMToMeters(1), radial+90) + + c[9]=c[1]:Translate(UTILS.NMToMeters(w2), radial+90) + + else + + ----------------------------- + -- Easy case of a long box -- + ----------------------------- + + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) + c[3]=c[2]:Translate( UTILS.NMToMeters(dx+l), radial) -- Stack 1 starts at 21 and is 7 NM. + c[4]=c[3]:Translate( UTILS.NMToMeters(w), radial+90) + c[5]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) + + end + + + -- Create an array of a square! + local p={} + for _i,_c in ipairs(c) do + if self.Debug then + --_c:SmokeBlue() + end + p[_i]=_c:GetVec2() + end + + -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. + -- So stay 0-5 NM (+1 NM error margin) port of carrier. + local zone=ZONE_POLYGON_BASE:New("CASE II/III Approach Corridor", p) + + return zone +end + + +--- Get zone of carrier. Carrier is approximated as rectangle. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE Zone surrounding the carrier. +function AIRBOSS:_GetZoneCarrierBox() + + -- Stern coordinate. + local S=self:_GetSternCoord() + + -- Current carrier heading. + local hdg=self:GetHeading(false) + + -- Coordinate array. + local p={} + + -- Starboard stern point. + p[1]=S:Translate(self.carrierparam.totwidthstarboard, hdg+90) + + -- Starboard bow point. + p[2]=p[1]:Translate(self.carrierparam.totlength, hdg) + + -- Port bow point. + p[3]=p[2]:Translate(self.carrierparam.totwidthstarboard+self.carrierparam.totwidthport, hdg-90) + + -- Port stern point. + p[4]=p[3]:Translate(self.carrierparam.totlength, hdg-180) + + -- Convert to vec2. + local vec2={} + for _,coord in ipairs(p) do + table.insert(vec2, coord:GetVec2()) + end + + -- Create polygon zone. + local zone=ZONE_POLYGON_BASE:New("Carrier Box Zone", vec2) + + return zone +end + +--- Get zone of landing runway. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. +function AIRBOSS:_GetZoneRunwayBox() + + -- Stern coordinate. + local S=self:_GetSternCoord() + + -- Current carrier heading. + local FB=self:GetFinalBearing(false) + + -- Coordinate array. + local p={} + + -- Points. + p[1]=S:Translate(self.carrierparam.rwywidth*0.5, FB+90) + p[2]=p[1]:Translate(self.carrierparam.rwylength, FB) + p[3]=p[2]:Translate(self.carrierparam.rwywidth, FB-90) + p[4]=p[3]:Translate(self.carrierparam.rwylength, FB-180) + + -- Convert to vec2. + local vec2={} + for _,coord in ipairs(p) do + table.insert(vec2, coord:GetVec2()) + end + + -- Create polygon zone. + local zone=ZONE_POLYGON_BASE:New("Landing Runway Zone", vec2) + + return zone +end + + +--- Get zone of primary abeam landing position of USS Tarawa. Box length and width 30 meters. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. +function AIRBOSS:_GetZoneAbeamLandingSpot() + + -- Primary landing Spot coordinate. + local S=self:_GetOptLandingCoordinate() + + -- Current carrier heading. + local FB=self:GetFinalBearing(false) + + -- Coordinate array. + local p={} + + -- Points. + p[1]=S:Translate( 15, FB):Translate(15, FB+90) -- Top-Right + p[2]=S:Translate(-15, FB):Translate(15, FB+90) -- Bottom-Right + p[3]=S:Translate(-15, FB):Translate(15, FB-90) -- Bottom-Left + p[4]=S:Translate( 15, FB):Translate(15, FB-90) -- Top-Left + + -- Convert to vec2. + local vec2={} + for _,coord in ipairs(p) do + table.insert(vec2, coord:GetVec2()) + end + + -- Create polygon zone. + local zone=ZONE_POLYGON_BASE:New("Abeam Landing Spot Zone", vec2) + + return zone +end + + +--- Get zone of the primary landing spot of the USS Tarawa. +-- @param #AIRBOSS self +-- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway. +function AIRBOSS:_GetZoneLandingSpot() + + -- Primary landing Spot coordinate. + local S=self:_GetLandingSpotCoordinate() + + -- Current carrier heading. + local FB=self:GetFinalBearing(false) + + -- Coordinate array. + local p={} + + -- Points. + p[1]=S:Translate( 10, FB):Translate(10, FB+90) -- Top-Right + p[2]=S:Translate(-10, FB):Translate(10, FB+90) -- Bottom-Right + p[3]=S:Translate(-10, FB):Translate(10, FB-90) -- Bottom-Left + p[4]=S:Translate( 10, FB):Translate(10, FB-90) -- Top-left + + -- Convert to vec2. + local vec2={} + for _,coord in ipairs(p) do + table.insert(vec2, coord:GetVec2()) + end + + -- Create polygon zone. + local zone=ZONE_POLYGON_BASE:New("Landing Spot Zone", vec2) + + return zone +end + + +--- Get holding zone of player. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @param #number stack Marshal stack number. +-- @return Core.Zone#ZONE Holding zone. +function AIRBOSS:_GetZoneHolding(case, stack) + + -- Holding zone. + local zoneHolding=nil --Core.Zone#ZONE + + -- Stack is <= 0 ==> no marshal zone. + if stack<=0 then + self:E(self.lid.."ERROR: Stack <= 0 in _GetZoneHolding!") + self:E({case=case, stack=stack}) + return nil + end + + -- Pattern altitude. + local patternalt, c1, c2=self:_GetMarshalAltitude(stack, case) + + -- Select case. + if case==1 then + -- CASE I + + -- Get current carrier heading. + local hdg=self:GetHeading() + + -- Distance to the post. + local D=UTILS.NMToMeters(2.5) + + -- Post 2.5 NM port of carrier. + local Post=self:GetCoordinate():Translate(D, hdg+270) + + -- Create holding zone. + zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", Post:GetVec2(), self.marshalradius) + + -- Delta pattern. + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", self.carrier:GetVec2(), UTILS.NMToMeters(5)) + end + + + else + -- CASE II/II + + -- Get radial. + local radial=self:GetRadial(case, false, true) + + -- Create an array of a rectangle. Length is 7 NM, width is 8 NM. One NM starboard to line up with the approach corridor. + local p={} + p[1]=c2:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c2 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard. + p[2]=c1:Translate(UTILS.NMToMeters(1), radial-90):GetVec2() --c1 is 7 NM further behind. Also translated 1 NM starboard. + p[3]=c1:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p3 7 NM port of carrier. + p[4]=c2:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p4 7 NM port of carrier. + + -- Square zone length=7NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier. + -- So stay 0-5 NM (+1 NM error margin) port of carrier. + zoneHolding=ZONE_POLYGON_BASE:New("CASE II/III Holding Zone", p) + end + + return zoneHolding +end + +--- Get zone where player are automatically commence when enter. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @param #number stack Stack for Case II/III as we commence from stack>=1. +-- @return Core.Zone#ZONE Holding zone. +function AIRBOSS:_GetZoneCommence(case, stack) + + -- Commence zone. + local zone + + if case==1 then + -- Case I + + -- Get current carrier heading. + local hdg=self:GetHeading() + + -- Distance to the zone. + local D=UTILS.NMToMeters(4.75) + + -- Zone radius. + local R=UTILS.NMToMeters(1) + + -- Three position + local Three=self:GetCoordinate():Translate(D, hdg+275) + + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + + local Dx=UTILS.NMToMeters(2.25) + + local Dz=UTILS.NMToMeters(2.25) + + R=UTILS.NMToMeters(1) + + Three=self:GetCoordinate():Translate(Dz, hdg-90):Translate(Dx, hdg-180) + + end + + -- Create holding zone. + zone=ZONE_RADIUS:New("CASE I Commence Zone", Three:GetVec2(), R) + + else + -- Case II/III + + stack=stack or 1 + + -- Start point at 21 NM for stack=1. + local l=20+stack + + -- Offset angle + local offset=self:GetRadial(case, false, true) + + -- Carrier position. + local cv=self:GetCoordinate() + + -- Polygon points. + local c={} + + c[1]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[2]=cv:Translate(UTILS.NMToMeters(l+2.5), offset):Translate(UTILS.NMToMeters(1), offset-90) + c[3]=cv:Translate(UTILS.NMToMeters(l+2.5), offset):Translate(UTILS.NMToMeters(1), offset+90) + c[4]=cv:Translate(UTILS.NMToMeters(l), offset):Translate(UTILS.NMToMeters(1), offset+90) + + -- Create an array of a square! + local p={} + for _i,_c in ipairs(c) do + p[_i]=_c:GetVec2() + end + + -- Zone polygon. + zone=ZONE_POLYGON_BASE:New("CASE II/III Commence Zone", p) + + end + + return zone +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ORIENTATION functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Provide info about player status on the fly. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_AttitudeMonitor(playerData) + + -- Player unit. + local unit=playerData.unit + + -- Aircraft attitude. + local aoa=unit:GetAoA() + local yaw=unit:GetYaw() + local roll=unit:GetRoll() + local pitch=unit:GetPitch() + + -- Distance to the boat. + local dist=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) + local dx,dz,rho,phi=self:_GetDistances(unit) + + -- Wind vector. + local wind=unit:GetCoordinate():GetWindWithTurbulenceVec3() + + -- Aircraft veloecity vector. + local velo=unit:GetVelocityVec3() + local vabs=UTILS.VecNorm(velo) + + local rwy=false + local step=playerData.step + if playerData.step==AIRBOSS.PatternStep.FINAL or + playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC or + playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_AL or + playerData.step==AIRBOSS.PatternStep.GROOVE_LC or + playerData.step==AIRBOSS.PatternStep.GROOVE_IW then + step=self:_GS(step,-1) + rwy=true + end + + -- Relative heading Aircraft to Carrier. + local relhead=self:_GetRelativeHeading(playerData.unit, rwy) + + --local lc=self:_GetOptLandingCoordinate() + --lc:FlareRed() + + -- Output + local text=string.format("Pattern step: %s", step) + text=text..string.format("\nAoA=%.1f° = %.1f Units | |V|=%.1f knots", aoa, self:_AoADeg2Units(playerData, aoa), UTILS.MpsToKnots(vabs)) + if self.Debug then + -- Velocity vector. + text=text..string.format("\nVx=%.1f Vy=%.1f Vz=%.1f m/s", velo.x, velo.y, velo.z) + --Wind vector. + text=text..string.format("\nWind Vx=%.1f Vy=%.1f Vz=%.1f m/s", wind.x, wind.y, wind.z) + end + text=text..string.format("\nPitch=%.1f° | Roll=%.1f° | Yaw=%.1f°", pitch, roll, yaw) + text=text..string.format("\nClimb Angle=%.1f° | Rate=%d ft/min", unit:GetClimbAngle(), velo.y*196.85) + local dist=self:_GetOptLandingCoordinate():Get3DDistance(playerData.unit) + -- Get player velocity in km/h. + local vplayer=playerData.unit:GetVelocityKMH() + -- Get carrier velocity in km/h. + local vcarrier=self.carrier:GetVelocityKMH() + -- Speed difference. + local dv=math.abs(vplayer-vcarrier) + local alt=self:_GetAltCarrier(playerData.unit) + text=text..string.format("\nDist=%.1f m Alt=%.1f m delta|V|=%.1f km/h", dist, alt, dv) + -- If in the groove, provide line up and glide slope error. + if playerData.step==AIRBOSS.PatternStep.FINAL or + playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM or + playerData.step==AIRBOSS.PatternStep.GROOVE_IC or + playerData.step==AIRBOSS.PatternStep.GROOVE_AR or + playerData.step==AIRBOSS.PatternStep.GROOVE_AL or + playerData.step==AIRBOSS.PatternStep.GROOVE_LC or + playerData.step==AIRBOSS.PatternStep.GROOVE_IW then + local lue=self:_Lineup(playerData.unit, true) + local gle=self:_Glideslope(playerData.unit) + text=text..string.format("\nGamma=%.1f° | Rho=%.1f°", relhead, phi) + text=text..string.format("\nLineUp=%.2f° | GlideSlope=%.2f° | AoA=%.1f Units", lue, gle, self:_AoADeg2Units(playerData, aoa)) + local grade, points, analysis=self:_LSOgrade(playerData) + text=text..string.format("\nTgroove=%.1f sec", self:_GetTimeInGroove(playerData)) + text=text..string.format("\nGrade: %s %.1f PT - %s", grade, points, analysis) + else + text=text..string.format("\nR=%.2f NM | X=%d Z=%d m", UTILS.MetersToNM(rho), dx, dz) + text=text..string.format("\nGamma=%.1f° | Rho=%.1f°", relhead, phi) + end + + MESSAGE:New(text, 1, nil , true):ToClient(playerData.client) +end + +--- Get glide slope of aircraft unit. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @param #number optangle (Optional) Return glide slope relative to this angle, i.e. the error from the optimal glide slope ~3.5 degrees. +-- @return #number Glide slope angle in degrees measured from the deck of the carrier and third wire. +function AIRBOSS:_Glideslope(unit, optangle) + + if optangle==nil then + if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then + optangle=3.0 + else + optangle=3.5 + end + end + -- Landing coordinate + local landingcoord=self:_GetOptLandingCoordinate() + + -- Distance from stern to aircraft. + local x=unit:GetCoordinate():Get2DDistance(landingcoord) + + -- Altitude of unit corrected by the deck height of the carrier. + local h=self:_GetAltCarrier(unit) + + -- Harrier should be 40-50 ft above the deck. + if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then + h=unit:GetAltitude()-(UTILS.FeetToMeters(50)+self.carrierparam.deckheight+2) + end + + -- Glide slope. + local glideslope=math.atan(h/x) + + -- Glide slope (error) in degrees. + local gs=math.deg(glideslope)-optangle + + return gs +end + +--- Get glide slope of aircraft unit. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @param #number optangle (Optional) Return glide slope relative to this angle, i.e. the error from the optimal glide slope ~3.5 degrees. +-- @return #number Glide slope angle in degrees measured from the deck of the carrier and third wire. +function AIRBOSS:_Glideslope2(unit, optangle) + + if optangle==nil then + if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then + optangle=3.0 + else + optangle=3.5 + end + end + -- Landing coordinate + local landingcoord=self:_GetOptLandingCoordinate() + + -- Distance from stern to aircraft. + local x=unit:GetCoordinate():Get3DDistance(landingcoord) + + -- Altitude of unit corrected by the deck height of the carrier. + local h=self:_GetAltCarrier(unit) + + -- Harrier should be 40-50 ft above the deck. + if unit:GetTypeName()==AIRBOSS.AircraftCarrier.AV8B then + h=unit:GetAltitude()-(UTILS.FeetToMeters(50)+self.carrierparam.deckheight+2) + end + + -- Glide slope. + local glideslope=math.asin(h/x) + + -- Glide slope (error) in degrees. + local gs=math.deg(glideslope)-optangle + + -- Debug. + self:T3(self.lid..string.format("Glide slope error = %.1f, x=%.1f h=%.1f", gs, x, h)) + + return gs +end + +--- Get line up of player wrt to carrier. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @param #boolean runway If true, include angled runway. +-- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right. +function AIRBOSS:_Lineup(unit, runway) + + -- Landing coordinate + local landingcoord=self:_GetOptLandingCoordinate() + + -- Vector to landing coord. + local A=landingcoord:GetVec3() + + -- Vector to player. + local B=unit:GetVec3() + + -- Vector from player to carrier. + local C=UTILS.VecSubstract(A, B) + + -- Only in 2D plane. + C.y=0.0 + + -- Orientation of carrier. + local X=self.carrier:GetOrientationX() + X.y=0.0 + + -- Rotate orientation to angled runway. + if runway then + X=UTILS.Rotate2D(X, -self.carrierparam.rwyangle) + end + + -- Projection of player pos on x component. + local x=UTILS.VecDot(X, C) + + -- Orientation of carrier. + local Z=self.carrier:GetOrientationZ() + Z.y=0.0 + + -- Rotate orientation to angled runway. + if runway then + Z=UTILS.Rotate2D(Z, -self.carrierparam.rwyangle) + end + + -- Projection of player pos on z component. + local z=UTILS.VecDot(Z, C) + + --- + local lineup=math.deg(math.atan2(z, x)) + + return lineup +end + +--- Get alitude of aircraft wrt carrier deck. Should give zero when the aircraft touched down. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #number Altitude in meters wrt carrier height. +function AIRBOSS:_GetAltCarrier(unit) + + -- TODO: Value 4 meters is for the Hornet. Adjust for Harrier, A4E and + + -- Altitude of unit corrected by the deck height of the carrier. + local h=unit:GetAltitude()-self.carrierparam.deckheight-2 + + return h +end + +--- Get optimal landing position of the aircraft. Usually between second and third wire. In case of Tarawa we take the abeam landing spot 120 ft abeam the 7.5 position. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Optimal landing coordinate. +function AIRBOSS:_GetOptLandingCoordinate() + + -- Stern coordinate. + local stern=self:_GetSternCoord() + + -- Final bearing. + local FB=self:GetFinalBearing(false) + + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + + -- Landing 100 ft abeam, 120 ft alt. + stern=self:_GetLandingSpotCoordinate():Translate(35, FB-90) + + -- Alitude 120 ft. + stern:SetAltitude(UTILS.FeetToMeters(120)) + + else + + -- Ideally we want to land between 2nd and 3rd wire. + if self.carrierparam.wire3 then + -- We take the position of the 3rd wire to approximately account for the length of the aircraft. + local w3=self.carrierparam.wire3 + stern=stern:Translate(w3, FB, true) + end + + -- Add 2 meters to account for aircraft height. + stern.y=stern.y+2 + + end + + return stern +end + +--- Get landing spot on Tarawa. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Primary landing spot coordinate. +function AIRBOSS:_GetLandingSpotCoordinate() + + -- Stern coordinate. + local stern=self:_GetSternCoord() + + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + + -- Landing 100 ft abeam, 120 alt. + local hdg=self:GetHeading() + + -- Primary landing spot 7.5 + stern=stern:Translate(57, hdg):SetAltitude(self.carrierparam.deckheight) + + end + + return stern +end + +--- Get true (or magnetic) heading of carrier. +-- @param #AIRBOSS self +-- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned. +-- @return #number Carrier heading in degrees. +function AIRBOSS:GetHeading(magnetic) + self:F3({magnetic=magnetic}) + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + -- Include magnetic declination. + if magnetic then + hdg=hdg-self.magvar + end + + -- Adjust negative values. + if hdg<0 then + hdg=hdg+360 + end + + return hdg +end + +--- Get base recovery course (BRC) of carrier. +-- The is the magnetic heading of the carrier. +-- @param #AIRBOSS self +-- @return #number BRC in degrees. +function AIRBOSS:GetBRC() + return self:GetHeading(true) +end + +--- Get wind direction and speed at carrier position. +-- @param #AIRBOSS self +-- @param #number alt Altitude ASL in meters. Default 50 m. +-- @param #boolean magnetic Direction including magnetic declination. +-- @param Core.Point#COORDINATE coord (Optional) Coordinate at which to get the wind. Default is current carrier position. +-- @return #number Direction the wind is blowing **from** in degrees. +-- @return #number Wind speed in m/s. +function AIRBOSS:GetWind(alt, magnetic, coord) + + -- Current position of the carrier or input. + local cv=coord or self:GetCoordinate() + + -- Wind direction and speed. By default at 50 meters ASL. + local Wdir, Wspeed=cv:GetWind(alt or 50) + + -- Include magnetic declination. + if magnetic then + Wdir=Wdir-self.magvar + -- Adjust negative values. + if Wdir<0 then + Wdir=Wdir+360 + end + end + + return Wdir, Wspeed +end + +--- Get wind speed on carrier deck parallel and perpendicular to runway. +-- @param #AIRBOSS self +-- @param #number alt Altitude in meters. Default 50 m. +-- @return #number Wind component parallel to runway im m/s. +-- @return #number Wind component perpendicular to runway in m/s. +-- @return #number Total wind strength in m/s. +function AIRBOSS:GetWindOnDeck(alt) + + -- Position of carrier. + local cv=self:GetCoordinate() + + -- Velocity vector of carrier. + local vc=self.carrier:GetVelocityVec3() + + -- Carrier orientation X. + local xc=self.carrier:GetOrientationX() + + -- Carrier orientation Z. + local zc=self.carrier:GetOrientationZ() + + -- Rotate back so that angled deck points to wind. + xc=UTILS.Rotate2D(xc, -self.carrierparam.rwyangle) + zc=UTILS.Rotate2D(zc, -self.carrierparam.rwyangle) + + -- Wind (from) vector + local vw=cv:GetWindWithTurbulenceVec3(alt or 50) + + -- Total wind velocity vector. + -- Carrier velocity has to be negative. If carrier drives in the direction the wind is blowing from, we have less wind in total. + local vT=UTILS.VecSubstract(vw, vc) + + -- || Parallel component. + local vpa=UTILS.VecDot(vT,xc) + + -- == Perpendicular component. + local vpp=UTILS.VecDot(vT,zc) + + -- Strength. + local vabs=UTILS.VecNorm(vT) + + -- We return positive values as head wind and negative values as tail wind. + --TODO: Check minus sign. + return -vpa, vpp, vabs +end + + +--- Get true (or magnetic) heading of carrier into the wind. This accounts for the angled runway. +-- @param #AIRBOSS self +-- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned. +-- @param Core.Point#COORDINATE coord (Optional) Coodinate from which heading is calculated. Default is current carrier position. +-- @return #number Carrier heading in degrees. +function AIRBOSS:GetHeadingIntoWind(magnetic, coord) + + -- Get direction the wind is blowing from. This is where we want to go. + local windfrom, vwind=self:GetWind(nil, nil, coord) + + -- Actually, we want the runway in the wind. + local intowind=windfrom-self.carrierparam.rwyangle + + -- If no wind, take current heading. + if vwind<0.1 then + intowind=self:GetHeading() + end + + -- Magnetic heading. + if magnetic then + intowind=intowind-self.magvar + end + + -- Adjust negative values. + if intowind<0 then + intowind=intowind+360 + end + + return intowind +end + +--- Get base recovery course (BRC) when the carrier would head into the wind. +-- This includes the current wind direction and accounts for the angled runway. +-- @param #AIRBOSS self +-- @return #number BRC into the wind in degrees. +function AIRBOSS:GetBRCintoWind() + -- BRC is the magnetic heading. + return self:GetHeadingIntoWind(true) +end + + +--- Get final bearing (FB) of carrier. +-- By default, the routine returns the magnetic FB depending on the current map (Caucasus, NTTR, Normandy, Persion Gulf etc). +-- The true bearing can be obtained by setting the *TrueNorth* parameter to true. +-- @param #AIRBOSS self +-- @param #boolean magnetic If true, magnetic FB is returned. +-- @return #number FB in degrees. +function AIRBOSS:GetFinalBearing(magnetic) + + -- First get the heading. + local fb=self:GetHeading(magnetic) + + -- Final baring = BRC including angled deck. + fb=fb+self.carrierparam.rwyangle + + -- Adjust negative values. + if fb<0 then + fb=fb+360 + end + + return fb +end + +--- Get radial with respect to carrier BRC or FB and (optionally) holding offset. +-- +-- * case=1: radial=FB-180 +-- * case=2: radial=HDG-180 (+offset) +-- * case=3: radial=FB-180 (+offset) +-- +-- @param #AIRBOSS self +-- @param #number case Recovery case. +-- @param #boolean magnetic If true, magnetic radial is returned. Default is true radial. +-- @param #boolean offset If true, inlcude holding offset. +-- @param #boolean inverse Return inverse, i.e. radial-180 degrees. +-- @return #number Radial in degrees. +function AIRBOSS:GetRadial(case, magnetic, offset, inverse) + + -- Case or current case. + case=case or self.case + + -- Radial. + local radial + + -- Select case. + if case==1 then + + -- Get radial. + radial=self:GetFinalBearing(magnetic)-180 + + elseif case==2 then + + -- Radial wrt to heading of carrier. + radial=self:GetHeading(magnetic)-180 + + -- Holding offset angle (+-15 or 30 degrees usually) + if offset then + radial=radial+self.holdingoffset + end + + elseif case==3 then + + -- Radial wrt angled runway. + radial=self:GetFinalBearing(magnetic)-180 + + -- Holding offset angle (+-15 or 30 degrees usually) + if offset then + radial=radial+self.holdingoffset + end + + end + + -- Adjust for negative values. + if radial<0 then + radial=radial+360 + end + + -- Inverse? + if inverse then + + -- Inverse radial + radial=radial-180 + + -- Adjust for negative values. + if radial<0 then + radial=radial+360 + end + + end + + return radial +end + +--- Get difference between to headings in degrees taking into accound the [0,360) periodocity. +-- @param #AIRBOSS self +-- @param #number hdg1 Heading one. +-- @param #number hdg2 Heading two. +-- @return #number Difference between the two headings in degrees. +function AIRBOSS:_GetDeltaHeading(hdg1, hdg2) + + local V={} --DCS#Vec3 + V.x=math.cos(math.rad(hdg1)) + V.y=0 + V.z=math.sin(math.rad(hdg1)) + + local W={} --DCS#Vec3 + W.x=math.cos(math.rad(hdg2)) + W.y=0 + W.z=math.sin(math.rad(hdg2)) + + local alpha=UTILS.VecAngle(V,W) + + return alpha +end + +--- Get relative heading of player wrt carrier. +-- This is the angle between the direction/orientation vector of the carrier and the direction/orientation vector of the provided unit. +-- Note that this is calculated in the X-Z plane, i.e. the altitude Y is not taken into account. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Player unit. +-- @param #boolean runway (Optional) If true, return relative heading of unit wrt to angled runway of the carrier. +-- @return #number Relative heading in degrees. An angle of 0 means, unit fly parallel to carrier. An angle of + or - 90 degrees means, unit flies perpendicular to carrier. +function AIRBOSS:_GetRelativeHeading(unit, runway) + + -- Direction vector of the carrier. + local vC=self.carrier:GetOrientationX() + + -- Include runway angle. + if runway then + vC=UTILS.Rotate2D(vC, -self.carrierparam.rwyangle) + end + + -- Direction vector of the unit. + local vP=unit:GetOrientationX() + + -- We only want the X-Z plane. Aircraft could fly parallel but ballistic and we dont want the "pitch" angle. + vC.y=0 ; vP.y=0 + + -- Get angle between the two orientation vectors in degrees. + local rhdg=UTILS.VecAngle(vC,vP) + + -- Return heading in degrees. + return rhdg +end + +--- Get relative velocity of player unit wrt to carrier +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Player unit. +-- @return #number Relative velocity in m/s. +function AIRBOSS:_GetRelativeVelocity(unit) + + local vC=self.carrier:GetVelocityVec3() + local vP=unit:GetVelocityVec3() + + -- Only X-Z plane is necessary here. + vC.y=0 ; vP.y=0 + + local v=UTILS.VecSubstract(vP, vC) + + return UTILS.VecNorm(v),v +end + + +--- Calculate distances between carrier and aircraft unit. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #number Distance [m] in the direction of the orientation of the carrier. +-- @return #number Distance [m] perpendicular to the orientation of the carrier. +-- @return #number Distance [m] to the carrier. +-- @return #number Angle [Deg] from carrier to plane. Phi=0 if the plane is directly behind the carrier, phi=90 if the plane is starboard, phi=180 if the plane is in front of the carrier. +function AIRBOSS:_GetDistances(unit) + + -- Vector to carrier + local a=self.carrier:GetVec3() + + -- Vector to player + local b=unit:GetVec3() + + -- Vector from carrier to player. + local c={x=b.x-a.x, y=0, z=b.z-a.z} + + -- Orientation of carrier. + local x=self.carrier:GetOrientationX() + + -- Projection of player pos on x component. + local dx=UTILS.VecDot(x,c) + + -- Orientation of carrier. + local z=self.carrier:GetOrientationZ() + + -- Projection of player pos on z component. + local dz=UTILS.VecDot(z,c) + + -- Polar coordinates. + local rho=math.sqrt(dx*dx+dz*dz) + + + -- Not exactly sure any more what I wanted to calculate here. + local phi=math.deg(math.atan2(dz,dx)) + + -- Correct for negative values. + if phi<0 then + phi=phi+360 + end + + return dx,dz,rho,phi +end + +--- Check limits for reaching next step. +-- @param #AIRBOSS self +-- @param #number X X position of player unit. +-- @param #number Z Z position of player unit. +-- @param #AIRBOSS.Checkpoint check Checkpoint. +-- @return #boolean If true, checkpoint condition for next step was reached. +function AIRBOSS:_CheckLimits(X, Z, check) + + -- Limits + local nextXmin=check.LimitXmin==nil or (check.LimitXmin and (check.LimitXmin<0 and X<=check.LimitXmin or check.LimitXmin>=0 and X>=check.LimitXmin)) + local nextXmax=check.LimitXmax==nil or (check.LimitXmax and (check.LimitXmax<0 and X>=check.LimitXmax or check.LimitXmax>=0 and X<=check.LimitXmax)) + local nextZmin=check.LimitZmin==nil or (check.LimitZmin and (check.LimitZmin<0 and Z<=check.LimitZmin or check.LimitZmin>=0 and Z>=check.LimitZmin)) + local nextZmax=check.LimitZmax==nil or (check.LimitZmax and (check.LimitZmax<0 and Z>=check.LimitZmax or check.LimitZmax>=0 and Z<=check.LimitZmax)) + + -- Proceed to next step if all conditions are fullfilled. + local next=nextXmin and nextXmax and nextZmin and nextZmax + + -- Debug info. + local text=string.format("step=%s: next=%s: X=%d Xmin=%s Xmax=%s | Z=%d Zmin=%s Zmax=%s", + check.name, tostring(next), X, tostring(check.LimitXmin), tostring(check.LimitXmax), Z, tostring(check.LimitZmin), tostring(check.LimitZmax)) + self:T3(self.lid..text) + + return next +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- LSO functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- LSO advice radio call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number glideslopeError Error in degrees. +-- @param #number lineupError Error in degrees. +function AIRBOSS:_LSOadvice(playerData, glideslopeError, lineupError) + + -- Advice time. + local advice=0 + + -- Glideslope high/low calls. + if glideslopeError>self.gle.HIGH then --1.5 then + -- "You're high!" + self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, true, nil, nil, true) + advice=advice+self.LSOCall.HIGH.duration + elseif glideslopeError>self.gle.High then --0.8 then + -- "You're high." + self:RadioTransmission(self.LSORadio, self.LSOCall.HIGH, false, nil, nil, true) + advice=advice+self.LSOCall.HIGH.duration + elseif glideslopeErrorself.lue.RIGHT then --3 then + -- "Right for lineup!" + self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, true, nil, nil, true) + advice=advice+self.LSOCall.RIGHTFORLINEUP.duration + elseif lineupError>self.lue.Right then -- 1 then + -- "Right for lineup." + self:RadioTransmission(self.LSORadio, self.LSOCall.RIGHTFORLINEUP, false, nil, nil, true) + advice=advice+self.LSOCall.RIGHTFORLINEUP.duration + else + -- "Good lineup." + end + + -- Get current AoA. + local AOA=playerData.unit:GetAoA() + + -- Get aircraft AoA parameters. + local acaoa=self:_GetAircraftAoA(playerData) + + -- Speed via AoA - not for the Harrier. + if playerData.actype~=AIRBOSS.AircraftCarrier.AV8B then + if AOA>acaoa.SLOW then + -- "Your're slow!" + self:RadioTransmission(self.LSORadio, self.LSOCall.SLOW, true, nil, nil, true) + advice=advice+self.LSOCall.SLOW.duration + --S=underline("SLO") + elseif AOA>acaoa.Slow then + -- "Your're slow." + self:RadioTransmission(self.LSORadio, self.LSOCall.SLOW, false, nil, nil, true) + advice=advice+self.LSOCall.SLOW.duration + --S="SLO" + elseif AOA>acaoa.OnSpeedMax then + -- No call. + --S=little("SLO") + elseif AOA 24 seconds: No Grade "--" +-- +-- If you manage to be between 16.4 and and 16.6 seconds, you will even get and okay underline "\_OK\_". +-- +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #string LSO grade for time in groove, i.e. \_OK\_, OK, (OK), --. +function AIRBOSS:_EvalGrooveTime(playerData) + + -- Time in groove. + local t=playerData.Tgroove + + local grade="" + if t<9 then + grade="--" + elseif t<12 then + grade="(OK)" + elseif t<22 then + grade="OK" + elseif t<=24 then + grade="(OK)" + else + grade="--" + end + + -- The unicorn! + if t>=16.4 and t<=16.6 then + grade="_OK_" + end + + return grade +end + +--- Grade approach. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #string LSO grade, i.g. _OK_, OK, (OK), --, etc. +-- @return #number Points. +-- @return #string LSO analysis of flight path. +function AIRBOSS:_LSOgrade(playerData) + + --- Count deviations. + local function count(base, pattern) + return select(2, string.gsub(base, pattern, "")) + end + + -- Analyse flight data and conver to LSO text. + local GXX,nXX=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.XX) + local GIM,nIM=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IM) + local GIC,nIC=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.IC) + local GAR,nAR=self:_Flightdata2Text(playerData, AIRBOSS.GroovePos.AR) + + -- Put everything together. + local G=GXX.." "..GIM.." ".." "..GIC.." "..GAR + + -- Count number of minor, normal and major deviations. + local N=nXX+nIM+nIC+nAR + local nL=count(G, '_')/2 + local nS=count(G, '%(') + local nN=N-nS-nL + + -- Groove time 16-18 sec for a unicorn. + local Tgroove=playerData.Tgroove + local TgrooveUnicorn=Tgroove and (Tgroove>=16.0 and Tgroove<=18.0) or false + + local grade + local points + if N==0 and TgrooveUnicorn then + -- No deviations, should be REALLY RARE! + grade="_OK_" + points=5.0 + G="Unicorn" + else + if nL>0 then + -- Larger deviations ==> "No grade" 2.0 points. + grade="--" + points=2.0 + elseif nN>0 then + -- No larger but average deviations ==> "Fair Pass" Pass with average deviations and corrections. + grade="(OK)" + points=3.0 + else + -- Only minor corrections + grade="OK" + points=4.0 + end + end + + -- Replace" )"( and "__" + G=G:gsub("%)%(", "") + G=G:gsub("__","") + + -- Debug info + local text="LSO grade:\n" + text=text..G.."\n" + text=text.."Grade = "..grade.." points = "..points.."\n" + text=text.."# of total deviations = "..N.."\n" + text=text.."# of large deviations _ = "..nL.."\n" + text=text.."# of normal deviations = "..nN.."\n" + text=text.."# of small deviations ( = "..nS.."\n" + self:T2(self.lid..text) + + -- Special cases. + if playerData.wop then + --------------------- + -- Pattern Waveoff -- + --------------------- + if playerData.lig then + -- Long In the Groove (LIG). + -- According to Stingers this is a CUT pass and gives 1.0 points. + grade="WO" + points=1.0 + G="LIG" + else + -- Other pattern WO + grade="WOP" + points=2.0 + G="n/a" + end + elseif playerData.wofd then + ----------------------- + -- Foul Deck Waveoff -- + ----------------------- + if playerData.landed then + --AIRBOSS wants to talk to you! + grade="CUT" + points=0.0 + else + grade="WOFD" + points=-1.0 + end + G="n/a" + elseif playerData.owo then + ----------------- + -- Own Waveoff -- + ----------------- + grade="OWO" + points=2.0 + if N==0 then + G="n/a" + end + elseif playerData.waveoff then + ------------- + -- Waveoff -- + ------------- + if playerData.landed then + --AIRBOSS wants to talk to you! + grade="CUT" + points=0.0 + else + grade="WO" + points=1.0 + end + elseif playerData.boltered then + -- Bolter + grade="-- (BOLTER)" + points=2.5 + end + + return grade, points, G +end + +--- Grade flight data. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string groovestep Step in the groove. +-- @param #AIRBOSS.GrooveData fdata Flight data in the groove. +-- @return #string LSO grade or empty string if flight data table is nil. +-- @return #number Number of deviations from perfect flight path. +function AIRBOSS:_Flightdata2Text(playerData, groovestep) + + local function little(text) + return string.format("(%s)",text) + end + local function underline(text) + return string.format("_%s_", text) + end + + -- Groove Data. + local fdata=playerData.groove[groovestep] --#AIRBOSS.GrooveData + + -- No flight data ==> return empty string. + if fdata==nil then + self:T3(self.lid.."Flight data is nil.") + return "", 0 + end + + -- Flight data. + local step=fdata.Step + local AOA=fdata.AoA + local GSE=fdata.GSE + local LUE=fdata.LUE + local ROL=fdata.Roll + + -- Aircraft specific AoA values. + local acaoa=self:_GetAircraftAoA(playerData) + + -- Speed via AoA. Depends on aircraft type. + local S=nil + if AOA>acaoa.SLOW then + S=underline("SLO") + elseif AOA>acaoa.Slow then + S="SLO" + elseif AOA>acaoa.OnSpeedMax then + S=little("SLO") + elseif AOAself.gle.HIGH then + A=underline("H") + elseif GSE>self.gle.High then + A="H" + elseif GSE>self.gle._max then + A=little("H") + elseif GSEself.lue.RIGHT then + D=underline("LUL") + elseif LUE>self.lue.Right then + D="LUL" + elseif LUE>self.lue._max then + D=little("LUL") + elseif LUEpos.Xmax then + self:T(string.format("Xmax: X=%d > %d=Xmax", X, pos.Xmax)) + abort=true + elseif pos.Zmin and Zpos.Zmax then + self:T(string.format("Zmax: Z=%d > %d=Zmax", Z, pos.Zmax)) + abort=true + end + + return abort +end + +--- Generate a text if a player is too far from where he should be. +-- @param #AIRBOSS self +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #AIRBOSS.Checkpoint posData Checkpoint data. +function AIRBOSS:_TooFarOutText(X, Z, posData) + + -- Intro. + local text="you are too " + + -- X text. + local xtext=nil + if posData.Xmin and XposData.Xmax then + if posData.Xmax>=0 then + xtext="far ahead of " + else + xtext="close to " + end + end + + -- Z text. + local ztext=nil + if posData.Zmin and ZposData.Zmax then + if posData.Zmax>=0 then + ztext="far starboard of " + else + ztext="too close to " + end + end + + -- Combine X-Z text. + if xtext and ztext then + text=text..xtext.." and "..ztext + elseif xtext then + text=text..xtext + elseif ztext then + text=text..ztext + end + + -- Complete the sentence + text=text.."the carrier." + + -- If no case could be identified. + if xtext==nil and ztext==nil then + text="you are too far from where you should be!" + end + + return text +end + +--- Pattern aborted. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #AIRBOSS.Checkpoint posData Checkpoint data. +-- @param #boolean patternwo (Optional) Pattern wave off. +function AIRBOSS:_AbortPattern(playerData, X, Z, posData, patternwo) + + -- Text where we are wrong. + local text=self:_TooFarOutText(X, Z, posData) + + -- Debug. + local dtext=string.format("Abort: X=%d Xmin=%s, Xmax=%s | Z=%d Zmin=%s Zmax=%s", X, tostring(posData.Xmin), tostring(posData.Xmax), Z, tostring(posData.Zmin), tostring(posData.Zmax)) + self:T(self.lid..dtext) + + -- Message to player. + self:MessageToPlayer(playerData, text, "LSO") + + if patternwo then + + -- Pattern wave off! + playerData.wop=true + + -- Add to debrief. + self:_AddToDebrief(playerData, string.format("Pattern wave off: %s", text)) + + -- Depart and re-enter radio message. + -- TODO: Radio should depend on player step. + self:RadioTransmission(self.LSORadio, self.LSOCall.DEPARTANDREENTER, false, 3, nil, nil, true) + + -- Next step debrief. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil + end + +end + +--- Display hint to player. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number delay Delay before playing sound messages. Default 0 sec. +-- @param #boolean soundoff If true, don't play and sound hint. +function AIRBOSS:_PlayerHint(playerData, delay, soundoff) + + -- No hint for the pros. + if not playerData.showhints then + return + end + + -- Get optimal altitude, distance and speed. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt,debriefAlt,callAlt=self:_AltitudeCheck(playerData, alt) + + -- Get speed hint. + local hintSpeed,debriefSpeed,callSpeed=self:_SpeedCheck(playerData, speed) + + -- Get AoA hint. + local hintAoA,debriefAoA,callAoA=self:_AoACheck(playerData, aoa) + + -- Get distance to the boat hint. + local hintDist,debriefDist,callDist=self:_DistanceCheck(playerData, dist) + + -- Message to player. + local hint="" + if hintAlt and hintAlt~="" then + hint=hint.."\n"..hintAlt + end + if hintSpeed and hintSpeed~="" then + hint=hint.."\n"..hintSpeed + end + if hintAoA and hintAoA~="" then + hint=hint.."\n"..hintAoA + end + if hintDist and hintDist~="" then + hint=hint.."\n"..hintDist + end + + -- Debriefing text. + local debrief="" + if debriefAlt and debriefAlt~="" then + debrief=debrief.."\n- "..debriefAlt + end + if debriefSpeed and debriefSpeed~="" then + debrief=debrief.."\n- "..debriefSpeed + end + if debriefAoA and debriefAoA~="" then + debrief=debrief.."\n- "..debriefAoA + end + if debriefDist and debriefDist~="" then + debrief=debrief.."\n- "..debriefDist + end + + -- Add step to debriefing. + if debrief~="" then + self:_AddToDebrief(playerData, debrief) + end + + -- Voice hint. + delay=delay or 0 + if not soundoff then + if callAlt then + self:Sound2Player(playerData, self.LSORadio, callAlt, false, delay) + delay=delay+callAlt.duration+0.5 + end + if callSpeed then + self:Sound2Player(playerData, self.LSORadio, callSpeed, false, delay) + delay=delay+callSpeed.duration+0.5 + end + if callAoA then + self:Sound2Player(playerData, self.LSORadio, callAoA, false, delay) + delay=delay+callAoA.duration+0.5 + end + if callDist then + self:Sound2Player(playerData, self.LSORadio, callDist, false, delay) + delay=delay+callDist.duration+0.5 + end + end + + -- ARC IN info. + if playerData.step==AIRBOSS.PatternStep.ARCIN then + + -- Hint turn and set TACAN. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Get inverse magnetic radial without offset ==> FB for Case II or BRC for Case III. + local radial=self:GetRadial(playerData.case, true, false, true) + local turn="right" + if self.holdingoffset<0 then + turn="left" + end + hint=hint..string.format("\nTurn %s and select TACAN %03d°.", turn, radial) + end + + end + + -- DIRTUP additonal info. + if playerData.step==AIRBOSS.PatternStep.DIRTYUP then + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + hint=hint.."\nFAF! Checks completed. Nozzles 50°." + else + --TODO: Tomcat? + hint=hint.."\nDirty up! Hook, gear and flaps down." + end + end + end + + -- BULLSEYE additonal info. + if playerData.step==AIRBOSS.PatternStep.BULLSEYE then + -- Hint follow the needles. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + if playerData.actype==AIRBOSS.AircraftCarrier.HORNET then + hint=hint..string.format("\nIntercept glideslope and follow the needles.") + else + hint=hint..string.format("\nIntercept glideslope.") + end + end + end + + -- Message to player. + if hint~="" then + local text=string.format("%s%s", playerData.step, hint) + self:MessageToPlayer(playerData, hint, "AIRBOSS", "") + end +end + + +--- Display hint for flight students about the (next) step. Message is displayed after one second. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string step Step for which hint is given. +function AIRBOSS:_StepHint(playerData, step) + + -- Set step. + step=step or playerData.step + + -- Message is only for "Flight Students". + if playerData.difficulty==AIRBOSS.Difficulty.EASY and playerData.showhints then + + -- Get optimal parameters at step. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData, step) + + -- Hint: + local hint="" + + -- Altitude. + if alt then + hint=hint..string.format("\nAltitude %d ft", UTILS.MetersToFeet(alt)) + end + + -- AoA. + if aoa then + hint=hint..string.format("\nAoA %.1f", self:_AoADeg2Units(playerData, aoa)) + end + + -- Speed. + if speed then + hint=hint..string.format("\nSpeed %d knots", UTILS.MpsToKnots(speed)) + end + + -- Distance to the boat. + if dist then + hint=hint..string.format("\nDistance to the boat %.1f NM", UTILS.MetersToNM(dist)) + end + + -- Late break. + if step==AIRBOSS.PatternStep.LATEBREAK then + if playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + hint=hint.."\nWing Sweep 20°, Gear DOWN < 280 KIAS." + end + end + + -- Abeam. + if step==AIRBOSS.PatternStep.ABEAM then + if playerData.actype==AIRBOSS.AircraftCarrier.AV8B then + hint=hint.."\nNozzles 50°-60°. Antiskid OFF. Lights OFF." + elseif playerData.actype==AIRBOSS.AircraftCarrier.F14A or playerData.actype==AIRBOSS.AircraftCarrier.F14B then + hint=hint.."\nSlats/Flaps EXTENDED < 225 KIAS. DLC SELECTED. Auto Throttle IF DESIRED." + else + hint=hint.."\nDirty up! Gear DOWN, flaps DOWN. Check hook down." + end + end + + -- Check if there was actually anything to tell. + if hint~="" then + + -- Compile text if any. + local text=string.format("Optimal setup at next step %s:%s", step, hint) + + -- Send hint to player. + self:MessageToPlayer(playerData, text, "AIRBOSS", "", nil, false, 1) + + end + + end +end + + +--- Evaluate player's altitude at checkpoint. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number altopt Optimal altitude in meters. +-- @return #string Feedback text. +-- @return #string Debriefing text. +-- @return #AIRBOSS.RadioCall Radio call. +function AIRBOSS:_AltitudeCheck(playerData, altopt) + + if altopt==nil then + return nil, nil + end + + -- Player altitude. + local altitude=playerData.unit:GetAltitude() + + -- Get relative score. + local lowscore, badscore=self:_GetGoodBadScore(playerData) + + -- Altitude error +-X% + local _error=(altitude-altopt)/altopt*100 + + -- Radio call for flight students. + local radiocall=nil --#AIRBOSS.RadioCall + + local hint="" + if _error>badscore then + --hint=string.format("You're high.") + radiocall=self:_NewRadioCall(self.LSOCall.HIGH, "Paddles", "") + elseif _error>lowscore then + --hint= string.format("You're slightly high.") + radiocall=self:_NewRadioCall(self.LSOCall.HIGH, "Paddles", "") + elseif _error<-badscore then + --hint=string.format("You're low. ") + radiocall=self:_NewRadioCall(self.LSOCall.LOW, "Paddles", "") + elseif _error<-lowscore then + --hint=string.format("You're slightly low.") + radiocall=self:_NewRadioCall(self.LSOCall.LOW, "Paddles", "") + else + hint=string.format("Good altitude. ") + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Also inform students about the optimal altitude. + hint=hint..string.format("Optimal altitude is %d ft.", UTILS.MetersToFeet(altopt)) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep it short normally. + hint="" + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. + hint="" + end + + -- Debrief text. + local debrief=string.format("Altitude %d ft = %d%% deviation from %d ft.", UTILS.MetersToFeet(altitude), _error, UTILS.MetersToFeet(altopt)) + + return hint, debrief,radiocall +end + +--- Score for correct AoA. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #number optaoa Optimal AoA. +-- @return #string Feedback message text or easy and normal difficulty level or nil for hard. +-- @return #string Debriefing text. +-- @return #AIRBOSS.RadioCall Radio call. +function AIRBOSS:_AoACheck(playerData, optaoa) + + if optaoa==nil then + return nil, nil + end + + -- Get relative score. + local lowscore, badscore = self:_GetGoodBadScore(playerData) + + -- Player AoA + local aoa=playerData.unit:GetAoA() + + -- Altitude error +-X% + local _error=(aoa-optaoa)/optaoa*100 + + -- Get aircraft AoA parameters. + local aircraftaoa=self:_GetAircraftAoA(playerData) + + -- Radio call for flight students. + local radiocall=nil --#AIRBOSS.RadioCall + + -- Rate aoa. + local hint="" + if aoa>=aircraftaoa.SLOW then + --hint="Your're slow!" + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "Paddles", "") + elseif aoa>=aircraftaoa.Slow then + --hint="Your're slow." + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "Paddles", "") + elseif aoa>=aircraftaoa.OnSpeedMax then + hint="Your're a little slow. " + elseif aoa>=aircraftaoa.OnSpeedMin then + hint="You're on speed. " + elseif aoa>=aircraftaoa.Fast then + hint="You're a little fast. " + elseif aoa>=aircraftaoa.FAST then + --hint="Your're fast." + radiocall=self:_NewRadioCall(self.LSOCall.FAST, "Paddles", "") + else + --hint="You're fast!" + radiocall=self:_NewRadioCall(self.LSOCall.FAST, "Paddles", "") + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Also inform students about optimal value. + hint=hint..string.format("Optimal AoA is %.1f.", self:_AoADeg2Units(playerData, optaoa)) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep is short normally. + hint="" + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. + hint="" + end + + -- Debriefing text. + local debrief=string.format("AoA %.1f = %d%% deviation from %.1f.", self:_AoADeg2Units(playerData, aoa), _error, self:_AoADeg2Units(playerData, optaoa)) + + return hint, debrief,radiocall +end + +--- Evaluate player's speed. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number speedopt Optimal speed in m/s. +-- @return #string Feedback text. +-- @return #string Debriefing text. +-- @return #AIRBOSS.RadioCall Radio call. +function AIRBOSS:_SpeedCheck(playerData, speedopt) + + if speedopt==nil then + return nil, nil + end + + -- Player altitude. + local speed=playerData.unit:GetVelocityMPS() + + -- Get relative score. + local lowscore, badscore=self:_GetGoodBadScore(playerData) + + -- Altitude error +-X% + local _error=(speed-speedopt)/speedopt*100 + + -- Radio call for flight students. + local radiocall=nil --#AIRBOSS.RadioCall + + local hint="" + if _error>badscore then + --hint=string.format("You're fast.") + radiocall=self:_NewRadioCall(self.LSOCall.FAST, "AIRBOSS", "") + elseif _error>lowscore then + --hint= string.format("You're slightly fast.") + radiocall=self:_NewRadioCall(self.LSOCall.FAST, "AIRBOSS", "") + elseif _error<-badscore then + --hint=string.format("You're slow.") + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "AIRBOSS", "") + elseif _error<-lowscore then + --hint=string.format("You're slightly slow.") + radiocall=self:_NewRadioCall(self.LSOCall.SLOW, "AIRBOSS", "") + else + hint=string.format("Good speed. ") + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint..string.format("Optimal speed is %d knots.", UTILS.MpsToKnots(speedopt)) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep is short normally. + hint="" + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for pros. + hint="" + end + + -- Debrief text. + local debrief=string.format("Speed %d knots = %d%% deviation from %d knots.", UTILS.MpsToKnots(speed), _error, UTILS.MpsToKnots(speedopt)) + + return hint, debrief, radiocall +end + +--- Evaluate player's distance to the boat at checkpoint. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #number optdist Optimal distance in meters. +-- @return #string Feedback message text. +-- @return #string Debriefing text. +-- @return #AIRBOSS.RadioCall Distance radio call. Not implemented yet. +function AIRBOSS:_DistanceCheck(playerData, optdist) + + if optdist==nil then + return nil, nil + end + + -- Distance to carrier. + local distance=playerData.unit:GetCoordinate():Get2DDistance(self:GetCoordinate()) + + -- Get relative score. + local lowscore, badscore = self:_GetGoodBadScore(playerData) + + -- Altitude error +-X% + local _error=(distance-optdist)/optdist*100 + + local hint + if _error>badscore then + hint=string.format("You're too far from the boat!") + elseif _error>lowscore then + hint=string.format("You're slightly too far from the boat.") + elseif _error<-badscore then + hint=string.format( "You're too close to the boat!") + elseif _error<-lowscore then + hint=string.format("You're slightly too far from the boat.") + else + hint=string.format("Good distance to the boat.") + end + + -- Extend or decrease depending on skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Also inform students about optimal value. + hint=hint..string.format(" Optimal distance is %.1f NM.", UTILS.MetersToNM(optdist)) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep it short normally. + hint="" + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + -- No hint at all for the pros. + hint="" + end + + -- Debriefing text. + local debrief=string.format("Distance %.1f NM = %d%% deviation from %.1f NM.",UTILS.MetersToNM(distance), _error, UTILS.MetersToNM(optdist)) + + return hint, debrief, nil +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DEBRIEFING +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Append text to debriefing. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string hint Debrief text of this step. +-- @param #string step (Optional) Current step in the pattern. Default from playerData. +function AIRBOSS:_AddToDebrief(playerData, hint, step) + step=step or playerData.step + table.insert(playerData.debrief, {step=step, hint=hint}) +end + +--- Debrief player and set next step. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Debrief(playerData) + self:F(self.lid..string.format("Debriefing of player %s.", playerData.name)) + + -- Delete scheduler ID. + playerData.debriefschedulerID=nil + + -- Switch attitude monitor off if on. + playerData.attitudemonitor=false + + -- LSO grade, points, and flight data analyis. + local grade, points, analysis=self:_LSOgrade(playerData) + + -- Insert points to table of all points until player landed. + if points and points>=0 then + table.insert(playerData.points, points) + end + + -- Player has landed and is not airborne any more. + local Points=0 + if playerData.landed and not playerData.unit:InAir() then + + -- Average over all points received so far. + for _,_points in pairs(playerData.points) do + Points=Points+_points + end + + -- This is the final points. + Points=Points/#playerData.points + + -- Reset points array. + playerData.points={} + else + -- Player boltered or was waved off ==> We display the normal points. + Points=points + end + + -- My LSO grade. + local mygrade={} --#AIRBOSS.LSOgrade + mygrade.grade=grade + mygrade.points=points + mygrade.details=analysis + mygrade.wire=playerData.wire + mygrade.Tgroove=playerData.Tgroove + if playerData.landed and not playerData.unit:InAir() then + mygrade.finalscore=Points + end + mygrade.case=playerData.case + local windondeck=self:GetWindOnDeck() + mygrade.wind=tostring(UTILS.Round(UTILS.MpsToKnots(windondeck), 1)) + mygrade.modex=playerData.onboard + mygrade.airframe=playerData.actype + mygrade.carriertype=self.carriertype + mygrade.carriername=self.alias + mygrade.theatre=self.theatre + mygrade.mitime=UTILS.SecondsToClock(timer.getAbsTime()) + mygrade.midate=UTILS.GetDCSMissionDate() + mygrade.osdate="n/a" + if os then + mygrade.osdate=os.date() --os.date("%d.%m.%Y") + end + + -- Save trap sheet. + if playerData.trapon and self.trapsheet then + self:_SaveTrapSheet(playerData, mygrade) + end + + -- Add LSO grade to player grades table. + table.insert(self.playerscores[playerData.name], mygrade) + + -- Trigger grading event. + self:LSOGrade(playerData, mygrade) + + -- LSO grade: (OK) 3.0 PT - LURIM + local text=string.format("%s %.1f PT - %s", grade, Points, analysis) + if Points==-1 then + text=string.format("%s n/a PT - Foul deck", grade, Points, analysis) + end + + -- Wire and Groove time only if not pattern WO. + if not (playerData.wop or playerData.wofd) then + + -- Wire trapped. Not if pattern WI. + if playerData.wire and playerData.wire<=4 then + text=text..string.format(" %d-wire", playerData.wire) + end + + -- Time in the groove. Only Case I/II and not pattern WO. + if playerData.Tgroove and playerData.Tgroove<=360 and playerData.case<3 then + text=text..string.format("\nTime in the groove %.1f seconds: %s", playerData.Tgroove, self:_EvalGrooveTime(playerData)) + end + + end + + -- Copy debriefing text. + playerData.lastdebrief=UTILS.DeepCopy(playerData.debrief) + + -- Info text. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + text=text..string.format("\nYour detailed debriefing can be found via the F10 radio menu.") + end + + -- Message. + self:MessageToPlayer(playerData, text, "LSO", "", 30, true) + + + -- Set step to undefined and check if other cases apply. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + + -- Check what happened? + if playerData.wop then + + ---------------------- + -- Pattern Wave Off -- + ---------------------- + + -- Next step? + -- TODO: CASE I: After bolter/wo turn left and climb to 600 ft and re-enter the pattern. But do not go to initial but reenter earlier? + -- TODO: CASE I: After pattern wo? go back to initial, I guess? + -- TODO: CASE III: After bolter/wo turn left and climb to 1200 ft and re-enter pattern? + -- TODO: CASE III: After pattern wo? No idea... + + -- Can become nil when I crashed and changed to observer. Which events are captured? Nil check for unit? + if playerData.unit:IsAlive() then + + -- Heading and distance tip. + local heading, distance + + if playerData.case==1 or playerData.case==2 then + + -- Next step: Initial again. + playerData.step=AIRBOSS.PatternStep.INITIAL + + -- Create a point 3.0 NM astern for re-entry. + local initial=self:GetCoordinate():Translate(UTILS.NMToMeters(3.5), self:GetRadial(2, false, false, false)) + + -- Get heading and distance to initial zone ~3 NM astern. + heading=playerData.unit:GetCoordinate():HeadingTo(initial) + distance=playerData.unit:GetCoordinate():Get2DDistance(initial) + + elseif playerData.case==3 then + + -- Next step? Bullseye for now. + -- TODO: Could be DIRTY UP or PLATFORM or even back to MARSHAL STACK? + playerData.step=AIRBOSS.PatternStep.BULLSEYE + + -- Get heading and distance to bullseye zone ~3 NM astern. + local zone=self:_GetZoneBullseye(playerData.case) + + heading=playerData.unit:GetCoordinate():HeadingTo(zone:GetCoordinate()) + distance=playerData.unit:GetCoordinate():Get2DDistance(zone:GetCoordinate()) + + end + + -- Re-enter message. + local text=string.format("fly heading %03d° for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 5) + + else + + -- Unit does not seem to be alive! + -- TODO: What now? + self:E(self.lid..string.format("ERROR: Player unit not alive!")) + + end + + elseif playerData.wofd then + + --------------- + -- Foul Deck -- + --------------- + + if playerData.unit:InAir() then + + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER + + else + + -- Welcome aboard! + self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) + + -- Airboss talkto! + local text=string.format("deck was fouled but you landed anyway. Airboss wants to talk to you!") + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) + + end + + elseif playerData.owo then + + ------------------ + -- Own Wave Off -- + ------------------ + + if playerData.unit:InAir() then + + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER + + else + + -- Welcome aboard! + -- NOTE: This should not happen as owo is only triggered if player flew past the carrier. + self:E(self.lid.."ERROR: player landed when OWO was issues. This should not happen. Please report!") + self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) + + end + + + elseif playerData.waveoff then + + -------------- + -- Wave Off -- + -------------- + + if playerData.unit:InAir() then + + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER + + else + + -- Welcome aboard! + self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) + + -- Airboss talkto! + local text=string.format("you were waved off but landed anyway. Airboss wants to talk to you!") + self:MessageToPlayer(playerData, text, "LSO", nil, nil, false, 3) + + end + + elseif playerData.boltered then + + -------------- + -- Boltered -- + -------------- + + if playerData.unit:InAir() then + + -- Bolter pattern. Then Abeam or bullseye. + playerData.step=AIRBOSS.PatternStep.BOLTER + + end + + elseif playerData.landed then + + ------------ + -- Landed -- + ------------ + + if not playerData.unit:InAir() then + + -- Welcome aboard! + self:Sound2Player(playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD) + + end + + else + + -- Message to player. + self:MessageToPlayer(playerData, "Undefined state after landing! Please report.", "ERROR", nil, 20) + + -- Next step. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + end + + -- Player landed and is not in air anymore. + if playerData.landed and not playerData.unit:InAir() then + -- Set recovered flag. + self:_RecoveredElement(playerData.unit) + + -- Check if all elements + self:_CheckSectionRecovered(playerData) + end + + -- Increase number of passes. + playerData.passes=playerData.passes+1 + + -- Next step hint for students if any. + self:_StepHint(playerData) + + -- Reinitialize player data for new approach. + self:_InitPlayer(playerData, playerData.step) + + -- Debug message. + MESSAGE:New(string.format("Player step %s.", playerData.step), 5, "DEBUG"):ToAllIf(self.Debug) + + -- Auto save player results. + if self.autosave and mygrade.finalscore then + self:Save(self.autosavepath, self.autosavefile) + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- CARRIER ROUTING Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Check for possible collisions between two coordinates. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE coordto Coordinate to which the collision is check. +-- @param Core.Point#COORDINATE coordfrom Coordinate from which the collision is check. +-- @return #boolean If true, surface type ahead is not deep water. +-- @return #number Max free distance in meters. +function AIRBOSS:_CheckCollisionCoord(coordto, coordfrom) + + -- Increment in meters. + local dx=100 + + -- From coordinate. Default 500 in front of the carrier. + local d=0 + if coordfrom then + d=0 + else + d=250 + coordfrom=self:GetCoordinate():Translate(d, self:GetHeading()) + end + + -- Distance between the two coordinates. + local dmax=coordfrom:Get2DDistance(coordto) + + -- Direction. + local direction=coordfrom:HeadingTo(coordto) + + -- Scan path between the two coordinates. + local clear=true + while d<=dmax do + + -- Check point. + local cp=coordfrom:Translate(d, direction) + + -- Check if surface type is water. + if not cp:IsSurfaceTypeWater() then + + -- Debug mark points. + if self.Debug then + local st=cp:GetSurfaceType() + cp:MarkToAll(string.format("Collision check surface type %d", st)) + end + + -- Collision WARNING! + clear=false + break + end + + -- Increase distance. + d=d+dx + end + + local text="" + if clear then + text=string.format("Path into direction %03d° is clear for the next %.1f NM.", direction, UTILS.MetersToNM(d)) + else + text=string.format("Detected obstacle at distance %.1f NM into direction %03d°.", UTILS.MetersToNM(d), direction) + end + self:T2(self.lid..text) + + return not clear, d +end + + +--- Check Collision. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE fromcoord Coordinate from which the path to the next WP is calculated. Default current carrier position. +-- @return #boolean If true, surface type ahead is not deep water. +function AIRBOSS:_CheckFreePathToNextWP(fromcoord) + + -- Position. + fromcoord=fromcoord or self:GetCoordinate():Translate(250, self:GetHeading()) + + -- Next wp = current+1 (or last) + local Nnextwp=math.min(self.currentwp+1, #self.waypoints) + + -- Next waypoint. + local nextwp=self.waypoints[Nnextwp] --Core.Point#COORDINATE + + -- Check for collision. + local collision=self:_CheckCollisionCoord(nextwp, fromcoord) + + return collision +end + +--- Find free path to the next waypoint. +-- @param #AIRBOSS self +function AIRBOSS:_Pathfinder() + + -- Heading and current coordiante. + local hdg=self:GetHeading() + local cv=self:GetCoordinate() + + -- Possible directions. + local directions={-20, 20, -30, 30, -40, 40, -50, 50, -60, 60, -70, 70, -80, 80, -90, 90, -100, 100} + + -- Starboard turns up to 90 degrees. + for _,_direction in pairs(directions) do + + -- New direction. + local direction=hdg+_direction + + -- Check for collisions in the next 20 NM of the current direction. + local _, dfree=self:_CheckCollisionCoord(cv:Translate(UTILS.NMToMeters(20), direction), cv) + + -- Loop over distances and find the first one which gives a clear path to the next waypoint. + local distance=500 + while distance<=dfree do + + -- Coordinate from which we calculate the path. + local fromcoord=cv:Translate(distance, direction) + + -- Check for collision between point and next waypoint. + local collision=self:_CheckFreePathToNextWP(fromcoord) + + -- Debug info. + self:T2(self.lid..string.format("Pathfinder d=%.1f m, direction=%03d°, collision=%s", distance, direction, tostring(collision))) + + -- If path is clear, we start a little detour. + if not collision then + self:CarrierDetour(fromcoord) + return + end + + distance=distance+500 + end + end +end + + +--- Carrier resumes the route at its next waypoint. +--@param #AIRBOSS self +--@param Core.Point#COORDINATE gotocoord (Optional) First goto this coordinate before resuming route. +--@return #AIRBOSS self +function AIRBOSS:CarrierResumeRoute(gotocoord) + + -- Make carrier resume its route. + AIRBOSS._ResumeRoute(self.carrier:GetGroup(), self, gotocoord) + + return self +end + + +--- Let the carrier make a detour to a given point. When it reaches the point, it will resume its normal route. +-- @param #AIRBOSS self +-- @param Core.Point#COORDINATE coord Coordinate of the detour. +-- @param #number speed Speed in knots. Default is current carrier velocity. +-- @param #boolean uturn (Optional) If true, carrier will go back to where it came from before it resumes its route to the next waypoint. +-- @param #number uspeed Speed in knots after U-turn. Default is same as before. +-- @param Core.Point#COORDINATE tcoord Additional coordinate to make turn smoother. +-- @return #AIRBOSS self +function AIRBOSS:CarrierDetour(coord, speed, uturn, uspeed, tcoord) + + -- Current coordinate of the carrier. + local pos0=self:GetCoordinate() + + -- Current speed in knots. + local vel0=self.carrier:GetVelocityKNOTS() + + -- Default. If speed is not given we take the current speed but at least 5 knots. + speed=speed or math.max(vel0, 5) + + -- Speed in km/h. At least 2 knots. + local speedkmh=math.max(UTILS.KnotsToKmph(speed), UTILS.KnotsToKmph(2)) + + -- Turn speed in km/h. At least 10 knots. + local cspeedkmh=math.max(self.carrier:GetVelocityKMH(), UTILS.KnotsToKmph(10)) + + -- U-turn speed in km/h. + local uspeedkmh=UTILS.KnotsToKmph(uspeed or speed) + + -- Waypoint table. + local wp={} + + -- Waypoint at current position. + table.insert(wp, pos0:WaypointGround(cspeedkmh)) + + -- Waypooint to help the turn. + if tcoord then + table.insert(wp, tcoord:WaypointGround(cspeedkmh)) + end + + -- Detour waypoint. + table.insert(wp, coord:WaypointGround(speedkmh)) + + -- U-turn waypoint. If enabled, go back to where you came from. + if uturn then + table.insert(wp, pos0:WaypointGround(uspeedkmh)) + end + + -- Get carrier group. + local group=self.carrier:GetGroup() + + -- Passing waypoint taskfunction + local TaskResumeRoute=group:TaskFunction("AIRBOSS._ResumeRoute", self) + + -- Set task to restart route at the last point. + group:SetTaskWaypoint(wp[#wp], TaskResumeRoute) + + -- Debug mark. + if self.Debug then + if tcoord then + tcoord:MarkToAll(string.format("Detour Turn Help WP. Speed %.1f knots", UTILS.KmphToKnots(cspeedkmh))) + end + coord:MarkToAll(string.format("Detour Waypoint. Speed %.1f knots", UTILS.KmphToKnots(speedkmh))) + if uturn then + pos0:MarkToAll(string.format("Detour U-turn WP. Speed %.1f knots", UTILS.KmphToKnots(uspeedkmh))) + end + end + + -- Detour switch true. + self.detour=true + + -- Route carrier into the wind. + self.carrier:Route(wp) +end + +--- Let the carrier turn into the wind. +-- @param #AIRBOSS self +-- @param #number time Time in seconds. +-- @param #number vdeck Speed on deck m/s. Carrier will +-- @param #boolean uturn Make U-turn and go back to initial after downwind leg. +-- @return #AIRBOSS self +function AIRBOSS:CarrierTurnIntoWind(time, vdeck, uturn) + + -- Wind speed. + local _,vwind=self:GetWind() + + -- Speed of carrier in m/s but at least 2 knots. + local vtot=math.max(vdeck-vwind, UTILS.KnotsToMps(2)) + + -- Distance to travel + local dist=vtot*time + + -- Speed in knots + local speedknots=UTILS.MpsToKnots(vtot) + local distNM=UTILS.MetersToNM(dist) + + -- Debug output + self:I(self.lid..string.format("Carrier steaming into the wind (%.1f kts). Distance=%.1f NM, Speed=%.1f knots, Time=%d sec.", UTILS.MpsToKnots(vwind), distNM, speedknots, time)) + + -- Get heading into the wind accounting for angled runway. + local hiw=self:GetHeadingIntoWind() + + -- Current heading. + local hdg=self:GetHeading() + + -- Heading difference. + local deltaH=self:_GetDeltaHeading(hdg, hiw) + + local Cv=self:GetCoordinate() + + local Ctiw=nil --Core.Point#COORDINATE + local Csoo=nil --Core.Point#COORDINATE + + -- Define path depending on turn angle. + if deltaH<45 then + -- Small turn. + + -- Point in the right direction to help turning. + Csoo=Cv:Translate(750, hdg):Translate(750, hiw) + + -- Heading into wind from Csoo. + local hsw=self:GetHeadingIntoWind(false, Csoo) + + -- Into the wind coord. + Ctiw=Csoo:Translate(dist, hsw) + + elseif deltaH<90 then + -- Medium turn. + + -- Point in the right direction to help turning. + Csoo=Cv:Translate(900, hdg):Translate(900, hiw) + + -- Heading into wind from Csoo. + local hsw=self:GetHeadingIntoWind(false, Csoo) + + -- Into the wind coord. + Ctiw=Csoo:Translate(dist, hsw) + + elseif deltaH<135 then + -- Large turn backwards. + + -- Point in the right direction to help turning. + Csoo=Cv:Translate(1100, hdg-90):Translate(1000, hiw) + + -- Heading into wind from Csoo. + local hsw=self:GetHeadingIntoWind(false, Csoo) + + -- Into the wind coord. + Ctiw=Csoo:Translate(dist, hsw) + + else + -- Huge turn backwards. + + -- Point in the right direction to help turning. + Csoo=Cv:Translate(1200, hdg-90):Translate(1000, hiw) + + -- Heading into wind from Csoo. + local hsw=self:GetHeadingIntoWind(false, Csoo) + + -- Into the wind coord. + Ctiw=Csoo:Translate(dist, hsw) + + end + + + -- Return to coordinate if collision is detected. + self.Creturnto=self:GetCoordinate() + + -- Next waypoint. + local nextwp=self:_GetNextWaypoint() + + -- For downwind, we take the velocity at the next WP. + local vdownwind=UTILS.MpsToKnots(nextwp:GetVelocity()) + + -- Make sure we move at all in case the speed at the waypoint is zero. + if vdownwind<1 then + vdownwind=10 + end + + -- Let the carrier make a detour from its route but return to its current position. + self:CarrierDetour(Ctiw, speedknots, uturn, vdownwind, Csoo) + + -- Set switch that we are currently turning into the wind. + self.turnintowind=true + + return self +end + +--- Get next waypoint of the carrier. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Coordinate of the next waypoint. +-- @return #number Number of waypoint. +function AIRBOSS:_GetNextWaypoint() + + -- Next waypoint. + local Nextwp=nil + if self.currentwp==#self.waypoints then + Nextwp=1 + else + Nextwp=self.currentwp+1 + end + + -- Debug output + local text=string.format("Current WP=%d/%d, next WP=%d", self.currentwp, #self.waypoints, Nextwp) + self:T2(self.lid..text) + + -- Next waypoint. + local nextwp=self.waypoints[Nextwp] --Core.Point#COORDINATE + + return nextwp,Nextwp +end + + +--- Initialize Mission Editor waypoints. +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:_InitWaypoints() + + -- Waypoints of group as defined in the ME. + local Waypoints=self.carrier:GetGroup():GetTemplateRoutePoints() + + -- Init array. + self.waypoints={} + + -- Set waypoint table. + for i,point in ipairs(Waypoints) do + + -- Coordinate of the waypoint + local coord=COORDINATE:New(point.x, point.alt, point.y) + + -- Set velocity of the coordinate. + coord:SetVelocity(point.speed) + + -- Add to table. + table.insert(self.waypoints, coord) + + -- Debug info. + if self.Debug then + coord:MarkToAll(string.format("Carrier Waypoint %d, Speed=%.1f knots", i, UTILS.MpsToKnots(point.speed))) + end + + end + + return self +end + +--- Patrol carrier. +-- @param #AIRBOSS self +-- @param #number n Next waypoint number. +-- @return #AIRBOSS self +function AIRBOSS:_PatrolRoute(n) + + -- Get next waypoint coordinate and number. + local nextWP, N=self:_GetNextWaypoint() + + -- Default resume is to next waypoint. + n=n or N + + -- Get carrier group. + local CarrierGroup=self.carrier:GetGroup() + + -- Waypoints table. + local Waypoints={} + + -- Create a waypoint from the current coordinate. + local wp=self:GetCoordinate():WaypointGround(CarrierGroup:GetVelocityKMH()) + + -- Add current position as first waypoint. + table.insert(Waypoints, wp) + + -- Loop over waypoints. + for i=n,#self.waypoints do + local coord=self.waypoints[i] --Core.Point#COORDINATE + + -- Create a waypoint from the coordinate. + local wp=coord:WaypointGround(UTILS.MpsToKmph(coord.Velocity)) + + -- Passing waypoint taskfunction + local TaskPassingWP=CarrierGroup:TaskFunction("AIRBOSS._PassingWaypoint", self, i, #self.waypoints) + + -- Call task function when carrier arrives at waypoint. + CarrierGroup:SetTaskWaypoint(wp, TaskPassingWP) + + -- Add waypoint to table. + table.insert(Waypoints, wp) + end + + -- Route carrier group. + CarrierGroup:Route(Waypoints) + + return self +end + + + + +--- Estimated the carrier position at some point in the future given the current waypoints and speeds. +-- @param #AIRBOSS self +-- @return DCS#time ETA abs. time in seconds. +function AIRBOSS:_GetETAatNextWP() + + -- Current waypoint + local cwp=self.currentwp + + -- Current abs. time. + local tnow=timer.getAbsTime() + + -- Current position. + local p=self:GetCoordinate() + + -- Current velocity [m/s]. + local v=self.carrier:GetVelocityMPS() + + -- Next waypoint. + local nextWP=self:_GetNextWaypoint() + + -- Distance to next waypoint. + local s=p:Get2DDistance(nextWP) + + -- Distance to next waypoint. + --local s=0 + --if #self.waypoints>cwp then + -- s=p:Get2DDistance(self.waypoints[cwp+1]) + --end + + -- v=s/t <==> t=s/v + local t=s/v + + -- ETA + local eta=t+tnow + + return eta +end + +--- Check if carrier is turning. If turning started or stopped, we inform the players via radio message. +-- @param #AIRBOSS self +function AIRBOSS:_CheckCarrierTurning() + + -- Current orientation of carrier. + local vNew=self.carrier:GetOrientationX() + + -- Last orientation from 30 seconds ago. + local vLast=self.Corientlast + + -- We only need the X-Z plane. + vNew.y=0 ; vLast.y=0 + + -- Angle between current heading and last time we checked ~30 seconds ago. + local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast))) + + -- Last orientation becomes new orientation + self.Corientlast=vNew + + -- Carrier is turning when its heading changed by at least one degree since last check. + local turning=math.abs(deltaLast)>=1 + + -- Check if turning stopped. (Carrier was turning but is not any more.) + if self.turning and not turning then + + -- Get final bearing. + local FB=self:GetFinalBearing(true) + + -- Marshal radio call: "99, new final bearing XYZ degrees." + self:_MarshalCallNewFinalBearing(FB) + + end + + -- Check if turning started. (Carrier was not turning and is now.) + if turning and not self.turning then + + -- Get heading. + local hdg + if self.turnintowind then + -- We are now steaming into the wind. + hdg=self:GetHeadingIntoWind(false) + else + -- We turn towards the next waypoint. + hdg=self:GetCoordinate():HeadingTo(self:_GetNextWaypoint()) + end + + -- Magnetic! + hdg=hdg-self.magvar + if hdg<0 then + hdg=360+hdg + end + + -- Radio call: "99, Carrier starting turn to heading XYZ degrees". + self:_MarshalCallCarrierTurnTo(hdg) + end + + -- Update turning. + self.turning=turning +end + +--- Check if heading or position of carrier have changed significantly. +-- @param #AIRBOSS self +function AIRBOSS:_CheckPatternUpdate() + + ---------------------------------------- + -- TODO: Make parameters input values -- + ---------------------------------------- + + -- Min 10 min between pattern updates. + local dTPupdate=10*60 + + -- Update if carrier moves by more than 2.5 NM. + local Dupdate=UTILS.NMToMeters(2.5) + + -- Update if carrier turned by more than 5°. + local Hupdate=5 + + ----------------------- + -- Time Update Check -- + ----------------------- + + -- Time since last pattern update + local dt=timer.getTime()-self.Tpupdate + + -- Check whether at least 10 min between updates and not turning currently. + if dt=Hupdate then + self:T(self.lid..string.format("Carrier heading changed by %d°.", deltaHeading)) + Hchange=true + end + + --------------------------- + -- Distance Update Check -- + --------------------------- + + -- Get current position and orientation of carrier. + local pos=self:GetCoordinate() + + -- Get distance to saved position. + local dist=pos:Get2DDistance(self.Cposition) + + -- Check if carrier moved more than ~10 km. + local Dchange=false + if dist>=Dupdate then + self:T(self.lid..string.format("Carrier position changed by %.1f NM.", UTILS.MetersToNM(dist))) + Dchange=true + end + + ---------------------------- + -- Update Marshal Flights -- + ---------------------------- + + -- If heading or distance changed ==> update marshal AI patterns. + if Hchange or Dchange then + + -- Loop over all marshal flights + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Update marshal pattern of AI keeping the same stack. + if flight.ai then + self:_MarshalAI(flight, flight.flag) + end + + end + + -- Reset parameters for next update check. + self.Corientation=vNew + self.Cposition=pos + self.Tpupdate=timer.getTime() + end + +end + +--- Function called when a group is passing a waypoint. +--@param Wrapper.Group#GROUP group Group that passed the waypoint +--@param #AIRBOSS airboss Airboss object. +--@param #number i Waypoint number that has been reached. +--@param #number final Final waypoint number. +function AIRBOSS._PassingWaypoint(group, airboss, i, final) + + -- Debug message. + local text=string.format("Group %s passing waypoint %d of %d.", group:GetName(), i, final) + + -- Debug smoke and marker. + if airboss.Debug and false then + local pos=group:GetCoordinate() + pos:SmokeRed() + local MarkerID=pos:MarkToAll(string.format("Group %s reached waypoint %d", group:GetName(), i)) + end + + -- Debug message. + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) + + -- Set current waypoint. + airboss.currentwp=i + + -- Passing Waypoint event. + airboss:PassingWaypoint(i) + + -- Reactivate beacons. + --airboss:_ActivateBeacons() + + -- If final waypoint reached, do route all over again. + if i==final and final>1 and airboss.adinfinitum then + airboss:_PatrolRoute() + end +end + +--- Carrier Strike Group resumes the route of the waypoints defined in the mission editor. +--@param Wrapper.Group#GROUP group Carrier Strike Group that passed the waypoint. +--@param #AIRBOSS airboss Airboss object. +--@param Core.Point#COORDINATE gotocoord Go to coordinate before route is resumed. +function AIRBOSS._ResumeRoute(group, airboss, gotocoord) + + -- Get next waypoint + local nextwp,Nextwp=airboss:_GetNextWaypoint() + + -- Speed set at waypoint. + local speedkmh=nextwp.Velocity*3.6 + + -- If speed at waypoint is zero, we set it to 10 knots. + if speedkmh<1 then + speedkmh=UTILS.KnotsToKmph(10) + end + + -- Waypoints array. + local waypoints={} + + -- Current position. + local c0=group:GetCoordinate() + + -- Current positon as first waypoint. + local wp0=c0:WaypointGround(speedkmh) + table.insert(waypoints, wp0) + + -- First goto this coordinate. + if gotocoord then + + --gotocoord:MarkToAll(string.format("Goto waypoint speed=%.1f km/h", speedkmh)) + + local headingto=c0:HeadingTo(gotocoord) + + local hdg1=airboss:GetHeading() + local hdg2=c0:HeadingTo(gotocoord) + local delta=airboss:_GetDeltaHeading(hdg1, hdg2) + + --env.info(string.format("FF hdg1=%d, hdg2=%d, delta=%d", hdg1, hdg2, delta)) + + + -- Add additional turn points + if delta>90 then + + -- Turn radius 3 NM. + local turnradius=UTILS.NMToMeters(3) + + local gotocoordh=c0:Translate(turnradius, hdg1+45) + --gotocoordh:MarkToAll(string.format("Goto help waypoint 1 speed=%.1f km/h", speedkmh)) + + local wp=gotocoordh:WaypointGround(speedkmh) + table.insert(waypoints, wp) + + gotocoordh=c0:Translate(turnradius, hdg1+90) + --gotocoordh:MarkToAll(string.format("Goto help waypoint 2 speed=%.1f km/h", speedkmh)) + + wp=gotocoordh:WaypointGround(speedkmh) + table.insert(waypoints, wp) + + end + + local wp1=gotocoord:WaypointGround(speedkmh) + table.insert(waypoints, wp1) + + end + + -- Debug message. + local text=string.format("Carrier is resuming route. Next waypoint %d, Speed=%.1f knots.", Nextwp, UTILS.KmphToKnots(speedkmh)) + + -- Debug message. + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:I(airboss.lid..text) + + -- Loop over all remaining waypoints. + for i=Nextwp, #airboss.waypoints do + + -- Coordinate of the next WP. + local coord=airboss.waypoints[i] --Core.Point#COORDINATE + + -- Speed in km/h of that WP. Velocity is in m/s. + local speed=coord.Velocity*3.6 + + -- If speed is zero we set it to 10 knots. + if speed<1 then + speed=UTILS.KnotsToKmph(10) + end + + --coord:MarkToAll(string.format("Resume route WP %d, speed=%.1f km/h", i, speed)) + + -- Create waypoint. + local wp=coord:WaypointGround(speed) + + -- Passing waypoint task function. + local TaskPassingWP=group:TaskFunction("AIRBOSS._PassingWaypoint", airboss, i, #airboss.waypoints) + + -- Call task function when carrier arrives at waypoint. + group:SetTaskWaypoint(wp, TaskPassingWP) + + -- Add waypoints to table. + table.insert(waypoints, wp) + end + + -- Set turn into wind switch false. + airboss.turnintowind=false + airboss.detour=false + + -- Route group. + group:Route(waypoints) +end + +--- Function called when a group has reached the holding zone. +--@param Wrapper.Group#GROUP group Group that reached the holding zone. +--@param #AIRBOSS airboss Airboss object. +--@param #AIRBOSS.FlightGroup flight Flight group that has reached the holding zone. +function AIRBOSS._ReachedHoldingZone(group, airboss, flight) + + -- Debug message. + local text=string.format("Flight %s reached holding zone.", group:GetName()) + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) + + -- Debug mark. + if airboss.Debug then + group:GetCoordinate():MarkToAll(text) + end + + -- Set holding flag true and set timestamp for marshal time check. + if flight then + flight.holding=true + flight.time=timer.getAbsTime() + end +end + +--- Function called when a group should be send to the Marshal stack. If stack is full, it is send to wait. +--@param Wrapper.Group#GROUP group Group that reached the holding zone. +--@param #AIRBOSS airboss Airboss object. +--@param #AIRBOSS.FlightGroup flight Flight group that has reached the holding zone. +function AIRBOSS._TaskFunctionMarshalAI(group, airboss, flight) + + -- Debug message. + local text=string.format("Flight %s is send to marshal.", group:GetName()) + MESSAGE:New(text,10):ToAllIf(airboss.Debug) + airboss:T(airboss.lid..text) + + -- Get the next free stack for current recovery case. + local stack=airboss:_GetFreeStack(flight.ai) + + if stack then + + -- Send AI to marshal stack. + airboss:_MarshalAI(flight, stack) + + else + + -- Send AI to orbit outside 10 NM zone and wait until the next Marshal stack is available. + if not airboss:_InQueue(airboss.Qwaiting, flight.group) then + airboss:_WaitAI(flight) + end + + end + + -- If it came from refueling. + if flight.refueling==true then + airboss:I(airboss.lid..string.format("Flight group %s finished refueling task.", flight.groupname)) + end + + -- Not refueling any more in case it was. + flight.refueling=false + +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- MISC functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get aircraft nickname. +-- @param #AIRBOSS self +-- @param #string actype Aircraft type name. +-- @return #string Aircraft nickname. E.g. "Hornet" for the F/A-18C or "Tomcat" For the F-14A. +function AIRBOSS:_GetACNickname(actype) + + local nickname="unknown" + if actype==AIRBOSS.AircraftCarrier.A4EC then + nickname="Skyhawk" + elseif actype==AIRBOSS.AircraftCarrier.AV8B then + nickname="Harrier" + elseif actype==AIRBOSS.AircraftCarrier.E2D then + nickname="Hawkeye" + elseif actype==AIRBOSS.AircraftCarrier.F14A_AI or actype==AIRBOSS.AircraftCarrier.F14A or actype==AIRBOSS.AircraftCarrier.F14B then + nickname="Tomcat" + elseif actype==AIRBOSS.AircraftCarrier.FA18C or actype==AIRBOSS.AircraftCarrier.HORNET then + nickname="Hornet" + elseif actype==AIRBOSS.AircraftCarrier.S3B or actype==AIRBOSS.AircraftCarrier.S3BTANKER then + nickname="Viking" + end + + return nickname +end + +--- Get onboard number of player or client. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #string Onboard number as string. +function AIRBOSS:_GetOnboardNumberPlayer(group) + return self:_GetOnboardNumbers(group, true) +end + +--- Get onboard numbers of all units in a group. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @param #boolean playeronly If true, return the onboard number for player or client skill units. +-- @return #table Table of onboard numbers. +function AIRBOSS:_GetOnboardNumbers(group, playeronly) + --self:F({groupname=group:GetName}) + + -- Get group name. + local groupname=group:GetName() + + -- Debug text. + local text=string.format("Onboard numbers of group %s:", groupname) + + -- Units of template group. + local units=group:GetTemplate().units + + -- Get numbers. + local numbers={} + for _,unit in pairs(units) do + + -- Onboard number and unit name. + local n=tostring(unit.onboard_num) + local name=unit.name + local skill=unit.skill + + -- Debug text. + text=text..string.format("\n- unit %s: onboard #=%s skill=%s", name, n, skill) + + if playeronly and skill=="Client" or skill=="Player" then + -- There can be only one player in the group, so we skip everything else. + return n + end + + -- Table entry. + numbers[name]=n + end + + -- Debug info. + self:T2(self.lid..text) + + return numbers +end + + +--- Get Tower frequency of carrier. +-- @param #AIRBOSS self +function AIRBOSS:_GetTowerFrequency() + + -- Tower frequency in MHz + self.TowerFreq=0 + + -- Get Template of Strike Group + local striketemplate=self.carrier:GetGroup():GetTemplate() + + -- Find the carrier unit. + for _,unit in pairs(striketemplate.units) do + if self.carrier:GetName()==unit.name then + self.TowerFreq=unit.frequency/1000000 + return + end + end +end + +--- Get error margin depending on player skill. +-- +-- * Flight students: 10% and 20% +-- * Naval Aviators: 5% and 10% +-- * TOPGUN Graduates: 2.5% and 5% +-- +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @return #number Error margin for still being okay. +-- @return #number Error margin for really sucking. +function AIRBOSS:_GetGoodBadScore(playerData) + + local lowscore + local badscore + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + lowscore=10 + badscore=20 + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + lowscore=5 + badscore=10 + elseif playerData.difficulty==AIRBOSS.Difficulty.HARD then + lowscore=2.5 + badscore=5 + end + + return lowscore, badscore +end + +--- Check if aircraft is capable of landing on this aircraft carrier. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. (Will also work with groups as given parameter.) +-- @return #boolean If true, aircraft can land on a carrier. +function AIRBOSS:_IsCarrierAircraft(unit) + + -- Get aircraft type name + local aircrafttype=unit:GetTypeName() + + -- Special case for Harrier which can only land on Tarawa. + if aircrafttype==AIRBOSS.AircraftCarrier.AV8B then + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + return true + else + return false + end + end + + -- Also only Harriers can land on the Tarawa. + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + if aircrafttype~=AIRBOSS.AircraftCarrier.AV8B then + return false + end + end + + -- Loop over all other known carrier capable aircraft. + for _,actype in pairs(AIRBOSS.AircraftCarrier) do + + -- Check if this is a carrier capable aircraft type. + if actype==aircrafttype then + return true + end + end + + -- No carrier carrier aircraft. + return false +end + +--- Checks if a human player sits in the unit. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Aircraft unit. +-- @return #boolean If true, human player inside the unit. +function AIRBOSS:_IsHumanUnit(unit) + + -- Get player unit or nil if no player unit. + local playerunit=self:_GetPlayerUnitAndName(unit:GetName()) + + if playerunit then + return true + else + return false + end +end + +--- Checks if a group has a human player. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Aircraft group. +-- @return #boolean If true, human player inside group. +function AIRBOSS:_IsHuman(group) + + -- Get all units of the group. + local units=group:GetUnits() + + -- Loop over all units. + for _,_unit in pairs(units) do + -- Check if unit is human. + local human=self:_IsHumanUnit(_unit) + if human then + return true + end + end + + return false +end + +--- Get fuel state in pounds. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. +-- @return #number Fuel state in pounds. +function AIRBOSS:_GetFuelState(unit) + + -- Get relative fuel [0,1]. + local fuel=unit:GetFuel() + + -- Get max weight of fuel in kg. + local maxfuel=self:_GetUnitMasses(unit) + + -- Fuel state, i.e. what let's + local fuelstate=fuel*maxfuel + + -- Debug info. + self:T2(self.lid..string.format("Unit %s fuel state = %.1f kg = %.1f lbs", unit:GetName(), fuelstate, UTILS.kg2lbs(fuelstate))) + + return UTILS.kg2lbs(fuelstate) +end + +--- Convert altitude from meters to angels (thousands of feet). +-- @param #AIRBOSS self +-- @param alt Alitude in meters. +-- @return #number Altitude in Anglels = thousands of feet using math.floor(). +function AIRBOSS:_GetAngels(alt) + + if alt then + local angels=UTILS.Round(UTILS.MetersToFeet(alt)/1000, 0) + return angels + else + return 0 + end + +end + +--- Get unit masses especially fuel from DCS descriptor values. +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined. +-- @return #number Mass of fuel in kg. +-- @return #number Empty weight of unit in kg. +-- @return #number Max weight of unit in kg. +-- @return #number Max cargo weight in kg. +function AIRBOSS:_GetUnitMasses(unit) + + -- Get DCS descriptors table. + local Desc=unit:GetDesc() + + -- Mass of fuel in kg. + local massfuel=Desc.fuelMassMax or 0 + + -- Mass of empty unit in km. + local massempty=Desc.massEmpty or 0 + + -- Max weight of unit in kg. + local massmax=Desc.massMax or 0 + + -- Rest is cargo. + local masscargo=massmax-massfuel-massempty + + -- Debug info. + self:T2(self.lid..string.format("Unit %s mass fuel=%.1f kg, empty=%.1f kg, max=%.1f kg, cargo=%.1f kg", unit:GetName(), massfuel, massempty, massmax, masscargo)) + + return massfuel, massempty, massmax, masscargo +end + +--- Get player data from unit object +-- @param #AIRBOSS self +-- @param Wrapper.Unit#UNIT unit Unit in question. +-- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. +function AIRBOSS:_GetPlayerDataUnit(unit) + if unit:IsAlive() then + local unitname=unit:GetName() + local playerunit,playername=self:_GetPlayerUnitAndName(unitname) + if playerunit and playername then + return self.players[playername] + end + end + return nil +end + + +--- Get player data from group object. +-- @param #AIRBOSS self +-- @param Wrapper.Group#GROUP group Group in question. +-- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists. +function AIRBOSS:_GetPlayerDataGroup(group) + local units=group:GetUnits() + for _,unit in pairs(units) do + local playerdata=self:_GetPlayerDataUnit(unit) + if playerdata then + return playerdata + end + end + return nil +end + +--- Returns the unit of a player and the player name from the self.players table if it exists. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @return #string Name of player or nil. +function AIRBOSS:_GetPlayerUnit(_unitName) + + for _,_player in pairs(self.players) do + + local player=_player --#AIRBOSS.PlayerData + + if player.unit and player.unit:GetName()==_unitName then + self:T(self.lid..string.format("Found player=%s unit=%s in players table.", tostring(player.name), tostring(_unitName))) + return player.unit, player.name + end + + end + + return nil,nil +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player or nil. +-- @return #string Name of the player or nil. +function AIRBOSS:_GetPlayerUnitAndName(_unitName) + self:F2(_unitName) + + if _unitName ~= nil then + + -- First, let's look up all current players. + local u,pn=self:_GetPlayerUnit(_unitName) + + -- Return + if u and pn then + return u, pn + end + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + + if DCSunit then + + -- Get player name if any. + local playername=DCSunit:getPlayerName() + + -- Unit object. + local unit=UNIT:Find(DCSunit) + + -- Debug. + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + + -- Check if enverything is there. + if DCSunit and unit and playername then + self:T(self.lid..string.format("Found DCS unit %s with player %s.", tostring(_unitName), tostring(playername))) + return unit, playername + end + + end + + end + + -- Return nil if we could not find a player. + return nil,nil +end + +--- Get carrier coalition. +-- @param #AIRBOSS self +-- @return #number Coalition side of carrier. +function AIRBOSS:GetCoalition() + return self.carrier:GetCoalition() +end + +--- Get carrier coordinate. +-- @param #AIRBOSS self +-- @return Core.Point#COORDINATE Carrier coordinate. +function AIRBOSS:GetCoordinate() + return self.carrier:GetCoordinate() +end + + +--- Get static weather of this mission from env.mission.weather. +-- @param #AIRBOSS self +-- @param #table Clouds table which has entries "thickness", "density", "base", "iprecptns". +-- @param #number Visibility distance in meters. +-- @param #table Fog table, which has entries "thickness", "visibility" or nil if fog is disabled in the mission. +-- @param #number Dust density or nil if dust is disabled in the mission. +function AIRBOSS:_GetStaticWeather() + + -- Weather data from mission file. + local weather=env.mission.weather + + -- Clouds + --[[ + ["clouds"] = + { + ["thickness"] = 430, + ["density"] = 7, + ["base"] = 0, + ["iprecptns"] = 1, + }, -- end of ["clouds"] + ]] + local clouds=weather.clouds + + -- Visibilty distance in meters. + local visibility=weather.visibility.distance + + -- Dust + --[[ + ["enable_dust"] = false, + ["dust_density"] = 0, + ]] + local dust=nil + if weather.enable_dust==true then + dust=weather.dust_density + end + + -- Fog + --[[ + ["enable_fog"] = false, + ["fog"] = + { + ["thickness"] = 0, + ["visibility"] = 25, + }, -- end of ["fog"] + ]] + local fog=nil + if weather.enable_fog==true then + fog=weather.fog + end + + + return clouds, visibility, fog, dust +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MESSAGE Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function called by DCS timer. Unused. +-- @param #table param Parameters. +-- @param #number time Time. +function AIRBOSS._CheckRadioQueueT(param, time) + AIRBOSS._CheckRadioQueue(param.airboss, param.radioqueue, param.name) + return time+0.05 +end + +--- Radio queue item. +-- @type AIRBOSS.Radioitem +-- @field #number Tplay Abs time when transmission should be played. +-- @field #number Tstarted Abs time when transmission began to play. +-- @field #boolean isplaying Currently playing. +-- @field #AIRBOSS.Radio radio Radio object. +-- @field #AIRBOSS.RadioCall call Radio call. +-- @field #boolean loud If true, play loud version of file. +-- @field #number interval Interval in seconds after the last sound was played. + +--- Check radio queue for transmissions to be broadcasted. +-- @param #AIRBOSS self +-- @param #table radioqueue The radio queue. +-- @param #string name Name of the queue. +function AIRBOSS:_CheckRadioQueue(radioqueue, name) + + --env.info(string.format("FF %s #radioqueue %d", name, #radioqueue)) + + -- Check if queue is empty. + if #radioqueue==0 then + + if name=="LSO" then + self:T(self.lid..string.format("Stopping LSO radio queue.")) + self.radiotimer:Stop(self.RQLid) + self.RQLid=nil + elseif name=="MARSHAL" then + self:T(self.lid..string.format("Stopping Marshal radio queue.")) + self.radiotimer:Stop(self.RQMid) + self.RQMid=nil + end + + return + end + + -- Get current abs time. + local _time=timer.getAbsTime() + + local playing=false + local next=nil --#AIRBOSS.Radioitem + local _remove=nil + for i,_transmission in ipairs(radioqueue) do + local transmission=_transmission --#AIRBOSS.Radioitem + + -- Check if transmission time has passed. + if _time>=transmission.Tplay then + + -- Check if transmission is currently playing. + if transmission.isplaying then + + -- Check if transmission is finished. + if _time>=transmission.Tstarted+transmission.call.duration then + + -- Transmission over. + transmission.isplaying=false + _remove=i + + if transmission.radio.alias=="LSO" then + self.TQLSO=_time + elseif transmission.radio.alias=="MARSHAL" then + self.TQMarshal=_time + end + + else -- still playing + + -- Transmission is still playing. + playing=true + + end + + else -- not playing yet + + local Tlast=nil + if transmission.interval then + if transmission.radio.alias=="LSO" then + Tlast=self.TQLSO + elseif transmission.radio.alias=="MARSHAL" then + Tlast=self.TQMarshal + end + end + + if transmission.interval==nil then + + -- Not playing ==> this will be next. + if next==nil then + next=transmission + end + + else + + if _time-Tlast>=transmission.interval then + next=transmission + else + + end + end + + -- We got a transmission or one with an interval that is not due yet. No need for anything else. + if next or Tlast then + break + end + + end + + else + + -- Transmission not due yet. + + end + end + + -- Found a new transmission. + if next~=nil and not playing then + self:Broadcast(next.radio, next.call, next.loud) + next.isplaying=true + next.Tstarted=_time + end + + -- Remove completed calls from queue. + if _remove then + table.remove(radioqueue, _remove) + end + + return +end + +--- Add Radio transmission to radio queue. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio Radio sending the transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud If true, play loud sound file version. +-- @param #number delay Delay in seconds, before the message is broadcasted. +-- @param #number interval Interval in seconds after the last sound has been played. +-- @param #boolean click If true, play radio click at the end. +-- @param #boolean pilotcall If true, it's a pilot call. +function AIRBOSS:RadioTransmission(radio, call, loud, delay, interval, click, pilotcall) + self:F2({radio=radio, call=call, loud=loud, delay=delay, interval=interval, click=click}) + + -- Nil check. + if radio==nil or call==nil then + return + end + + -- Create a new radio transmission item. + local transmission={} --#AIRBOSS.Radioitem + + transmission.radio=radio + transmission.call=call + transmission.Tplay=timer.getAbsTime()+(delay or 0) + transmission.interval=interval + transmission.isplaying=false + transmission.Tstarted=nil + transmission.loud=loud and call.loud + + -- Player onboard number if sender has one. + if self:_IsOnboard(call.modexsender) then + self:_Number2Radio(radio, call.modexsender, delay, 0.3, pilotcall) + end + + -- Play onboard number if receiver has one. + if self:_IsOnboard(call.modexreceiver) then + self:_Number2Radio(radio, call.modexreceiver, delay, 0.3, pilotcall) + end + + -- Add transmission to the right queue. + local caller="" + if radio.alias=="LSO" then + + table.insert(self.RQLSO, transmission) + + caller="LSOCall" + + -- Schedule radio queue checks. + if not self.RQLid then + self:T(self.lid..string.format("Starting LSO radio queue.")) + self.RQLid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQLSO, "LSO"}, 0.02, 0.05) + end + + elseif radio.alias=="MARSHAL" then + + table.insert(self.RQMarshal, transmission) + + caller="MarshalCall" + + if not self.RQMid then + self:T(self.lid..string.format("Starting Marhal radio queue.")) + self.RQMid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQMarshal, "MARSHAL"}, 0.02, 0.05) + end + + end + + -- Append radio click sound at the end of the transmission. + if click then + self:RadioTransmission(radio, self[caller].CLICK, false, delay) + end +end + + +--- Check if a call needs a subtitle because the complete voice overs are not available. +-- @param #AIRBOSS self +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @return #boolean If true, call needs a subtitle. +function AIRBOSS:_NeedsSubtitle(call) + -- Currently we play the noise file. + if call.file==self.MarshalCall.NOISE.file or call.file==self.LSOCall.NOISE.file then + return true + else + return false + end +end + +--- Broadcast radio message. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio Radio sending transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud Play loud version of file. +function AIRBOSS:Broadcast(radio, call, loud) + self:F(call) + + -- Check which sound output method to use. + if not self.usersoundradio then + + ---------------------------- + -- Transmission via Radio -- + ---------------------------- + + -- Get unit sending the transmission. + local sender=self:_GetRadioSender(radio) + + -- Construct file name and subtitle. + local filename=self:_RadioFilename(call, loud, radio.alias) + + -- Create subtitle for transmission. + local subtitle=self:_RadioSubtitle(radio, call, loud) + + -- Debug. + self:T({filename=filename, subtitle=subtitle}) + + if sender then + + -- Broadcasting from aircraft. Only players tuned in to the right frequency will see the message. + self:T(self.lid..string.format("Broadcasting from aircraft %s", sender:GetName())) + + -- Command to set the Frequency for the transmission. + local commandFrequency={ + id="SetFrequency", + params={ + frequency=radio.frequency*1000000, -- Frequency in Hz. + modulation=radio.modulation, + }} + + -- Command to tranmit the call. + local commandTransmit={ + id = "TransmitMessage", + params = { + file=filename, + duration=call.subduration or 5, + subtitle=subtitle, + loop=false, + }} + + -- Set commend for frequency + sender:SetCommand(commandFrequency) + + -- Set command for radio transmission. + sender:SetCommand(commandTransmit) + + else + + -- Broadcasting from carrier. No subtitle possible. Need to send messages to players. + self:T(self.lid..string.format("Broadcasting from carrier via trigger.action.radioTransmission().")) + + -- Transmit from carrier position. + local vec3=self.carrier:GetPositionVec3() + + -- Transmit via trigger. + trigger.action.radioTransmission(filename, vec3, radio.modulation, false, radio.frequency*1000000, 100) + + -- Display subtitle of message to players. + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Message to all players in CCA that have subtites on. + if playerData.unit:IsInZone(self.zoneCCA) and playerData.actype~=AIRBOSS.AircraftCarrier.A4EC then + + -- Only to players with subtitle on or if noise is played. + if playerData.subtitles or self:_NeedsSubtitle(call) then + + -- Messages to marshal to everyone. Messages on LSO radio only to those in the pattern. + if radio.alias=="MARSHAL" or (radio.alias=="LSO" and self:_InQueue(self.Qpattern, playerData.group)) then + + -- Message to player. + self:MessageToPlayer(playerData, subtitle, nil, "", call.subduration or 5) + + end + + end + + end + end + end + end + + ---------------- + -- Easy Comms -- + ---------------- + + -- Workaround for the community A-4E-C as long as their radios are not functioning properly. + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Easy comms if globally activated but definitly for all player in the community A-4E. + if self.usersoundradio or playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + + -- Messages to marshal to everyone. Messages on LSO radio only to those in the pattern. + if radio.alias=="MARSHAL" or (radio.alias=="LSO" and self:_InQueue(self.Qpattern, playerData.group)) then + + -- User sound to players (inside CCA). + self:Sound2Player(playerData, radio, call, loud) + end + + end + end + +end + +--- Player user sound to player if he is inside the CCA. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #AIRBOSS.Radio radio The radio used for transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud If true, play loud sound file version. +-- @param #number delay Delay in seconds, before the message is broadcasted. +function AIRBOSS:Sound2Player(playerData, radio, call, loud, delay) + + -- Only to players inside the CCA. + if playerData.unit:IsInZone(self.zoneCCA) and call then + + -- Construct file name. + local filename=self:_RadioFilename(call, loud, radio.alias) + + -- Get Subtitle + local subtitle=self:_RadioSubtitle(radio, call, loud) + + -- Play sound file via usersound trigger. + USERSOUND:New(filename):ToGroup(playerData.group, delay) + + -- Only to players with subtitle on or if noise is played. + if playerData.subtitles or self:_NeedsSubtitle(call) then + self:MessageToPlayer(playerData, subtitle, nil, "", call.subduration, false, delay) + end + + end +end + +--- Create radio subtitle from radio call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio The radio used for transmission. +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud If true, append "!" else ".". +-- @return #string Subtitle to be displayed. +function AIRBOSS:_RadioSubtitle(radio, call, loud) + + -- No subtitle if call is nil, or subtitle is nil or subtitle is empty. + if call==nil or call.subtitle==nil or call.subtitle=="" then + return "" + end + + -- Sender + local sender=call.sender or radio.alias + if call.modexsender then + sender=call.modexsender + end + + -- Modex of receiver. + local receiver=call.modexreceiver or "" + + -- Init subtitle. + local subtitle=string.format("%s: %s", sender, call.subtitle) + if receiver and receiver~="" then + subtitle=string.format("%s: %s, %s", sender, receiver, call.subtitle) + end + + -- Last character of the string. + local lastchar=string.sub(subtitle, -1) + + -- Append ! or . + if loud then + if lastchar=="." or lastchar=="!" then + subtitle=string.sub(subtitle, 1,-1) + end + subtitle=subtitle.."!" + else + if lastchar=="!" then + -- This also okay. + elseif lastchar=="." then + -- Nothing to do. + else + subtitle=subtitle.."." + end + end + + return subtitle +end + +--- Get full file name for radio call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles. +-- @param #boolean loud Use loud version of file if available. +-- @param #string channel Radio channel alias "LSO" or "LSOCall", "MARSHAL" or "MarshalCall". +-- @return #string The file name of the radio sound. +function AIRBOSS:_RadioFilename(call, loud, channel) + + -- Construct file name and subtitle. + local prefix=call.file or "" + local suffix=call.suffix or "ogg" + + -- Path to sound files. Default is in the ME + local path=self.soundfolder or "l10n/DEFAULT/" + + -- Check for special LSO and Marshal sound folders. + if string.find(call.file, "LSO-") and channel and (channel=="LSO" or channel=="LSOCall") then + path=self.soundfolderLSO or path + end + if string.find(call.file, "MARSHAL-") and channel and (channel=="MARSHAL" or channel=="MarshalCall") then + path=self.soundfolderMSH or path + end + + -- Loud version. + if loud then + prefix=prefix.."_Loud" + end + + -- File name inclusing path in miz file. + local filename=string.format("%s%s.%s", path, prefix, suffix) + + return filename +end + +--- Send text message to player client. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay) + + if playerData and message and message~="" then + + -- Default duration. + duration=duration or self.Tmessage + + -- Format message. + local text + if receiver and receiver=="" then + -- No (blank) receiver. + text=string.format("%s", message) + else + -- Default "receiver" is onboard number of player. + receiver=receiver or playerData.onboard + text=string.format("%s, %s", receiver, message) + end + self:T(self.lid..text) + + if delay and delay>0 then + -- Delayed call. + --SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear}, delay) + self:ScheduleOnce(delay, self.MessageToPlayer, self, playerData, message, sender, receiver, duration, clear) + else + + -- Wait until previous sound finished. + local wait=0 + + -- Onboard number to get the attention. + if receiver==playerData.onboard then + + -- Which voice over number to use. + if sender and (sender=="LSO" or sender=="MARSHAL" or sender=="AIRBOSS") then + + -- User sound of board number. + wait=wait+self:_Number2Sound(playerData, sender, receiver) + + end + end + + -- Negative. + if string.find(text:lower(), "negative") then + local filename=self:_RadioFilename(self.MarshalCall.NEGATIVE, false, "MARSHAL") + USERSOUND:New(filename):ToGroup(playerData.group, wait) + wait=wait+self.MarshalCall.NEGATIVE.duration + end + + -- Affirm. + if string.find(text:lower(), "affirm") then + local filename=self:_RadioFilename(self.MarshalCall.AFFIRMATIVE, false, "MARSHAL") + USERSOUND:New(filename):ToGroup(playerData.group, wait) + wait=wait+self.MarshalCall.AFFIRMATIVE.duration + end + + -- Roger. + if string.find(text:lower(), "roger") then + local filename=self:_RadioFilename(self.MarshalCall.ROGER, false, "MARSHAL") + USERSOUND:New(filename):ToGroup(playerData.group, wait) + wait=wait+self.MarshalCall.ROGER.duration + end + + -- Play click sound to end message. + if wait>0 then + local filename=self:_RadioFilename(self.MarshalCall.CLICK) + USERSOUND:New(filename):ToGroup(playerData.group, wait) + end + + -- Text message to player client. + if playerData.client then + MESSAGE:New(text, duration, sender, clear):ToClient(playerData.client) + end + + end + + end +end + + +--- Send text message to all players in the pattern queue. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +function AIRBOSS:MessageToPattern(message, sender, receiver, duration, clear, delay) + + -- Create new (fake) radio call to show the subtitile. + local call=self:_NewRadioCall(self.LSOCall.NOISE, sender or "LSO", message, duration, receiver, sender) + + -- Dummy radio transmission to display subtitle only to those who tuned in. + self:RadioTransmission(self.LSORadio, call, false, delay, nil, true) + +end + +--- Send text message to all players in the marshal queue. +-- Message format will be "SENDER: RECCEIVER, MESSAGE". +-- @param #AIRBOSS self +-- @param #string message The message to send. +-- @param #string sender The person who sends the message or nil. +-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver. +-- @param #number duration Display message duration. Default 10 seconds. +-- @param #boolean clear If true, clear screen from previous messages. +-- @param #number delay Delay in seconds, before the message is displayed. +function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, delay) + + -- Create new (fake) radio call to show the subtitile. + local call=self:_NewRadioCall(self.MarshalCall.NOISE, sender or "MARSHAL", message, duration, receiver, sender) + + -- Dummy radio transmission to display subtitle only to those who tuned in. + self:RadioTransmission(self.MarshalRadio, call, false, delay, nil, true) + +end + +--- Generate a new radio call (deepcopy) from an existing default call. +-- @param #AIRBOSS self +-- @param #AIRBOSS.RadioCall call Radio call to be enhanced. +-- @param #string sender Sender of the message. Default is the radio alias. +-- @param #string subtitle Subtitle of the message. Default from original radio call. Use "" for no subtitle. +-- @param #number subduration Time in seconds the subtitle is displayed. Default 10 seconds. +-- @param #string modexreceiver Onboard number of the receiver or nil. +-- @param #string modexsender Onboard number of the sender or nil. +function AIRBOSS:_NewRadioCall(call, sender, subtitle, subduration, modexreceiver, modexsender) + + -- Create a new call + local newcall=UTILS.DeepCopy(call) --#AIRBOSS.RadioCall + + -- Sender for displaying the subtitle. + newcall.sender=sender + + -- Subtitle of the message. + newcall.subtitle=subtitle or call.subtitle + + -- Duration of subtitle display. + newcall.subduration=subduration or self.Tmessage + + -- Tail number of the receiver. + if self:_IsOnboard(modexreceiver) then + newcall.modexreceiver=modexreceiver + end + + -- Tail number of the sender. + if self:_IsOnboard(modexsender) then + newcall.modexsender=modexsender + end + + return newcall +end + +--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio Airboss radio data. +-- @return Wrapper.Unit#UNIT Sending aircraft unit or nil if was not setup, is not an aircraft or is not alive. +function AIRBOSS:_GetRadioSender(radio) + + -- Check if we have a sending aircraft. + local sender=nil --Wrapper.Unit#UNIT + + -- Try the general default. + if self.senderac then + sender=UNIT:FindByName(self.senderac) + end + + -- Try the specific marshal unit. + if radio.alias=="MARSHAL" then + if self.radiorelayMSH then + sender=UNIT:FindByName(self.radiorelayMSH) + end + end + + -- Try the specific LSO unit. + if radio.alias=="LSO" then + if self.radiorelayLSO then + sender=UNIT:FindByName(self.radiorelayLSO) + end + end + + -- Check that sender is alive and an aircraft. + if sender and sender:IsAlive() and sender:IsAir() then + return sender + end + + return nil +end + +--- Check if text is an onboard number of a flight. +-- @param #AIRBOSS self +-- @param #string text Text to check. +-- @return #boolean If true, text is an onboard number of a flight. +function AIRBOSS:_IsOnboard(text) + + -- Nil check. + if text==nil then + return false + end + + -- Message to all. + if text=="99" then + return true + end + + -- Loop over all flights. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Loop over all onboard number of that flight. + for _,onboard in pairs(flight.onboardnumbers) do + if text==onboard then + return true + end + end + + end + + return false +end + +--- Convert a number (as string) into an outsound and play it to a player group. E.g. for board number or headings. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +-- @param #string sender Who is sending the call, either "LSO" or "MARSHAL". +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +-- @return #number Duration of the call in seconds. +function AIRBOSS:_Number2Sound(playerData, sender, number, delay) + + -- Default. + delay=delay or 0 + + --- Split string into characters. + local function _split(str) + local chars={} + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + return chars + end + + -- Sender + local Sender + if sender=="LSO" then + Sender="LSOCall" + elseif sender=="MARSHAL" or sender=="AIRBOSS" then + Sender="MarshalCall" + else + self:E(self.lid..string.format("ERROR: Unknown radio sender %s!", tostring(sender))) + return + end + + -- Split string into characters. + local numbers=_split(number) + + local wait=0 + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + -- Convert to N0, N1, ... + local N=string.format("N%s", n) + + -- Radio call. + local call=self[Sender][N] --#AIRBOSS.RadioCall + + -- Create file name. + local filename=self:_RadioFilename(call, false, Sender) + + -- Play sound. + USERSOUND:New(filename):ToGroup(playerData.group, delay+wait) + + -- Wait until this call is over before playing the next. + wait=wait+call.duration + end + + return wait +end + +--- Convert a number (as string) into a radio message. +-- E.g. for board number or headings. +-- @param #AIRBOSS self +-- @param #AIRBOSS.Radio radio Radio used for transmission. +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +-- @param #number interval Interval between the next call. +-- @param #boolean pilotcall If true, use pilot sound files. +-- @return #number Duration of the call in seconds. +function AIRBOSS:_Number2Radio(radio, number, delay, interval, pilotcall) + + --- Split string into characters. + local function _split(str) + local chars={} + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + return chars + end + + -- Sender. + local Sender="" + if radio.alias=="LSO" then + Sender="LSOCall" + elseif radio.alias=="MARSHAL" then + Sender="MarshalCall" + else + self:E(self.lid..string.format("ERROR: Unknown radio alias %s!", tostring(radio.alias))) + end + + if pilotcall then + Sender="PilotCall" + end + + -- Split string into characters. + local numbers=_split(number) + + local wait=0 + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + -- Convert to N0, N1, ... + local N=string.format("N%s", n) + + -- Radio call. + local call=self[Sender][N] --#AIRBOSS.RadioCall + + if interval and i==1 then + -- Transmit. + self:RadioTransmission(radio, call, false, delay, interval) + else + self:RadioTransmission(radio, call, false, delay) + end + + -- Add up duration of the number. + wait=wait+call.duration + end + + -- Return the total duration of the call. + return wait +end + + +--- AI aircraft calls the ball. +-- @param #AIRBOSS self +-- @param #string modex Tail number. +-- @param #string nickname Aircraft nickname. +-- @param #number fuelstate Aircraft fuel state in thouthands of pounds. +function AIRBOSS:_LSOCallAircraftBall(modex, nickname, fuelstate) + + -- Pilot: "405, Hornet Ball, 3.2" + local text=string.format("%s Ball, %.1f.", nickname, fuelstate) + + -- Debug message. + self:I(self.lid..text) + + -- Nickname UPPERCASE. + local NICKNAME=nickname:upper() + + -- Fuel state. + local FS=UTILS.Split(string.format("%.1f", fuelstate), ".") + + -- Create new call to display complete subtitle. + local call=self:_NewRadioCall(self.PilotCall[NICKNAME], modex, text, self.Tmessage, nil, modex) + + -- Hornet .. + self:RadioTransmission(self.LSORadio, call, nil, nil, nil, nil, true) + -- Ball, + self:RadioTransmission(self.LSORadio, self.PilotCall.BALL, nil, nil, nil, nil, true) + -- X.. + self:_Number2Radio(self.LSORadio, FS[1], nil, nil, true) + -- Point.. + self:RadioTransmission(self.LSORadio, self.PilotCall.POINT, nil, nil, nil, nil, true) + -- Y. + self:_Number2Radio(self.LSORadio, FS[2], nil, nil, true) + + -- CLICK! + self:RadioTransmission(self.LSORadio, self.LSOCall.CLICK) + +end + +--- AI is bingo and goes to the recovery tanker. +-- @param #AIRBOSS self +-- @param #string modex Tail number. +function AIRBOSS:_MarshalCallGasAtTanker(modex) + + -- Subtitle. + local text=string.format("Bingo fuel! Going for gas at the recovery tanker.") + + -- Debug message. + self:I(self.lid..text) + + -- Create new call to display complete subtitle. + local call=self:_NewRadioCall(self.PilotCall.BINGOFUEL, modex, text, self.Tmessage, nil, modex) + + -- MODEX, bingo fuel! + self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, nil, true) + + -- Going for fuel at the recovery tanker. Click! + self:RadioTransmission(self.MarshalRadio, self.PilotCall.GASATTANKER, nil, nil, nil, true, true) + +end + +--- AI is bingo and goes to the divert field. +-- @param #AIRBOSS self +-- @param #string modex Tail number. +-- @param #string divertname Name of the divert field. +function AIRBOSS:_MarshalCallGasAtDivert(modex, divertname) + + -- Subtitle. + local text=string.format("Bingo fuel! Going for gas at divert field %s.", divertname) + + -- Debug message. + self:I(self.lid..text) + + -- Create new call to display complete subtitle. + local call=self:_NewRadioCall(self.PilotCall.BINGOFUEL, modex, text, self.Tmessage, nil, modex) + + -- MODEX, bingo fuel! + self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, nil, true) + + -- Going for fuel at the divert field. Click! + self:RadioTransmission(self.MarshalRadio, self.PilotCall.GASATDIVERT, nil, nil, nil, true, true) + +end + + +--- Inform everyone that recovery ops are stopped and deck is closed. +-- @param #AIRBOSS self +-- @param #number case Recovery case. +function AIRBOSS:_MarshalCallRecoveryStopped(case) + + -- Subtitle. + local text=string.format("Case %d recovery ops are stopped. Deck is closed.", case) + + -- Debug message. + self:I(self.lid..text) + + -- Create new call to display complete subtitle. + local call=self:_NewRadioCall(self.MarshalCall.CASE, "AIRBOSS", text, self.Tmessage, "99") + + -- 99, Case.. + self:RadioTransmission(self.MarshalRadio, call) + -- X. + self:_Number2Radio(self.MarshalRadio, tostring(case)) + -- recovery ops are stopped. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.RECOVERYOPSSTOPPED, nil, nil, 0.2) + -- Deck is closed. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DECKCLOSED, nil, nil, nil, true) + +end + +--- Inform everyone that recovery is paused and will resume at a certain time. +-- @param #AIRBOSS self +function AIRBOSS:_MarshalCallRecoveryPausedUntilFurtherNotice() + + -- Create new call. Subtitle already set. + local call=self:_NewRadioCall(self.MarshalCall.RECOVERYPAUSEDNOTICE, "AIRBOSS", nil, self.Tmessage, "99") + + -- 99, aircraft recovery is paused until further notice. + self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, true) + +end + +--- Inform everyone that recovery is paused and will resume at a certain time. +-- @param #AIRBOSS self +-- @param #string clock Time. +function AIRBOSS:_MarshalCallRecoveryPausedResumedAt(clock) + + -- Get relevant part of clock. + local _clock=UTILS.Split(clock, "+") + local CT=UTILS.Split(_clock[1], ":") + + -- Subtitle. + local text=string.format("aircraft recovery is paused and will be resumed at %s.", clock) + + -- Debug message. + self:I(self.lid..text) + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.RECOVERYPAUSEDRESUMED, "AIRBOSS", text, self.Tmessage, "99") + + -- 99, aircraft recovery is paused and will resume at... + self:RadioTransmission(self.MarshalRadio, call) + + -- XY.. (hours) + self:_Number2Radio(self.MarshalRadio, CT[1]) + -- XY (minutes).. + self:_Number2Radio(self.MarshalRadio, CT[2]) + -- hours. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOURS, nil, nil, nil, true) + +end + + +--- Inform flight that he is cleared for recovery. +-- @param #AIRBOSS self +-- @param #string modex Tail number. +-- @param #number case Recovery case. +function AIRBOSS:_MarshalCallClearedForRecovery(modex, case) + + -- Subtitle. + local text=string.format("you're cleared for Case %d recovery.", case) + + -- Debug message. + self:I(self.lid..text) + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.CLEAREDFORRECOVERY, "MARSHAL", text, self.Tmessage, modex) + + -- Two second delay. + local delay=2 + + -- XYZ, you're cleared for case.. + self:RadioTransmission(self.MarshalRadio, call, nil, delay) + -- X.. + self:_Number2Radio(self.MarshalRadio, tostring(case), delay) + -- recovery. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.RECOVERY, nil, delay, nil, true) + +end + +--- Inform everyone that recovery is resumed after pause. +-- @param #AIRBOSS self +function AIRBOSS:_MarshalCallResumeRecovery() + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.RESUMERECOVERY, "AIRBOSS", nil, self.Tmessage, "99") + + -- 99, aircraft recovery resumed. Click! + self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, true) + +end + +--- Inform everyone about new final bearing. +-- @param #AIRBOSS self +-- @param #number FB Final Bearing in degrees. +function AIRBOSS:_MarshalCallNewFinalBearing(FB) + + -- Subtitle. + local text=string.format("new final bearing %03d°.", FB) + + -- Debug message. + self:I(self.lid..text) + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.NEWFB, "AIRBOSS", text, self.Tmessage, "99") + + -- 99, new final bearing.. + self:RadioTransmission(self.MarshalRadio, call) + -- XYZ.. + self:_Number2Radio(self.MarshalRadio, string.format("%03d", FB), nil, 0.2) + -- Degrees. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true) + +end + +--- Compile a radio call when Marshal tells a flight the holding alitude. +-- @param #AIRBOSS self +-- @param #number hdg Heading in degrees. +function AIRBOSS:_MarshalCallCarrierTurnTo(hdg) + + -- Subtitle. + local text=string.format("carrier is now starting turn to heading %03d°.", hdg) + + -- Debug message. + self:I(self.lid..text) + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.CARRIERTURNTOHEADING, "AIRBOSS", text, self.Tmessage, "99") + + -- 99, turning to heading... + self:RadioTransmission(self.MarshalRadio, call) + -- XYZ.. + self:_Number2Radio(self.MarshalRadio, string.format("%03d", hdg), nil, 0.2) + -- Degrees. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true) + +end + +--- Compile a radio call when Marshal tells a flight the holding alitude. +-- @param #AIRBOSS self +-- @param #string modex Tail number. +-- @param #number nwaiting Number of flights already waiting. +function AIRBOSS:_MarshalCallStackFull(modex, nwaiting) + + -- Subtitle. + local text=string.format("Marshal stack is currently full. Hold outside 10 NM zone and wait for further instructions. ") + if nwaiting==1 then + text=text..string.format("There is one flight ahead of you.") + elseif nwaiting>1 then + text=text..string.format("There are %d flights ahead of you.", nwaiting) + else + text=text..string.format("You are next in line.") + end + + -- Debug message. + self:I(self.lid..text) + + -- Create new call with full subtitle. + local call=self:_NewRadioCall(self.MarshalCall.STACKFULL, "AIRBOSS", text, self.Tmessage, modex) + + -- XYZ, Marshal stack is currently full. + self:RadioTransmission(self.MarshalRadio, call, nil, nil, nil, true) +end + +--- Compile a radio call when Marshal tells a flight the holding alitude. +-- @param #AIRBOSS self +function AIRBOSS:_MarshalCallRecoveryStart(case) + + -- Marshal radial. + local radial=self:GetRadial(case, true, true, false) + + -- Debug output. + local text=string.format("Starting aircraft recovery Case %d ops.", case) + if case>1 then + text=text..string.format(" Marshal radial %03d°.", radial) + end + self:T(self.lid..text) + + -- New call including the subtitle. + local call=self:_NewRadioCall(self.MarshalCall.STARTINGRECOVERY, "AIRBOSS", text, self.Tmessage, "99") + + -- 99, Starting aircraft recovery case.. + self:RadioTransmission(self.MarshalRadio, call) + -- X.. + self:_Number2Radio(self.MarshalRadio,tostring(case), nil, 0.1) + -- ops. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.OPS) + + --Marshal Radial + if case>1 then + -- Marshal radial.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.MARSHALRADIAL) + -- XYZ.. + self:_Number2Radio(self.MarshalRadio, string.format("%03d", radial), nil, 0.2) + -- Degrees. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true) + end + +end + +--- Compile a radio call when Marshal tells a flight the holding alitude. +-- @param #AIRBOSS self +-- @param #string modex Tail number. +-- @param #number case Recovery case. +-- @param #number brc Base recovery course. +-- @param #number altitude Holding alitude. +-- @param #string charlie Charlie Time estimate. +-- @param #number qfe Alitmeter inHg. +function AIRBOSS:_MarshalCallArrived(modex, case, brc, altitude, charlie, qfe) + self:F({modex=modex,case=case,brc=brc,altitude=altitude,charlie=charlie,qfe=qfe}) + + -- Split strings etc. + local angels=self:_GetAngels(altitude) + --local QFE=UTILS.Split(tostring(UTILS.Round(qfe,2)), ".") + local QFE=UTILS.Split(string.format("%.2f", qfe), ".") + local clock=UTILS.Split(charlie, "+") + local CT=UTILS.Split(clock[1], ":") + + -- Subtitle text. + local text=string.format("Case %d, expected BRC %03d°, hold at angels %d. Expected Charlie Time %s. Altimeter %.2f. Report see me.", case, brc, angels, charlie, qfe) + + -- Debug message. + self:I(self.lid..text) + + -- Create new call to display complete subtitle. + local casecall=self:_NewRadioCall(self.MarshalCall.CASE, "MARSHAL", text, self.Tmessage, modex) + + -- Case.. + self:RadioTransmission(self.MarshalRadio, casecall) + -- X. + self:_Number2Radio(self.MarshalRadio, tostring(case)) + + -- Expected.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, nil, 0.5) + -- BRC.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.BRC) + -- XYZ... + self:_Number2Radio(self.MarshalRadio, string.format("%03d", brc)) + -- Degrees. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.DEGREES) + + + -- Hold at.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOLDATANGELS, nil, nil, 0.5) + -- X. + self:_Number2Radio(self.MarshalRadio, tostring(angels)) + + -- Expected.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.EXPECTED, nil, nil, 0.5) + -- Charlie time.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.CHARLIETIME) + -- XY.. (hours) + self:_Number2Radio(self.MarshalRadio, CT[1]) + -- XY (minutes). + self:_Number2Radio(self.MarshalRadio, CT[2]) + -- hours. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.HOURS) + + + -- Altimeter.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.ALTIMETER, nil, nil, 0.5) + -- XY.. + self:_Number2Radio(self.MarshalRadio, QFE[1]) + -- Point.. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.POINT) + -- XY. + self:_Number2Radio(self.MarshalRadio, QFE[2]) + + -- Report see me. Click! + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.REPORTSEEME, nil, nil, 0.5, true) + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MENU Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Add menu commands for player. +-- @param #AIRBOSS self +-- @param #string _unitName Name of player unit. +function AIRBOSS:_AddF10Commands(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check for player unit. + if _unit and playername then + + -- Get group and ID. + local group=_unit:GetGroup() + local gid=group:GetID() + + if group and gid then + + if not self.menuadded[gid] then + + -- Enable switch so we don't do this twice. + self.menuadded[gid]=true + + -- Set menu root path. + local _rootPath=nil + if AIRBOSS.MenuF10Root then + ------------------------ + -- MISSON LEVEL MENUE -- + ------------------------ + + if self.menusingle then + -- F10/Airboss/... + _rootPath=AIRBOSS.MenuF10Root + else + -- F10/Airboss//... + _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10Root) + end + + else + ------------------------ + -- GROUP LEVEL MENUES -- + ------------------------ + + -- Main F10 menu: F10/Airboss/ + if AIRBOSS.MenuF10[gid]==nil then + AIRBOSS.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "Airboss") + end + + + if self.menusingle then + -- F10/Airboss/... + _rootPath=AIRBOSS.MenuF10[gid] + else + -- F10/Airboss//... + _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) + end + + end + + + -------------------------------- + -- F10/Airboss//F1 Help + -------------------------------- + local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) + -- F10/Airboss//F1 Help/F1 Mark Zones + if self.menumarkzones then + local _markPath=missionCommands.addSubMenuForGroup(gid, "Mark Zones", _helpPath) + -- F10/Airboss//F1 Help/F1 Mark Zones/ + if self.menusmokezones then + missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F1 + end + missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F2 + if self.menusmokezones then + missionCommands.addCommandForGroup(gid, "Smoke Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) -- F3 + end + missionCommands.addCommandForGroup(gid, "Flare Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) -- F4 + end + -- F10/Airboss//F1 Help/F2 Skill Level + local _skillPath=missionCommands.addSubMenuForGroup(gid, "Skill Level", _helpPath) + -- F10/Airboss//F1 Help/F2 Skill Level/ + missionCommands.addCommandForGroup(gid, "Flight Student", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.EASY) -- F1 + missionCommands.addCommandForGroup(gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.NORMAL) -- F2 + missionCommands.addCommandForGroup(gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.HARD) -- F3 + missionCommands.addCommandForGroup(gid, "Hints On/Off", _skillPath, self._SetHintsOnOff, self, _unitName) -- F4 + -- F10/Airboss//F1 Help/ + missionCommands.addCommandForGroup(gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "Attitude Monitor", _helpPath, self._DisplayAttitude, self, _unitName) -- F4 + missionCommands.addCommandForGroup(gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName) -- F5 + missionCommands.addCommandForGroup(gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName) -- F6 + missionCommands.addCommandForGroup(gid, "Subtitles On/Off", _helpPath, self._SubtitlesOnOff, self, _unitName) -- F7 + missionCommands.addCommandForGroup(gid, "Trapsheet On/Off", _helpPath, self._TrapsheetOnOff, self, _unitName) -- F8 + + ------------------------------------- + -- F10/Airboss//F2 Kneeboard + ------------------------------------- + local _kneeboardPath=missionCommands.addSubMenuForGroup(gid, "Kneeboard", _rootPath) + -- F10/Airboss//F2 Kneeboard/F1 Results + local _resultsPath=missionCommands.addSubMenuForGroup(gid, "Results", _kneeboardPath) + -- F10/Airboss//F2 Kneeboard/F1 Results/ + missionCommands.addCommandForGroup(gid, "Greenie Board", _resultsPath, self._DisplayScoreBoard, self, _unitName) -- F1 + missionCommands.addCommandForGroup(gid, "My LSO Grades", _resultsPath, self._DisplayPlayerGrades, self, _unitName) -- F2 + missionCommands.addCommandForGroup(gid, "Last Debrief", _resultsPath, self._DisplayDebriefing, self, _unitName) -- F3 + + -- F10/Airboss//F2 Kneeboard/F2 Skipper/ + if self.skipperMenu then + local _skipperPath =missionCommands.addSubMenuForGroup(gid, "Skipper", _kneeboardPath) + local _menusetspeed=missionCommands.addSubMenuForGroup(gid, "Set Speed", _skipperPath) + missionCommands.addCommandForGroup(gid, "10 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 10) + missionCommands.addCommandForGroup(gid, "15 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 15) + missionCommands.addCommandForGroup(gid, "20 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 20) + missionCommands.addCommandForGroup(gid, "25 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 25) + missionCommands.addCommandForGroup(gid, "30 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 30) + local _menusetrtime=missionCommands.addSubMenuForGroup(gid, "Set Time", _skipperPath) + missionCommands.addCommandForGroup(gid, "15 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 15) + missionCommands.addCommandForGroup(gid, "30 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 30) + missionCommands.addCommandForGroup(gid, "45 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 45) + missionCommands.addCommandForGroup(gid, "60 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 60) + missionCommands.addCommandForGroup(gid, "90 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 90) + local _menusetrtime=missionCommands.addSubMenuForGroup(gid, "Set Marshal Radial", _skipperPath) + missionCommands.addCommandForGroup(gid, "+30°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 30) + missionCommands.addCommandForGroup(gid, "+15°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 15) + missionCommands.addCommandForGroup(gid, "0°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 0) + missionCommands.addCommandForGroup(gid, "-15°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, -15) + missionCommands.addCommandForGroup(gid, "-30°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, -30) + missionCommands.addCommandForGroup(gid, "U-turn On/Off", _skipperPath, self._SkipperRecoveryUturn, self, _unitName) + missionCommands.addCommandForGroup(gid, "Start CASE I", _skipperPath, self._SkipperStartRecovery, self, _unitName, 1) + missionCommands.addCommandForGroup(gid, "Start CASE II", _skipperPath, self._SkipperStartRecovery, self, _unitName, 2) + missionCommands.addCommandForGroup(gid, "Start CASE III",_skipperPath, self._SkipperStartRecovery, self, _unitName, 3) + missionCommands.addCommandForGroup(gid, "Stop Recovery", _skipperPath, self._SkipperStopRecovery, self, _unitName) + end + + -- F10/Airboss// + ------------------------- + missionCommands.addCommandForGroup(gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "Request Commence", _rootPath, self._RequestCommence, self, _unitName) -- F4 + missionCommands.addCommandForGroup(gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName) -- F5 + missionCommands.addCommandForGroup(gid, "Spinning", _rootPath, self._RequestSpinning, self, _unitName) -- F6 + missionCommands.addCommandForGroup(gid, "Emergency Landing", _rootPath, self._RequestEmergency, self, _unitName) -- F7 + missionCommands.addCommandForGroup(gid, "[Reset My Status]", _rootPath, self._ResetPlayerStatus, self, _unitName) -- F8 + end + else + self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName)) + end + else + self:E(self.lid..string.format("ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.", _unitName)) + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- SKIPPER MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Reset player status. Player is removed from all queues and its status is set to undefined. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +-- @param #number case Recovery case. +function AIRBOSS:_SkipperStartRecovery(_unitName, case) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Inform player. + local text=string.format("affirm, Case %d recovery will start in 5 min for %d min. Wind on deck %d knots. U-turn=%s.", case, self.skipperTime, self.skipperSpeed, tostring(self.skipperUturn)) + if case>1 then + text=text..string.format(" Marshal radial %d°.", self.skipperOffset) + end + if self:IsRecovering() then + text="negative, carrier is already recovering." + self:MessageToPlayer(playerData, text, "AIRBOSS") + return + end + self:MessageToPlayer(playerData, text, "AIRBOSS") + + -- Recovery staring in 5 min for 30 min. + local t0=timer.getAbsTime()+5*60 + local t9=t0+self.skipperTime*60 + local C0=UTILS.SecondsToClock(t0) + local C9=UTILS.SecondsToClock(t9) + + -- Carrier will turn into the wind. Wind on deck 25 knots. U-turn on. + self:AddRecoveryWindow(C0, C9, case, self.skipperOffset, true, self.skipperSpeed, self.skipperUturn) + + end + end +end + +--- Skipper Stop recovery function. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_SkipperStopRecovery(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Inform player. + local text="roger, stopping recovery right away." + if not self:IsRecovering() then + text="negative, carrier is currently not recovering." + self:MessageToPlayer(playerData, text, "AIRBOSS") + return + end + self:MessageToPlayer(playerData, text, "AIRBOSS") + + self:RecoveryStop() + end + end +end + +--- Skipper set recovery offset angle. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +-- @param #number offset Recovery holding offset angle in degrees for Case II/III. +function AIRBOSS:_SkipperRecoveryOffset(_unitName, offset) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Inform player. + local text=string.format("roger, relative CASE II/III Marshal radial set to %d°.", offset) + self:MessageToPlayer(playerData, text, "AIRBOSS") + + self.skipperOffset=offset + end + end +end + +--- Skipper set recovery time. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +-- @param #number time Recovery time in minutes. +function AIRBOSS:_SkipperRecoveryTime(_unitName, time) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Inform player. + local text=string.format("roger, manual recovery time set to %d min.", time) + self:MessageToPlayer(playerData, text, "AIRBOSS") + + self.skipperTime=time + + end + end +end + +--- Skipper set recovery speed. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +-- @param #number speed Recovery speed in knots. +function AIRBOSS:_SkipperRecoverySpeed(_unitName, speed) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Inform player. + local text=string.format("roger, wind on deck set to %d knots.", speed) + self:MessageToPlayer(playerData, text, "AIRBOSS") + + self.skipperSpeed=speed + end + end +end + +--- Skipper set recovery speed. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_SkipperRecoveryUturn(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + self.skipperUturn=not self.skipperUturn + + -- Inform player. + local text=string.format("roger, U-turn is now %s.", tostring(self.skipperUturn)) + self:MessageToPlayer(playerData, text, "AIRBOSS") + + end + end +end + + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- ROOT MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Reset player status. Player is removed from all queues and its status is set to undefined. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_ResetPlayerStatus(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Inform player. + local text="roger, status reset executed! You have been removed from all queues." + self:MessageToPlayer(playerData, text, "AIRBOSS") + + -- Remove flight from queues. Collapse marshal stack if necessary. + -- Section members are removed from the Spinning queue. If flight is member, he is removed from the section. + self:_RemoveFlight(playerData) + + -- Stop pending debrief scheduler. + if playerData.debriefschedulerID and self.Scheduler then + self.Scheduler:Stop(playerData.debriefschedulerID) + end + + -- Initialize player data. + self:_InitPlayer(playerData) + + end + end +end + +--- Request marshal. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestMarshal(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Check if player is in CCA + local inCCA=playerData.unit:IsInZone(self.zoneCCA) + + if inCCA then + + if self:_InQueue(self.Qmarshal, playerData.group) then + + -- Flight group is already in marhal queue. + local text=string.format("negative, you are already in the Marshal queue. New marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif self:_InQueue(self.Qpattern, playerData.group) then + + -- Flight group is already in pattern queue. + local text=string.format("negative, you are already in the Pattern queue. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif self:_InQueue(self.Qwaiting, playerData.group) then + + -- Flight group is already in pattern queue. + local text=string.format("negative, you are in the Waiting queue with %d flights ahead of you. Marshal request denied!", #self.Qwaiting) + self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif not _unit:InAir() then + + -- Flight group is already in pattern queue. + local text=string.format("negative, you are not airborne. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif playerData.name~=playerData.seclead then + + -- Flight group is already in pattern queue. + local text=string.format("negative, your section lead %s needs to request Marshal.", playerData.seclead) + self:MessageToPlayer(playerData, text, "MARSHAL") + + else + + -- Get next free Marshal stack. + local freestack=self:_GetFreeStack(playerData.ai) + + -- Check if stack is available. For Case I the number is limited. + if freestack then + + -- Add flight to marshal stack. + self:_MarshalPlayer(playerData, freestack) + + else + + -- Add flight to waiting queue. + self:_WaitPlayer(playerData) + + end + + end + + else + + -- Flight group is not in CCA yet. + local text=string.format("negative, you are not inside CCA. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + end + end + end +end + +--- Request emergency landing. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestEmergency(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + local text="" + if not self.emergency then + + -- Mission designer did not allow emergency landing. + text="negative, no emergency landings on my carrier. We are currently busy. See how you get along!" + + elseif not _unit:InAir() then + + -- Carrier zone. + local zone=self:_GetZoneCarrierBox() + + -- Check if player is on the carrier. + if playerData.unit:IsInZone(zone) then + + -- Bolter pattern. + text="roger, you are now technically in the bolter pattern. Your next step after takeoff is abeam!" + + -- Get flight lead. + local lead=self:_GetFlightLead(playerData) + + -- Set set for lead. + self:_SetPlayerStep(lead, AIRBOSS.PatternStep.BOLTER) + + -- Also set bolter pattern for all members. + for _,sec in pairs(lead.section) do + local sectionmember=sec --#AIRBOSS.PlayerData + self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.BOLTER) + end + + -- Remove flight from waiting queue just in case. + self:_RemoveFlightFromQueue(self.Qwaiting, lead) + + if self:_InQueue(self.Qmarshal, lead.group) then + -- Remove flight from Marshal queue and add to pattern. + self:_RemoveFlightFromMarshalQueue(lead) + else + -- Add flight to pattern if he was not. + if not self:_InQueue(self.Qpattern, lead.group) then + self:_AddFlightToPatternQueue(lead) + end + end + + else + -- Flight group is not in air. + text=string.format("negative, you are not airborne. Request denied!") + end + + else + + -- Cleared. + text="affirmative, you can bypass the pattern and are cleared for final approach!" + + -- Now, if player is in the marshal or waiting queue he will be removed. But the new leader should stay in or not. + local lead=self:_GetFlightLead(playerData) + + -- Set set for lead. + self:_SetPlayerStep(lead, AIRBOSS.PatternStep.EMERGENCY) + + -- Also set emergency landing for all members. + for _,sec in pairs(lead.section) do + local sectionmember=sec --#AIRBOSS.PlayerData + self:_SetPlayerStep(sectionmember, AIRBOSS.PatternStep.EMERGENCY) + + -- Remove flight from spinning queue just in case (everone can spin on his own). + self:_RemoveFlightFromQueue(self.Qspinning, sectionmember) + end + + -- Remove flight from waiting queue just in case. + self:_RemoveFlightFromQueue(self.Qwaiting, lead) + + if self:_InQueue(self.Qmarshal, lead.group) then + -- Remove flight from Marshal queue and add to pattern. + self:_RemoveFlightFromMarshalQueue(lead) + else + -- Add flight to pattern if he was not. + if not self:_InQueue(self.Qpattern, lead.group) then + self:_AddFlightToPatternQueue(lead) + end + end + + end + + -- Send message. + self:MessageToPlayer(playerData, text, "AIRBOSS") + + end + + end +end + +--- Request spinning. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestSpinning(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + local text="" + if not self:_InQueue(self.Qpattern, playerData.group) then + + -- Player not in pattern queue. + text="negative, you have to be in the pattern to spin it!" + + elseif playerData.step==AIRBOSS.PatternStep.SPINNING then + + -- Player is already spinning. + text="negative, you are already spinning." + + -- Check if player is in the right step. + elseif not (playerData.step==AIRBOSS.PatternStep.BREAKENTRY or + playerData.step==AIRBOSS.PatternStep.EARLYBREAK or + playerData.step==AIRBOSS.PatternStep.LATEBREAK) then + + -- Player is not in the right step. + text="negative, you have to be in the right step to spin it!" + + else + + -- Set player step. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.SPINNING) + + -- Add player to spinning queue. + table.insert(self.Qspinning, playerData) + + -- 405, Spin it! Click. + local call=self:_NewRadioCall(self.LSOCall.SPINIT, "AIRBOSS", "Spin it!", self.Tmessage, playerData.onboard) + self:RadioTransmission(self.LSORadio, call, nil, nil, nil, true) + + -- Some advice. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + local text="Climb to 1200 feet and proceed to the initial again." + self:MessageToPlayer(playerData, text, "INSTRUCTOR", "") + end + + return + end + + -- Send message. + self:MessageToPlayer(playerData, text, "AIRBOSS") + + end + end +end + +--- Request to commence landing approach. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_RequestCommence(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Check if unit is in CCA. + local text="" + local cleared=false + if _unit:IsInZone(self.zoneCCA) then + + -- Get stack value. + local stack=playerData.flag + + -- Number of airborne aircraft currently in pattern. + local _,npattern=self:_GetQueueInfo(self.Qpattern) + + -- TODO: Check distance to initial or platform. Only allow commence if < max distance. Otherwise say bearing. + + if self:_InQueue(self.Qpattern, playerData.group) then + + -- Flight group is already in pattern queue. + text=string.format("negative, %s, you are already in the Pattern queue.", playerData.name) + + elseif not _unit:InAir() then + + -- Flight group is already in pattern queue. + text=string.format("negative, %s, you are not airborne.", playerData.name) + + elseif playerData.seclead~=playerData.name then + + -- Flight group is already in pattern queue. + text=string.format("negative, %s, your section leader %s has to request commence!", playerData.name, playerData.seclead) + + elseif stack>1 then + + -- We are in a higher stack. + text=string.format("negative, %s, it's not your turn yet! You are in stack no. %s.", playerData.name, stack) + + elseif npattern>=self.Nmaxpattern then + + -- Patern is full! + text=string.format("negative ghostrider, pattern is full!\nThere are %d aircraft currently in the pattern.", npattern) + + elseif self:IsRecovering()==false and not self.airbossnice then + + -- Carrier is not recovering right now. + if self.recoverywindow then + local clock=UTILS.SecondsToClock(self.recoverywindow.START) + text=string.format("negative, carrier is currently not recovery. Next window will open at %s.", clock) + else + text=string.format("negative, carrier is not recovering. No future windows planned.") + end + + elseif not self:_InQueue(self.Qmarshal, playerData.group) and not self.airbossnice then + + text="negative, you have to request Marshal before you can commence." + + else + + ----------------------- + -- Positive Response -- + ----------------------- + + text=text.."roger." + + -- Carrier is not recovering but Airboss has a good day. + if not self:IsRecovering() then + text=text.." Carrier is not recovering currently! However, you are cleared anyway as I have a nice day." + end + + -- If player is not in the Marshal queue set player case to current case. + if not self:_InQueue(self.Qmarshal, playerData.group) then + + -- Set current case. + playerData.case=self.case + + -- Hint about TACAN bearing. + if self.TACANon and playerData.difficulty~=AIRBOSS.Difficulty.HARD then + -- Get inverse magnetic radial potential offset. + local radial=self:GetRadial(playerData.case, true, true, true) + if playerData.case==1 then + -- For case 1 we want the BRC but above routine return FB. + radial=self:GetBRC() + end + text=text..string.format("\nSelect TACAN %03d°, Channel %d%s (%s).\n", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) + end + + -- TODO: Inform section members. + + -- Set case of section members as well. Not sure if necessary any more since it is set as soon as the recovery case is changed. + for _,flight in pairs(playerData.section) do + flight.case=playerData.case + end + + -- Add player to pattern queue. Usually this is done when the stack is collapsed but this player is not in the Marshal queue. + self:_AddFlightToPatternQueue(playerData) + end + + -- Clear player for commence. + cleared=true + end + + else + -- This flight is not yet registered! + text=string.format("negative, %s, you are not inside the CCA!", playerData.name) + end + + -- Debug + self:T(self.lid..text) + + -- Send message. + self:MessageToPlayer(playerData, text, "MARSHAL") + + -- Check if player was cleard. Need to do this after the message above is displayed. + if cleared then + -- Call commence routine. No zone check. NOTE: Commencing will set step for all section members as well. + self:_Commencing(playerData, false) + end + end + end +end + +--- Player requests refueling. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_RequestRefueling(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Check if there is a recovery tanker defined. + local text + if self.tanker then + + -- Check if player is in CCA. + if _unit:IsInZone(self.zoneCCA) then + + -- Check if tanker is running or refueling or returning. + if self.tanker:IsRunning() or self.tanker:IsRefueling() then + + -- Get alt of tanker in angels. + --local angels=UTILS.Round(UTILS.MetersToFeet(self.tanker.altitude)/1000, 0) + local angels=self:_GetAngels(self.tanker.altitude) + + -- Tanker is up and running. + text=string.format("affirmative, proceed to tanker at angels %d.", angels) + + -- State TACAN channel of tanker if defined. + if self.tanker.TACANon then + text=text..string.format("\nTanker TACAN channel %d%s (%s).", self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) + text=text..string.format("\nRadio frequency %.3f MHz AM.", self.tanker.RadioFreq) + end + + -- Tanker is currently refueling. Inform player. + if self.tanker:IsRefueling() then + text=text.."\nTanker is currently refueling. You might have to queue up." + end + + -- Collapse marshal stack if player is in queue. + self:_RemoveFlightFromMarshalQueue(playerData, true) + + -- Set step to refueling. + self:_SetPlayerStep(playerData, AIRBOSS.PatternStep.REFUELING) + + -- Inform section and set step. + for _,sec in pairs(playerData.section) do + local sectext="follow your section leader to the tanker." + self:MessageToPlayer(sec, sectext, "MARSHAL") + self:_SetPlayerStep(sec, AIRBOSS.PatternStep.REFUELING) + end + + elseif self.tanker:IsReturning() then + -- Tanker is RTB. + text="negative, tanker is RTB. Request denied!\nWait for the tanker to be back on station if you can." + end + + else + text="negative, you are not inside the CCA yet." + end + else + text="negative, no refueling tanker available." + end + + -- Send message. + self:MessageToPlayer(playerData, text, "MARSHAL") + end + end +end + + +--- Remove a member from the player's section. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player +-- @param #AIRBOSS.PlayerData sectionmember The section member to be removed. +-- @return #boolean If true, flight was a section member and could be removed. False otherwise. +function AIRBOSS:_RemoveSectionMember(playerData, sectionmember) + -- Loop over all flights in player's section + for i,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + if flight.name==sectionmember.name then + table.remove(playerData.section, i) + return true + end + end + return false +end + +--- Set all flights within 100 meters to be part of my section. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_SetSection(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Coordinate of flight lead. + local mycoord=_unit:GetCoordinate() + + -- Max distance up to which section members are allowed. + local dmax=100 + + -- Check if player is in Marshal or pattern queue already. + local text + if self.NmaxSection==0 then + text=string.format("negative, setting sections is disabled in this mission. You stay alone.") + elseif self:_InQueue(self.Qmarshal,playerData.group) then + text=string.format("negative, you are already in the Marshal queue. Setting section not possible any more!") + elseif self:_InQueue(self.Qpattern, playerData.group) then + text=string.format("negative, you are already in the Pattern queue. Setting section not possible any more!") + else + + -- Check if player is member of another section already. If so, remove him from his current section. + if playerData.seclead~=playerData.name then + local lead=self.players[playerData.seclead] --#AIRBOSS.PlayerData + if lead then + + -- Remove player from his old section lead. + local removed=self:_RemoveSectionMember(lead, playerData) + if removed then + self:MessageToPlayer(lead, string.format("Flight %s has been removed from your section.", playerData.name), "AIRBOSS", "", 5) + self:MessageToPlayer(playerData, string.format("You have been removed from %s's section.", lead.name), "AIRBOSS", "", 5) + end + + end + end + + -- Potential section members. + local section={} + + -- Loop over all registered flights. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Only human flight groups excluding myself. Also only flights that dont have a section itself (would get messy) or are part of another section (no double membership). + if flight.ai==false and flight.groupname~=playerData.groupname and #flight.section==0 and flight.seclead==flight.name then + + -- Distance (3D) to other flight group. + local distance=flight.group:GetCoordinate():Get3DDistance(mycoord) + + -- Check distance. + if distance remove it. + if not gotit then + self:MessageToPlayer(flight, string.format("you were removed from %s's section and are on your own now.", playerData.name), "AIRBOSS", "", 5) + flight.seclead=flight.name + self:_RemoveSectionMember(playerData, flight) + end + end + + -- Remove all flights that are currently in the player's section already from scanned potential new section members. + for i,_new in pairs(section) do + local newflight=_new.flight --#AIRBOSS.PlayerData + for _,_flight in pairs(playerData.section) do + local currentflight=_flight --#AIRBOSS.PlayerData + if newflight.name==currentflight.name then + table.remove(section, i) + end + end + end + + -- Init section table. Should not be necessary as all members are removed anyhow above. + --playerData.section={} + + -- Output text. + text=string.format("Registered flight section:") + text=text..string.format("\n- %s (lead)", playerData.seclead) + -- Old members that stay (if any). + for _,_flight in pairs(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + text=text..string.format("\n- %s", flight.name) + end + -- New members (if any). + for i=1,math.min(self.NmaxSection-#playerData.section, #section) do + local flight=section[i].flight --#AIRBOSS.PlayerData + + -- New flight members. + text=text..string.format("\n- %s", flight.name) + + -- Set section lead of player flight. + flight.seclead=playerData.name + + -- Set case of f + flight.case=playerData.case + + -- Inform player that he is now part of a section. + self:MessageToPlayer(flight, string.format("your section lead is now %s.", playerData.name), "AIRBOSS") + + -- Add flight to section table. + table.insert(playerData.section, flight) + end + + -- Section is empty. + if #playerData.section==0 then + text=text..string.format("\n- No other human flights found within radius of %.1f meters!", dmax) + end + + end + + -- Message to section lead. + self:MessageToPlayer(playerData, text, "MARSHAL") + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RESULTS MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Display top 10 player scores. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_DisplayScoreBoard(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + + -- Results table. + local _playerResults={} + + -- Calculate average points for all players. + for playerName,playerGrades in pairs(self.playerscores) do + + if playerGrades then + + -- Loop over all grades + local Paverage=0 + local n=0 + for _,_grade in pairs(playerGrades) do + local grade=_grade --#AIRBOSS.LSOgrade + + -- Add up only final scores for the average. + if grade.finalscore then --grade.points>=0 then + Paverage=Paverage+grade.finalscore + n=n+1 + else + -- Case when the player just leaves after an unfinished pass, e.g bolter, without landing. + -- But this should now be solved by deleteing all unfinished results. + end + end + + -- We dont want to devide by zero. + if n>0 then + _playerResults[playerName]=Paverage/n + end + + end + end + + -- Message text. + local text = string.format("Greenie Board (top ten):") + local i=1 + for _playerName,_points in UTILS.spairs(_playerResults, function(t, a, b) return t[b] < t[a] end) do + + -- Text. + text=text..string.format("\n[%d] %s %.1f||", i,_playerName, _points) + + -- All player grades. + local playerGrades=self.playerscores[_playerName] + + -- Add grades of passes. We use the actual grade of each pass here and not the average after player has landed. + for _,_grade in pairs(playerGrades) do + local grade=_grade --#AIRBOSS.LSOgrade + if grade.finalscore then + text=text..string.format("%.1f|", grade.points) + elseif grade.points>=0 then -- Only points >=0 as foul deck gives -1. + text=text..string.format("(%.1f)", grade.points) + end + end + + -- Display only the top ten. + i=i+1 + if i>10 then + break + end + end + + -- If no results yet. + if i==1 then + text=text.."\nNo results yet." + end + + -- Send message. + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData.client then + MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) + end + + end +end + +--- Display top 10 player scores. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_DisplayPlayerGrades(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Grades of player: + local text=string.format("Your last 10 grades, %s:", _playername) + + -- All player grades. + local playerGrades=self.playerscores[_playername] or {} + + local p=0 -- Average points. + local n=0 -- Number of final passes. + local m=0 -- Number of total passes. + --for i,_grade in pairs(playerGrades) do + for i=#playerGrades,1,-1 do + --local grade=_grade --#AIRBOSS.LSOgrade + local grade=playerGrades[i] --#AIRBOSS.LSOgrade + + -- Check if points >=0. For foul deck WO we give -1 and pass is not counted. + if grade.points>=0 then + + -- Show final points or points of pass. + local points=grade.finalscore or grade.points + + -- Display max 10 results. + if m<10 then + text=text..string.format("\n[%d] %s %.1f PT - %s", i, grade.grade, points, grade.details) + + -- Wire trapped if any. + if grade.wire and grade.wire<=4 then + text=text..string.format(" %d-wire", grade.wire) + end + + -- Time in the groove if any. + if grade.Tgroove and grade.Tgroove<=360 then + text=text..string.format(" Tgroove=%.1f s", grade.Tgroove) + end + end + + -- Add up final points. + if grade.finalscore then + p=p+grade.finalscore + n=n+1 + end + + -- Total passes + m=m+1 + end + end + + + if n>0 then + text=text..string.format("\nAverage points = %.1f", p/n) + else + text=text..string.format("\nNo data available.") + end + + -- Send message. + if playerData.client then + MESSAGE:New(text, 30, nil, true):ToClient(playerData.client) + end + end + end +end + +--- Display last debriefing. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_DisplayDebriefing(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Debriefing text. + local text=string.format("Debriefing:") + + -- Check if data is present. + if #playerData.lastdebrief>0 then + text=text..string.format("\n================================\n") + for _,_data in pairs(playerData.lastdebrief) do + local step=_data.step + local comment=_data.hint + text=text..string.format("* %s:",step) + text=text..string.format("%s\n", comment) + end + else + text=text.." Nothing to show yet." + end + + -- Send debrief message to player + self:MessageToPlayer(playerData, text, nil , "", 30, true) + + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- KNEEBOARD MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Display marshal or pattern queue. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +-- @param #string qname Name of the queue. +function AIRBOSS:_DisplayQueue(_unitname, qname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Queue to display. + local queue=nil + if qname=="Marshal" then + queue=self.Qmarshal + elseif qname=="Pattern" then + queue=self.Qpattern + elseif qname=="Waiting" then + queue=self.Qwaiting + end + + -- Number of group and units in queue + local Nqueue,nqueue=self:_GetQueueInfo(queue, playerData.case) + + local text=string.format("%s Queue:", qname) + if #queue==0 then + text=text.." empty" + else + local N=0 + if qname=="Marshal" then + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + local charlie=self:_GetCharlieTime(flight) + local Charlie=UTILS.SecondsToClock(charlie) + local stack=flight.flag + local angels=self:_GetAngels(self:_GetMarshalAltitude(stack, flight.case)) + local _,nunit,nsec=self:_GetFlightUnits(flight, true) + local nick=self:_GetACNickname(flight.actype) + N=N+nunit + text=text..string.format("\n[Stack %d] %s (%s*%d+%d): Case %d, Angels %d, Charlie %s", stack, flight.onboard, nick, nunit, nsec, flight.case, angels, tostring(Charlie)) + end + elseif qname=="Pattern" or qname=="Waiting" then + for i,_flight in pairs(queue) do + local flight=_flight --#AIRBOSS.FlightGroup + local _,nunit,nsec=self:_GetFlightUnits(flight, true) + local nick=self:_GetACNickname(flight.actype) + local ptime=UTILS.SecondsToClock(timer.getAbsTime()-flight.time) + N=N+nunit + text=text..string.format("\n[%d] %s (%s*%d+%d): Case %d, T=%s", i, flight.onboard, nick, nunit, nsec, flight.case, ptime) + end + end + text=text..string.format("\nTotal AC: %d (airborne %d)", N, nqueue) + end + + -- Send message. + self:MessageToPlayer(playerData, text, nil, "", nil, true) + end + end +end + + +--- Report information about carrier. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_DisplayCarrierInfo(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Current coordinates. + local coord=self:GetCoordinate() + + -- Carrier speed and heading. + local carrierheading=self.carrier:GetHeading() + local carrierspeed=UTILS.MpsToKnots(self.carrier:GetVelocityMPS()) + + -- TACAN/ICLS. + local tacan="unknown" + local icls="unknown" + if self.TACANon and self.TACANchannel~=nil then + tacan=string.format("%d%s (%s)", self.TACANchannel, self.TACANmode, self.TACANmorse) + end + if self.ICLSon and self.ICLSchannel~=nil then + icls=string.format("%d (%s)", self.ICLSchannel, self.ICLSmorse) + end + + -- Wind on flight deck + local wind=UTILS.MpsToKnots(select(1, self:GetWindOnDeck())) + + -- Get groups, units in queues. + local Nmarshal,nmarshal = self:_GetQueueInfo(self.Qmarshal, playerData.case) + local Npattern,npattern = self:_GetQueueInfo(self.Qpattern) + local Nspinning,nspinning = self:_GetQueueInfo(self.Qspinning) + local Nwaiting,nwaiting = self:_GetQueueInfo(self.Qwaiting) + local Ntotal,ntotal = self:_GetQueueInfo(self.flights) + + -- Current abs time. + local Tabs=timer.getAbsTime() + + -- Get recovery times of carrier. + local recoverytext="Recovery time windows (max 5):" + if #self.recoverytimes==0 then + recoverytext=recoverytext.." none." + else + -- Loop over recovery windows. + local rw=0 + for _,_recovery in pairs(self.recoverytimes) do + local recovery=_recovery --#AIRBOSS.Recovery + -- Only include current and future recovery windows. + if Tabs=5 then + -- Break the loop after 5 recovery times. + break + end + end + end + end + + -- Recovery tanker TACAN text. + local tankertext=nil + if self.tanker then + tankertext=string.format("Recovery tanker frequency %.3f MHz\n", self.tanker.RadioFreq) + if self.tanker.TACANon then + tankertext=tankertext..string.format("Recovery tanker TACAN %d%s (%s)",self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse) + else + tankertext=tankertext.."Recovery tanker TACAN n/a" + end + end + + -- Carrier FSM state. Idle is not clear enough. + local state=self:GetState() + if state=="Idle" then + state="Deck closed" + end + if self.turning then + state=state.." (turning currently)" + end + + -- Message text. + local text=string.format("%s info:\n", self.alias) + text=text..string.format("================================\n") + text=text..string.format("Carrier state: %s\n", state) + if self.case==1 then + text=text..string.format("Case %d recovery ops\n", self.case) + else + local radial=self:GetRadial(self.case, true, true, false) + text=text..string.format("Case %d recovery ops\nMarshal radial %03d°\n", self.case, radial) + end + text=text..string.format("BRC %03d° - FB %03d°\n", self:GetBRC(), self:GetFinalBearing(true)) + text=text..string.format("Speed %.1f kts - Wind on deck %.1f kts\n", carrierspeed, wind) + text=text..string.format("Tower frequency %.3f MHz\n", self.TowerFreq) + text=text..string.format("Marshal radio %.3f MHz\n", self.MarshalFreq) + text=text..string.format("LSO radio %.3f MHz\n", self.LSOFreq) + text=text..string.format("TACAN Channel %s\n", tacan) + text=text..string.format("ICLS Channel %s\n", icls) + if tankertext then + text=text..tankertext.."\n" + end + text=text..string.format("# A/C total %d (%d)\n", Ntotal, ntotal) + text=text..string.format("# A/C marshal %d (%d)\n", Nmarshal, nmarshal) + text=text..string.format("# A/C pattern %d (%d) - spinning %d (%d)\n", Npattern, npattern, Nspinning, nspinning) + text=text..string.format("# A/C waiting %d (%d)\n", Nwaiting, nwaiting) + text=text..string.format(recoverytext) + self:T2(self.lid..text) + + -- Send message. + self:MessageToPlayer(playerData, text, nil, "", 30, true) + + else + self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) + end + end + +end + + +--- Report weather conditions at the carrier location. Temperature, QFE pressure and wind data. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_DisplayCarrierWeather(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Message text. + local text="" + + -- Current coordinates. + local coord=self:GetCoordinate() + + -- Get atmospheric data at carrier location. + local T=coord:GetTemperature() + local P=coord:GetPressure() + + -- Get wind direction (magnetic) and strength. + local Wd,Ws=self:GetWind(nil, true) + + -- Get Beaufort wind scale. + local Bn,Bd=UTILS.BeaufortScale(Ws) + + -- Wind on flight deck. + local WodPA,WodPP=self:GetWindOnDeck() + local WodPA=UTILS.MpsToKnots(WodPA) + local WodPP=UTILS.MpsToKnots(WodPP) + + local WD=string.format('%03d°', Wd) + local Ts=string.format("%d°C",T) + + local tT=string.format("%d°C",T) + local tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) + local tP=string.format("%.2f inHg", UTILS.hPa2inHg(P)) + + -- Report text. + text=text..string.format("Weather Report at Carrier %s:\n", self.alias) + text=text..string.format("================================\n") + text=text..string.format("Temperature %s\n", tT) + text=text..string.format("Wind from %s at %s (%s)\n", WD, tW, Bd) + text=text..string.format("Wind on deck || %.1f kts, == %.1f kts\n", WodPA, WodPP) + text=text..string.format("QFE %.1f hPa = %s", P, tP) + + -- More info only reliable if Mission uses static weather. + if self.staticweather then + local clouds, visibility, fog, dust=self:_GetStaticWeather() + text=text..string.format("\nVisibility %.1f NM", UTILS.MetersToNM(visibility)) + text=text..string.format("\nCloud base %d ft", UTILS.MetersToFeet(clouds.base)) + text=text..string.format("\nCloud thickness %d ft", UTILS.MetersToFeet(clouds.thickness)) + text=text..string.format("\nCloud density %d", clouds.density) + text=text..string.format("\nPrecipitation %d", clouds.iprecptns) + if fog then + text=text..string.format("\nFog thickness %d ft", UTILS.MetersToFeet(fog.thickness)) + text=text..string.format("\nFog visibility %d ft", UTILS.MetersToFeet(fog.visibility)) + else + text=text..string.format("\nNo fog") + end + if dust then + text=text..string.format("\nDust density %d", dust) + else + text=text..string.format("\nNo dust") + end + end + + -- Debug output. + self:T2(self.lid..text) + + -- Send message to player group. + self:MessageToPlayer(self.players[playername], text, nil, "", 30, true) + + else + self:E(self.lid..string.format("ERROR! Could not find player unit in CarrierWeather! Unit name = %s", _unitname)) + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- HELP MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set difficulty level. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +-- @param #AIRBOSS.Difficulty difficulty Difficulty level. +function AIRBOSS:_SetDifficulty(_unitname, difficulty) + self:T2({difficulty=difficulty, unitname=_unitname}) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.difficulty=difficulty + local text=string.format("roger, your skill level is now: %s.", difficulty) + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + else + self:E(self.lid..string.format("ERROR: Could not get player data for player %s.", playername)) + end + + -- Set hints as well. + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + playerData.showhints=false + else + playerData.showhints=true + end + + end +end + +--- Turn player's aircraft attitude display on or off. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_SetHintsOnOff(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Invert hints. + playerData.showhints=not playerData.showhints + + -- Inform player. + local text="" + if playerData.showhints==true then + text=string.format("roger, hints are now ON.") + else + text=string.format("affirm, hints are now OFF.") + end + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + + end + end +end + +--- Turn player's aircraft attitude display on or off. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_DisplayAttitude(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.attitudemonitor=not playerData.attitudemonitor + end + end + +end + +--- Turn radio subtitles of player on or off. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_SubtitlesOnOff(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.subtitles=not playerData.subtitles + -- Inform player. + local text="" + if playerData.subtitles==true then + text=string.format("roger, subtitiles are now ON.") + elseif playerData.subtitles==false then + text=string.format("affirm, subtitiles are now OFF.") + end + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + end + end + +end + +--- Turn radio subtitles of player on or off. +-- @param #AIRBOSS self +-- @param #string _unitname Name of the player unit. +function AIRBOSS:_TrapsheetOnOff(_unitname) + self:F2(_unitname) + + -- Get player unit and player name. + local unit, playername = self:_GetPlayerUnitAndName(_unitname) + + -- Check if we have a player. + if unit and playername then + + -- Player data. + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Check if option is enabled at all. + local text="" + if self.trapsheet then + + -- Invert current setting. + playerData.trapon=not playerData.trapon + + -- Inform player. + if playerData.trapon==true then + text=string.format("roger, your trapsheets are now SAVED.") + else + text=string.format("affirm, your trapsheets are NOT SAVED.") + end + + else + text="negative, trap sheet data recorder is broken on this carrier." + end + + -- Message to player. + self:MessageToPlayer(playerData, text, nil, playerData.name, 5) + end + end + +end + + +--- Display player status. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +function AIRBOSS:_DisplayPlayerStatus(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Pattern step text. + local steptext=playerData.step + if playerData.step==AIRBOSS.PatternStep.HOLDING then + if playerData.holding==nil then + steptext="Transit to Marshal" + elseif playerData.holding==false then + steptext="Marshal (outside zone)" + elseif playerData.holding==true then + steptext="Marshal Stack Holding" + end + end + + -- Stack. + local stack=playerData.flag + + -- Stack text. + local stacktext=nil + if stack>0 then + local stackalt=self:_GetMarshalAltitude(stack) + local angels=self:_GetAngels(stackalt) + stacktext=string.format("Marshal Stack %d, Angels %d\n", stack, angels) + + + -- Hint about TACAN bearing. + if playerData.step==AIRBOSS.PatternStep.HOLDING and playerData.case>1 then + -- Get inverse magnetic radial potential offset. + local radial=self:GetRadial(playerData.case, true, true, true) + stacktext=stacktext..string.format("Select TACAN %03d°, %d DME\n", radial, angels+15) + end + end + + -- Fuel and fuel state. + local fuel=playerData.unit:GetFuel()*100 + local fuelstate=self:_GetFuelState(playerData.unit) + + -- Number of units in group. + local _,nunitsGround=self:_GetFlightUnits(playerData, true) + local _,nunitsAirborne=self:_GetFlightUnits(playerData, false) + + -- Player data. + local text=string.format("Status of player %s (%s)\n", playerData.name, playerData.callsign) + text=text..string.format("================================\n") + text=text..string.format("Step: %s\n", steptext) + if stacktext then + text=text..stacktext + end + text=text..string.format("Recovery Case: %d\n", playerData.case) + text=text..string.format("Skill Level: %s\n", playerData.difficulty) + text=text..string.format("Modex: %s (%s)\n", playerData.onboard, self:_GetACNickname(playerData.actype)) + text=text..string.format("Fuel State: %.1f lbs/1000 (%.1f %%)\n", fuelstate/1000, fuel) + text=text..string.format("# units: %d (%d airborne)\n", nunitsGround, nunitsAirborne) + text=text..string.format("Section Lead: %s (%d/%d)", tostring(playerData.seclead), #playerData.section+1, self.NmaxSection+1) + for _,_sec in pairs(playerData.section) do + local sec=_sec --#AIRBOSS.PlayerData + text=text..string.format("\n- %s", sec.name) + end + + if playerData.step==AIRBOSS.PatternStep.INITIAL then + + -- Create a point 3.0 NM astern for re-entry. + local zoneinitial=self:GetCoordinate():Translate(UTILS.NMToMeters(3.5), self:GetRadial(2, false, false, false)) + + -- Heading and distance to initial zone. + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(zoneinitial) + local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(zoneinitial)) + local brc=self:GetBRC() + + -- Help player to find its way to the initial zone. + text=text..string.format("\nTo Initial: Fly heading %03d° for %.1f NM and turn to BRC %03d°", flyhdg, flydist, brc) + + elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then + + -- Coordinate of the platform zone. + local zoneplatform=self:_GetZonePlatform(playerData.case):GetCoordinate() + + -- Heading and distance to platform zone. + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(zoneplatform) + local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(zoneplatform)) + + -- Get heading. + local hdg=self:GetRadial(playerData.case, true, true, true) + + -- Help player to find its way to the initial zone. + text=text..string.format("\nTo Platform: Fly heading %03d° for %.1f NM and turn to %03d°", flyhdg, flydist, hdg) + + end + + -- Send message. + self:MessageToPlayer(playerData, text, nil, "", 30, true) + else + self:E(self.lid..string.format("ERROR: playerData=nil. Unit name=%s, player name=%s", _unitName, _playername)) + end + else + self:E(self.lid..string.format("ERROR: could not find player for unit %s", _unitName)) + end + +end + +--- Mark current marshal zone of player by either smoke or flares. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +-- @param #boolean flare If true, flare the zone. If false, smoke the zone. +function AIRBOSS:_MarkMarshalZone(_unitName, flare) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Get player stack and recovery case. + local stack=playerData.flag + local case=playerData.case + + local text="" + if stack>0 then + + -- Get current holding zone. + local zoneHolding=self:_GetZoneHolding(case, stack) + + -- Get Case I commence zone at three position. + local zoneThree=self:_GetZoneCommence(case, stack) + + -- Pattern alitude. + local patternalt=self:_GetMarshalAltitude(stack, case) + + -- Flare and smoke at the ground. + patternalt=5 + + -- Roger! + text="roger, marking" + if flare then + + -- Marshal WHITE flares. + text=text..string.format("\n* Marshal zone stack %d with WHITE flares.", stack) + zoneHolding:FlareZone(FLARECOLOR.White, 45, nil, patternalt) + + -- Commence RED flares. + text=text.."\n* Commence zone with RED flares." + zoneThree:FlareZone(FLARECOLOR.Red, 45, nil, patternalt) + + else + + -- Marshal WHITE smoke. + text=text..string.format("\n* Marshal zone stack %d with WHITE smoke.", stack) + zoneHolding:SmokeZone(SMOKECOLOR.White, 45, patternalt) + + -- Commence RED smoke + text=text.."\n* Commence zone with RED smoke." + zoneThree:SmokeZone(SMOKECOLOR.Red, 45, patternalt) + + end + + else + text="negative, you are currently not in a Marshal stack. No zones will be marked!" + end + + -- Send message to player. + self:MessageToPlayer(playerData, text, "MARSHAL", playerData.name) + end + end + +end + + +--- Mark CASE I or II/II zones by either smoke or flares. +-- @param #AIRBOSS self +-- @param #string _unitName Name of the player unit. +-- @param #boolean flare If true, flare the zone. If false, smoke the zone. +function AIRBOSS:_MarkCaseZones(_unitName, flare) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + if playerData then + + -- Player's recovery case. + local case=playerData.case + + -- Initial + local text=string.format("affirm, marking CASE %d zones", case) + + -- Flare or smoke? + if flare then + + ----------- + -- Flare -- + ----------- + + -- Case I/II: Initial + if case==1 or case==2 then + text=text.."\n* initial with GREEN flares" + self:_GetZoneInitial(case):FlareZone(FLARECOLOR.Green, 45) + end + + -- Case II/III: approach corridor + if case==2 or case==3 then + text=text.."\n* approach corridor with GREEN flares" + self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green, 45) + end + + -- Case II/III: platform + if case==2 or case==3 then + text=text.."\n* platform with RED flares" + self:_GetZonePlatform(case):FlareZone(FLARECOLOR.Red, 45) + end + + -- Case III: dirty up + if case==3 then + text=text.."\n* dirty up with YELLOW flares" + self:_GetZoneDirtyUp(case):FlareZone(FLARECOLOR.Yellow, 45) + end + + -- Case II/III: arc in/out + if case==2 or case==3 then + if math.abs(self.holdingoffset)>0 then + self:_GetZoneArcIn(case):FlareZone(FLARECOLOR.White, 45) + text=text.."\n* arc turn in with WHITE flares" + self:_GetZoneArcOut(case):FlareZone(FLARECOLOR.White, 45) + text=text.."\n* arc trun out with WHITE flares" + end + end + + -- Case III: bullseye + if case==3 then + text=text.."\n* bullseye with GREEN flares" + self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.Green, 45) + end + + -- Tarawa landing spots. + if self.carriertype==AIRBOSS.CarrierType.TARAWA then + text=text.."\n* abeam landing stop with RED flares" + -- Abeam landing spot zone. + local ALSPT=self:_GetZoneAbeamLandingSpot() + ALSPT:FlareZone(FLARECOLOR.Red, 5, nil, UTILS.FeetToMeters(110)) + -- Primary landing spot zone. + text=text.."\n* primary landing spot with GREEN flares" + local LSPT=self:_GetZoneLandingSpot() + LSPT:FlareZone(FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight) + end + + else + + ----------- + -- Smoke -- + ----------- + + -- Case I/II: Initial + if case==1 or case==2 then + text=text.."\n* initial with GREEN smoke" + self:_GetZoneInitial(case):SmokeZone(SMOKECOLOR.Green, 45) + end + + -- Case II/III: Approach Corridor + if case==2 or case==3 then + text=text.."\n* approach corridor with GREEN smoke" + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + end + + -- Case II/III: platform + if case==2 or case==3 then + text=text.."\n* platform with RED smoke" + self:_GetZonePlatform(case):SmokeZone(SMOKECOLOR.Red, 45) + end + + -- Case II/III: arc in/out if offset>0. + if case==2 or case==3 then + if math.abs(self.holdingoffset)>0 then + self:_GetZoneArcIn(case):SmokeZone(SMOKECOLOR.Blue, 45) + text=text.."\n* arc turn in with BLUE smoke" + self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) + text=text.."\n* arc trun out with BLUE smoke" + end + end + + -- Case III: dirty up + if case==3 then + text=text.."\n* dirty up with ORANGE smoke" + self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) + end + + -- Case III: bullseye + if case==3 then + text=text.."\n* bullseye with GREEN smoke" + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.Green, 45) + end + + end + + -- Send message to player. + self:MessageToPlayer(playerData, text, "MARSHAL", playerData.name) + end + end + +end + +--- LSO radio check. Will broadcase LSO message at given LSO frequency. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_LSORadioCheck(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData then + -- Broadcase LSO radio check message on LSO radio. + self:RadioTransmission(self.LSORadio, self.LSOCall.RADIOCHECK, nil, nil, nil, true) + end + end +end + +--- Marshal radio check. Will broadcase Marshal message at given Marshal frequency. +-- @param #AIRBOSS self +-- @param #string _unitName Name fo the player unit. +function AIRBOSS:_MarshalRadioCheck(_unitName) + self:F(_unitName) + + -- Get player unit and name. + local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) + + -- Check if we have a unit which is a player. + if _unit and _playername then + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + if playerData then + -- Broadcase Marshal radio check message on Marshal radio. + self:RadioTransmission(self.MarshalRadio, self.MarshalCall.RADIOCHECK, nil, nil, nil, true) + end + end +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +-- Persistence Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + +--- Save trapsheet data. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +-- @param #AIRBOSS.LSOgrade grade LSO grad data. +function AIRBOSS:_SaveTrapSheet(playerData, grade) + + -- Nothing to save. + if playerData.trapsheet==nil or #playerData.trapsheet==0 or not io then + return + end + + --- Function that saves data to file + local function _savefile(filename, data) + local f = io.open(filename, "wb") + if f then + f:write(data) + f:close() + else + self:E(self.lid..string.format("ERROR: could not save trap sheet to file %s.\nFile may contain invalid characters.", tostring(filename))) + end + end + + -- Set path or default. + local path=self.trappath + if lfs then + path=path or lfs.writedir() + end + + + -- Create unused file name. + local filename=nil + for i=1,9999 do + + -- Create file name + if self.trapprefix then + filename=string.format("%s_%s-%04d.csv", self.trapprefix, playerData.actype, i) + else + filename=string.format("AIRBOSS-%s_Trapsheet-%s_%s-%04d.csv", self.alias, playerData.name, playerData.actype, i) + end + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local _exists=UTILS.FileExists(filename) + if not _exists then + break + end + end + + + -- Info + local text=string.format("Saving player %s trapsheet to file %s", playerData.name, filename) + self:I(self.lid..text) + + -- Header line + local data="#Time,Rho,X,Z,Alt,AoA,GSE,LUE,Vtot,Vy,Gamma,Pitch,Roll,Yaw,Step,Grade,Points,Details\n" + + local g0=playerData.trapsheet[1] --#AIRBOSS.GrooveData + local T0=g0.Time + + --for _,_groove in ipairs(playerData.trapsheet) do + for i=1,#playerData.trapsheet do + --local groove=_groove --#AIRBOSS.GrooveData + local groove=playerData.trapsheet[i] + local t=groove.Time-T0 + local a=UTILS.MetersToNM(groove.Rho or 0) + local b=-groove.X or 0 + local c=groove.Z or 0 + local d=UTILS.MetersToFeet(groove.Alt or 0) + local e=groove.AoA or 0 + local f=groove.GSE or 0 + local g=-groove.LUE or 0 + local h=UTILS.MpsToKnots(groove.Vel or 0) + local i=(groove.Vy or 0)*196.85 + local j=groove.Gamma or 0 + local k=groove.Pitch or 0 + local l=groove.Roll or 0 + local m=groove.Yaw or 0 + local n=self:_GS(groove.Step, -1) or "n/a" + local o=groove.Grade or "n/a" + local p=groove.GradePoints or 0 + local q=groove.GradeDetail or "n/a" + -- t a b c d e f g h i j k l m n o p q + data=data..string.format("%.2f,%.3f,%.1f,%.1f,%.1f,%.2f,%.2f,%.2f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%s,%s,%.1f,%s\n",t,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q) + end + + -- Save file. + _savefile(filename, data) +end + +--- On before "Save" event. Checks if io and lfs are available. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onbeforeSave(From, Event, To, path, filename) + + -- Check io module is available. + if not io then + self:E(self.lid.."ERROR: io not desanitized. Can't save player grades.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\DCS\" folder.") + end + + return true +end + +--- On after "Save" event. Player data is saved to file. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory or your "Saved Games" folder if lfs was desanitized. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onafterSave(From, Event, To, path, filename) + + --- Function that saves data to file + local function _savefile(filename, data) + local f = assert(io.open(filename, "wb")) + f:write(data) + f:close() + end + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set file name. + filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv", self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Header line + local scores="Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Wind,Modex,Airframe,Carrier Type,Carrier Name,Theatre,Mission Time,Mission Date,OS Date\n" + + -- Loop over all players. + local n=0 + for playername,grades in pairs(self.playerscores) do + + -- Loop over player grades table. + for i,_grade in pairs(grades) do + local grade=_grade --#AIRBOSS.LSOgrade + + -- Check some stuff that could be nil. + local wire="n/a" + if grade.wire and grade.wire<=4 then + wire=tostring(grade.wire) + end + + local Tgroove="n/a" + if grade.Tgroove and grade.Tgroove<=360 and grade.case<3 then + Tgroove=tostring(UTILS.Round(grade.Tgroove, 1)) + end + + local finalscore="n/a" + if grade.finalscore then + finalscore=tostring(UTILS.Round(grade.finalscore, 1)) + end + + -- Compile grade line. + scores=scores..string.format("%s,%d,%s,%.1f,%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", + playername, i, finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, grade.case, + grade.wind, grade.modex, grade.airframe, grade.carriertype, grade.carriername, grade.theatre, grade.mitime, grade.midate, grade.osdate) + n=n+1 + end + end + + -- Info + local text=string.format("Saving %d player LSO grades to file %s", n, filename) + self:I(self.lid..text) + + -- Save file. + _savefile(filename, scores) +end + + +--- On before "Load" event. Checks if the file that the player grades from exists. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path (Optional) Path where the file is loaded from. Default is the DCS installation root directory or your "Saved Games\\DCS" folder if lfs was desanizized. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onbeforeLoad(From, Event, To, path, filename) + + --- Function that check if a file exists. + local function _fileexists(name) + local f=io.open(name,"r") + if f~=nil then + io.close(f) + return true + else + return false + end + end + + -- Check io module is available. + if not io then + self:E(self.lid.."WARNING: io not desanitized. Can't load player grades.") + return false + end + + -- Check default path. + if path==nil and not lfs then + self:E(self.lid.."WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\DCS\" folder.") + end + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set file name. + filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv", self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local exists=_fileexists(filename) + + if exists then + return true + else + self:E(self.lid..string.format("WARNING: Player LSO grades file %s does not exist.", filename)) + return false + end + +end + + +--- On after "Load" event. Loads grades of all players from file. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path Path where the file is loaded from. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if lfs was desanizied. +-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-_LSOgrades.csv". +function AIRBOSS:onafterLoad(From, Event, To, path, filename) + + --- Function that load data from a file. + local function _loadfile(filename) + local f=assert(io.open(filename, "rb")) + local data=f:read("*all") + f:close() + return data + end + + -- Set path or default. + if lfs then + path=path or lfs.writedir() + end + + -- Set file name. + filename=filename or string.format("AIRBOSS-%s_LSOgrades.csv", self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Info message. + local text=string.format("Loading player LSO grades from file %s", filename) + MESSAGE:New(text,10):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Load asset data from file. + local data=_loadfile(filename) + + -- Split by line break. + local playergrades=UTILS.Split(data,"\n") + + -- Remove first header line. + table.remove(playergrades, 1) + + -- Init player scores table. + self.playerscores={} + + -- Loop over all lines. + local n=0 + for _,gradeline in pairs(playergrades) do + + -- Parameters are separated by commata. + local gradedata=UTILS.Split(gradeline, ",") + + -- Debug info. + self:T2(gradedata) + + -- Grade table + local grade={} --#AIRBOSS.LSOgrade + + --- Line format: + -- playername, i, grade.finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, case, + -- time, wind, airframe, modex, carriertype, carriername, theatre, date + local playername=gradedata[1] + if gradedata[3]~=nil and gradedata[3]~="n/a" then + grade.finalscore=tonumber(gradedata[3]) + end + grade.points=tonumber(gradedata[4]) + grade.grade=tostring(gradedata[5]) + grade.details=tostring(gradedata[6]) + if gradedata[7]~=nil and gradedata[7]~="n/a" then + grade.wire=tonumber(gradedata[7]) + end + if gradedata[8]~=nil and gradedata[8]~="n/a" then + grade.Tgroove=tonumber(gradedata[8]) + end + grade.case=tonumber(gradedata[9]) + -- new + grade.wind=gradedata[10] or "n/a" + grade.modex=gradedata[11] or "n/a" + grade.airframe=gradedata[12] or "n/a" + grade.carriertype=gradedata[13] or "n/a" + grade.carriername=gradedata[14] or "n/a" + grade.theatre=gradedata[15] or "n/a" + grade.mitime=gradedata[16] or "n/a" + grade.midate=gradedata[17] or "n/a" + grade.osdate=gradedata[18] or "n/a" + + -- Init player table if necessary. + self.playerscores[playername]=self.playerscores[playername] or {} + + -- Add grade to table. + table.insert(self.playerscores[playername], grade) + + n=n+1 + + -- Debug info. + self:T2({playername, self.playerscores[playername]}) + end + + -- Info message. + local text=string.format("Loaded %d player LSO grades from file %s", n, filename) + self:I(self.lid..text) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua new file mode 100644 index 000000000..e43cd9cc7 --- /dev/null +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -0,0 +1,1698 @@ +--- **Ops** - Recovery tanker for carrier operations. +-- +-- Tanker aircraft flying a racetrack pattern overhead an aircraft carrier. +-- +-- **Main Features:** +-- +-- * Regular pattern update with respect to carrier position. +-- * No restrictions regarding carrier waypoints and heading. +-- * Automatic respawning when tanker runs out of fuel for 24/7 operations. +-- * Tanker can be spawned cold or hot on the carrier or at any other airbase or directly in air. +-- * Automatic AA TACAN beacon setting. +-- * Multiple tankers at the same carrier. +-- * Multiple carriers due to object oriented approach. +-- * Finite State Machine (FSM) implementation, which allows the mission designer to hook into certain events. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Special thanks to **HighwaymanEd** for testing and suggesting improvements! +-- +-- @module Ops.RecoveryTanker +-- @image Ops_RecoveryTanker.png + +--- RECOVERYTANKER class. +-- @type RECOVERYTANKER +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode. +-- @field #string lid Log debug id text. +-- @field Wrapper.Unit#UNIT carrier The carrier the tanker is attached to. +-- @field #string carriertype Carrier type. +-- @field #string tankergroupname Name of the late activated tanker template group. +-- @field Wrapper.Group#GROUP tanker Tanker group. +-- @field Wrapper.Airbase#AIRBASE airbase The home airbase object of the tanker. Normally the aircraft carrier. +-- @field Core.Radio#BEACON beacon Tanker TACAN beacon. +-- @field #number TACANchannel TACAN channel. Default 1. +-- @field #string TACANmode TACAN mode, i.e. "X" or "Y". Default "Y". Use only "Y" for AA TACAN stations! +-- @field #string TACANmorse TACAN morse code. Three letters identifying the TACAN station. Default "TKR". +-- @field #boolean TACANon If true, TACAN is automatically activated. If false, TACAN is disabled. +-- @field #number RadioFreq Radio frequency in MHz of the tanker. Default 251 MHz. +-- @field #string RadioModu Radio modulation "AM" or "FM". Default "AM". +-- @field #number speed Tanker speed when flying pattern. +-- @field #number altitude Tanker orbit pattern altitude. +-- @field #number distStern Race-track distance astern. distStern is <0. +-- @field #number distBow Race-track distance bow. distBow is >0. +-- @field #number Dupdate Pattern update when carrier changes its position by more than this distance (meters). +-- @field #number Hupdate Pattern update when carrier changes its heading by more than this number (degrees). +-- @field #number dTupdate Minimum time interval in seconds before the next pattern update can happen. +-- @field #number Tupdate Last time the pattern was updated. +-- @field #number takeoff Takeoff type (cold, hot, air). +-- @field #number lowfuel Low fuel threshold in percent. +-- @field #boolean respawn If true, tanker be respawned (default). If false, no respawning will happen. +-- @field #boolean respawninair If true, tanker will always be respawned in air. This has no impact on the initial spawn setting. +-- @field #boolean uncontrolledac If true, use and uncontrolled tanker group already present in the mission. +-- @field DCS#Vec3 orientation Orientation of the carrier. Used to monitor changes and update the pattern if heading changes significantly. +-- @field DCS#Vec3 orientlast Orientation of the carrier for checking if carrier is currently turning. +-- @field Core.Point#COORDINATE position Position of carrier. Used to monitor if carrier significantly changed its position and then update the tanker pattern. +-- @field #string alias Alias of the spawn group. +-- @field #number uid Unique ID of this tanker. +-- @field #boolean awacs If true, the groups gets the enroute task AWACS instead of tanker. +-- @field #number callsignname Number for the callsign name. +-- @field #number callsignnumber Number of the callsign name. +-- @field #string modex Tail number of the tanker. +-- @field #boolean eplrs If true, enable data link, e.g. if used as AWACS. +-- @field #boolean recovery If true, tanker will recover using the AIRBOSS marshal pattern. +-- @field #number terminaltype Terminal type of used parking spots on airbases. +-- @extends Core.Fsm#FSM + +--- Recovery Tanker. +-- +-- === +-- +-- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Main.png) +-- +-- # Recovery Tanker +-- +-- A recovery tanker acts as refueling unit flying overhead an aircraft carrier in order to supply incoming flights with gas if they go "*Bingo on the Ball*". +-- +-- # Simple Script +-- +-- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named **"USS Stennis"**. +-- +-- Secondly, you need to define a recovery tanker group in the mission editor and set it to **"LATE ACTIVATED"**. The name of the group we'll use is **"Texaco"**. +-- +-- The basic script is very simple and consists of only two lines: +-- +-- TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco") +-- TexacoStennis:Start() +-- +-- The first line will create a new RECOVERYTANKER object and the second line starts the process. +-- +-- With this setup, the tanker will be spawned on the USS Stennis with running engines. After it takes off, it will fly a position ~10 NM astern of the boat and from there start its +-- pattern. This is a counter clockwise racetrack pattern at angels 6. +-- +-- A TACAN beacon will be automatically activated at channel 1Y with morse code "TKR". See below how to change this setting. +-- +-- Note that the Tanker entry in the F10 radio menu will appear once the tanker is on station and not before. If you spawn the tanker cold or hot on the carrier, this will take ~10 minutes. +-- +-- Also note, that currently the only carrier capable aircraft in DCS is the S-3B Viking (tanker version). If you want to use another refueling aircraft, you need to activate air spawn +-- or set a different land based airport of the map. This will be explained below. +-- +-- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Pattern.jpg) +-- +-- The "downwind" leg of the pattern is normally used for refueling. +-- +-- Once the tanker runs out of fuel itself, it will return to the carrier, respawn with full fuel and take up its pattern again. +-- +-- # Options and Fine Tuning +-- +-- Several parameters can be customized by the mission designer via user API functions. +-- +-- ## Takeoff Type +-- +-- By default, the tanker is spawned with running engines on the carrier. The mission designer has set option to set the take off type via the @{#RECOVERYTANKER.SetTakeoff} function. +-- Or via shortcuts +-- +-- * @{#RECOVERYTANKER.SetTakeoffHot}(): Will set the takeoff to hot, which is also the default. +-- * @{#RECOVERYTANKER.SetTakeoffCold}(): Will set the takeoff type to cold, i.e. with engines off. +-- * @{#RECOVERYTANKER.SetTakeoffAir}(): Will set the takeoff type to air, i.e. the tanker will be spawned in air ~10 NM astern the carrier. +-- +-- For example, +-- TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco") +-- TexacoStennis:SetTakeoffAir() +-- TexacoStennis:Start() +-- will spawn the tanker several nautical miles astern the carrier. From there it will start its pattern. +-- +-- Spawning in air is not as realistic but can be useful do avoid DCS bugs and shortcomings like aircraft crashing into each other on the flight deck. +-- +-- **Note** that when spawning in air is set, the tanker will also not return to the boat, once it is out of fuel. Instead it will be respawned directly in air. +-- +-- If only the first spawning should happen on the carrier, one use the @{#RECOVERYTANKER.SetRespawnInAir}() function to command that all subsequent spawning +-- will happen in air. +-- +-- If the tanker should not be respawned at all, one can set @{#RECOVERYTANKER.SetRespawnOff}(). +-- +-- ## Pattern Parameters +-- +-- The racetrack pattern parameters can be fine tuned via the following functions: +-- +-- * @{#RECOVERYTANKER.SetAltitude}(*altitude*), where *altitude* is the pattern altitude in feet. Default 6000 ft. +-- * @{#RECOVERYTANKER.SetSpeed}(*speed*), where *speed* is the pattern speed in knots. Default is 274 knots TAS which results in ~250 KIAS. +-- * @{#RECOVERYTANKER.SetRacetrackDistances}(*distbow*, *diststern*), where *distbow* and *diststern* are the distances ahead and astern the boat (default 10 and 4 NM), respectively. +-- In principle, these number should be more like 8 and 6 NM but since the carrier is moving, we give translate the pattern points a bit forward. +-- +-- ## Home Base +-- +-- The home base is the airbase where the tanker is spawned (if not in air) and where it will go once it is running out of fuel. The default home base is the carrier itself. +-- The home base can be changed via the @{#RECOVERYTANKER.SetHomeBase}(*airbase*) function, where *airbase* can be a MOOSE @{Wrapper.Airbase#AIRBASE} object or simply the +-- name of the airbase passed as string. +-- +-- Note that only the S3B Viking is a refueling aircraft that is carrier capable. You can use other tanker aircraft types, e.g. the KC-130, but in this case you must either +-- set an airport of the map as home base or activate spawning in air via @{#RECOVERYTANKER.SetTakeoffAir}. +-- +-- ## TACAN +-- +-- A TACAN beacon for the tanker can be activated via scripting, i.e. no need to do this within the mission editor. +-- +-- The beacon is create with the @{#RECOVERYTANKER.SetTACAN}(*channel*, *morse*) function, where *channel* is the TACAN channel (a number), +-- and *morse* a three letter string that is send as morse code to identify the tanker: +-- +-- TexacoStennis:SetTACAN(10, "TKR") +-- +-- will activate a TACAN beacon 10Y with more code "TKR". +-- +-- If you do not set a TACAN beacon explicitly, it is automatically create on channel 1Y and morse code "TKR". +-- The mode is *always* "Y" for AA TACAN stations since mode "X" does not work! +-- +-- In order to completely disable the TACAN beacon, you can use the @{#RECOVERYTANKER.SetTACANoff}() function in your script. +-- +-- ## Radio +-- +-- The radio frequency on optionally modulation can be set via the @{#RECOVERYTANKER.SetRadio}(*frequency*, *modulation*) function. The first parameter denotes the radio frequency the tanker uses in MHz. +-- The second parameter is *optional* and sets the modulation to either AM (default) or FM. +-- +-- For example, +-- +-- TexacoStennis:SetRadio(260) +-- +-- will set the frequency of the tanker to 260 MHz AM. +-- +-- **Note** that if this is not set, the tanker frequency will be automatically set to **251 MHz AM**. +-- +-- ## Pattern Update +-- +-- The pattern of the tanker is updated if at least one of the two following conditions apply: +-- +-- * The aircraft carrier changes its position by more than 5 NM (see @{#RECOVERYTANKER.SetPatternUpdateDistance}) and/or +-- * The aircraft carrier changes its heading by more than 5 degrees (see @{#RECOVERYTANKER.SetPatternUpdateHeading}) +-- +-- **Note** that updating the pattern often leads to a more or less small disruption of the perfect racetrack pattern of the tanker. This is because a new waypoint and new racetrack points +-- need to be set as DCS task. This is the reason why the pattern is not constantly updated but rather when the position or heading of the carrier changes significantly. +-- +-- The maximum update frequency is set to 10 minutes. You can adjust this by @{#RECOVERYTANKER.SetPatternUpdateInterval}. +-- Also the pattern will not be updated whilst the carrier is turning or the tanker is currently refueling another unit. +-- +-- ## Callsign +-- +-- The callsign of the tanker can be set via the @{#RECOVERYTANKER.SetCallsign}(*callsignname*, *callsignnumber*) function. Both parameters are *numbers*. +-- The first parameter *callsignname* defines the name (1=Texaco, 2=Arco, 3=Shell). The second (optional) parameter specifies the first number and has to be between 1-9. +-- Also see [DCS_enum_callsigns](https://wiki.hoggitworld.com/view/DCS_enum_callsigns) and [DCS_command_setCallsign](https://wiki.hoggitworld.com/view/DCS_command_setCallsign). +-- +-- TexacoStennis:SetCAllsign(CALLSIGN.Tanker.Arco) +-- +-- For convenience, MOOSE has a CALLSIGN enumerator introduced. +-- +-- ## AWACS +-- +-- You can use the class also to have an AWACS orbiting overhead the carrier. This requires to add the @{#RECOVERYTANKER.SetAWACS}(*switch*, *eplrs*) function to the script, which sets the enroute tasks AWACS +-- as soon as the aircraft enters its pattern. Note that the EPLRS data link is enabled by default. To disable it, the second parameter *eplrs* must be set to *false*. +-- +-- A simple script could look like this: +-- +-- -- E-2D at USS Stennis spawning in air. +-- local awacsStennis=RECOVERYTANKER:New("USS Stennis", "E2D Group") +-- +-- -- Custom settings: +-- awacsStennis:SetAWACS() +-- awacsStennis:SetCallsign(CALLSIGN.AWACS.Wizard, 1) +-- awacsStennis:SetTakeoffAir() +-- awacsStennis:SetAltitude(20000) +-- awacsStennis:SetRadio(262) +-- awacsStennis:SetTACAN(2, "WIZ") +-- +-- -- Start AWACS. +-- awacsStennis:Start() +-- +-- # Finite State Machine +-- +-- The implementation uses a Finite State Machine (FSM). This allows the mission designer to hook in to certain events. +-- +-- * @{#RECOVERYTANKER.Start}: This event starts the FMS process and initialized parameters and spawns the tanker. DCS event handling is started. +-- * @{#RECOVERYTANKER.Status}: This event is called in regular intervals (~60 seconds) and checks the status of the tanker and carrier. It triggers other events if necessary. +-- * @{#RECOVERYTANKER.PatternUpdate}: This event commands the tanker to update its pattern +-- * @{#RECOVERYTANKER.RTB}: This events sends the tanker to its home base (usually the carrier). This is called once the tanker runs low on gas. +-- * @{#RECOVERYTANKER.RefuelStart}: This event is called when a tanker starts to refuel another unit. +-- * @{#RECOVERYTANKER.RefuelStop}: This event is called when a tanker stopped to refuel another unit. +-- * @{#RECOVERYTANKER.Run}: This event is called when the tanker resumes normal operations, e.g. after refueling stopped or tanker finished refueling. +-- * @{#RECOVERYTANKER.Stop}: This event stops the FSM by unhandling DCS events. +-- +-- The mission designer can capture these events by RECOVERYTANKER.OnAfter*Eventname* functions, e.g. @{#RECOVERYTANKER.OnAfterPatternUpdate}. +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the @{#RECOVERYTANKER} class should have the string "RECOVERYTANKER" in the corresponding line. +-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("RECOVERYTANKER") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- +-- ## Debug Mode +-- +-- You have the option to enable the debug mode for this class via the @{#RECOVERYTANKER.SetDebugModeON} function. +-- If enabled, text messages about the tanker status will be displayed on screen and marks of the pattern created on the F10 map. +-- +-- @field #RECOVERYTANKER +RECOVERYTANKER = { + ClassName = "RECOVERYTANKER", + Debug = false, + lid = nil, + carrier = nil, + carriertype = nil, + tankergroupname = nil, + tanker = nil, + airbase = nil, + beacon = nil, + TACANchannel = nil, + TACANmode = nil, + TACANmorse = nil, + TACANon = nil, + RadioFreq = nil, + RadioModu = nil, + altitude = nil, + speed = nil, + distStern = nil, + distBow = nil, + dTupdate = nil, + Dupdate = nil, + Hupdate = nil, + Tupdate = nil, + takeoff = nil, + lowfuel = nil, + respawn = nil, + respawninair = nil, + uncontrolledac = nil, + orientation = nil, + orientlast = nil, + position = nil, + alias = nil, + uid = 0, + awacs = nil, + callsignname = nil, + callsignnumber = nil, + modex = nil, + eplrs = nil, + recovery = nil, + terminaltype = nil, +} + +--- Unique ID (global). +-- @field #number UID Unique ID (global). +_RECOVERYTANKERID=0 + +--- Class version. +-- @field #string version +RECOVERYTANKER.version="1.0.9" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- DONE: Is alive check for tanker necessary? +-- DONE: Seamless change of position update. Get good updated waypoint and update position if tanker position is right. Not really possiple atm. +-- DONE: Check if TACAN mode "X" is allowed for AA TACAN stations. Nope +-- DONE: Check if tanker is going back to "Running" state after RTB and respawn. +-- DONE: Write documentation. +-- DONE: Trace functions self:T instead of self:I for less output. +-- DONE: Make pattern update parameters (distance, orientation) input parameters. +-- DONE: Add FSM event for pattern update. +-- DONE: Smarter pattern update function. E.g. (small) zone around carrier. Only update position when carrier leaves zone or changes heading? +-- DONE: Set AA TACAN. +-- DONE: Add refueling event/state. +-- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create new RECOVERYTANKER object. +-- @param #RECOVERYTANKER self +-- @param Wrapper.Unit#UNIT carrierunit Carrier unit. +-- @param #string tankergroupname Name of the late activated tanker aircraft template group. +-- @return #RECOVERYTANKER RECOVERYTANKER object. +function RECOVERYTANKER:New(carrierunit, tankergroupname) + + -- Inherit everthing from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #RECOVERYTANKER + + if type(carrierunit)=="string" then + self.carrier=UNIT:FindByName(carrierunit) + else + self.carrier=carrierunit + end + + -- Carrier type. + self.carriertype=self.carrier:GetTypeName() + + -- Tanker group name. + self.tankergroupname=tankergroupname + + -- Increase unique ID. + _RECOVERYTANKERID=_RECOVERYTANKERID+1 + + -- Unique ID of this tanker. + self.uid=_RECOVERYTANKERID + + -- Save self in static object. Easier to retrieve later. + self.carrier:SetState(self.carrier, string.format("RECOVERYTANKER_%d", self.uid) , self) + + -- Set unique spawn alias. + self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.tankergroupname, _RECOVERYTANKERID) + + -- Log ID. + self.lid=string.format("RECOVERYTANKER %s | ", self.alias) + + -- Init default parameters. + self:SetAltitude() + self:SetSpeed() + self:SetRacetrackDistances() + self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) + self:SetTakeoffHot() + self:SetLowFuelThreshold() + self:SetRespawnOnOff() + self:SetTACAN() + self:SetRadio() + self:SetPatternUpdateDistance() + self:SetPatternUpdateHeading() + self:SetPatternUpdateInterval() + self:SetAWACS(false) + self:SetRecoveryAirboss(false) + self.terminaltype=AIRBASE.TerminalType.OpenMedOrBig + + -- Debug trace. + if false then + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") -- Start the FSM. + self:AddTransition("*", "RefuelStart", "Refueling") -- Tanker has started to refuel another unit. + self:AddTransition("*", "RefuelStop", "Running") -- Tanker starts to refuel. + self:AddTransition("*", "Run", "Running") -- Tanker starts normal operation again. + self:AddTransition("Running", "RTB", "Returning") -- Tanker is returning to base (for fuel). + self:AddTransition("Returning", "Returned", "Returned") -- Tanker has returned to its airbase (i.e. landed). + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("Running", "PatternUpdate", "*") -- Update pattern wrt to carrier. + self:AddTransition("*", "Stop", "Stopped") -- Stop the FSM. + + + --- Triggers the FSM event "Start" that starts the recovery tanker. Initializes parameters and starts event handlers. + -- @function [parent=#RECOVERYTANKER] Start + -- @param #RECOVERYTANKER self + + --- Triggers the FSM event "Start" that starts the recovery tanker after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#RECOVERYTANKER] __Start + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + --- On after "Start" event function. Called when FSM is started. + -- @function [parent=#RECOVERYTANKER] OnAfterStart + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "RefuelStart" when the tanker starts refueling another aircraft. + -- @function [parent=#RECOVERYTANKER] RefuelStart + -- @param #RECOVERYTANKER self + -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker. + + --- On after "RefuelStart" event user function. Called when a the the tanker started to refuel another unit. + -- @function [parent=#RECOVERYTANKER] OnAfterRefuelStart + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker. + + + --- Triggers the FSM event "RefuelStop" when the tanker stops refueling another aircraft. + -- @function [parent=#RECOVERYTANKER] RefuelStop + -- @param #RECOVERYTANKER self + -- @param Wrapper.Unit#UNIT receiver Unit stoped receiving fuel from the tanker. + + --- On after "RefuelStop" event user function. Called when a the the tanker stopped to refuel another unit. + -- @function [parent=#RECOVERYTANKER] OnAfterRefuelStop + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT receiver Unit that received fuel from the tanker. + + + --- Triggers the FSM event "Run". Simply puts the group into "Running" state. + -- @function [parent=#RECOVERYTANKER] Run + -- @param #RECOVERYTANKER self + + --- Triggers delayed the FSM event "Run". Simply puts the group into "Running" state. + -- @function [parent=#RECOVERYTANKER] __Run + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "RTB" that sends the tanker home. + -- @function [parent=#RECOVERYTANKER] RTB + -- @param #RECOVERYTANKER self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + + --- Triggers the FSM event "RTB" that sends the tanker home after a delay. + -- @function [parent=#RECOVERYTANKER] __RTB + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + + --- On after "RTB" event user function. Called when a the the tanker returns to its home base. + -- @function [parent=#RECOVERYTANKER] OnAfterRTB + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to. + + + --- Triggers the FSM event "Returned" after the tanker has landed. + -- @function [parent=#RECOVERYTANKER] Returned + -- @param #RECOVERYTANKER self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the tanker has landed. + + --- Triggers the delayed FSM event "Returned" after the tanker has landed. + -- @function [parent=#RECOVERYTANKER] __Returned + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the tanker has landed. + + --- On after "Returned" event user function. Called when a the the tanker has landed at an airbase. + -- @function [parent=#RECOVERYTANKER] OnAfterReturned + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the tanker has landed. + + + --- Triggers the FSM event "Status" that updates the tanker status. + -- @function [parent=#RECOVERYTANKER] Status + -- @param #RECOVERYTANKER self + + --- Triggers the delayed FSM event "Status" that updates the tanker status. + -- @function [parent=#RECOVERYTANKER] __Status + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "PatternUpdate" that updates the pattern of the tanker wrt to the carrier position. + -- @function [parent=#RECOVERYTANKER] PatternUpdate + -- @param #RECOVERYTANKER self + + --- Triggers the delayed FSM event "PatternUpdate" that updates the pattern of the tanker wrt to the carrier position. + -- @function [parent=#RECOVERYTANKER] __PatternUpdate + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + --- On after "PatternEvent" event user function. Called when a the pattern of the tanker is updated. + -- @function [parent=#RECOVERYTANKER] OnAfterPatternUpdate + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Stop" that stops the recovery tanker. Event handlers are stopped. + -- @function [parent=#RECOVERYTANKER] Stop + -- @param #RECOVERYTANKER self + + --- Triggers the FSM event "Stop" that stops the recovery tanker after a delay. Event handlers are stopped. + -- @function [parent=#RECOVERYTANKER] __Stop + -- @param #RECOVERYTANKER self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set the speed the tanker flys in its orbit pattern. +-- @param #RECOVERYTANKER self +-- @param #number speed True air speed (TAS) in knots. Default 274 knots, which results in ~250 KIAS. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetSpeed(speed) + self.speed=UTILS.KnotsToMps(speed or 274) + return self +end + +--- Set orbit pattern altitude of the tanker. +-- @param #RECOVERYTANKER self +-- @param #number altitude Tanker altitude in feet. Default 6000 ft. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetAltitude(altitude) + self.altitude=UTILS.FeetToMeters(altitude or 6000) + return self +end + +--- Set race-track distances. +-- @param #RECOVERYTANKER self +-- @param #number distbow Distance [NM] in front of the carrier. Default 10 NM. +-- @param #number diststern Distance [NM] behind the carrier. Default 4 NM. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRacetrackDistances(distbow, diststern) + self.distBow=UTILS.NMToMeters(distbow or 10) + self.distStern=-UTILS.NMToMeters(diststern or 4) + return self +end + +--- Set minimum pattern update interval. After a pattern update this time interval has to pass before the next update is allowed. +-- @param #RECOVERYTANKER self +-- @param #number interval Min interval in minutes. Default is 10 minutes. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetPatternUpdateInterval(interval) + self.dTupdate=(interval or 10)*60 + return self +end + +--- Set pattern update distance threshold. Tanker will update its pattern when the carrier changes its position by more than this distance. +-- @param #RECOVERYTANKER self +-- @param #number distancechange Distance threshold in NM. Default 5 NM (=9.62 km). +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetPatternUpdateDistance(distancechange) + self.Dupdate=UTILS.NMToMeters(distancechange or 5) + return self +end + +--- Set pattern update heading threshold. Tanker will update its pattern when the carrier changes its heading by more than this value. +-- @param #RECOVERYTANKER self +-- @param #number headingchange Heading threshold in degrees. Default 5 degrees. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetPatternUpdateHeading(headingchange) + self.Hupdate=headingchange or 5 + return self +end + +--- Set low fuel state of tanker. When fuel is below this threshold, the tanker will RTB or be respawned if takeoff type is in air. +-- @param #RECOVERYTANKER self +-- @param #number fuelthreshold Low fuel threshold in percent. Default 10 % of max fuel. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetLowFuelThreshold(fuelthreshold) + self.lowfuel=fuelthreshold or 10 + return self +end + +--- Set home airbase of the tanker. This is the airbase where the tanker will go when it is out of fuel. +-- @param #RECOVERYTANKER self +-- @param Wrapper.Airbase#AIRBASE airbase The home airbase. Can be the airbase name or a Moose AIRBASE object. +-- @param #number terminaltype (Optional) The terminal type of parking spots used for spawning at airbases. Default AIRBASE.TerminalType.OpenMedOrBig. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetHomeBase(airbase, terminaltype) + if type(airbase)=="string" then + self.airbase=AIRBASE:FindByName(airbase) + else + self.airbase=airbase + end + if not self.airbase then + self:E(self.lid.."ERROR: Airbase is nil!") + end + if terminaltype then + self.terminaltype=terminaltype + end + return self +end + +--- Activate recovery by the AIRBOSS class. Tanker will get a Marshal stack and perform a CASE I, II or III recovery when RTB. +-- @param #RECOVERYTANKER self +-- @param #boolean switch If true or nil, recovery is done by AIRBOSS. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRecoveryAirboss(switch) + if switch==true or switch==nil then + self.recovery=true + else + self.recovery=false + end + return self +end + +--- Set that the group takes the roll of an AWACS instead of a refueling tanker. +-- @param #RECOVERYTANKER self +-- @param #boolean switch If true or nil, set roll AWACS. +-- @param #boolean eplrs If true or nil, enable EPLRS. If false, EPLRS will be off. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetAWACS(switch, eplrs) + if switch==nil or switch==true then + self.awacs=true + else + self.awacs=false + end + if eplrs==nil or eplrs==true then + self.eplrs=true + else + self.eplrs=false + end + + return self +end + + +--- Set callsign of the tanker group. +-- @param #RECOVERYTANKER self +-- @param #number callsignname Number +-- @param #number callsignnumber Number +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetCallsign(callsignname, callsignnumber) + self.callsignname=callsignname + self.callsignnumber=callsignnumber + return self +end + +--- Set modex (tail number) of the tanker. +-- @param #RECOVERYTANKER self +-- @param #number modex Tail number. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetModex(modex) + self.modex=modex + return self +end + +--- Set takeoff type. +-- @param #RECOVERYTANKER self +-- @param #number takeofftype Takeoff type. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoff(takeofftype) + self.takeoff=takeofftype + return self +end + +--- Set takeoff with engines running (hot). +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoffHot() + self:SetTakeoff(SPAWN.Takeoff.Hot) + return self +end + +--- Set takeoff with engines off (cold). +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoffCold() + self:SetTakeoff(SPAWN.Takeoff.Cold) + return self +end + +--- Set takeoff in air at the defined pattern altitude and ~10 NM astern the carrier. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTakeoffAir() + self:SetTakeoff(SPAWN.Takeoff.Air) + return self +end + +--- Enable respawning of tanker. Note that this is the default behaviour. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnOn() + self.respawn=true + return self +end + +--- Disable respawning of tanker. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnOff() + self.respawn=false + return self +end + +--- Set whether tanker shall be respawned or not. +-- @param #RECOVERYTANKER self +-- @param #boolean switch If true (or nil), tanker will be respawned. If false, tanker will not be respawned. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnOnOff(switch) + if switch==nil or switch==true then + self.respawn=true + else + self.respawn=false + end + return self +end + +--- Tanker will be respawned in air, even it was initially spawned on the carrier. +-- So only the first spawn will be on the carrier while all subsequent spawns will happen in air. +-- This allows for undisrupted operations and less problems on the carrier deck. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRespawnInAir() + self.respawninair=true + return self +end + +--- Use an uncontrolled aircraft already present in the mission rather than spawning a new tanker as initial recovery thanker. +-- This can be useful when interfaced with, e.g., a MOOSE @{Functional.Warehouse#WAREHOUSE}. +-- The group name is the one specified in the @{#RECOVERYTANKER.New} function. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetUseUncontrolledAircraft() + self.uncontrolledac=true + return self +end + + +--- Disable automatic TACAN activation. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTACANoff() + self.TACANon=false + return self +end + +--- Set TACAN channel of tanker. Note that mode is automatically set to "Y" for AA TACAN since only that works. +-- @param #RECOVERYTANKER self +-- @param #number channel TACAN channel. Default 1. +-- @param #string morse TACAN morse code identifier. Three letters. Default "TKR". +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetTACAN(channel, morse) + self.TACANchannel=channel or 1 + self.TACANmode="Y" + self.TACANmorse=morse or "TKR" + self.TACANon=true + return self +end + +--- Set radio frequency and optionally modulation of the tanker. +-- @param #RECOVERYTANKER self +-- @param #number frequency Radio frequency in MHz. Default 251 MHz. +-- @param #string modulation Radio modulation, either "AM" or "FM". Default "AM". +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetRadio(frequency, modulation) + self.RadioFreq=frequency or 251 + self.RadioModu=modulation or "AM" + return self +end + +--- Activate debug mode. Marks of pattern on F10 map and debug messages displayed on screen. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetDebugModeON() + self.Debug=true + return self +end + +--- Deactivate debug mode. This is also the default setting. +-- @param #RECOVERYTANKER self +-- @return #RECOVERYTANKER self +function RECOVERYTANKER:SetDebugModeOFF() + self.Debug=false + return self +end + +--- Check if tanker is currently returning to base. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker is returning to base. +function RECOVERYTANKER:IsReturning() + return self:is("Returning") +end + +--- Check if tanker has returned to base. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker has returned to base. +function RECOVERYTANKER:IsReturned() + return self:is("Returned") +end + +--- Check if tanker is currently operating. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker is operating. +function RECOVERYTANKER:IsRunning() + return self:is("Running") +end + +--- Check if tanker is currently refueling another aircraft. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, tanker is refueling. +function RECOVERYTANKER:IsRefueling() + return self:is("Refueling") +end + +--- Check if FMS was stopped. +-- @param #RECOVERYTANKER self +-- @return #boolean If true, is stopped. +function RECOVERYTANKER:IsStopped() + return self:is("Stopped") +end + +--- Alias of tanker spawn group. +-- @param #RECOVERYTANKER self +-- @return #string Alias of the tanker. +function RECOVERYTANKER:GetAlias() + return self.alias +end + +--- Get unit name of the spawned tanker. +-- @param #RECOVERYTANKER self +-- @return #string Name of the tanker unit or nil if it does not exist. +function RECOVERYTANKER:GetUnitName() + local unit=self.tanker:GetUnit(1) + if unit then + return unit:GetName() + end + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM states +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #RECOVERYTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RECOVERYTANKER:onafterStart(From, Event, To) + + -- Info on start. + self:I(string.format("Starting Recovery Tanker v%s for carrier unit %s of type %s for tanker group %s.", RECOVERYTANKER.version, self.carrier:GetName(), self.carriertype, self.tankergroupname)) + + -- Handle events. + self:HandleEvent(EVENTS.EngineShutdown) + self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.Refueling, self._RefuelingStart) --Need explicit functions since OnEventRefueling and OnEventRefuelingStop did not hook! + self:HandleEvent(EVENTS.RefuelingStop, self._RefuelingStop) + self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrDead) + self:HandleEvent(EVENTS.Dead, self._OnEventCrashOrDead) + + -- Spawn tanker. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. + local Spawn=SPAWN:NewWithAlias(self.tankergroupname, self.alias) + + -- Set radio frequency and modulation. + Spawn:InitRadioCommsOnOff(true) + Spawn:InitRadioFrequency(self.RadioFreq) + Spawn:InitRadioModulation(self.RadioModu) + Spawn:InitModex(self.modex) + + -- Spawn on carrier. + if self.takeoff==SPAWN.Takeoff.Air then + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + -- Spawn distance behind the carrier. + local dist=-self.distStern+UTILS.NMToMeters(4) + + -- Coordinate behind the carrier and slightly port. + local Carrier=self.carrier:GetCoordinate():Translate(dist, hdg+190):SetAltitude(self.altitude) + + -- Orientation of spawned group. + Spawn:InitHeading(hdg+10) + + -- Spawn at coordinate. + self.tanker=Spawn:SpawnFromCoordinate(Carrier) + + else + + -- Check if an uncontrolled tanker group was requested. + if self.uncontrolledac then + + -- Use an uncontrolled aircraft group. + self.tanker=GROUP:FindByName(self.tankergroupname) + + if self.tanker:IsAlive() then + + -- Start uncontrolled group. + self.tanker:StartUncontrolled() + + else + -- No group by that name! + self:E(string.format("ERROR: No uncontrolled (alive) tanker group with name %s could be found!", self.tankergroupname)) + return + end + + else + + -- Spawn tanker at airbase. + self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff, nil, self.terminaltype) + + end + + end + + -- Initialize route. self.distStern<0! + self:ScheduleOnce(1, self._InitRoute, self, -self.distStern+UTILS.NMToMeters(3)) + + -- Create tanker beacon. + if self.TACANon then + self:_ActivateTACAN(2) + end + + -- Set callsign. + if self.callsignname then + self.tanker:CommandSetCallsign(self.callsignname, self.callsignnumber, 2) + end + + -- Turn EPLRS datalink on. + if self.eplrs then + self.tanker:CommandEPLRS(true, 3) + end + + + -- Get initial orientation and position of carrier. + self.orientation=self.carrier:GetOrientationX() + self.orientlast=self.carrier:GetOrientationX() + self.position=self.carrier:GetCoordinate() + + -- Init status updates in 10 seconds. + self:__Status(10) +end + + +--- On after Status event. Checks player status. +-- @param #RECOVERYTANKER self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RECOVERYTANKER:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + if self.tanker and self.tanker:IsAlive() then + + --------------------- + -- TANKER is ALIVE -- + --------------------- + + -- Get fuel of tanker. + local fuel=self.tanker:GetFuel()*100 + local life=self.tanker:GetUnit(1):GetLife() + local life0=self.tanker:GetUnit(1):GetLife0() + local lifeR=self.tanker:GetUnit(1):GetLifeRelative() + + -- Report fuel and life. + local text=string.format("Recovery tanker %s: state=%s fuel=%.1f, life=%.1f/%.1f=%d", self.tanker:GetName(), self:GetState(), fuel, life, life0, lifeR*100) + self:T(self.lid..text) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + + -- Check if tanker is running and not RTBing or refueling. + if self:IsRunning() then + + -- Check fuel. + if fuel 100 meters, this should be another tanker. + if dist>100 then + return + end + + -- Info message. + local text=string.format("Recovery tanker %s started refueling unit %s", self.tanker:GetName(), receiver:GetName()) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- FMS state "Refueling". + self:RefuelStart(receiver) + end + +end + +--- Event handler for refueling stopped. +-- @param #RECOVERYTANKER self +-- @param Core.Event#EVENTDATA EventData Event data. +function RECOVERYTANKER:_RefuelingStop(EventData) + + if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() then + + -- Unit receiving fuel. + local receiver=EventData.IniUnit + + -- Get distance to tanker to check that unit is receiving fuel from this tanker. + local dist=receiver:GetCoordinate():Get2DDistance(self.tanker:GetCoordinate()) + + -- If distance > 100 meters, this should be another tanker. + if dist>100 then + return + end + + -- Info message. + local text=string.format("Recovery tanker %s stopped refueling unit %s", self.tanker:GetName(), receiver:GetName()) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- FSM state "Running". + self:RefuelStop(receiver) + end + +end + +--- A unit crashed or died. +-- @param #RECOVERYTANKER self +-- @param Core.Event#EVENTDATA EventData Event data. +function RECOVERYTANKER:_OnEventCrashOrDead(EventData) + self:F2({eventdata=EventData}) + + -- Check that there is an initiating unit in the event data. + if EventData and EventData.IniUnit then + + -- Crashed or dead unit. + local unit=EventData.IniUnit + local unitname=tostring(EventData.IniUnitName) + + -- Check that it was the tanker that crashed. + if EventData.IniGroupName==self.tanker:GetName() then + + -- Error message. + self:E(self.lid..string.format("Recovery tanker %s crashed!", unitname)) + + -- Stop FSM. + self:Stop() + + -- Restart. + if self.respawn then + self:__Start(5) + end + + end + + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- MISC functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Task function to +-- @param #RECOVERYTANKER self +function RECOVERYTANKER:_InitPatternTaskFunction() + + -- Name of the warehouse (static) object. + local carriername=self.carrier:GetName() + + -- Task script. + local DCSScript = {} + DCSScript[#DCSScript+1] = string.format('local mycarrier = UNIT:FindByName(\"%s\") ', carriername) -- The carrier unit that holds the self object. + DCSScript[#DCSScript+1] = string.format('local mytanker = mycarrier:GetState(mycarrier, \"RECOVERYTANKER_%d\") ', self.uid) -- Get the RECOVERYTANKER self object. + DCSScript[#DCSScript+1] = string.format('mytanker:PatternUpdate()') -- Call the function, e.g. mytanker.(self) + + -- Create task. + local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript))) + + return DCSTask +end + +--- Init waypoint after spawn. Tanker is first guided to a position astern the carrier and starts its racetrack pattern from there. +-- @param #RECOVERYTANKER self +-- @param #number dist Distance [NM] of initial waypoint astern carrier. Default 8 NM. +-- @param #number delay Delay before routing in seconds. Default 1 second. +function RECOVERYTANKER:_InitRoute(dist, delay) + + -- Defaults. + dist=dist or UTILS.NMToMeters(8) + delay=delay or 1 + + -- Debug message. + self:T(self.lid..string.format("Initializing route of recovery tanker %s.", self.tanker:GetName())) + + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() + + -- Carrier heading. + local hdg=self.carrier:GetHeading() + + -- First waypoint is ~10 NM behind and slightly port the boat. + local p=Carrier:Translate(dist, hdg+190):SetAltitude(self.altitude) + + -- Speed for waypoints in km/h. + -- This causes a problem, because the tanker might not be alive yet ==> We schedule the call of _InitRoute + local speed=self.tanker:GetSpeedMax()*0.8 + + -- Set to 280 knots and convert to km/h. + --local speed=280/0.539957 + + -- Debug mark. + if self.Debug then + p:MarkToAll(string.format("Enter Pattern WP: alt=%d ft, speed=%d kts", UTILS.MetersToFeet(self.altitude), speed*0.539957)) + end + + -- Task to update pattern when wp 2 is reached. + local task=self:_InitPatternTaskFunction() + + -- Waypoints. + local wp={} + if self.takeoff==SPAWN.Takeoff.Air then + wp[#wp+1]=self.tanker:GetCoordinate():SetAltitude(self.altitude):WaypointAirTurningPoint(nil, speed, {}, "Spawn Position") + else + wp[#wp+1]=Carrier:WaypointAirTakeOffParking() + end + wp[#wp+1]=p:WaypointAirTurningPoint(nil, speed, {task}, "Enter Pattern") + + -- Set route. + self.tanker:Route(wp, delay) + + -- Set state to Running. Necessary when tanker was RTB and respawned since it is probably in state "Returning". + self:__Run(1) + + -- No update yet, wait until the function is called (avoids checks if pattern update is needed). + self.Tupdate=nil +end + +--- Check if heading or position have changed significantly. +-- @param #RECOVERYTANKER self +-- @param #number dt Time since last update in seconds. +-- @return #boolean If true, heading and/or position have changed more than 5 degrees or 10 km, respectively. +function RECOVERYTANKER:_CheckPatternUpdate(dt) + + -- Get current position and orientation of carrier. + local pos=self.carrier:GetCoordinate() + + -- Current orientation of carrier. + local vNew=self.carrier:GetOrientationX() + + -- Reference orientation of carrier after the last update + local vOld=self.orientation + + -- Last orientation from 30 seconds ago. + local vLast=self.orientlast + + -- We only need the X-Z plane. + vNew.y=0 ; vOld.y=0 ; vLast.y=0 + + -- Get angle between old and new orientation vectors in rad and convert to degrees. + local deltaHeading=math.deg(math.acos(UTILS.VecDot(vNew,vOld)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vOld))) + + -- Angle between current heading and last time we checked ~30 seconds ago. + local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast))) + + -- Last orientation becomes new orientation + self.orientlast=vNew + + -- Carrier is turning when its heading changed by at least one degree since last check. + local turning=deltaLast>=1 + + -- Debug output if turning + if turning then + self:T2(self.lid..string.format("Carrier is turning. Delta Heading = %.1f", deltaLast)) + end + + -- Check if orientation changed. + local Hchange=false + if math.abs(deltaHeading)>=self.Hupdate then + self:T(self.lid..string.format("Carrier heading changed by %d degrees. Turning=%s.", deltaHeading, tostring(turning))) + Hchange=true + end + + -- Get distance to saved position. + local dist=pos:Get2DDistance(self.position) + + -- Check if carrier moved more than ~5 NM. + local Dchange=false + if dist>self.Dupdate then + self:T(self.lid..string.format("Carrier position changed by %.1f NM. Turning=%s.", UTILS.MetersToNM(dist), tostring(turning))) + Dchange=true + end + + -- Assume no update necessary. + local update=false + + -- No update if currently turning! Also must be running (not RTB or refueling) and T>~10 min since last position update. + if self:IsRunning() and dt>self.dTupdate and not turning then + + -- Update if heading or distance changed. + if Hchange or Dchange then + -- Debug message. + local text=string.format("Updating tanker %s pattern due to carrier position=%s or heading=%s change.", self.tanker:GetName(), tostring(Dchange), tostring(Hchange)) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Update pos and orientation. + self.orientation=vNew + self.position=pos + update=true + end + + end + + return update +end + +--- Activate TACAN of tanker. +-- @param #RECOVERYTANKER self +-- @param #number delay Delay in seconds. +function RECOVERYTANKER:_ActivateTACAN(delay) + + if delay and delay>0 then + + -- Schedule TACAN activation. + --SCHEDULER:New(nil, self._ActivateTACAN, {self}, delay) + self:ScheduleOnce(delay, RECOVERYTANKER._ActivateTACAN, self) + + else + + -- Get tanker unit. + local unit=self.tanker:GetUnit(1) + + -- Check if unit is alive. + if unit and unit:IsAlive() then + + -- Debug message. + local text=string.format("Activating TACAN beacon: channel=%d mode=%s, morse=%s.", self.TACANchannel, self.TACANmode, self.TACANmorse) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Create a new beacon and activate TACAN. + self.beacon=BEACON:New(unit) + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) + + else + self:E(self.lid.."ERROR: Recovery tanker is not alive!") + end + + end + +end + +--- Self made race track pattern. Not working as desired, since tanker changes course too rapidly after each waypoint. +-- @param #RECOVERYTANKER self +-- @return #table Table of pattern waypoints. +function RECOVERYTANKER:_Pattern() + + -- Carrier heading. + local hdg=self.carrier:GetHeading() + + -- Pattern altitude + local alt=self.altitude + + -- Carrier position. + local Carrier=self.carrier:GetCoordinate() + + local width=UTILS.NMToMeters(8) + + -- Define race-track pattern. + local p={} + p[1]=self.tanker:GetCoordinate() -- Tanker position + p[2]=Carrier:SetAltitude(alt) -- Carrier position + p[3]=p[2]:Translate(self.distBow, hdg) -- In front of carrier + p[4]=p[3]:Translate(width/math.sqrt(2), hdg-45) -- Middle front for smoother curve + -- Probably need one more to make it go -hdg at the waypoint. + p[5]=p[3]:Translate(width, hdg-90) -- In front on port + p[6]=p[5]:Translate(self.distStern-self.distBow, hdg) -- Behind on port (sterndist<0!) + p[7]=p[2]:Translate(self.distStern, hdg) -- Behind carrier + + local wp={} + for i=1,#p do + local coord=p[i] --Core.Point#COORDINATE + coord:MarkToAll(string.format("Waypoint %d", i)) + --table.insert(wp, coord:WaypointAirFlyOverPoint(nil , self.speed)) + table.insert(wp, coord:WaypointAirTurningPoint(nil , UTILS.MpsToKmph(self.speed))) + end + + return wp +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Ops/RescueHelo.lua b/Moose Development/Moose/Ops/RescueHelo.lua new file mode 100644 index 000000000..8e979ccf9 --- /dev/null +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -0,0 +1,1296 @@ +--- **Ops** - Rescue helicopter for carrier operations. +-- +-- Recue helicopter for carrier operations. +-- +-- **Main Features:** +-- +-- * Close formation with carrier. +-- * No restrictions regarding carrier waypoints and heading. +-- * Automatic respawning on empty fuel for 24/7 operations. +-- * Automatic rescuing of crashed or ejected pilots in the vicinity of the carrier. +-- * Multiple helos at different carriers due to object oriented approach. +-- * Finite State Machine (FSM) implementation. +-- +-- ## Known (DCS) Issues +-- +-- * CH-53E does only report 27.5% fuel even if fuel is set to 100% in the ME. See [bug report](https://forums.eagle.ru/showthread.php?t=223712) +-- * CH-53E does not accept USS Tarawa as landing airbase (even it can be spawned on it). +-- * Helos dont move away from their landing position on carriers. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Contributions: Flightcontrol (@{AI.AI_Formation} class being used here) +-- +-- @module Ops.RescueHelo +-- @image Ops_RescueHelo.png + +--- RESCUEHELO class. +-- @type RESCUEHELO +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Debug mode on/off. +-- @field #string lid Log debug id text. +-- @field Wrapper.Unit#UNIT carrier The carrier the helo is attached to. +-- @field #string carriertype Carrier type. +-- @field #string helogroupname Name of the late activated helo template group. +-- @field Wrapper.Group#GROUP helo Helo group. +-- @field #number takeoff Takeoff type. +-- @field Wrapper.Airbase#AIRBASE airbase The airbase object acting as home base of the helo. +-- @field Core.Set#SET_GROUP followset Follow group set. +-- @field AI.AI_Formation#AI_FORMATION formation AI_FORMATION object. +-- @field #number lowfuel Low fuel threshold of helo in percent. +-- @field #number altitude Altitude of helo in meters. +-- @field #number offsetX Offset in meters to carrier in longitudinal direction. +-- @field #number offsetZ Offset in meters to carrier in latitudinal direction. +-- @field Core.Zone#ZONE_RADIUS rescuezone Zone around the carrier in which helo will rescue crashed or ejected units. +-- @field #boolean respawn If true, helo be respawned (default). If false, no respawning will happen. +-- @field #boolean respawninair If true, helo will always be respawned in air. This has no impact on the initial spawn setting. +-- @field #boolean uncontrolledac If true, use and uncontrolled helo group already present in the mission. +-- @field #boolean rescueon If true, helo will rescue crashed pilots. If false, no recuing will happen. +-- @field #number rescueduration Time the rescue helicopter hovers over the crash site in seconds. +-- @field #number rescuespeed Speed in m/s the rescue helicopter hovers at over the crash site. +-- @field #boolean rescuestopboat If true, stop carrier during rescue operations. +-- @field #boolean carrierstop If true, route of carrier was stopped. +-- @field #number HeloFuel0 Initial fuel of helo in percent. Necessary due to DCS bug that helo with full tank does not return fuel via API function. +-- @field #boolean rtb If true, Helo will be return to base on the next status check. +-- @field #number hid Unit ID of the helo group. (Global) Running number. +-- @field #string alias Alias of the spawn group. +-- @field #number uid Unique ID of this helo. +-- @field #number modex Tail number of the helo. +-- @field #number dtFollow Follow time update interval in seconds. Default 1.0 sec. +-- @extends Core.Fsm#FSM + +--- Rescue Helo +-- +-- === +-- +-- ![Banner Image](..\Presentations\RESCUEHELO\RescueHelo_Main.png) +-- +-- # Recue Helo +-- +-- The rescue helo will fly in close formation with another unit, which is typically an aircraft carrier. +-- It's mission is to rescue crashed or ejected pilots. Well, and to look cool... +-- +-- # Simple Script +-- +-- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named "*USS Stennis*". +-- +-- Secondly, you need to define a rescue helicopter group in the mission editor and set it to "**LATE ACTIVATED**". The name of the group we'll use is "*Recue Helo*". +-- +-- The basic script is very simple and consists of only two lines. +-- +-- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:Start() +-- +-- The first line will create a new @{#RESCUEHELO} object via @{#RESCUEHELO.New} and the second line starts the process by calling @{#RESCUEHELO.Start}. +-- +-- **NOTE** that it is *very important* to define the RESCUEHELO object as **global** variable. Otherwise, the lua garbage collector will kill the formation for unknown reasons! +-- +-- By default, the helo will be spawned on the *USS Stennis* with hot engines. Then it will take off and go on station on the starboard side of the boat. +-- +-- Once the helo is out of fuel, it will return to the carrier. When the helo lands, it will be respawned immidiately and go back on station. +-- +-- If a unit crashes or a pilot ejects within a radius of 30 km from the USS Stennis, the helo will automatically fly to the crash side and +-- rescue to pilot. This will take around 5 minutes. After that, the helo will return to the Stennis, land there and bring back the poor guy. +-- When this is done, the helo will go back on station. +-- +-- # Fine Tuning +-- +-- The implementation allows to customize quite a few settings easily via user API functions. +-- +-- ## Takeoff Type +-- +-- By default, the helo is spawned with running engines on the carrier. The mission designer has set option to set the take off type via the @{#RESCUEHELO.SetTakeoff} function. +-- Or via shortcuts +-- +-- * @{#RESCUEHELO.SetTakeoffHot}(): Will set the takeoff to hot, which is also the default. +-- * @{#RESCUEHELO.SetTakeoffCold}(): Will set the takeoff type to cold, i.e. with engines off. +-- * @{#RESCUEHELO.SetTakeoffAir}(): Will set the takeoff type to air, i.e. the helo will be spawned in air near the unit which he follows. +-- +-- For example, +-- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:SetTakeoffAir() +-- RescueheloStennis:Start() +-- will spawn the helo near the USS Stennis in air. +-- +-- Spawning in air is not as realistic but can be useful do avoid DCS bugs and shortcomings like aircraft crashing into each other on the flight deck. +-- +-- **Note** that when spawning in air is set, the helo will also not return to the boat, once it is out of fuel. Instead it will be respawned in air. +-- +-- If only the first spawning should happen on the carrier, one use the @{#RESCUEHELO.SetRespawnInAir}() function to command that all subsequent spawning +-- will happen in air. +-- +-- If the helo should no be respawned at all, one can set @{#RESCUEHELO.SetRespawnOff}(). +-- +-- ## Home Base +-- +-- It is possible to define a "home base" other than the aircraft carrier using the @{#RESCUEHELO.SetHomeBase}(*airbase*) function, where *airbase* is +-- a @{Wrapper.Airbase#AIRBASE} object or simply the name of the airbase. +-- +-- For example, one could imagine a strike group, and the helo will be spawned from another ship which has a helo pad. +-- +-- RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo") +-- RescueheloStennis:SetHomeBase(AIRBASE:FindByName("USS Normandy")) +-- RescueheloStennis:Start() +-- +-- In this case, the helo will be spawned on the USS Normandy and then make its way to the USS Stennis to establish the formation. +-- Note that the distance to the mother ship should be rather small since the helo will go there very slowly. +-- +-- Once the helo runs out of fuel, it will return to the USS Normandy and not the Stennis for respawning. +-- +-- ## Formation Position +-- +-- The position of the helo relative to the mother ship can be tuned via the functions +-- +-- * @{#RESCUEHELO.SetAltitude}(*altitude*), where *altitude* is the altitude the helo flies at in meters. Default is 70 meters. +-- * @{#RESCUEHELO.SetOffsetX}(*distance*), where *distance is the distance in the direction of movement of the carrier. Default is 200 meters. +-- * @{#RESCUEHELO.SetOffsetZ}(*distance*), where *distance is the distance on the starboard side. Default is 100 meters. +-- +-- ## Rescue Operations +-- +-- By default the rescue helo will start a rescue operation if an aircraft crashes or a pilot ejects in the vicinity of the carrier. +-- This is restricted to aircraft of the same coalition as the rescue helo. Enemy (or neutral) pilots will be left on their own. +-- +-- The standard "rescue zone" has a radius of 15 NM (~28 km) around the carrier. The radius can be adjusted via the @{#RESCUEHELO.SetRescueZone}(*radius*) functions, +-- where *radius* is the radius of the zone in nautical miles. If you use multiple rescue helos in the same mission, you might want to ensure that the radii +-- are not overlapping so that two helos try to rescue the same pilot. But it should not hurt either way. +-- +-- Once the helo reaches the crash site, the rescue operation will last 5 minutes. This time can be changed by @{#RESCUEHELO.SetRescueDuration(*time*), +-- where *time* is the duration in minutes. +-- +-- During the rescue operation, the helo will hover (orbit) over the crash site at a speed of 5 knots. The speed can be set by @{#RESCUEHELO.SetRescueHoverSpeed}(*speed*), +-- where the *speed* is given in knots. +-- +-- If no rescue operations should be carried out by the helo, this option can be completely disabled by using @{#RESCUEHELO.SetRescueOff}(). +-- +-- # Finite State Machine +-- +-- The implementation uses a Finite State Machine (FSM). This allows the mission designer to hook in to certain events. +-- +-- * @{#RESCUEHELO.Start}: This eventfunction starts the FMS process and initialized parameters and spawns the helo. DCS event handling is started. +-- * @{#RESCUEHELO.Status}: This eventfunction is called in regular intervals (~60 seconds) and checks the status of the helo and carrier. It triggers other events if necessary. +-- * @{#RESCUEHELO.Rescue}: This eventfunction commands the helo to go on a rescue operation at a certain coordinate. +-- * @{#RESCUEHELO.RTB}: This eventsfunction sends the helo to its home base (usually the carrier). This is called once the helo runs low on gas. +-- * @{#RESCUEHELO.Run}: This eventfunction is called when the helo resumes normal operations and goes back on station. +-- * @{#RESCUEHELO.Stop}: This eventfunction stops the FSM by unhandling DCS events. +-- +-- The mission designer can capture these events by RESCUEHELO.OnAfter*Eventname* functions, e.g. @{#RESCUEHELO.OnAfterRescue}. +-- +-- # Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the @{#RESCUEHELO} class should have the string "RESCUEHELO" in the corresponding line. +-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("RESCUEHELO") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details. +-- +-- ## Debug Mode +-- +-- You have the option to enable the debug mode for this class via the @{#RESCUEHELO.SetDebugModeON} function. +-- If enabled, text messages about the helo status will be displayed on screen and marks of the pattern created on the F10 map. +-- +-- +-- @field #RESCUEHELO +RESCUEHELO = { + ClassName = "RESCUEHELO", + Debug = false, + lid = nil, + carrier = nil, + carriertype = nil, + helogroupname = nil, + helo = nil, + airbase = nil, + takeoff = nil, + followset = nil, + formation = nil, + lowfuel = nil, + altitude = nil, + offsetX = nil, + offsetZ = nil, + rescuezone = nil, + respawn = nil, + respawninair = nil, + uncontrolledac = nil, + rescueon = nil, + rescueduration = nil, + rescuespeed = nil, + rescuestopboat = nil, + HeloFuel0 = nil, + rtb = nil, + carrierstop = nil, + alias = nil, + uid = 0, + modex = nil, + dtFollow = nil, +} + +--- Unique ID (global). +-- @field #number uid Unique ID (global). +_RESCUEHELOID=0 + +--- Class version. +-- @field #string version +RESCUEHELO.version="1.1.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- NOPE: Add messages for rescue mission. +-- NOPE: Add option to stop carrier while rescue operation is in progress? Done but NOT working. Postponed... +-- DONE: Write documentation. +-- DONE: Add option to deactivate the rescuing. +-- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse. +-- DONE: Add rescue event when aircraft crashes. +-- DONE: Make offset input parameter. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new RESCUEHELO object. +-- @param #RESCUEHELO self +-- @param Wrapper.Unit#UNIT carrierunit Carrier unit object or simply the unit name. +-- @param #string helogroupname Name of the late activated rescue helo template group. +-- @return #RESCUEHELO RESCUEHELO object. +function RESCUEHELO:New(carrierunit, helogroupname) + + -- Inherit everthing from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #RESCUEHELO + + -- Catch case when just the unit name is passed. + if type(carrierunit)=="string" then + self.carrier=UNIT:FindByName(carrierunit) + else + self.carrier=carrierunit + end + + -- Carrier type. + self.carriertype=self.carrier:GetTypeName() + + -- Helo group name. + self.helogroupname=helogroupname + + -- Increase ID. + _RESCUEHELOID=_RESCUEHELOID+1 + + -- Unique ID of this helo. + self.uid=_RESCUEHELOID + + -- Save self in static object. Easier to retrieve later. + self.carrier:SetState(self.carrier, string.format("RESCUEHELO_%d", self.uid) , self) + + -- Set unique spawn alias. + self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.helogroupname, _RESCUEHELOID) + + -- Log ID. + self.lid=string.format("RESCUEHELO %s | ", self.alias) + + -- Init defaults. + self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName())) + self:SetTakeoffHot() + self:SetLowFuelThreshold() + self:SetAltitude() + self:SetOffsetX() + self:SetOffsetZ() + self:SetRespawnOn() + self:SetRescueOn() + self:SetRescueZone() + self:SetRescueHoverSpeed() + self:SetRescueDuration() + self:SetFollowTimeInterval() + self:SetRescueStopBoatOff() + + -- Some more. + self.rtb=false + self.carrierstop=false + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("Running", "Rescue", "Rescuing") + self:AddTransition("Running", "RTB", "Returning") + self:AddTransition("Rescuing", "RTB", "Returning") + self:AddTransition("Returning", "Returned", "Returned") + self:AddTransition("Running", "Run", "Running") + self:AddTransition("Returned", "Run", "Running") + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Stop", "Stopped") + + + --- Triggers the FSM event "Start" that starts the rescue helo. Initializes parameters and starts event handlers. + -- @function [parent=#RESCUEHELO] Start + -- @param #RESCUEHELO self + + --- Triggers the FSM event "Start" that starts the rescue helo after a delay. Initializes parameters and starts event handlers. + -- @function [parent=#RESCUEHELO] __Start + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + --- On after "Start" event function. Called when FSM is started. + -- @function [parent=#RESCUEHELO] OnAfterStart + -- @param #RECOVERYTANKER self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- Triggers the FSM event "Rescue" that sends the helo on a rescue mission to a specifc coordinate. + -- @function [parent=#RESCUEHELO] Rescue + -- @param #RESCUEHELO self + -- @param Core.Point#COORDINATE RescueCoord Coordinate where the resue mission takes place. + + --- Triggers the delayed FSM event "Rescue" that sends the helo on a rescue mission to a specifc coordinate. + -- @function [parent=#RESCUEHELO] __Rescue + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + -- @param Core.Point#COORDINATE RescueCoord Coordinate where the resue mission takes place. + + --- On after "Rescue" event user function. Called when a the the helo goes on a rescue mission. + -- @function [parent=#RESCUEHELO] OnAfterRescue + -- @param #RESCUEHELO self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE RescueCoord Crash site where the rescue operation takes place. + + + --- Triggers the FSM event "RTB" that sends the helo home. + -- @function [parent=#RESCUEHELO] RTB + -- @param #RESCUEHELO self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. + + --- Triggers the FSM event "RTB" that sends the helo home after a delay. + -- @function [parent=#RESCUEHELO] __RTB + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. + + --- On after "RTB" event user function. Called when a the the helo returns to its home base. + -- @function [parent=#RESCUEHELO] OnAfterRTB + -- @param #RESCUEHELO self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base. + + + --- Triggers the FSM event "Returned" after the helo has landed. + -- @function [parent=#RESCUEHELO] Returned + -- @param #RESCUEHELO self + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed. + + --- Triggers the delayed FSM event "Returned" after the helo has landed. + -- @function [parent=#RESCUEHELO] __Returned + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed. + + --- On after "Returned" event user function. Called when a the the helo has landed at an airbase. + -- @function [parent=#RESCUEHELO] OnAfterReturned + -- @param #RESCUEHELO self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed. + + + --- Triggers the FSM event "Run". + -- @function [parent=#RESCUEHELO] Run + -- @param #RESCUEHELO self + + --- Triggers the delayed FSM event "Run". + -- @function [parent=#RESCUEHELO] __Run + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status" that updates the helo status. + -- @function [parent=#RESCUEHELO] Status + -- @param #RESCUEHELO self + + --- Triggers the delayed FSM event "Status" that updates the helo status. + -- @function [parent=#RESCUEHELO] __Status + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Stop" that stops the rescue helo. Event handlers are stopped. + -- @function [parent=#RESCUEHELO] Stop + -- @param #RESCUEHELO self + + --- Triggers the FSM event "Stop" that stops the rescue helo after a delay. Event handlers are stopped. + -- @function [parent=#RESCUEHELO] __Stop + -- @param #RESCUEHELO self + -- @param #number delay Delay in seconds. + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set low fuel state of helo. When fuel is below this threshold, the helo will RTB or be respawned if takeoff type is in air. +-- @param #RESCUEHELO self +-- @param #number threshold Low fuel threshold in percent. Default 5%. +-- @return #RESCUEHELO self +function RESCUEHELO:SetLowFuelThreshold(threshold) + self.lowfuel=threshold or 5 + return self +end + +--- Set home airbase of the helo. This is the airbase where the helo is spawned (if not in air) and will go when it is out of fuel. +-- @param #RESCUEHELO self +-- @param Wrapper.Airbase#AIRBASE airbase The home airbase. Can be the airbase name (passed as a string) or a Moose AIRBASE object. +-- @return #RESCUEHELO self +function RESCUEHELO:SetHomeBase(airbase) + if type(airbase)=="string" then + self.airbase=AIRBASE:FindByName(airbase) + else + self.airbase=airbase + end + if not self.airbase then + self:E(self.lid.."ERROR: Airbase is nil!") + end + return self +end + +--- Set rescue zone radius. Crashed or ejected units inside this radius of the carrier will be rescued if possible. +-- @param #RESCUEHELO self +-- @param #number radius Radius of rescue zone in nautical miles. Default is 15 NM. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueZone(radius) + radius=UTILS.NMToMeters(radius or 15) + self.rescuezone=ZONE_UNIT:New("Rescue Zone", self.carrier, radius) + return self +end + +--- Set rescue hover speed. +-- @param #RESCUEHELO self +-- @param #number speed Speed in knots. Default 5 kts. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueHoverSpeed(speed) + self.rescuespeed=UTILS.KnotsToMps(speed or 5) + return self +end + +--- Set rescue duration. This is the time it takes to rescue a pilot at the crash site. +-- @param #RESCUEHELO self +-- @param #number duration Duration in minutes. Default 5 min. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueDuration(duration) + self.rescueduration=(duration or 5)*60 + return self +end + +--- Activate rescue option. Crashed and ejected pilots will be rescued. This is the default setting. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueOn() + self.rescueon=true + return self +end + +--- Deactivate rescue option. Crashed and ejected pilots will not be rescued. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueOff() + self.rescueon=false + return self +end + +--- Stop carrier during rescue operations. NOT WORKING! +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueStopBoatOn() + self.rescuestopboat=true + return self +end + +--- Do not stop carrier during rescue operations. This is the default setting. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRescueStopBoatOff() + self.rescuestopboat=false + return self +end + + +--- Set takeoff type. +-- @param #RESCUEHELO self +-- @param #number takeofftype Takeoff type. Default SPAWN.Takeoff.Hot. +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoff(takeofftype) + self.takeoff=takeofftype or SPAWN.Takeoff.Hot + return self +end + +--- Set takeoff with engines running (hot). +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffHot() + self:SetTakeoff(SPAWN.Takeoff.Hot) + return self +end + +--- Set takeoff with engines off (cold). +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffCold() + self:SetTakeoff(SPAWN.Takeoff.Cold) + return self +end + +--- Set takeoff in air near the carrier. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetTakeoffAir() + self:SetTakeoff(SPAWN.Takeoff.Air) + return self +end + +--- Set altitude of helo. +-- @param #RESCUEHELO self +-- @param #number alt Altitude in meters. Default 70 m. +-- @return #RESCUEHELO self +function RESCUEHELO:SetAltitude(alt) + self.altitude=alt or 70 + return self +end + +--- Set offset parallel to orientation of carrier. +-- @param #RESCUEHELO self +-- @param #number distance Offset distance in meters. Default 200 m (~660 ft). +-- @return #RESCUEHELO self +function RESCUEHELO:SetOffsetX(distance) + self.offsetX=distance or 200 + return self +end + +--- Set offset perpendicular to orientation to carrier. +-- @param #RESCUEHELO self +-- @param #number distance Offset distance in meters. Default 240 m (~780 ft). +-- @return #RESCUEHELO self +function RESCUEHELO:SetOffsetZ(distance) + self.offsetZ=distance or 240 + return self +end + + +--- Enable respawning of helo. Note that this is the default behaviour. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnOn() + self.respawn=true + return self +end + +--- Disable respawning of helo. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnOff() + self.respawn=false + return self +end + +--- Set whether helo shall be respawned or not. +-- @param #RESCUEHELO self +-- @param #boolean switch If true (or nil), helo will be respawned. If false, helo will not be respawned. +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnOnOff(switch) + if switch==nil or switch==true then + self.respawn=true + else + self.respawn=false + end + return self +end + +--- Helo will be respawned in air, even it was initially spawned on the carrier. +-- So only the first spawn will be on the carrier while all subsequent spawns will happen in air. +-- This allows for undisrupted operations and less problems on the carrier deck. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetRespawnInAir() + self.respawninair=true + return self +end + +--- Set modex (tail number) of the helo. +-- @param #RESCUEHELO self +-- @param #number modex Tail number. +-- @return #RESCUEHELO self +function RESCUEHELO:SetModex(modex) + self.modex=modex + return self +end + +--- Set follow time update interval. +-- @param #RESCUEHELO self +-- @param #number dt Time interval in seconds. Default 1.0 sec. +-- @return #RESCUEHELO self +function RESCUEHELO:SetFollowTimeInterval(dt) + self.dtFollow=dt or 1.0 + return self +end + +--- Use an uncontrolled aircraft already present in the mission rather than spawning a new helo as initial rescue helo. +-- This can be useful when interfaced with, e.g., a warehouse. +-- The group name is the one specified in the @{#RESCUEHELO.New} function. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetUseUncontrolledAircraft() + self.uncontrolledac=true + return self +end + +--- Activate debug mode. Display debug messages on screen. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetDebugModeON() + self.Debug=true + return self +end + +--- Deactivate debug mode. This is also the default setting. +-- @param #RESCUEHELO self +-- @return #RESCUEHELO self +function RESCUEHELO:SetDebugModeOFF() + self.Debug=false + return self +end + +--- Check if helo is returning to base. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is returning to base. +function RESCUEHELO:IsReturning() + return self:is("Returning") +end + +--- Check if helo is operating. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is operating. +function RESCUEHELO:IsRunning() + return self:is("Running") +end + +--- Check if helo is on a rescue mission. +-- @param #RESCUEHELO self +-- @return #boolean If true, helo is rescuing somebody. +function RESCUEHELO:IsRescuing() + return self:is("Rescuing") +end + +--- Check if FMS was stopped. +-- @param #RESCUEHELO self +-- @return #boolean If true, is stopped. +function RESCUEHELO:IsStopped() + return self:is("Stopped") +end + +--- Alias of helo spawn group. +-- @param #RESCUEHELO self +-- @return #string Alias of the helo. +function RESCUEHELO:GetAlias() + return self.alias +end + +--- Get unit name of the spawned helo. +-- @param #RESCUEHELO self +-- @return #string Name of the helo unit or nil if it does not exist. +function RESCUEHELO:GetUnitName() + local unit=self.helo:GetUnit(1) + if unit then + return unit:GetName() + end + return nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- EVENT functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Handle landing event of rescue helo. +-- @param #RESCUEHELO self +-- @param Core.Event#EVENTDATA EventData Event data. +function RESCUEHELO:OnEventLand(EventData) + local group=EventData.IniGroup --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + -- Group name that landed. + local groupname=group:GetName() + + -- Check that it was our helo that landed. + if groupname==self.helo:GetName() then + + local airbase=nil --Wrapper.Airbase#AIRBASE + local airbasename="unknown" + if EventData.Place then + airbase=EventData.Place + airbasename=airbase:GetName() + end + + -- Respawn the Helo. + local text=string.format("Rescue helo group %s landed at airbase %s.", groupname, airbasename) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Helo has rescued someone. + -- TODO: Add "Rescued" event. + if self:IsRescuing() then + self:T(self.lid..string.format("Rescue helo %s returned from rescue operation.", groupname)) + end + + -- Check if takeoff air or respawn in air is set. Landing event should not happen unless the helo was on a rescue mission. + if self.takeoff==SPAWN.Takeoff.Air or self.respawninair then + + if not self:IsRescuing() then + + self:E(self.lid..string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true and no rescue operation in progress.", groupname)) + + end + end + + -- Trigger returned event. Respawn at current airbase. + self:__Returned(3, airbase) + + end + end +end + +--- A unit crashed or a player ejected. +-- @param #RESCUEHELO self +-- @param Core.Event#EVENTDATA EventData Event data. +function RESCUEHELO:_OnEventCrashOrEject(EventData) + self:F2({eventdata=EventData}) + + -- NOTE: Careful here. Eject and crash events will probably happen for the same unit! + + -- Check that there is an initiating unit in the event data. + if EventData and EventData.IniUnit then + + -- Crashed or ejected unit. + local unit=EventData.IniUnit + local unitname=tostring(EventData.IniUnitName) + + -- Check that it was not the rescue helo itself that crashed. + if EventData.IniGroupName~=self.helo:GetName() then + + -- Debug. + local text=string.format("Unit %s crashed or ejected.", unitname) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:I(self.lid..text) + + -- Get coordinate of unit. + local coord=unit:GetCoordinate() + + if coord and self.rescuezone:IsCoordinateInZone(coord) then + + -- This does not seem to work any more. Is:Alive returns flase on ejection. + -- Unit "alive" and in our rescue zone. + --if unit:IsAlive() and unit:IsInZone(self.rescuezone) then + -- Get coordinate of crashed unit. + --local coord=unit:GetCoordinate() + + -- Debug mark on map. + if self.Debug then + coord:MarkToCoalition(self.lid..string.format("Crash site of unit %s.", unitname), self.helo:GetCoalition()) + end + + -- Check that coalition is the same. + local rightcoalition=EventData.IniGroup:GetCoalition()==self.helo:GetCoalition() + + -- Only rescue if helo is "running" and not, e.g., rescuing already. + if self:IsRunning() and self.rescueon and rightcoalition then + self:Rescue(coord) + end + + end + + else + + -- Error message. + self:E(self.lid..string.format("Rescue helo %s crashed!", unitname)) + + -- Stop FSM. + self:Stop() + + -- Restart. + if self.respawn then + self:__Start(5) + end + + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM states +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue. +-- @param #RESCUEHELO self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RESCUEHELO:onafterStart(From, Event, To) + + -- Events are handled my MOOSE. + local text=string.format("Starting Rescue Helo Formation v%s for carrier unit %s of type %s.", RESCUEHELO.version, self.carrier:GetName(), self.carriertype) + self:I(self.lid..text) + + -- Handle events. + self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrEject) + self:HandleEvent(EVENTS.Ejection, self._OnEventCrashOrEject) + + -- Delay before formation is started. + local delay=120 + + -- Spawn helo. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine. + local Spawn=SPAWN:NewWithAlias(self.helogroupname, self.alias) + + -- Set modex for spawn. + Spawn:InitModex(self.modex) + + -- Spawn in air or at airbase. + if self.takeoff==SPAWN.Takeoff.Air then + + -- Carrier heading + local hdg=self.carrier:GetHeading() + + -- Spawn distance in front of carrier. + local dist=UTILS.NMToMeters(0.2) + + -- Coordinate behind the carrier. Altitude at least 100 meters for spawning because it drops down a bit. + local Carrier=self.carrier:GetCoordinate():Translate(dist, hdg):SetAltitude(math.max(100, self.altitude)) + + -- Orientation of spawned group. + Spawn:InitHeading(hdg) + + -- Spawn at coordinate. + self.helo=Spawn:SpawnFromCoordinate(Carrier) + + -- Start formation in 1 seconds + delay=1 + + else + + -- Check if an uncontrolled helo group was requested. + if self.uncontrolledac then + + -- Use an uncontrolled aircraft group. + self.helo=GROUP:FindByName(self.helogroupname) + + if self.helo and self.helo:IsAlive() then + + -- Start uncontrolled group. + self.helo:StartUncontrolled() + + -- Delay before formation is started. + delay=60 + + else + -- No group of that name! + self:E(string.format("ERROR: No uncontrolled (alive) rescue helo group with name %s could be found!", self.helogroupname)) + return + end + + else + + -- Spawn at airbase. + self.helo=Spawn:SpawnAtAirbase(self.airbase, self.takeoff, nil, AIRBASE.TerminalType.HelicopterUsable) + + -- Delay before formation is started. + if self.takeoff==SPAWN.Takeoff.Runway then + delay=5 + elseif self.takeoff==SPAWN.Takeoff.Hot then + delay=30 + elseif self.takeoff==SPAWN.Takeoff.Cold then + delay=60 + end + + end + + end + + -- Set of group(s) to follow Mother. + self.followset=SET_GROUP:New() + self.followset:AddGroup(self.helo) + + -- Get initial fuel. + self.HeloFuel0=self.helo:GetFuel() + + -- Define AI Formation object. + self.formation=AI_FORMATION:New(self.carrier, self.followset, "Helo Formation with Carrier", "Follow Carrier at given parameters.") + + -- Formation parameters. + self.formation:FormationCenterWing(-self.offsetX, 50, math.abs(self.altitude), 50, self.offsetZ, 50) + + -- Set follow time interval. + self.formation:SetFollowTimeInterval(self.dtFollow) + + -- Formation mode. + self.formation:SetFlightModeFormation(self.helo) + + -- Start formation FSM. + self.formation:__Start(delay) + + -- Init status check + self:__Status(1) +end + +--- On after Status event. Checks player status. +-- @param #RESCUEHELO self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function RESCUEHELO:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + -- Check if helo is running and not RTBing already or rescuing. + if self.helo and self.helo:IsAlive() then + + ------------------- + -- HELO is ALIVE -- + ------------------- + + -- Get (relative) fuel wrt to initial fuel of helo (DCS bug https://forums.eagle.ru/showthread.php?t=223712) + local fuel=self.helo:GetFuel()*100 + local fuelrel=fuel/self.HeloFuel0 + local life=self.helo:GetUnit(1):GetLife() + local life0=self.helo:GetUnit(1):GetLife0() + local lifeR=self.helo:GetUnit(1):GetLifeRelative() + + -- Report current fuel. + local text=string.format("Rescue Helo %s: state=%s fuel=%.1f, rel.fuel=%.1f, life=%.1f/%.1f=%d", self.helo:GetName(), self:GetState(), fuel, fuelrel, life, life0, lifeR*100) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + if self:IsRunning() then + + -- Check if fuel is low. + if fuel Coordinates @@ -153,7 +162,7 @@ end -- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. -- @return #TASKINFO self function TASKINFO:AddThreat( ThreatText, ThreatLevel, Order, Detail, Keep ) - self:AddInfo( "Threat", ThreatText .. " [" .. string.rep( "â– ", ThreatLevel ) .. string.rep( "â–¡", 10 - ThreatLevel ) .. "]", Order, Detail, Keep ) + self:AddInfo( "Threat", " [" .. string.rep( "â– ", ThreatLevel ) .. string.rep( "â–¡", 10 - ThreatLevel ) .. "]:" .. ThreatText, Order, Detail, Keep ) return self end @@ -297,75 +306,63 @@ function TASKINFO:Report( Report, Detail, ReportGroup, Task ) for Key, Data in UTILS.spairs( self.Info.Set, function( t, a, b ) return t[a].Order < t[b].Order end ) do - self:F( { Key = Key, Detail = Detail, Data = Data } ) - if Data.Detail:find( Detail ) then local Text = "" - if Key == "TaskName" then + local ShowKey = ( Data.ShowKey == nil or Data.ShowKey == true ) + if Key == "TaskName" then Key = nil Text = Data.Data - end - if Key == "Coordinate" then + elseif Data.Type and Data.Type == "Coordinate" then local Coordinate = Data.Data -- Core.Point#COORDINATE Text = Coordinate:ToString( ReportGroup:GetUnit(1), nil, Task ) - end - if Key == "Threat" then + elseif Key == "Threat" then local DataText = Data.Data -- #string Text = DataText - end - if Key == "Counting" then + elseif Key == "Counting" then local DataText = Data.Data -- #string Text = DataText - end - if Key == "Targets" then + elseif Key == "Targets" then local DataText = Data.Data -- #string Text = DataText - end - if Key == "QFE" then + elseif Key == "QFE" then local Coordinate = Data.Data -- Core.Point#COORDINATE Text = Coordinate:ToStringPressure( ReportGroup:GetUnit(1), nil, Task ) - end - if Key == "Temperature" then + elseif Key == "Temperature" then local Coordinate = Data.Data -- Core.Point#COORDINATE Text = Coordinate:ToStringTemperature( ReportGroup:GetUnit(1), nil, Task ) - end - if Key == "Wind" then + elseif Key == "Wind" then local Coordinate = Data.Data -- Core.Point#COORDINATE Text = Coordinate:ToStringWind( ReportGroup:GetUnit(1), nil, Task ) - end - if Key == "Cargo" then + elseif Key == "Cargo" then + local DataText = Data.Data -- #string + Text = DataText + elseif Key == "Friendlies" then + local DataText = Data.Data -- #string + Text = DataText + elseif Key == "Players" then + local DataText = Data.Data -- #string + Text = DataText + else local DataText = Data.Data -- #string Text = DataText end - if Key == "Friendlies" then - local DataText = Data.Data -- #string - Text = DataText - end - if Key == "Players" then - local DataText = Data.Data -- #string - Text = DataText - end - if Line < math.floor( Data.Order / 10 ) then if Line == 0 then - if Text ~= "" then - Report:AddIndent( LineReport:Text( ", " ), "-" ) - end + Report:AddIndent( LineReport:Text( ", " ), "-" ) else - if Text ~= "" then - Report:AddIndent( LineReport:Text( ", " ) ) - end + Report:AddIndent( LineReport:Text( ", " ) ) end LineReport = REPORT:New() Line = math.floor( Data.Order / 10 ) end if Text ~= "" then - LineReport:Add( ( Key and ( Key .. ":" ) or "" ) .. Text ) + LineReport:Add( ( ( Key and ShowKey == true ) and ( Key .. ": " ) or "" ) .. Text ) end + end end + Report:AddIndent( LineReport:Text( ", " ) ) - end diff --git a/Moose Development/Moose/Tasking/Task_A2A.lua b/Moose Development/Moose/Tasking/Task_A2A.lua index bd12b7e43..2da9be005 100644 --- a/Moose Development/Moose/Tasking/Task_A2A.lua +++ b/Moose Development/Moose/Tasking/Task_A2A.lua @@ -292,7 +292,9 @@ do -- TASK_A2A --- Return the relative distance to the target vicinity from the player, in order to sort the targets in the reports per distance from the threats. -- @param #TASK_A2A self - function TASK_A2A:ReportOrder( ReportGroup ) + function TASK_A2A:ReportOrder( ReportGroup ) + self:UpdateTaskInfo( self.DetectedItem ) + local Coordinate = self.TaskInfo:GetData( "Coordinate" ) local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) @@ -351,6 +353,26 @@ do -- TASK_A2A end end + --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection. + -- @param #TASK_A2A self + -- @param #number AutoAssignMethod The method to be applied to the task. + -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center. + -- @param Wrapper.Group#GROUP TaskGroup The player group. + function TASK_A2A:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup ) + + if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then + return math.random( 1, 9 ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then + local Coordinate = self.TaskInfo:GetData( "Coordinate" ) + local Distance = Coordinate:Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) + return math.floor( Distance ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then + return 1 + end + + return 0 + end + end diff --git a/Moose Development/Moose/Tasking/Task_A2G.lua b/Moose Development/Moose/Tasking/Task_A2G.lua index 634234654..f4ccf40d6 100644 --- a/Moose Development/Moose/Tasking/Task_A2G.lua +++ b/Moose Development/Moose/Tasking/Task_A2G.lua @@ -295,6 +295,8 @@ do -- TASK_A2G --- Return the relative distance to the target vicinity from the player, in order to sort the targets in the reports per distance from the threats. -- @param #TASK_A2G self function TASK_A2G:ReportOrder( ReportGroup ) + self:UpdateTaskInfo( self.DetectedItem ) + local Coordinate = self.TaskInfo:GetData( "Coordinate" ) local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) @@ -355,6 +357,27 @@ do -- TASK_A2G end end + + --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection. + -- @param #TASK_A2G self + -- @param #number AutoAssignMethod The method to be applied to the task. + -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center. + -- @param Wrapper.Group#GROUP TaskGroup The player group. + function TASK_A2G:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup ) + + if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then + return math.random( 1, 9 ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then + local Coordinate = self.TaskInfo:GetData( "Coordinate" ) + local Distance = Coordinate:Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) + self:F({Distance=Distance}) + return math.floor( Distance ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then + return 1 + end + + return 0 + end end diff --git a/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua index 499efcf89..b7ae6ba17 100644 --- a/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua @@ -633,7 +633,7 @@ do -- TASK_A2G_DISPATCHER --DetectedSet:Flush( self ) local DetectedItemID = DetectedItem.ID - local TaskIndex = DetectedItem.ID + local TaskIndex = DetectedItem.Index local DetectedItemChanged = DetectedItem.Changed self:F( { DetectedItemChanged = DetectedItemChanged, DetectedItemID = DetectedItemID, TaskIndex = TaskIndex } ) @@ -649,6 +649,7 @@ do -- TASK_A2G_DISPATCHER if TargetSetUnit then if Task:IsInstanceOf( TASK_A2G_SEAD ) then Task:SetTargetSetUnit( TargetSetUnit ) + Task:SetDetection( Detection, DetectedItem ) Task:UpdateTaskInfo( DetectedItem ) TargetsReport:Add( Detection:GetChangeText( DetectedItem ) ) else @@ -659,7 +660,7 @@ do -- TASK_A2G_DISPATCHER if TargetSetUnit then if Task:IsInstanceOf( TASK_A2G_CAS ) then Task:SetTargetSetUnit( TargetSetUnit ) - Task:SetDetection( Detection, TaskIndex ) + Task:SetDetection( Detection, DetectedItem ) Task:UpdateTaskInfo( DetectedItem ) TargetsReport:Add( Detection:GetChangeText( DetectedItem ) ) else @@ -671,7 +672,7 @@ do -- TASK_A2G_DISPATCHER if TargetSetUnit then if Task:IsInstanceOf( TASK_A2G_BAI ) then Task:SetTargetSetUnit( TargetSetUnit ) - Task:SetDetection( Detection, TaskIndex ) + Task:SetDetection( Detection, DetectedItem ) Task:UpdateTaskInfo( DetectedItem ) TargetsReport:Add( Detection:GetChangeText( DetectedItem ) ) else diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua index 97a067220..b327ab6a3 100644 --- a/Moose Development/Moose/Tasking/Task_CARGO.lua +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -1381,6 +1381,23 @@ do -- TASK_CARGO return 0 end + + --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection. + -- @param #TASK_CARGO self + -- @param #number AutoAssignMethod The method to be applied to the task. + -- @param Wrapper.Group#GROUP TaskGroup The player group. + function TASK_CARGO:GetAutoAssignPriority( AutoAssignMethod, TaskGroup ) + + if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then + return math.random( 1, 9 ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then + return 0 + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then + return 1 + end + + return 0 + end diff --git a/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua new file mode 100644 index 000000000..0c05ba18b --- /dev/null +++ b/Moose Development/Moose/Tasking/Task_Capture_Dispatcher.lua @@ -0,0 +1,396 @@ +--- **Tasking** - Creates and manages player TASK_ZONE_CAPTURE tasks. +-- +-- The **TASK_CAPTURE_DISPATCHER** allows you to setup various tasks for let human +-- players capture zones in a co-operation effort. +-- +-- The dispatcher will implement for you mechanisms to create capture zone tasks: +-- +-- * As setup by the mission designer. +-- * Dynamically capture zone tasks. +-- +-- +-- +-- **Specific features:** +-- +-- * Creates a task to capture zones and achieve mission goals. +-- * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled. +-- * Co-operation tasking, so a player joins a group of players executing the same task. +-- +-- +-- **A complete task menu system to allow players to:** +-- +-- * Join the task, abort the task. +-- * Mark the location of the zones to capture on the map. +-- * Provide details of the zones. +-- * Route to the zones. +-- * Display the task briefing. +-- +-- +-- **A complete mission menu system to allow players to:** +-- +-- * Join a task, abort the task. +-- * Display task reports. +-- * Display mission statistics. +-- * Mark the task locations on the map. +-- * Provide details of the zones. +-- * Display the mission briefing. +-- * Provide status updates as retrieved from the command center. +-- * Automatically assign a random task as part of a mission. +-- * Manually assign a specific task as part of a mission. +-- +-- +-- **A settings system, using the settings menu:** +-- +-- * Tweak the duration of the display of messages. +-- * Switch between metric and imperial measurement system. +-- * Switch between coordinate formats used in messages: BR, BRA, LL DMS, LL DDM, MGRS. +-- * Various other options. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.Task_Zone_Capture_Dispatcher +-- @image MOOSE.JPG + +do -- TASK_CAPTURE_DISPATCHER + + --- TASK_CAPTURE_DISPATCHER class. + -- @type TASK_CAPTURE_DISPATCHER + -- @extends Tasking.Task_Manager#TASK_MANAGER + -- @field TASK_CAPTURE_DISPATCHER.ZONE ZONE + + --- @type TASK_CAPTURE_DISPATCHER.CSAR + -- @field Wrapper.Unit#UNIT PilotUnit + -- @field Tasking.Task#TASK Task + + + --- Implements the dynamic dispatching of capture zone tasks. + -- + -- The **TASK_CAPTURE_DISPATCHER** allows you to setup various tasks for let human + -- players capture zones in a co-operation effort. + -- + -- Let's explore **step by step** how to setup the task capture zone dispatcher. + -- + -- # 1. Setup a mission environment. + -- + -- It is easy, as it works just like any other task setup, so setup a command center and a mission. + -- + -- ## 1.1. Create a command center. + -- + -- First you need to create a command center using the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor. + -- The command assumes that you´ve setup a group in the mission editor with the name HQ. + -- This group will act as the command center object. + -- It is a good practice to mark this group as invisible and invulnerable. + -- + -- local CommandCenter = COMMANDCENTER + -- :New( GROUP:FindByName( "HQ" ), "HQ" ) -- Create the CommandCenter. + -- + -- ## 1.2. Create a mission. + -- + -- Tasks work in a **mission**, which groups these tasks to achieve a joint **mission goal**. A command center can **govern multiple missions**. + -- + -- Create a new mission, using the @{Tasking.Mission#MISSION.New}() constructor. + -- + -- -- Declare the Mission for the Command Center. + -- local Mission = MISSION + -- :New( CommandCenter, + -- "Overlord", + -- "High", + -- "Capture the blue zones.", + -- coalition.side.RED + -- ) + -- + -- + -- # 2. Dispatch a **capture zone** task. + -- + -- So, now that we have a command center and a mission, we now create the capture zone task. + -- We create the capture zone task using the @{#TASK_CAPTURE_DISPATCHER.AddCaptureZoneTask}() constructor. + -- + -- ## 2.1. Create the capture zones. + -- + -- Because a capture zone task will not generate the capture zones, you'll need to create them first. + -- + -- + -- -- We define here a capture zone; of the type ZONE_CAPTURE_COALITION. + -- -- The zone to be captured has the name Alpha, and was defined in the mission editor as a trigger zone. + -- CaptureZone = ZONE:New( "Alpha" ) + -- CaptureZoneCoalitionApha = ZONE_CAPTURE_COALITION:New( CaptureZone, coalition.side.RED ) + -- + -- ## 2.2. Create a set of player groups. + -- + -- What is also needed, is to have a set of @{Core.Group}s defined that contains the clients of the players. + -- + -- -- Allocate the player slots, which must be aircraft (airplanes or helicopters), that can be manned by players. + -- -- We use the method FilterPrefixes to filter those player groups that have client slots, as defined in the mission editor. + -- -- In this example, we filter the groups where the name starts with "Blue Player", which captures the blue player slots. + -- local PlayerGroupSet = SET_GROUP:New():FilterPrefixes( "Blue Player" ):FilterStart() + -- + -- ## 2.3. Setup the capture zone task. + -- + -- First, we need to create a TASK_CAPTURE_DISPATCHER object. + -- + -- TaskCaptureZoneDispatcher = TASK_CAPTURE_DISPATCHER:New( Mission, PilotGroupSet ) + -- + -- So, the variable `TaskCaptureZoneDispatcher` will contain the object of class TASK_CAPTURE_DISPATCHER, + -- which will allow you to dispatch capture zone tasks: + -- + -- * for mission `Mission`, as was defined in section 1.2. + -- * for the group set `PilotGroupSet`, as was defined in section 2.2. + -- + -- Now that we have `TaskDispatcher` object, we can now **create the TaskCaptureZone**, using the @{#TASK_CAPTURE_DISPATCHER.AddCaptureZoneTask}() method! + -- + -- local TaskCaptureZone = TaskCaptureZoneDispatcher:AddCaptureZoneTask( + -- "Capture zone Alpha", + -- CaptureZoneCoalitionAlpha, + -- "Fly to zone Alpha and eliminate all enemy forces to capture it." ) + -- + -- As a result of this code, the `TaskCaptureZone` (returned) variable will contain an object of @{#TASK_CAPTURE_ZONE}! + -- We pass to the method the title of the task, and the `CaptureZoneCoalitionAlpha`, which is the zone to be captured, as defined in section 2.1! + -- This returned `TaskCaptureZone` object can now be used to setup additional task configurations, or to control this specific task with special events. + -- + -- And you're done! As you can see, it is a small bit of work, but the reward is great. + -- And, because all this is done using program interfaces, you can easily build a mission to capture zones yourself! + -- Based on various events happening within your mission, you can use the above methods to create new capture zones, + -- and setup a new capture zone task and assign it to a group of players, while your mission is running! + -- + -- + -- + -- @field #TASK_CAPTURE_DISPATCHER + TASK_CAPTURE_DISPATCHER = { + ClassName = "TASK_CAPTURE_DISPATCHER", + Mission = nil, + Tasks = {}, + Zones = {}, + ZoneCount = 0, + } + + + + TASK_CAPTURE_DISPATCHER.AI_A2G_Dispatcher = nil -- AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER + + --- TASK_CAPTURE_DISPATCHER constructor. + -- @param #TASK_CAPTURE_DISPATCHER self + -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done. + -- @param Core.Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission. + -- @return #TASK_CAPTURE_DISPATCHER self + function TASK_CAPTURE_DISPATCHER:New( Mission, SetGroup ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, TASK_MANAGER:New( SetGroup ) ) -- #TASK_CAPTURE_DISPATCHER + + self.Mission = Mission + + self:AddTransition( "Started", "Assign", "Started" ) + self:AddTransition( "Started", "ZoneCaptured", "Started" ) + + self:__StartTasks( 5 ) + + return self + end + + + --- Link a task capture dispatcher from the other coalition to understand its plan for defenses. + -- This is used for the tactical overview, so the players also know the zones attacked by the other coalition! + -- @param #TASK_CAPTURE_DISPATCHER self + -- @param #TASK_CAPTURE_DISPATCHER DefenseTaskCaptureDispatcher + function TASK_CAPTURE_DISPATCHER:SetDefenseTaskCaptureDispatcher( DefenseTaskCaptureDispatcher ) + + self.DefenseTaskCaptureDispatcher = DefenseTaskCaptureDispatcher + end + + + --- Get the linked task capture dispatcher from the other coalition to understand its plan for defenses. + -- This is used for the tactical overview, so the players also know the zones attacked by the other coalition! + -- @param #TASK_CAPTURE_DISPATCHER self + -- @return #TASK_CAPTURE_DISPATCHER + function TASK_CAPTURE_DISPATCHER:GetDefenseTaskCaptureDispatcher() + + return self.DefenseTaskCaptureDispatcher + end + + + --- Link an AI A2G dispatcher from the other coalition to understand its plan for defenses. + -- This is used for the tactical overview, so the players also know the zones attacked by the other AI A2G dispatcher! + -- @param #TASK_CAPTURE_DISPATCHER self + -- @param AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER DefenseAIA2GDispatcher + function TASK_CAPTURE_DISPATCHER:SetDefenseAIA2GDispatcher( DefenseAIA2GDispatcher ) + + self.DefenseAIA2GDispatcher = DefenseAIA2GDispatcher + end + + + --- Get the linked AI A2G dispatcher from the other coalition to understand its plan for defenses. + -- This is used for the tactical overview, so the players also know the zones attacked by the AI A2G dispatcher! + -- @param #TASK_CAPTURE_DISPATCHER self + -- @return AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER + function TASK_CAPTURE_DISPATCHER:GetDefenseAIA2GDispatcher() + + return self.DefenseAIA2GDispatcher + end + + + --- Add a capture zone task. + -- @param #TASK_CAPTURE_DISPATCHER self + -- @param #string TaskPrefix (optional) The prefix of the capture zone task. + -- If no TaskPrefix is given, then "Capture" will be used as the TaskPrefix. + -- The TaskPrefix will be appended with a . + a number of 3 digits, if the TaskPrefix already exists in the task collection. + -- @param Functional.CaptureZoneCoalition#ZONE_CAPTURE_COALITION CaptureZone The zone of the coalition to be captured as the task goal. + -- @param #string Briefing The briefing of the task to be shown to the player. + -- @return Tasking.Task_Capture_Zone#TASK_CAPTURE_ZONE + -- @usage + -- + -- + function TASK_CAPTURE_DISPATCHER:AddCaptureZoneTask( TaskPrefix, CaptureZone, Briefing ) + + local TaskName = TaskPrefix or "Capture" + if self.Zones[TaskName] then + self.ZoneCount = self.ZoneCount + 1 + TaskName = string.format( "%s.%03d", TaskName, self.ZoneCount ) + end + + self.Zones[TaskName] = {} + self.Zones[TaskName].CaptureZone = CaptureZone + self.Zones[TaskName].Briefing = Briefing + self.Zones[TaskName].Task = nil + self.Zones[TaskName].TaskPrefix = TaskPrefix + + self:ManageTasks() + + return self.Zones[TaskName] and self.Zones[TaskName].Task + end + + + --- Link an AI_A2G_DISPATCHER to the TASK_CAPTURE_DISPATCHER. + -- @param #TASK_CAPTURE_DISPATCHER self + -- @param AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER AI_A2G_Dispatcher The AI Dispatcher to be linked to the tasking. + -- @return Tasking.Task_Capture_Zone#TASK_CAPTURE_ZONE + function TASK_CAPTURE_DISPATCHER:Link_AI_A2G_Dispatcher( AI_A2G_Dispatcher ) + + self.AI_A2G_Dispatcher = AI_A2G_Dispatcher -- AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER + AI_A2G_Dispatcher.Detection:LockDetectedItems() + + return self + end + + + --- Assigns tasks to the @{Core.Set#SET_GROUP}. + -- @param #TASK_CAPTURE_DISPATCHER self + -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. + function TASK_CAPTURE_DISPATCHER:ManageTasks() + self:F() + + local AreaMsg = {} + local TaskMsg = {} + local ChangeMsg = {} + + local Mission = self.Mission + + if Mission:IsIDLE() or Mission:IsENGAGED() then + + local TaskReport = REPORT:New() + + -- Checking the task queue for the dispatcher, and removing any obsolete task! + for TaskIndex, TaskData in pairs( self.Tasks ) do + local Task = TaskData -- Tasking.Task#TASK + if Task:IsStatePlanned() then + -- Here we need to check if the pilot is still existing. +-- Task = self:RemoveTask( TaskIndex ) + end + + end + + -- Now that all obsolete tasks are removed, loop through the Zone tasks. + for TaskName, CaptureZone in pairs( self.Zones ) do + + if not CaptureZone.Task then + -- New Transport Task + CaptureZone.Task = TASK_CAPTURE_ZONE:New( Mission, self.SetGroup, TaskName, CaptureZone.CaptureZone, CaptureZone.Briefing ) + CaptureZone.Task.TaskPrefix = CaptureZone.TaskPrefix -- We keep the TaskPrefix for further reference! + Mission:AddTask( CaptureZone.Task ) + TaskReport:Add( TaskName ) + + -- Link the Task Dispatcher to the capture zone task, because it is used on the UpdateTaskInfo. + CaptureZone.Task:SetDispatcher( self ) + CaptureZone.Task:UpdateTaskInfo() + + function CaptureZone.Task.OnEnterAssigned( Task, From, Event, To ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Unlock( Task.TaskZoneName ) -- This will unlock the zone to be defended by AI. + end + CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = true + end + + function CaptureZone.Task.OnEnterSuccess( Task, From, Event, To ) + --self:Success( Task ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + end + CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = false + end + + function CaptureZone.Task.OnEnterCancelled( Task, From, Event, To ) + self:Cancelled( Task ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + end + CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = false + end + + function CaptureZone.Task.OnEnterFailed( Task, From, Event, To ) + self:Failed( Task ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + end + CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = false + end + + function CaptureZone.Task.OnEnterAborted( Task, From, Event, To ) + self:Aborted( Task ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + end + CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = false + end + + -- Now broadcast the onafterCargoPickedUp event to the Task Cargo Dispatcher. + function CaptureZone.Task.OnAfterCaptured( Task, From, Event, To, TaskUnit ) + self:Captured( Task, Task.TaskPrefix, TaskUnit ) + if self.AI_A2G_Dispatcher then + self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI. + end + CaptureZone.Task:UpdateTaskInfo() + CaptureZone.Task.ZoneGoal.Attacked = false + end + + end + + end + + + -- TODO set menus using the HQ coordinator + Mission:GetCommandCenter():SetMenu() + + local TaskText = TaskReport:Text(", ") + + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" then + Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup ) + end + end + + end + + return true + end + +end diff --git a/Moose Development/Moose/Tasking/TaskZoneCapture.lua b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua similarity index 58% rename from Moose Development/Moose/Tasking/TaskZoneCapture.lua rename to Moose Development/Moose/Tasking/Task_Capture_Zone.lua index fb1ab81c5..3ca128b98 100644 --- a/Moose Development/Moose/Tasking/TaskZoneCapture.lua +++ b/Moose Development/Moose/Tasking/Task_Capture_Zone.lua @@ -47,7 +47,7 @@ do -- TASK_ZONE_GOAL -- @param Tasking.Mission#MISSION Mission -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. -- @param #string TaskName The name of the Task. - -- @param Core.ZoneGoal#ZONE_GOAL ZoneGoal + -- @param Core.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoal -- @return #TASK_ZONE_GOAL self function TASK_ZONE_GOAL:New( Mission, SetGroup, TaskName, ZoneGoal, TaskType, TaskBriefing ) local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType, TaskBriefing ) ) -- #TASK_ZONE_GOAL @@ -59,19 +59,25 @@ do -- TASK_ZONE_GOAL local Fsm = self:GetUnitProcess() - Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "StartMonitoring", Rejected = "Reject" } ) - Fsm:AddTransition( "Assigned", "StartMonitoring", "Monitoring" ) Fsm:AddTransition( "Monitoring", "Monitor", "Monitoring", {} ) - Fsm:AddTransition( "Monitoring", "RouteTo", "Monitoring" ) Fsm:AddProcess( "Monitoring", "RouteToZone", ACT_ROUTE_ZONE:New(), {} ) - --Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" ) - --Fsm:AddTransition( "Accounted", "Success", "Success" ) Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) Fsm:AddTransition( "Failed", "Fail", "Failed" ) self:SetTargetZone( self.ZoneGoal:GetZone() ) + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task#TASK Task + function Fsm:OnAfterAssigned( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + self:__StartMonitoring( 0.1 ) + self:__RouteToZone( 0.1 ) + end --- Test -- @param #FSM_PROCESS self @@ -80,7 +86,6 @@ do -- TASK_ZONE_GOAL function Fsm:onafterStartMonitoring( TaskUnit, Task ) self:F( { self } ) self:__Monitor( 0.1 ) - self:__RouteTo( 0.1 ) end --- Monitor Loop @@ -101,7 +106,7 @@ do -- TASK_ZONE_GOAL -- Determine the first Unit from the self.TargetSetUnit if Task:GetTargetZone( TaskUnit ) then - self:__RouteTo( 0.1 ) + self:__RouteToZone( 0.1 ) end end @@ -160,37 +165,37 @@ do -- TASK_ZONE_GOAL end -do -- TASK_ZONE_CAPTURE +do -- TASK_CAPTURE_ZONE - --- The TASK_ZONE_CAPTURE class - -- @type TASK_ZONE_CAPTURE + --- The TASK_CAPTURE_ZONE class + -- @type TASK_CAPTURE_ZONE -- @field Core.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoal -- @extends #TASK_ZONE_GOAL - --- # TASK_ZONE_CAPTURE class, extends @{Tasking.TaskZoneGoal#TASK_ZONE_GOAL} + --- # TASK_CAPTURE_ZONE class, extends @{Tasking.TaskZoneGoal#TASK_ZONE_GOAL} -- - -- The TASK_ZONE_CAPTURE class defines an Suppression or Extermination of Air Defenses task for a human player to be executed. + -- The TASK_CAPTURE_ZONE class defines an Suppression or Extermination of Air Defenses task for a human player to be executed. -- These tasks are important to be executed as they will help to achieve air superiority at the vicinity. -- - -- The TASK_ZONE_CAPTURE is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create SEAD tasks + -- The TASK_CAPTURE_ZONE is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create SEAD tasks -- based on detected enemy ground targets. -- - -- @field #TASK_ZONE_CAPTURE - TASK_ZONE_CAPTURE = { - ClassName = "TASK_ZONE_CAPTURE", + -- @field #TASK_CAPTURE_ZONE + TASK_CAPTURE_ZONE = { + ClassName = "TASK_CAPTURE_ZONE", } - --- Instantiates a new TASK_ZONE_CAPTURE. - -- @param #TASK_ZONE_CAPTURE self + --- Instantiates a new TASK_CAPTURE_ZONE. + -- @param #TASK_CAPTURE_ZONE self -- @param Tasking.Mission#MISSION Mission -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. -- @param #string TaskName The name of the Task. -- @param Core.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoalCoalition -- @param #string TaskBriefing The briefing of the task. - -- @return #TASK_ZONE_CAPTURE self - function TASK_ZONE_CAPTURE:New( Mission, SetGroup, TaskName, ZoneGoalCoalition, TaskBriefing) - local self = BASE:Inherit( self, TASK_ZONE_GOAL:New( Mission, SetGroup, TaskName, ZoneGoalCoalition, "CAPTURE", TaskBriefing ) ) -- #TASK_ZONE_CAPTURE + -- @return #TASK_CAPTURE_ZONE self + function TASK_CAPTURE_ZONE:New( Mission, SetGroup, TaskName, ZoneGoalCoalition, TaskBriefing) + local self = BASE:Inherit( self, TASK_ZONE_GOAL:New( Mission, SetGroup, TaskName, ZoneGoalCoalition, "CAPTURE", TaskBriefing ) ) -- #TASK_CAPTURE_ZONE self:F() Mission:AddTask( self ) @@ -206,42 +211,84 @@ do -- TASK_ZONE_CAPTURE "Capture Zone " .. self.TaskZoneName ) - self:UpdateTaskInfo() + self:UpdateTaskInfo( true ) + + self:SetGoal( self.ZoneGoal.Goal ) return self end - --- Instantiates a new TASK_ZONE_CAPTURE. - -- @param #TASK_ZONE_CAPTURE self - function TASK_ZONE_CAPTURE:UpdateTaskInfo() - - + --- Instantiates a new TASK_CAPTURE_ZONE. + -- @param #TASK_CAPTURE_ZONE self + function TASK_CAPTURE_ZONE:UpdateTaskInfo( Persist ) + + Persist = Persist or false + local ZoneCoordinate = self.ZoneGoal:GetZone():GetCoordinate() - self.TaskInfo:AddCoordinate( ZoneCoordinate, 0, "SOD" ) - self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD" ) - self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD" ) + self.TaskInfo:AddTaskName( 0, "MSOD", Persist ) + self.TaskInfo:AddCoordinate( ZoneCoordinate, 1, "SOD", Persist ) +-- self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD", Persist ) +-- self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD", Persist ) + local SetUnit = self.ZoneGoal:GetScannedSetUnit() + local ThreatLevel, ThreatText = SetUnit:CalculateThreatLevelA2G() + local ThreatCount = SetUnit:Count() + self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 20, "MOD", Persist ) + self.TaskInfo:AddInfo( "Remaining Units", ThreatCount, 21, "MOD", Persist, true) + + if self.Dispatcher then + local DefenseTaskCaptureDispatcher = self.Dispatcher:GetDefenseTaskCaptureDispatcher() -- Tasking.Task_Capture_Dispatcher#TASK_CAPTURE_DISPATCHER + + if DefenseTaskCaptureDispatcher then + -- Loop through all zones of the player Defenses, and check which zone has an assigned task! + -- The Zones collection contains a Task. This Task is checked if it is assigned. + -- If Assigned, then this task will be the task that is the closest to the defense zone. + for TaskName, CaptureZone in pairs( DefenseTaskCaptureDispatcher.Zones or {} ) do + local Task = CaptureZone.Task -- Tasking.Task_Capture_Zone#TASK_CAPTURE_ZONE + if Task and Task:IsStateAssigned() then -- We also check assigned. + -- Now we register the defense player zone information to the task report. + self.TaskInfo:AddInfo( "Defense Player Zone", Task.ZoneGoal:GetName(), 30, "MOD", Persist ) + self.TaskInfo:AddCoordinate( Task.ZoneGoal:GetZone():GetCoordinate(), 31, "MOD", Persist, false, "Defense Player Coordinate" ) + end + end + end + local DefenseAIA2GDispatcher = self.Dispatcher:GetDefenseAIA2GDispatcher() -- AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER + + if DefenseAIA2GDispatcher then + -- Loop through all the tasks of the AI Defenses, and check which zone is involved in the defenses and is active! + for Defender, Task in pairs( DefenseAIA2GDispatcher:GetDefenderTasks() or {} ) do + local DetectedItem = DefenseAIA2GDispatcher:GetDefenderTaskTarget( Defender ) + if DetectedItem then + local DetectedZone = DefenseAIA2GDispatcher.Detection:GetDetectedItemZone( DetectedItem ) + if DetectedZone then + self.TaskInfo:AddInfo( "Defense AI Zone", DetectedZone:GetName(), 40, "MOD", Persist ) + self.TaskInfo:AddCoordinate( DetectedZone:GetCoordinate(), 41, "MOD", Persist, false, "Defense AI Coordinate" ) + end + end + end + end + end + end - function TASK_ZONE_CAPTURE:ReportOrder( ReportGroup ) - local Coordinate = self:GetData( "Coordinate" ) - --local Coordinate = self.TaskInfo.Coordinates.TaskInfoText + function TASK_CAPTURE_ZONE:ReportOrder( ReportGroup ) + + local Coordinate = self.TaskInfo:GetCoordinate() local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate ) return Distance end - --- @param #TASK_ZONE_CAPTURE self + --- @param #TASK_CAPTURE_ZONE self -- @param Wrapper.Unit#UNIT TaskUnit - function TASK_ZONE_CAPTURE:OnAfterGoal( From, Event, To, PlayerUnit, PlayerName ) + function TASK_CAPTURE_ZONE:OnAfterGoal( From, Event, To, PlayerUnit, PlayerName ) - self:F( { PlayerUnit = PlayerUnit } ) + self:F( { PlayerUnit = PlayerUnit, Achieved = self.ZoneGoal.Goal:IsAchieved() } ) if self.ZoneGoal then if self.ZoneGoal.Goal:IsAchieved() then - self:Success() local TotalContributions = self.ZoneGoal.Goal:GetTotalContributions() local PlayerContributions = self.ZoneGoal.Goal:GetPlayerContributions() self:F( { TotalContributions = TotalContributions, PlayerContributions = PlayerContributions } ) @@ -251,11 +298,32 @@ do -- TASK_ZONE_CAPTURE Scoring:_AddMissionGoalScore( self.Mission, PlayerName, "Zone " .. self.ZoneGoal:GetZoneName() .." captured", PlayerContribution * 200 / TotalContributions ) end end + self:Success() end end self:__Goal( -10, PlayerUnit, PlayerName ) end + --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection. + -- @param #TASK_CAPTURE_ZONE self + -- @param #number AutoAssignMethod The method to be applied to the task. + -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center. + -- @param Wrapper.Group#GROUP TaskGroup The player group. + function TASK_CAPTURE_ZONE:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup, AutoAssignReference ) + + if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then + return math.random( 1, 9 ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then + local Coordinate = self.TaskInfo:GetCoordinate() + local Distance = Coordinate:Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() ) + return math.floor( Distance ) + elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then + return 1 + end + + return 0 + end + end diff --git a/Moose Development/Moose/Tasking/Task_Manager.lua b/Moose Development/Moose/Tasking/Task_Manager.lua index 8c2d76406..e4f4ee9a7 100644 --- a/Moose Development/Moose/Tasking/Task_Manager.lua +++ b/Moose Development/Moose/Tasking/Task_Manager.lua @@ -185,7 +185,6 @@ do -- TASK_MANAGER -- @param #TASK_MANAGER self -- @return #TASK_MANAGER self function TASK_MANAGER:ManageTasks() - self:E() end diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua new file mode 100644 index 000000000..da73fd4b6 --- /dev/null +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -0,0 +1,245 @@ +--- **Utilities** Enumerators. +-- +-- An enumerator is a variable that holds a constant value. Enumerators are very useful because they make the code easier to read and to change in general. +-- +-- For example, instead of using the same value at multiple different places in your code, you should use a variable set to that value. +-- If, for whatever reason, the value needs to be changed, you only have to change the variable once and do not have to search through you code and reset +-- every value by hand. +-- +-- Another big advantage is that the LDT intellisense "knows" the enumerators. So you can use the autocompletion feature and do not have to keep all the +-- values in your head or look them up in the docs. +-- +-- DCS itself provides a lot of enumerators for various things. See [Enumerators](https://wiki.hoggitworld.com/view/Category:Enumerators) on Hoggit. +-- +-- Other Moose classe also have enumerators. For example, the AIRBASE class has enumerators for airbase names. +-- +-- @module ENUMS +-- @image MOOSE.JPG + +--- [DCS Enum world](https://wiki.hoggitworld.com/view/DCS_enum_world) +-- @type ENUMS + +--- Because ENUMS are just better practice. +-- +-- The ENUMS class adds some handy variables, which help you to make your code better and more general. +-- +-- @field #ENUMS +ENUMS = {} + +--- Rules of Engagement. +-- @type ENUMS.ROE +-- @field #number WeaponFree AI will engage any enemy group it detects. Target prioritization is based based on the threat of the target. +-- @field #number OpenFireWeaponFree AI will engage any enemy group it detects, but will prioritize targets specified in the groups tasking. +-- @field #number OpenFire AI will engage only targets specified in its taskings. +-- @field #number ReturnFire AI will only engage threats that shoot first. +-- @field #number WeaponHold AI will hold fire under all circumstances. +ENUMS.ROE = { + WeaponFree=0, + OpenFireWeaponFree=1, + OpenFire=2, + ReturnFire=3, + WeaponHold=4, + } + +--- Reaction On Threat. +-- @type ENUMS.ROT +-- @field #number NoReaction No defensive actions will take place to counter threats. +-- @field #number PassiveDefense AI will use jammers and other countermeasures in an attempt to defeat the threat. AI will not attempt a maneuver to defeat a threat. +-- @field #number EvadeFire AI will react by performing defensive maneuvers against incoming threats. AI will also use passive defense. +-- @field #number BypassAndEscape AI will attempt to avoid enemy threat zones all together. This includes attempting to fly above or around threats. +-- @field #number AllowAbortMission If a threat is deemed severe enough the AI will abort its mission and return to base. +ENUMS.ROT = { + NoReaction=0, + PassiveDefense=1, + EvadeFire=2, + BypassAndEscape=3, + AllowAbortMission=4, +} + +--- Weapon types. See the [Weapon Flag](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag) enumerotor on hoggit wiki. +-- @type ENUMS.WeaponFlag +ENUMS.WeaponFlag={ + -- Bombs + LGB = 2, + TvGB = 4, + SNSGB = 8, + HEBomb = 16, + Penetrator = 32, + NapalmBomb = 64, + FAEBomb = 128, + ClusterBomb = 256, + Dispencer = 512, + CandleBomb = 1024, + ParachuteBomb = 2147483648, + -- Rockets + LightRocket = 2048, + MarkerRocket = 4096, + CandleRocket = 8192, + HeavyRocket = 16384, + -- Air-To-Surface Missiles + AntiRadarMissile = 32768, + AntiShipMissile = 65536, + AntiTankMissile = 131072, + FireAndForgetASM = 262144, + LaserASM = 524288, + TeleASM = 1048576, + CruiseMissile = 2097152, + AntiRadarMissile2 = 1073741824, + -- Air-To-Air Missiles + SRAM = 4194304, + MRAAM = 8388608, + LRAAM = 16777216, + IR_AAM = 33554432, + SAR_AAM = 67108864, + AR_AAM = 134217728, + --- Guns + GunPod = 268435456, + BuiltInCannon = 536870912, + --- + -- Combinations + -- + -- Bombs + GuidedBomb = 14, -- (LGB + TvGB + SNSGB) + AnyUnguidedBomb = 2147485680, -- (HeBomb + Penetrator + NapalmBomb + FAEBomb + ClusterBomb + Dispencer + CandleBomb + ParachuteBomb) + AnyBomb = 2147485694, -- (GuidedBomb + AnyUnguidedBomb) + --- Rockets + AnyRocket = 30720, -- LightRocket + MarkerRocket + CandleRocket + HeavyRocket + --- Air-To-Surface Missiles + GuidedASM = 1572864, -- (LaserASM + TeleASM) + TacticalASM = 1835008, -- (GuidedASM + FireAndForgetASM) + AnyASM = 4161536, -- (AntiRadarMissile + AntiShipMissile + AntiTankMissile + FireAndForgetASM + GuidedASM + CruiseMissile) + AnyASM2 = 1077903360, -- 4161536+1073741824, + --- Air-To-Air Missiles + AnyAAM = 264241152, -- IR_AAM + SAR_AAM + AR_AAM + SRAAM + MRAAM + LRAAM + AnyAutonomousMissile = 36012032, -- IR_AAM + AntiRadarMissile + AntiShipMissile + FireAndForgetASM + CruiseMissile + AnyMissile = 268402688, -- AnyASM + AnyAAM + --- Guns + Cannons = 805306368, -- GUN_POD + BuiltInCannon + --- + -- Even More Genral + Auto = 3221225470, -- Any Weapon (AnyBomb + AnyRocket + AnyMissile + Cannons) + AutoDCS = 1073741822, -- Something if often see + AnyAG = 2956984318, -- Any Air-To-Ground Weapon + AnyAA = 264241152, -- Any Air-To-Air Weapon + AnyUnguided = 2952822768, -- Any Unguided Weapon + AnyGuided = 268402702, -- Any Guided Weapon +} + +--- Mission tasks. +-- @type ENUMS.MissionTask +-- @field #string NOTHING No special task. Group can perform the minimal tasks: Orbit, Refuelling, Follow and Aerobatics. +-- @field #string AFAC Forward Air Controller Air. Can perform the tasks: Attack Group, Attack Unit, FAC assign group, Bombing, Attack Map Object. +-- @field #string ANTISHIPSTRIKE Naval ops. Can perform the tasks: Attack Group, Attack Unit. +-- @field #string AWACS AWACS. +-- @field #string CAP Combat Air Patrol. +-- @field #string CAS Close Air Support. +-- @field #string ESCORT Escort another group. +-- @field #string FIGHTERSWEEP Fighter sweep. +-- @field #string GROUNDATTACK Ground attack. +-- @field #string INTERCEPT Intercept. +-- @field #string PINPOINTSTRIKE Pinpoint strike. +-- @field #string RECONNAISSANCE Reconnaissance mission. +-- @field #string REFUELING Refueling mission. +-- @field #string RUNWAYATTACK Attack the runway of an airdrome. +-- @field #string SEAD Suppression of Enemy Air Defenses. +-- @field #string TRANSPORT Troop transport. +ENUMS.MissionTask={ + NOTHING="Nothing", + AFAC="AFAC", + ANTISHIPSTRIKE="Antiship Strike", + AWACS="AWACS", + CAP="CAP", + CAS="CAS", + ESCORT="Escort", + FIGHTERSWEEP="Fighter Sweep", + GROUNDATTACK="Ground Attack", + INTERCEPT="Intercept", + PINPOINTSTRIKE="Pinpoint Strike", + RECONNAISSANCE="Reconnaissance", + REFUELING="Refueling", + RUNWAYATTACK="Runway Attack", + SEAD="SEAD", + TRANSPORT="Transport", +} + +--- Formations (new). See the [Formations](https://wiki.hoggitworld.com/view/DCS_enum_formation) on hoggit wiki. +-- @type ENUMS.Formation +ENUMS.Formation={} +ENUMS.Formation.FixedWing={} +ENUMS.Formation.FixedWing.LineAbreast={} +ENUMS.Formation.FixedWing.LineAbreast.Close = 65537 +ENUMS.Formation.FixedWing.LineAbreast.Open = 65538 +ENUMS.Formation.FixedWing.LineAbreast.Group = 65539 +ENUMS.Formation.FixedWing.Trail={} +ENUMS.Formation.FixedWing.Trail.Close = 131073 +ENUMS.Formation.FixedWing.Trail.Open = 131074 +ENUMS.Formation.FixedWing.Trail.Group = 131075 +ENUMS.Formation.FixedWing.Wedge={} +ENUMS.Formation.FixedWing.Wedge.Close = 196609 +ENUMS.Formation.FixedWing.Wedge.Open = 196610 +ENUMS.Formation.FixedWing.Wedge.Group = 196611 +ENUMS.Formation.FixedWing.EchelonRight={} +ENUMS.Formation.FixedWing.EchelonRight.Close = 262145 +ENUMS.Formation.FixedWing.EchelonRight.Open = 262146 +ENUMS.Formation.FixedWing.EchelonRight.Group = 262147 +ENUMS.Formation.FixedWing.EchelonLeft={} +ENUMS.Formation.FixedWing.EchelonLeft.Close = 327681 +ENUMS.Formation.FixedWing.EchelonLeft.Open = 327682 +ENUMS.Formation.FixedWing.EchelonLeft.Group = 327683 +ENUMS.Formation.FixedWing.FingerFour={} +ENUMS.Formation.FixedWing.FingerFour.Close = 393217 +ENUMS.Formation.FixedWing.FingerFour.Open = 393218 +ENUMS.Formation.FixedWing.FingerFour.Group = 393219 +ENUMS.Formation.FixedWing.Spread={} +ENUMS.Formation.FixedWing.Spread.Close = 458753 +ENUMS.Formation.FixedWing.Spread.Open = 458754 +ENUMS.Formation.FixedWing.Spread.Group = 458755 +ENUMS.Formation.FixedWing.BomberElement={} +ENUMS.Formation.FixedWing.BomberElement.Close = 786433 +ENUMS.Formation.FixedWing.BomberElement.Open = 786434 +ENUMS.Formation.FixedWing.BomberElement.Group = 786435 +ENUMS.Formation.FixedWing.BomberElementHeight={} +ENUMS.Formation.FixedWing.BomberElementHeight.Close = 851968 +ENUMS.Formation.FixedWing.FighterVic={} +ENUMS.Formation.FixedWing.FighterVic.Close = 917505 +ENUMS.Formation.FixedWing.FighterVic.Open = 917506 +ENUMS.Formation.RotaryWing={} +ENUMS.Formation.RotaryWing.Column={} +ENUMS.Formation.RotaryWing.Column.D70=720896 +ENUMS.Formation.RotaryWing.Wedge={} +ENUMS.Formation.RotaryWing.Wedge.D70=8 +ENUMS.Formation.RotaryWing.FrontRight={} +ENUMS.Formation.RotaryWing.FrontRight.D300=655361 +ENUMS.Formation.RotaryWing.FrontRight.D600=655362 +ENUMS.Formation.RotaryWing.FrontLeft={} +ENUMS.Formation.RotaryWing.FrontLeft.D300=655617 +ENUMS.Formation.RotaryWing.FrontLeft.D600=655618 +ENUMS.Formation.RotaryWing.EchelonRight={} +ENUMS.Formation.RotaryWing.EchelonRight.D70 =589825 +ENUMS.Formation.RotaryWing.EchelonRight.D300=589826 +ENUMS.Formation.RotaryWing.EchelonRight.D600=589827 +ENUMS.Formation.RotaryWing.EchelonLeft={} +ENUMS.Formation.RotaryWing.EchelonLeft.D70 =590081 +ENUMS.Formation.RotaryWing.EchelonLeft.D300=590082 +ENUMS.Formation.RotaryWing.EchelonLeft.D600=590083 + +--- Formations (old). The old format is a simplified version of the new formation enums, which allow more sophisticated settings. +-- See the [Formations](https://wiki.hoggitworld.com/view/DCS_enum_formation) on hoggit wiki. +-- @type ENUMS.FormationOld +ENUMS.FormationOld={} +ENUMS.FormationOld.FixedWing={} +ENUMS.FormationOld.FixedWing.LineAbreast=1 +ENUMS.FormationOld.FixedWing.Trail=2 +ENUMS.FormationOld.FixedWing.Wedge=3 +ENUMS.FormationOld.FixedWing.EchelonRight=4 +ENUMS.FormationOld.FixedWing.EchelonLeft=5 +ENUMS.FormationOld.FixedWing.FingerFour=6 +ENUMS.FormationOld.FixedWing.SpreadFour=7 +ENUMS.FormationOld.FixedWing.BomberElement=12 +ENUMS.FormationOld.FixedWing.BomberElementHeight=13 +ENUMS.FormationOld.FixedWing.FighterVic=14 +ENUMS.FormationOld.RotaryWing={} +ENUMS.FormationOld.RotaryWing.Wedge=8 +ENUMS.FormationOld.RotaryWing.Echelon=9 +ENUMS.FormationOld.RotaryWing.Front=10 +ENUMS.FormationOld.RotaryWing.Column=11 \ No newline at end of file diff --git a/Moose Development/Moose/Utilities/Routines.lua b/Moose Development/Moose/Utilities/Routines.lua index c63359c2e..15e4cc1c8 100644 --- a/Moose Development/Moose/Utilities/Routines.lua +++ b/Moose Development/Moose/Utilities/Routines.lua @@ -21,6 +21,11 @@ routines.build = 22 -- Utils- conversion, Lua utils, etc. routines.utils = {} +routines.utils.round = function(number, decimals) + local power = 10^decimals + return math.floor(number * power) / power +end + --from http://lua-users.org/wiki/CopyTable routines.utils.deepCopy = function(object) local lookup_table = {} @@ -1626,6 +1631,11 @@ function routines.getGroupRoute(groupIdent, task) -- same as getGroupPoints bu for point_num, point in pairs(group_data.route.points) do local routeData = {} + if env.mission.version > 7 then + routeData.name = env.getValueDictByKey(point.name) + else + routeData.name = point.name + end if not point.point then routeData.x = point.x routeData.y = point.y diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 42dce2b58..5bba94cc1 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -1,5 +1,4 @@ ---- This module contains derived utilities taken from the MIST framework, --- which are excellent tools to be reused in an OO environment!. +--- This module contains derived utilities taken from the MIST framework, which are excellent tools to be reused in an OO environment. -- -- ### Authors: -- @@ -33,18 +32,90 @@ FLARECOLOR = trigger.flareColor -- #FLARECOLOR --- Big smoke preset enum. -- @type BIGSMOKEPRESET BIGSMOKEPRESET = { - SmallSmokeAndFire=0, - MediumSmokeAndFire=1, - LargeSmokeAndFire=2, - HugeSmokeAndFire=3, - SmallSmoke=4, - MediumSmoke=5, - LargeSmoke=6, - HugeSmoke=7, + SmallSmokeAndFire=1, + MediumSmokeAndFire=2, + LargeSmokeAndFire=3, + HugeSmokeAndFire=4, + SmallSmoke=5, + MediumSmoke=6, + LargeSmoke=7, + HugeSmoke=8, } +--- DCS map as returned by env.mission.theatre. +-- @type DCSMAP +-- @field #string Caucasus Caucasus map. +-- @field #string Normandy Normandy map. +-- @field #string NTTR Nevada Test and Training Range map. +-- @field #string PersianGulf Persian Gulf map. +DCSMAP = { + Caucasus="Caucasus", + NTTR="Nevada", + Normandy="Normandy", + PersianGulf="PersianGulf" +} + + +--- See [DCS_enum_callsigns](https://wiki.hoggitworld.com/view/DCS_enum_callsigns) +-- @type CALLSIGN +CALLSIGN={ + -- Aircraft + Aircraft={ + Enfield=1, + Springfield=2, + Uzi=3, + Colt=4, + Dodge=5, + Ford=6, + Chevy=7, + Pontiac=8, + -- A-10A or A-10C + Hawg=9, + Boar=10, + Pig=11, + Tusk=12, + }, + -- AWACS + AWACS={ + Overlord=1, + Magic=2, + Wizard=3, + Focus=4, + Darkstar=5, + }, + -- Tanker + Tanker={ + Texaco=1, + Arco=2, + Shell=3, + }, + -- JTAC + JTAC={ + Axeman=1, + Darknight=2, + Warrier=3, + Pointer=4, + Eyeball=5, + Moonbeam=6, + Whiplash=7, + Finger=8, + Pinpoint=9, + Ferret=10, + Shaba=11, + Playboy=12, + Hammer=13, + Jaguar=14, + Deathstar=15, + Anvil=16, + Firefly=17, + Mantis=18, + Badger=19, + }, +} --#CALLSIGN + --- Utilities static class. -- @type UTILS +-- @field #number _MarkID Marker index counter. Running number when marker is added. UTILS = { _MarkID = 1 } @@ -112,7 +183,9 @@ UTILS.IsInstanceOf = function( object, className ) end ---from http://lua-users.org/wiki/CopyTable +--- Deep copy a table. See http://lua-users.org/wiki/CopyTable +-- @param #table object The input table. +-- @return #table Copy of the input table. UTILS.DeepCopy = function(object) local lookup_table = {} local function _copy(object) @@ -133,7 +206,8 @@ UTILS.DeepCopy = function(object) end --- porting in Slmod's serialize_slmod2 +--- Porting in Slmod's serialize_slmod2. +-- @param #table tbl Input table. UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function lookup_table = {} @@ -250,7 +324,11 @@ UTILS.FeetToMeters = function(feet) end UTILS.KnotsToKmph = function(knots) - return knots* 1.852 + return knots * 1.852 +end + +UTILS.KmphToKnots = function(knots) + return knots / 1.852 end UTILS.KmphToMps = function( kmph ) @@ -265,23 +343,54 @@ UTILS.MiphToMps = function( miph ) return miph * 0.44704 end +--- Convert meters per second to miles per hour. +-- @param #number mps Speed in m/s. +-- @return #number Speed in miles per hour. UTILS.MpsToMiph = function( mps ) return mps / 0.44704 end +--- Convert meters per second to knots. +-- @param #number knots Speed in m/s. +-- @return #number Speed in knots. UTILS.MpsToKnots = function( mps ) - return mps * 3600 / 1852 + return mps * 1.94384 --3600 / 1852 end +--- Convert knots to meters per second. +-- @param #number knots Speed in knots. +-- @return #number Speed in m/s. UTILS.KnotsToMps = function( knots ) - return knots * 1852 / 3600 + return knots / 1.94384 --* 1852 / 3600 end +--- Convert temperature from Celsius to Farenheit. +-- @param #number Celcius Temperature in degrees Celsius. +-- @return #number Temperature in degrees Farenheit. UTILS.CelciusToFarenheit = function( Celcius ) return Celcius * 9/5 + 32 end +--- Convert pressure from hecto Pascal (hPa) to inches of mercury (inHg). +-- @param #number hPa Pressure in hPa. +-- @return #number Pressure in inHg. +UTILS.hPa2inHg = function( hPa ) + return hPa * 0.0295299830714 +end +--- Convert pressure from hecto Pascal (hPa) to millimeters of mercury (mmHg). +-- @param #number hPa Pressure in hPa. +-- @return #number Pressure in mmHg. +UTILS.hPa2mmHg = function( hPa ) + return hPa * 0.7500615613030 +end + +--- Convert kilo gramms (kg) to pounds (lbs). +-- @param #number kg Mass in kg. +-- @return #number Mass in lbs. +UTILS.kg2lbs = function( kg ) + return kg * 2.20462 +end --[[acc: in DM: decimal point of minutes. @@ -335,15 +444,16 @@ UTILS.tostringLL = function( lat, lon, acc, DMS) local secFrmtStr -- create the formatting string for the seconds place secFrmtStr = '%02d' --- if acc <= 0 then -- no decimal place. --- secFrmtStr = '%02d' --- else --- local width = 3 + acc -- 01.310 - that's a width of 6, for example. --- secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' --- end + if acc <= 0 then -- no decimal place. + secFrmtStr = '%02d' + else + local width = 3 + acc -- 01.310 - that's a width of 6, for example. Acc is limited to 2 for DMS! + secFrmtStr = '%0' .. width .. '.' .. acc .. 'f' + end - return string.format('%03d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. ' ' - .. string.format('%03d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi + -- 024� 23' 12"N or 024� 23' 12.03"N + return string.format('%03d°', latDeg)..string.format('%02d', latMin)..'\''..string.format(secFrmtStr, latSec)..'"'..latHemi..' ' + .. string.format('%03d°', lonDeg)..string.format('%02d', lonMin)..'\''..string.format(secFrmtStr, lonSec)..'"'..lonHemi else -- degrees, decimal minutes. latMin = UTILS.Round(latMin, acc) @@ -367,20 +477,40 @@ UTILS.tostringLL = function( lat, lon, acc, DMS) minFrmtStr = '%0' .. width .. '.' .. acc .. 'f' end - return string.format('%03d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' - .. string.format('%03d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi + -- 024 23'N or 024 23.123'N + return string.format('%03d°', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. ' ' + .. string.format('%03d°', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi end end -- acc- the accuracy of each easting/northing. 0, 1, 2, 3, 4, or 5. UTILS.tostringMGRS = function(MGRS, acc) --R2.1 + if acc == 0 then return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph else - return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', UTILS.Round(MGRS.Easting/(10^(5-acc)), 0)) - .. ' ' .. string.format('%0' .. acc .. 'd', UTILS.Round(MGRS.Northing/(10^(5-acc)), 0)) + + -- Test if Easting/Northing have less than 4 digits. + --MGRS.Easting=123 -- should be 00123 + --MGRS.Northing=5432 -- should be 05432 + + -- Truncate rather than round MGRS grid! + local Easting=tostring(MGRS.Easting) + local Northing=tostring(MGRS.Northing) + + -- Count number of missing digits. Easting/Northing should have 5 digits. However, it is passed as a number. Therefore, any leading zeros would not be displayed by lua. + local nE=5-string.len(Easting) + local nN=5-string.len(Northing) + + -- Get leading zeros (if any). + for i=1,nE do Easting="0"..Easting end + for i=1,nN do Northing="0"..Northing end + + -- Return MGRS string. + return string.format("%s %s %s %s", MGRS.UTMZone, MGRS.MGRSDigraph, string.sub(Easting, 1, acc), string.sub(Northing, 1, acc)) end + end @@ -425,6 +555,57 @@ function UTILS.spairs( t, order ) end end + +-- Here is a customized version of pairs, which I called kpairs because it iterates over the table in a sorted order, based on a function that will determine the keys as reference first. +function UTILS.kpairs( t, getkey, order ) + -- collect the keys + local keys = {} + local keyso = {} + for k, o in pairs(t) do keys[#keys+1] = k keyso[#keyso+1] = getkey( o ) end + + -- if order function given, sort by it by passing the table and keys a, b, + -- otherwise just sort the keys + if order then + table.sort(keys, function(a,b) return order(t, a, b) end) + else + table.sort(keys) + end + + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keyso[i], t[keys[i]] + end + end +end + +-- Here is a customized version of pairs, which I called rpairs because it iterates over the table in a random order. +function UTILS.rpairs( t ) + -- collect the keys + + local keys = {} + for k in pairs(t) do keys[#keys+1] = k end + + local random = {} + local j = #keys + for i = 1, j do + local k = math.random( 1, #keys ) + random[i] = keys[k] + table.remove( keys, k ) + end + + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if random[i] then + return random[i], t[random[i]] + end + end +end + -- get a new mark ID for markings function UTILS.GetMarkID() @@ -512,8 +693,9 @@ end --- Convert time in seconds to hours, minutes and seconds. -- @param #number seconds Time in seconds, e.g. from timer.getAbsTime() function. +-- @param #boolean short (Optional) If true, use short output, i.e. (HH:)MM:SS without day. -- @return #string Time in format Hours:Minutes:Seconds+Days (HH:MM:SS+D). -function UTILS.SecondsToClock(seconds) +function UTILS.SecondsToClock(seconds, short) -- Nil check. if seconds==nil then @@ -526,20 +708,28 @@ function UTILS.SecondsToClock(seconds) -- Seconds of this day. local _seconds=seconds%(60*60*24) - if seconds <= 0 then + if seconds<0 then return nil else local hours = string.format("%02.f", math.floor(_seconds/3600)) local mins = string.format("%02.f", math.floor(_seconds/60 - (hours*60))) local secs = string.format("%02.f", math.floor(_seconds - hours*3600 - mins *60)) local days = string.format("%d", seconds/(60*60*24)) - return hours..":"..mins..":"..secs.."+"..days + local clock=hours..":"..mins..":"..secs.."+"..days + if short then + if hours=="00" then + clock=mins..":"..secs + else + clock=hours..":"..mins..":"..secs + end + end + return clock end end --- Convert clock time from hours, minutes and seconds to seconds. -- @param #string clock String of clock time. E.g., "06:12:35" or "5:1:30+1". Format is (H)H:(M)M:((S)S)(+D) H=Hours, M=Minutes, S=Seconds, D=Days. --- @param #number Seconds. Corresponds to what you cet from timer.getAbsTime() function. +-- @return #number Seconds. Corresponds to what you cet from timer.getAbsTime() function. function UTILS.ClockToSeconds(clock) -- Nil check. @@ -551,7 +741,7 @@ function UTILS.ClockToSeconds(clock) local seconds=0 -- Split additional days. - local dsplit=UTILS.split(clock, "+") + local dsplit=UTILS.Split(clock, "+") -- Convert days to seconds. if #dsplit>1 then @@ -680,3 +870,277 @@ function UTILS.VecCross(a, b) return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x} end +--- Calculate the difference between two 3D vectors by substracting the x,y,z components from each other. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return DCS#Vec3 Vector c=a-b with c(i)=a(i)-b(i), i=x,y,z. +function UTILS.VecSubstract(a, b) + return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z} +end + +--- Calculate the total vector of two 3D vectors by adding the x,y,z components of each other. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return DCS#Vec3 Vector c=a+b with c(i)=a(i)+b(i), i=x,y,z. +function UTILS.VecAdd(a, b) + return {x=a.x+b.x, y=a.y+b.y, z=a.z+b.z} +end + +--- Calculate the angle between two 3D vectors. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return #number Angle alpha between and b in degrees. alpha=acos(a*b)/(|a||b|), (* denotes the dot product). +function UTILS.VecAngle(a, b) + + local cosalpha=UTILS.VecDot(a,b)/(UTILS.VecNorm(a)*UTILS.VecNorm(b)) + + local alpha=0 + if cosalpha>=0.9999999999 then --acos(1) is not defined. + alpha=0 + elseif cosalpha<=-0.999999999 then --acos(-1) is not defined. + alpha=math.pi + else + alpha=math.acos(cosalpha) + end + + return math.deg(alpha) +end + +--- Calculate "heading" of a 3D vector in the X-Z plane. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @return #number Heading in degrees in [0,360). +function UTILS.VecHdg(a) + local h=math.deg(math.atan2(a.z, a.x)) + if h<0 then + h=h+360 + end + return h +end + +--- Calculate the difference between two "heading", i.e. angles in [0,360) deg. +-- @param #number h1 Heading one. +-- @param #number h2 Heading two. +-- @return #number Heading difference in degrees. +function UTILS.HdgDiff(h1, h2) + + -- Angle in rad. + local alpha= math.rad(tonumber(h1)) + local beta = math.rad(tonumber(h2)) + + -- Runway vector. + local v1={x=math.cos(alpha), y=0, z=math.sin(alpha)} + local v2={x=math.cos(beta), y=0, z=math.sin(beta)} + + local delta=UTILS.VecAngle(v1, v2) + + return math.abs(delta) +end + + +--- Rotate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param #number angle Rotation angle in degrees. +-- @return DCS#Vec3 Vector rotated in the (x,z) plane. +function UTILS.Rotate2D(a, angle) + + local phi=math.rad(angle) + + local x=a.z + local y=a.x + + local Z=x*math.cos(phi)-y*math.sin(phi) + local X=x*math.sin(phi)+y*math.cos(phi) + local Y=a.y + + local A={x=X, y=Y, z=Z} + + return A +end + + + +--- Converts a TACAN Channel/Mode couple into a frequency in Hz. +-- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X". +-- @param #string TACANMode The TACAN mode, i.e. the "X" in "10X". +-- @return #number Frequency in Hz or #nil if parameters are invalid. +function UTILS.TACANToFrequency(TACANChannel, TACANMode) + + if type(TACANChannel) ~= "number" then + return nil -- error in arguments + end + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end + + +--- Returns the DCS map/theatre as optained by env.mission.theatre +-- @return #string DCS map name. +function UTILS.GetDCSMap() + return env.mission.theatre +end + +--- Returns the mission date. This is the date the mission started. +-- @return #string Mission date in yyyy/mm/dd format. +function UTILS.GetDCSMissionDate() + local year=tostring(env.mission.date.Year) + local month=tostring(env.mission.date.Month) + local day=tostring(env.mission.date.Day) + return string.format("%s/%s/%s", year, month, day) +end + + +--- Returns the magnetic declination of the map. +-- Returned values for the current maps are: +-- +-- * Caucasus +6 (East), year ~ 2011 +-- * NTTR +12 (East), year ~ 2011 +-- * Normandy -10 (West), year ~ 1944 +-- * Persian Gulf +2 (East), year ~ 2011 +-- @param #string map (Optional) Map for which the declination is returned. Default is from env.mission.theatre +-- @return #number Declination in degrees. +function UTILS.GetMagneticDeclination(map) + + -- Map. + map=map or UTILS.GetDCSMap() + + local declination=0 + if map==DCSMAP.Caucasus then + declination=6 + elseif map==DCSMAP.NTTR then + declination=12 + elseif map==DCSMAP.Normandy then + declination=-10 + elseif map==DCSMAP.PersianGulf then + declination=2 + else + declination=0 + end + + return declination +end + +--- Checks if a file exists or not. This requires **io** to be desanitized. +-- @param #string file File that should be checked. +-- @return #boolean True if the file exists, false if the file does not exist or nil if the io module is not available and the check could not be performed. +function UTILS.FileExists(file) + if io then + local f=io.open(file, "r") + if f~=nil then + io.close(f) + return true + else + return false + end + else + return nil + end +end + +--- Checks the current memory usage collectgarbage("count"). Info is printed to the DCS log file. Time stamp is the current mission runtime. +-- @param #boolean output If true, print to DCS log file. +-- @return #number Memory usage in kByte. +function UTILS.CheckMemory(output) + local time=timer.getTime() + local clock=UTILS.SecondsToClock(time) + local mem=collectgarbage("count") + if output then + env.info(string.format("T=%s Memory usage %d kByte = %.2f MByte", clock, mem, mem/1024)) + end + return mem +end + + +--- Get the coalition name from its numerical ID, e.g. coaliton.side.RED. +-- @param #number Coalition The coalition ID. +-- @return #string The coalition name, i.e. "Neutral", "Red" or "Blue" (or "Unknown"). +function UTILS.GetCoalitionName(Coalition) + + if Coalition then + if Coalition==coalition.side.BLUE then + return "Blue" + elseif Coalition==coalition.side.RED then + return "Red" + elseif Coalition==coalition.side.NEUTRAL then + return "Neutral" + else + return "Unknown" + end + else + return "Unknown" + end + +end + +--- Get the modulation name from its numerical value. +-- @param #number Modulation The modulation enumerator number. Can be either 0 or 1. +-- @return #string The modulation name, i.e. "AM"=0 or "FM"=1. Anything else will return "Unknown". +function UTILS.GetModulationName(Modulation) + + if Modulation then + if Modulation==0 then + return "AM" + elseif Modulation==1 then + return "FM" + else + return "Unknown" + end + else + return "Unknown" + end + +end + +--- Get the callsign name from its enumerator value +-- @param #number Callsign The enumerator callsign. +-- @return #string The callsign name or "Ghostrider". +function UTILS.GetCallsignName(Callsign) + + for name, value in pairs(CALLSIGN.Aircraft) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.AWACS) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.JTAC) do + if value==Callsign then + return name + end + end + + for name, value in pairs(CALLSIGN.Tanker) do + if value==Callsign then + return name + end + end + + return "Ghostrider" +end diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 71b020489..b27fd5f07 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -13,6 +13,9 @@ --- @type AIRBASE +-- @field #string ClassName Name of the class, i.e. "AIRBASE". +-- @field #table CategoryName Names of airbase categories. +-- @field #number activerwyno Active runway number (forced). -- @extends Wrapper.Positionable#POSITIONABLE --- Wrapper class to handle the DCS Airbase objects: @@ -54,6 +57,7 @@ AIRBASE = { [Airbase.Category.HELIPAD] = "Helipad", [Airbase.Category.SHIP] = "Ship", }, + activerwyno=nil, } --- Enumeration to identify the airbases in the Caucasus region. @@ -120,7 +124,6 @@ AIRBASE.Caucasus = { -- * AIRBASE.Nevada.Jean_Airport -- * AIRBASE.Nevada.Laughlin_Airport -- * AIRBASE.Nevada.Lincoln_County --- * AIRBASE.Nevada.Mellan_Airstrip -- * AIRBASE.Nevada.Mesquite -- * AIRBASE.Nevada.Mina_Airport_3Q0 -- * AIRBASE.Nevada.North_Las_Vegas @@ -140,7 +143,6 @@ AIRBASE.Nevada = { ["Jean_Airport"] = "Jean Airport", ["Laughlin_Airport"] = "Laughlin Airport", ["Lincoln_County"] = "Lincoln County", - ["Mellan_Airstrip"] = "Mellan Airstrip", ["Mesquite"] = "Mesquite", ["Mina_Airport_3Q0"] = "Mina Airport 3Q0", ["North_Las_Vegas"] = "North Las Vegas", @@ -181,7 +183,7 @@ AIRBASE.Nevada = { -- * AIRBASE.Normandy.Needs_Oar_Point -- * AIRBASE.Normandy.Funtington -- * AIRBASE.Normandy.Tangmere --- * AIRBASE.Normandy.Ford +-- * AIRBASE.Normandy.Ford_AF -- @field Normandy AIRBASE.Normandy = { ["Saint_Pierre_du_Mont"] = "Saint Pierre du Mont", @@ -214,52 +216,79 @@ AIRBASE.Normandy = { ["Needs_Oar_Point"] = "Needs Oar Point", ["Funtington"] = "Funtington", ["Tangmere"] = "Tangmere", - ["Ford"] = "Ford", - } + ["Ford_AF"] = "Ford_AF", + ["Goulet"] = "Goulet", + ["Argentan"] = "Argentan", + ["Vrigny"] = "Vrigny", + ["Essay"] = "Essay", + ["Hauterive"] = "Hauterive", + ["Barville"] = "Barville", + ["Conches"] = "Conches", +} --- These are all airbases of the Persion Gulf Map: --- --- * AIRBASE.PersianGulf.Fujairah_Intl --- * AIRBASE.PersianGulf.Qeshm_Island --- * AIRBASE.PersianGulf.Sir_Abu_Nuayr +-- +-- * AIRBASE.PersianGulf.Abu_Dhabi_International_Airport -- * AIRBASE.PersianGulf.Abu_Musa_Island_Airport +-- * AIRBASE.PersianGulf.Al-Bateen_Airport +-- * AIRBASE.PersianGulf.Al_Ain_International_Airport +-- * AIRBASE.PersianGulf.Al_Dhafra_AB +-- * AIRBASE.PersianGulf.Al_Maktoum_Intl +-- * AIRBASE.PersianGulf.Al_Minhad_AB +-- * AIRBASE.PersianGulf.Bandar_e_Jask_airfield -- * AIRBASE.PersianGulf.Bandar_Abbas_Intl -- * AIRBASE.PersianGulf.Bandar_Lengeh --- * AIRBASE.PersianGulf.Tunb_Island_AFB --- * AIRBASE.PersianGulf.Havadarya --- * AIRBASE.PersianGulf.Lar_Airbase --- * AIRBASE.PersianGulf.Sirri_Island --- * AIRBASE.PersianGulf.Tunb_Kochak --- * AIRBASE.PersianGulf.Al_Dhafra_AB -- * AIRBASE.PersianGulf.Dubai_Intl --- * AIRBASE.PersianGulf.Al_Maktoum_Intl +-- * AIRBASE.PersianGulf.Fujairah_Intl +-- * AIRBASE.PersianGulf.Havadarya +-- * AIRBASE.PersianGulf.Jiroft_Airport +-- * AIRBASE.PersianGulf.Kerman_Airport -- * AIRBASE.PersianGulf.Khasab --- * AIRBASE.PersianGulf.Al_Minhad_AB +-- * AIRBASE.PersianGulf.Kish_International_Airport +-- * AIRBASE.PersianGulf.Lar_Airbase +-- * AIRBASE.PersianGulf.Lavan_Island_Airport +-- * AIRBASE.PersianGulf.Liwa_Airbase +-- * AIRBASE.PersianGulf.Qeshm_Island +-- * AIRBASE.PersianGulf.Ras_Al_Khaimah_International_Airport +-- * AIRBASE.PersianGulf.Sas_Al_Nakheel_Airport -- * AIRBASE.PersianGulf.Sharjah_Intl -- * AIRBASE.PersianGulf.Shiraz_International_Airport --- * AIRBASE.PersianGulf.Kerman_Airport +-- * AIRBASE.PersianGulf.Sir_Abu_Nuayr +-- * AIRBASE.PersianGulf.Sirri_Island +-- * AIRBASE.PersianGulf.Tunb_Island_AFB +-- * AIRBASE.PersianGulf.Tunb_Kochak -- @field PersianGulf AIRBASE.PersianGulf = { - ["Fujairah_Intl"] = "Fujairah Intl", - ["Qeshm_Island"] = "Qeshm Island", - ["Sir_Abu_Nuayr"] = "Sir Abu Nuayr", + ["Abu_Dhabi_International_Airport"] = "Abu Dhabi International Airport", ["Abu_Musa_Island_Airport"] = "Abu Musa Island Airport", + ["Al_Ain_International_Airport"] = "Al Ain International Airport", + ["Al_Bateen_Airport"] = "Al-Bateen Airport", + ["Al_Dhafra_AB"] = "Al Dhafra AB", + ["Al_Maktoum_Intl"] = "Al Maktoum Intl", + ["Al_Minhad_AB"] = "Al Minhad AB", ["Bandar_Abbas_Intl"] = "Bandar Abbas Intl", ["Bandar_Lengeh"] = "Bandar Lengeh", - ["Tunb_Island_AFB"] = "Tunb Island AFB", + ["Bandar_e_Jask_airfield"] = "Bandar-e-Jask airfield", + ["Dubai_Intl"] = "Dubai Intl", + ["Fujairah_Intl"] = "Fujairah Intl", ["Havadarya"] = "Havadarya", - ["Lar_Airbase"] = "Lar Airbase", - ["Sirri_Island"] = "Sirri Island", - ["Tunb_Kochak"] = "Tunb Kochak", - ["Al_Dhafra_AB"] = "Al Dhafra AB", - ["Dubai_Intl"] = "Dubai Intl", - ["Al_Maktoum_Intl"] = "Al Maktoum Intl", + ["Jiroft_Airport"] = "Jiroft Airport", + ["Kerman_Airport"] = "Kerman Airport", ["Khasab"] = "Khasab", - ["Al_Minhad_AB"] = "Al Minhad AB", + ["Kish_International_Airport"] = "Kish International Airport", + ["Lar_Airbase"] = "Lar Airbase", + ["Lavan_Island_Airport"] = "Lavan Island Airport", + ["Liwa_Airbase"] = "Liwa Airbase", + ["Qeshm_Island"] = "Qeshm Island", + ["Ras_Al_Khaimah"] = "Ras Al Khaimah", + ["Sas_Al_Nakheel_Airport"] = "Sas Al Nakheel Airport", ["Sharjah_Intl"] = "Sharjah Intl", ["Shiraz_International_Airport"] = "Shiraz International Airport", - ["Kerman_Airport"] = "Kerman Airport", - } + ["Sir_Abu_Nuayr"] = "Sir Abu Nuayr", + ["Sirri_Island"] = "Sirri Island", + ["Tunb_Island_AFB"] = "Tunb Island AFB", + ["Tunb_Kochak"] = "Tunb Kochak", +} --- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". -- @type AIRBASE.ParkingSpot @@ -281,7 +310,7 @@ AIRBASE.PersianGulf = { -- * AIRBASE.TerminalType.OpenMed = 72: Open/Shelter air airplane only. -- * AIRBASE.TerminalType.OpenBig = 104: Open air spawn points. Generally larger but does not guarantee large aircraft are capable of spawning there. -- * AIRBASE.TerminalType.OpenMedOrBig = 176: Combines OpenMed and OpenBig spots. --- * AIRBASE.TerminalType.HelicopterUnsable = 216: Combines HelicopterOnly, OpenMed and OpenBig. +-- * AIRBASE.TerminalType.HelicopterUsable = 216: Combines HelicopterOnly, OpenMed and OpenBig. -- * AIRBASE.TerminalType.FighterAircraft = 244: Combines Shelter. OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. -- -- @type AIRBASE.TerminalType @@ -291,7 +320,7 @@ AIRBASE.PersianGulf = { -- @field #number OpenMed 72: Open/Shelter air airplane only. -- @field #number OpenBig 104: Open air spawn points. Generally larger but does not guarantee large aircraft are capable of spawning there. -- @field #number OpenMedOrBig 176: Combines OpenMed and OpenBig spots. --- @field #number HelicopterUnsable 216: Combines HelicopterOnly, OpenMed and OpenBig. +-- @field #number HelicopterUsable 216: Combines HelicopterOnly, OpenMed and OpenBig. -- @field #number FighterAircraft 244: Combines Shelter. OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. AIRBASE.TerminalType = { Runway=16, @@ -304,6 +333,14 @@ AIRBASE.TerminalType = { FighterAircraft=244, } +--- Runway data. +-- @type AIRBASE.Runway +-- @field #number heading Heading of the runway in degrees. +-- @field #string idx Runway ID: heading 070° ==> idx="07". +-- @field #number length Length of runway in meters. +-- @field Core.Point#COORDINATE position Position of runway start. +-- @field Core.Point#COORDINATE endpoint End point of runway. + -- Registration. --- Create a new AIRBASE from DCSAirbase. @@ -312,9 +349,16 @@ AIRBASE.TerminalType = { -- @return Wrapper.Airbase#AIRBASE function AIRBASE:Register( AirbaseName ) - local self = BASE:Inherit( self, POSITIONABLE:New( AirbaseName ) ) + local self = BASE:Inherit( self, POSITIONABLE:New( AirbaseName ) ) --#AIRBASE self.AirbaseName = AirbaseName - self.AirbaseZone = ZONE_RADIUS:New( AirbaseName, self:GetVec2(), 2500 ) + self.AirbaseID = self:GetID(true) + local vec2=self:GetVec2() + if vec2 then + self.AirbaseZone = ZONE_RADIUS:New( AirbaseName, vec2, 2500 ) + else + self:E(string.format("ERROR: Cound not get position Vec2 of airbase %s", AirbaseName)) + end + return self end @@ -341,6 +385,26 @@ function AIRBASE:FindByName( AirbaseName ) return AirbaseFound end +--- Find a AIRBASE in the _DATABASE by its ID. +-- @param #AIRBASE self +-- @param #number id Airbase ID. +-- @return #AIRBASE self +function AIRBASE:FindByID(id) + + for name,_airbase in pairs(_DATABASE.AIRBASES) do + local airbase=_airbase --#AIRBASE + + local aid=tonumber(airbase:GetID(true)) + + if aid==id then + return airbase + end + + end + + return nil +end + --- Get the DCS object of an airbase -- @param #AIRBASE self -- @return DCS#Airbase DCS airbase object. @@ -363,19 +427,77 @@ end --- Get all airbases of the current map. This includes ships and FARPS. -- @param DCS#Coalition coalition (Optional) Return only airbases belonging to the specified coalition. By default, all airbases of the map are returned. +-- @param #number category (Optional) Return only airbases of a certain category, e.g. Airbase.Category.FARP -- @return #table Table containing all airbase objects of the current map. -function AIRBASE.GetAllAirbases(coalition) +function AIRBASE.GetAllAirbases(coalition, category) local airbases={} - for _,airbase in pairs(_DATABASE.AIRBASES) do + for _,_airbase in pairs(_DATABASE.AIRBASES) do + local airbase=_airbase --#AIRBASE if (coalition~=nil and airbase:GetCoalition()==coalition) or coalition==nil then - table.insert(airbases, airbase) + if category==nil or category==airbase:GetAirbaseCategory() then + table.insert(airbases, airbase) + end end end return airbases end +--- Get ID of the airbase. +-- @param #AIRBASE self +-- @param #boolean unique (Optional) If true, ships will get a negative sign as the unit ID might be the same as an airbase ID. Default off! +-- @return #number The airbase ID. +function AIRBASE:GetID(unique) + + if self.AirbaseID then + + return unique and self.AirbaseID or math.abs(self.AirbaseID) + + else + + for DCSAirbaseId, DCSAirbase in ipairs(world.getAirbases()) do + + -- Get the airbase name. + local AirbaseName = DCSAirbase:getName() + + -- This gives the incorrect value to be inserted into the airdromeID for DCS 2.5.6! + local airbaseID=tonumber(DCSAirbase:getID()) + + local airbaseCategory=self:GetAirbaseCategory() + + --env.info(string.format("FF airbase=%s id=%s category=%s", tostring(AirbaseName), tostring(airbaseID), tostring(airbaseCategory))) + + -- No way AFIK to get the DCS version. So we check if the event exists. That should tell us if we are on DCS 2.5.6 or prior to that. + --[[ + if world.event.S_EVENT_KILL and world.event.S_EVENT_KILL>0 and airbaseCategory==Airbase.Category.AIRDROME then + + -- We have to take the key value of this loop! + airbaseID=DCSAirbaseId + + -- Now another quirk: for Caucasus, we need to add 11 to the key value to get the correct ID. See https://forums.eagle.ru/showpost.php?p=4210774&postcount=11 + if UTILS.GetDCSMap()==DCSMAP.Caucasus then + airbaseID=airbaseID+11 + end + end + ]] + + if AirbaseName==self.AirbaseName then + if airbaseCategory==Airbase.Category.SHIP then + -- Ships get a negative sign as their unit number might be the same as the ID of another airbase. + return unique and -airbaseID or airbaseID + else + return airbaseID + end + end + + end + + end + + return nil +end + --- Returns a table of parking data for a given airbase. If the optional parameter *available* is true only available parking will be returned, otherwise all parking at the base is returned. Term types have the following enumerated values: -- @@ -490,7 +612,7 @@ function AIRBASE:GetParkingSpotsCoordinates(termtype) -- Put coordinates of free spots into table. local spots={} - for _,parkingspot in pairs(parkingdata) do + for _,parkingspot in ipairs(parkingdata) do -- Coordinates on runway are not returned unless explicitly requested. if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then @@ -533,12 +655,15 @@ function AIRBASE:GetParkingSpotsTable(termtype) local spots={} for _,_spot in pairs(parkingdata) do if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) then + self:T2({_spot=_spot}) local _free=_isfree(_spot) local _coord=COORDINATE:NewFromVec3(_spot.vTerminalPos) table.insert(spots, {Coordinate=_coord, TerminalID=_spot.Term_Index, TerminalType=_spot.Term_Type, TOAC=_spot.TO_AC, Free=_free, TerminalID0=_spot.Term_Index_0, DistToRwy=_spot.fDistToRW}) end end + self:T2({ spots = spots } ) + return spots end @@ -555,7 +680,7 @@ function AIRBASE:GetFreeParkingSpotsTable(termtype, allowTOAC) -- Put coordinates of free spots into table. local freespots={} for _,_spot in pairs(parkingfree) do - if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) then + if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) and _spot.Term_Index>0 then if (allowTOAC and allowTOAC==true) or _spot.TO_AC==false then local _coord=COORDINATE:NewFromVec3(_spot.vTerminalPos) table.insert(freespots, {Coordinate=_coord, TerminalID=_spot.Term_Index, TerminalType=_spot.Term_Type, TOAC=_spot.TO_AC, Free=true, TerminalID0=_spot.Term_Index_0, DistToRwy=_spot.fDistToRW}) @@ -566,6 +691,31 @@ function AIRBASE:GetFreeParkingSpotsTable(termtype, allowTOAC) return freespots end +--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase. +-- @param #AIRBASE self +-- @param #number TerminalID The terminal ID of the parking spot. +-- @return #AIRBASE.ParkingSpot Table free parking spots. Table has the elements ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". +function AIRBASE:GetParkingSpotData(TerminalID) + self:F({TerminalID=TerminalID}) + + -- Get parking data. + local parkingdata=self:GetParkingSpotsTable() + + -- Debug output. + self:T2({parkingdata=parkingdata}) + + for _,_spot in pairs(parkingdata) do + local spot=_spot --#AIRBASE.ParkingSpot + self:T({TerminalID=spot.TerminalID,TerminalType=spot.TerminalType}) + if TerminalID==spot.TerminalID then + return spot + end + end + + self:E("ERROR: Could not find spot with Terminal ID="..tostring(TerminalID)) + return nil +end + --- Place markers of parking spots on the F10 map. -- @param #AIRBASE self -- @param #AIRBASE.TerminalType termtype Terminal type for which marks should be placed. @@ -632,31 +782,20 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, verysafe=false end - -- Get the size of an object. - local function _GetObjectSize(unit,mooseobject) - if mooseobject then - unit=unit:GetDCSObject() - end - if unit and unit:isExist() then - local DCSdesc=unit:getDesc() - if DCSdesc.box then - local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x) - 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) - return math.max(x,z), x , y, z - end - end - return 0,0,0,0 - end - -- Function calculating the overlap of two (square) objects. - local function _overlap(object1, mooseobject1, object2, mooseobject2, dist) - local l1=_GetObjectSize(object1, mooseobject1) - local l2=_GetObjectSize(object2, mooseobject2) - local safedist=(l1/2+l2/2)*1.1 - local safe = (dist > safedist) - self:T3(string.format("l1=%.1f l2=%.1f s=%.1f d=%.1f ==> safe=%s", l1,l2,safedist,dist,tostring(safe))) - return safe + local function _overlap(object1, object2, dist) + local pos1=object1 --Wrapper.Positionable#POSITIONABLE + local pos2=object2 --Wrapper.Positionable#POSITIONABLE + local r1=pos1:GetBoundingRadius() + local r2=pos2:GetBoundingRadius() + if r1 and r2 then + local safedist=(r1+r2)*1.1 + local safe = (dist > safedist) + self:T2(string.format("r1=%.1f r2=%.1f s=%.1f d=%.1f ==> safe=%s", r1, r2, safedist, dist, tostring(safe))) + return safe + else + return true + end end -- Get airport name. @@ -671,7 +810,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Get the aircraft size, i.e. it's longest side of x,z. local aircraft=group:GetUnit(1) - local _aircraftsize, ax,ay,az=_GetObjectSize(aircraft, true) + local _aircraftsize, ax,ay,az=aircraft:GetObjectSize() -- Number of spots we are looking for. Note that, e.g. grouping can require a number different from the group size! local _nspots=nspots or group:GetSize() @@ -699,13 +838,15 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE local _termid=parkingspot.TerminalID + self:T2({_termid=_termid}) + if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) then -- Very safe uses the DCS getParking() info to check if a spot is free. Unfortunately, the function returns free=false until the aircraft has actually taken-off. if verysafe and (parkingspot.Free==false or parkingspot.TOAC==true) then -- DCS getParking() routine returned that spot is not free. - self:E(string.format("%s: Parking spot id %d NOT free (or aircraft has not taken off yet). Free=%s, TOAC=%s.", airport, parkingspot.TerminalID, tostring(parkingspot.Free), tostring(parkingspot.TOAC))) + self:T(string.format("%s: Parking spot id %d NOT free (or aircraft has not taken off yet). Free=%s, TOAC=%s.", airport, parkingspot.TerminalID, tostring(parkingspot.Free), tostring(parkingspot.TOAC))) else @@ -717,16 +858,13 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Check all units. for _,unit in pairs(_units) do - -- Unis are now returned as MOOSE units not DCS units! - --local _vec3=unit:getPoint() - --local _coord=COORDINATE:NewFromVec3(_vec3) local _coord=unit:GetCoordinate() local _dist=_coord:Get2DDistance(_spot) - local _safe=_overlap(aircraft, true, unit, true,_dist) + local _safe=_overlap(aircraft, unit, _dist) if markobstacles then - local l,x,y,z=_GetObjectSize(unit) - _coord:MarkToAll(string.format("Unit %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", unit:getName(),x,y,z,l,_dist, _termid, tostring(_safe))) + local l,x,y,z=unit:GetObjectSize() + _coord:MarkToAll(string.format("Unit %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", unit:GetName(),x,y,z,l,_dist, _termid, tostring(_safe))) end if scanunits and not _safe then @@ -736,13 +874,14 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Check all statics. for _,static in pairs(_statics) do + local _static=STATIC:Find(static) local _vec3=static:getPoint() local _coord=COORDINATE:NewFromVec3(_vec3) local _dist=_coord:Get2DDistance(_spot) - local _safe=_overlap(aircraft, true, static, false,_dist) + local _safe=_overlap(aircraft,_static,_dist) if markobstacles then - local l,x,y,z=_GetObjectSize(static) + local l,x,y,z=_static:GetObjectSize() _coord:MarkToAll(string.format("Static %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", static:getName(),x,y,z,l,_dist, _termid, tostring(_safe))) end @@ -753,13 +892,14 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Check all scenery. for _,scenery in pairs(_sceneries) do + local _scenery=SCENERY:Register(scenery:getTypeName(), scenery) local _vec3=scenery:getPoint() local _coord=COORDINATE:NewFromVec3(_vec3) local _dist=_coord:Get2DDistance(_spot) - local _safe=_overlap(aircraft, true, scenery, false,_dist) + local _safe=_overlap(aircraft,_scenery,_dist) if markobstacles then - local l,x,y,z=_GetObjectSize(scenery) + local l,x,y,z=scenery:GetObjectSize(scenery) _coord:MarkToAll(string.format("Scenery %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", scenery:getTypeName(),x,y,z,l,_dist, _termid, tostring(_safe))) end @@ -771,7 +911,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Now check the already given spots so that we do not put a large aircraft next to one we already assigned a nearby spot. for _,_takenspot in pairs(validspots) do local _dist=_takenspot.Coordinate:Get2DDistance(_spot) - local _safe=_overlap(aircraft, true, aircraft, true,_dist) + local _safe=_overlap(aircraft, aircraft, _dist) if not _safe then occupied=true end @@ -779,13 +919,14 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, --_spot:MarkToAll(string.format("Parking spot %d free=%s", parkingspot.TerminalID, tostring(not occupied))) if occupied then - self:T(string.format("%s: Parking spot id %d occupied.", airport, _termid)) + self:I(string.format("%s: Parking spot id %d occupied.", airport, _termid)) else - self:E(string.format("%s: Parking spot id %d free.", airport, _termid)) + self:I(string.format("%s: Parking spot id %d free.", airport, _termid)) if nvalid<_nspots then table.insert(validspots, {Coordinate=_spot, TerminalID=_termid}) end nvalid=nvalid+1 + self:I(string.format("%s: Parking spot id %d free. Nfree=%d/%d.", airport, _termid, nvalid,_nspots)) end end -- loop over units @@ -813,7 +954,7 @@ function AIRBASE:CheckOnRunWay(group, radius, despawn) radius=radius or 50 -- We only check at real airbases (not FARPS or ships). - if self:GetDesc().category~=Airbase.Category.AIRDROME then + if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME then return false end @@ -878,6 +1019,22 @@ function AIRBASE:CheckOnRunWay(group, radius, despawn) return false end +--- Get category of airbase. +-- @param #AIRBASE self +-- @return #number Category of airbase from GetDesc().category. +function AIRBASE:GetAirbaseCategory() + local desc=self:GetDesc() + local category=Airbase.Category.AIRDROME + + if desc and desc.category then + category=desc.category + else + self:E(string.format("ERROR: Cannot get category of airbase %s due to DCS 2.5.6 bug! Assuming it is an AIRDROME for now...", tostring(self.AirbaseName))) + end + return category +end + + --- Helper function to check for the correct terminal type including "artificial" ones. -- @param #number Term_Type Termial type from getParking routine. -- @param #AIRBASE.TerminalType termtype Terminal type from AIRBASE.TerminalType enumerator. @@ -922,4 +1079,186 @@ function AIRBASE._CheckTerminalType(Term_Type, termtype) end return match -end \ No newline at end of file +end + +--- Get runways data. Only for airdromes! +-- @param #AIRBASE self +-- @param #number magvar (Optional) Magnetic variation in degrees. +-- @param #boolean mark (Optional) Place markers with runway data on F10 map. +-- @return #table Runway data. +function AIRBASE:GetRunwayData(magvar, mark) + + -- Runway table. + local runways={} + + if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME then + return {} + end + + -- Get spawn points on runway. + local runwaycoords=self:GetParkingSpotsCoordinates(AIRBASE.TerminalType.Runway) + + -- Magnetic declination. + magvar=magvar or UTILS.GetMagneticDeclination() + + local N=#runwaycoords + local dN=2 + local ex=false + + local name=self:GetName() + if name==AIRBASE.Nevada.Jean_Airport or + name==AIRBASE.Nevada.Creech_AFB or + name==AIRBASE.PersianGulf.Abu_Dhabi_International_Airport or + name==AIRBASE.PersianGulf.Dubai_Intl or + name==AIRBASE.PersianGulf.Shiraz_International_Airport or + name==AIRBASE.PersianGulf.Kish_International_Airport then + + N=#runwaycoords/2 + dN=1 + ex=true + end + + + for i=1,N,dN do + + local j=i+1 + if ex then + --j=N+i + j=#runwaycoords-i+1 + end + + -- Coordinates of the two runway points. + local c1=runwaycoords[i] --Core.Point#COORDINATES + local c2=runwaycoords[j] --Core.Point#COORDINATES + + -- Heading of runway. + local hdg=c1:HeadingTo(c2) + + -- Runway ID: heading=070° ==> idx="07" + local idx=string.format("%02d", UTILS.Round((hdg-magvar)/10, 0)) + + -- Runway table. + local runway={} --#AIRBASE.Runway + runway.heading=hdg + runway.idx=idx + runway.length=c1:Get2DDistance(c2) + runway.position=c1 + runway.endpoint=c2 + + -- Debug info. + self:T(string.format("Airbase %s: Adding runway id=%s, heading=%03d, length=%d m", self:GetName(), runway.idx, runway.heading, runway.length)) + + -- Debug mark + if mark then + runway.position:MarkToAll(string.format("Runway %s: true heading=%03d (magvar=%d), length=%d m", runway.idx, runway.heading, magvar, runway.length)) + end + + -- Add runway. + table.insert(runways, runway) + + end + + -- Get inverse runways + local inverse={} + for _,_runway in pairs(runways) do + local r=_runway --#AIRBASE.Runway + + local runway={} --#AIRBASE.Runway + runway.heading=r.heading-180 + if runway.heading<0 then + runway.heading=runway.heading+360 + end + runway.idx=string.format("%02d", math.max(0, UTILS.Round((runway.heading-magvar)/10, 0))) + runway.length=r.length + runway.position=r.endpoint + runway.endpoint=r.position + + -- Debug info. + self:T(string.format("Airbase %s: Adding runway id=%s, heading=%03d, length=%d m", self:GetName(), runway.idx, runway.heading, runway.length)) + + -- Debug mark + if mark then + runway.position:MarkToAll(string.format("Runway %s: true heading=%03d (magvar=%d), length=%d m", runway.idx, runway.heading, magvar, runway.length)) + end + + -- Add runway. + table.insert(inverse, runway) + end + + -- Add inverse runway. + for _,runway in pairs(inverse) do + table.insert(runways, runway) + end + + return runways +end + +--- Set the active runway in case it cannot be determined by the wind direction. +-- @param #AIRBASE self +-- @param #number iactive Number of the active runway in the runway data table. +function AIRBASE:SetActiveRunway(iactive) + self.activerwyno=iactive +end + +--- Get the active runway based on current wind direction. +-- @param #AIRBASE self +-- @param #number magvar (Optional) Magnetic variation in degrees. +-- @return #AIRBASE.Runway Active runway data table. +function AIRBASE:GetActiveRunway(magvar) + + -- Get runways data (initialize if necessary). + local runways=self:GetRunwayData(magvar) + + -- Return user forced active runway if it was set. + if self.activerwyno then + return runways[self.activerwyno] + end + + -- Get wind vector. + local Vwind=self:GetCoordinate():GetWindWithTurbulenceVec3() + local norm=UTILS.VecNorm(Vwind) + + -- Active runway number. + local iact=1 + + -- Check if wind is blowing (norm>0). + if norm>0 then + + -- Normalize wind (not necessary). + Vwind.x=Vwind.x/norm + Vwind.y=0 + Vwind.z=Vwind.z/norm + + -- Loop over runways. + local dotmin=nil + for i,_runway in pairs(runways) do + local runway=_runway --#AIRBASE.Runway + + -- Angle in rad. + local alpha=math.rad(runway.heading) + + -- Runway vector. + local Vrunway={x=math.cos(alpha), y=0, z=math.sin(alpha)} + + -- Dot product: parallel component of the two vectors. + local dot=UTILS.VecDot(Vwind, Vrunway) + + -- Debug. + --env.info(string.format("runway=%03d° dot=%.3f", runway.heading, dot)) + + -- New min? + if dotmin==nil or dot setting this to trace. + self:T( { ControllableName = self:GetName(), DCSTask = DCSTask } ) else BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } ) end @@ -391,6 +408,8 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime ) if not WaitTime or WaitTime == 0 then SetTask( self, DCSTask ) + -- See above. + self:T( { ControllableName = self:GetName(), DCSTask = DCSTask } ) else self.TaskScheduler:Schedule( self, SetTask, { DCSTask }, WaitTime ) end @@ -422,24 +441,23 @@ end --- Return a condition section for a controlled task. -- @param #CONTROLLABLE self --- @param DCS#Time time --- @param #string userFlag --- @param #boolean userFlagValue --- @param #string condition --- @param DCS#Time duration --- @param #number lastWayPoint +-- @param DCS#Time time DCS mission time. +-- @param #string userFlag Name of the user flag. +-- @param #boolean userFlagValue User flag value *true* or *false*. Could also be numeric, i.e. either 0=*false* or 1=*true*. Other numeric values don't work! +-- @param #string condition Lua string. +-- @param DCS#Time duration Duration in seconds. +-- @param #number lastWayPoint Last waypoint. -- return DCS#Task function CONTROLLABLE:TaskCondition( time, userFlag, userFlagValue, condition, duration, lastWayPoint ) - self:F2( { time, userFlag, userFlagValue, condition, duration, lastWayPoint } ) --[[ - StopCondition = { - time = Time, - userFlag = string, - userFlagValue = boolean, - condition = string, - duration = Time, - lastWaypoint = number, + StopCondition = { + time = Time, + userFlag = string, + userFlagValue = boolean, + condition = string, + duration = Time, + lastWaypoint = number, } --]] @@ -451,7 +469,6 @@ function CONTROLLABLE:TaskCondition( time, userFlag, userFlagValue, condition, d DCSStopCondition.duration = duration DCSStopCondition.lastWayPoint = lastWayPoint - self:T3( { DCSStopCondition } ) return DCSStopCondition end @@ -461,11 +478,8 @@ end -- @param DCS#DCSStopCondition DCSStopCondition -- @return DCS#Task function CONTROLLABLE:TaskControlled( DCSTask, DCSStopCondition ) - self:F2( { DCSTask, DCSStopCondition } ) - local DCSTaskControlled - - DCSTaskControlled = { + local DCSTaskControlled = { id = 'ControlledTask', params = { task = DCSTask, @@ -473,7 +487,6 @@ function CONTROLLABLE:TaskControlled( DCSTask, DCSStopCondition ) } } - self:T3( { DCSTaskControlled } ) return DCSTaskControlled end @@ -482,22 +495,14 @@ end -- @param DCS#TaskArray DCSTasks Array of @{DCSTasking.Task#Task} -- @return DCS#Task function CONTROLLABLE:TaskCombo( DCSTasks ) - self:F2( { DCSTasks } ) - local DCSTaskCombo - - DCSTaskCombo = { + local DCSTaskCombo = { id = 'ComboTask', params = { tasks = DCSTasks } } - for TaskID, Task in ipairs( DCSTasks ) do - self:T( Task ) - end - - self:T3( { DCSTaskCombo } ) return DCSTaskCombo end @@ -506,11 +511,8 @@ end -- @param DCS#Command DCSCommand -- @return DCS#Task function CONTROLLABLE:TaskWrappedAction( DCSCommand, Index ) - self:F2( { DCSCommand } ) - local DCSTaskWrappedAction - - DCSTaskWrappedAction = { + local DCSTaskWrappedAction = { id = "WrappedAction", enabled = true, number = Index or 1, @@ -520,7 +522,6 @@ function CONTROLLABLE:TaskWrappedAction( DCSCommand, Index ) }, } - self:T3( { DCSTaskWrappedAction } ) return DCSTaskWrappedAction end @@ -540,9 +541,9 @@ end ---- Executes a command action +--- Executes a command action for the CONTROLLABLE. -- @param #CONTROLLABLE self --- @param DCS#Command DCSCommand +-- @param DCS#Command DCSCommand The command to be executed. -- @return #CONTROLLABLE self function CONTROLLABLE:SetCommand( DCSCommand ) self:F2( DCSCommand ) @@ -566,14 +567,14 @@ end -- @usage -- --- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class. -- HeliGroup = GROUP:FindByName( "Helicopter" ) --- +-- -- --- Route the helicopter back to the FARP after 60 seconds. -- -- We use the SCHEDULER class to do this. -- SCHEDULER:New( nil, -- function( HeliGroup ) -- local CommandRTB = HeliGroup:CommandSwitchWayPoint( 2, 8 ) -- HeliGroup:SetCommand( CommandRTB ) --- end, { HeliGroup }, 90 +-- end, { HeliGroup }, 90 -- ) function CONTROLLABLE:CommandSwitchWayPoint( FromWayPoint, ToWayPoint ) self:F2( { FromWayPoint, ToWayPoint } ) @@ -594,11 +595,11 @@ end -- Use the result in the method @{#CONTROLLABLE.SetCommand}(). -- A value of true will make the ground group stop, a value of false will make it continue. -- Note that this can only work on GROUP level, although individual UNITs can be commanded, the whole GROUP will react. --- --- Example missions: --- +-- +-- Example missions: +-- -- * GRP-310 --- +-- -- @param #CONTROLLABLE self -- @param #boolean StopRoute true if the ground unit needs to stop, false if it needs to continue to move. -- @return DCS#Task @@ -623,28 +624,239 @@ end -- @return #CONTROLLABLE self function CONTROLLABLE:StartUncontrolled(delay) if delay and delay>0 then - SCHEDULER:New(nil, CONTROLLABLE.StartUncontrolled, {self}, delay) + SCHEDULER:New(nil, CONTROLLABLE.StartUncontrolled, {self}, delay) else self:SetCommand({id='Start', params={}}) end return self end --- TASKS FOR AIR CONTROLLABLES +--- Give the CONTROLLABLE the command to activate a beacon. See [DCS_command_activateBeacon](https://wiki.hoggitworld.com/view/DCS_command_activateBeacon) on Hoggit. +-- For specific beacons like TACAN use the more convenient @{#BEACON} class. +-- Note that a controllable can only have one beacon activated at a time with the execption of ICLS. +-- @param #CONTROLLABLE self +-- @param Core.Radio#BEACON.Type Type Beacon type (VOR, DME, TACAN, RSBN, ILS etc). +-- @param Core.Radio#BEACON.System System Beacon system (VOR, DME, TACAN, RSBN, ILS etc). +-- @param #number Frequency Frequency in Hz the beacon is running on. Use @{#UTILS.TACANToFrequency} to generate a frequency for TACAN beacons. +-- @param #number UnitID The ID of the unit the beacon is attached to. Usefull if more units are in one group. +-- @param #number Channel Channel the beacon is using. For, e.g. TACAN beacons. +-- @param #string ModeChannel The TACAN mode of the beacon, i.e. "X" or "Y". +-- @param #boolean AA If true, create and Air-Air beacon. IF nil, automatically set if CONTROLLABLE depending on whether unit is and aircraft or not. +-- @param #string Callsign Morse code identification callsign. +-- @param #boolean Bearing If true, beacon provides bearing information - if supported by the unit the beacon is attached to. +-- @param #number Delay (Optional) Delay in seconds before the beacon is activated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing, Delay) + AA=AA or self:IsAir() + UnitID=UnitID or self:GetID() + + -- Command + local CommandActivateBeacon= { + id = "ActivateBeacon", + params = { + ["type"] = Type, + ["system"] = System, + ["frequency"] = Frequency, + ["unitId"] = UnitID, + ["channel"] = Channel, + ["modeChannel"] = ModeChannel, + ["AA"] = AA, + ["callsign"] = Callsign, + ["bearing"] = Bearing, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateBeacon, {self, Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing}, Delay) + else + self:SetCommand(CommandActivateBeacon) + end + + return self +end + +--- Activate ICLS system of the CONTROLLABLE. The controllable should be an aircraft carrier! +-- @param #CONTROLLABLE self +-- @param #number Channel ICLS channel. +-- @param #number UnitID The ID of the unit the ICLS system is attached to. Useful if more units are in one group. +-- @param #string Callsign Morse code identification callsign. +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateICLS(Channel, UnitID, Callsign, Delay) + + -- Command to activate ICLS system. + local CommandActivateICLS= { + id = "ActivateICLS", + params= { + ["type"] = BEACON.Type.ICLS, + ["channel"] = Channel, + ["unitId"] = UnitID, + ["callsign"] = Callsign, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateICLS, {self}, Delay) + else + self:SetCommand(CommandActivateICLS) + end + + return self +end + + +--- Deactivate the active beacon of the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the beacon is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateBeacon(Delay) + + -- Command to deactivate + local CommandDeactivateBeacon={id='DeactivateBeacon', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateBeacon, {self}, Delay) + else + self:SetCommand(CommandDeactivateBeacon) + end + + return self +end + +--- Deactivate the ICLS of the CONTROLLABLE. +-- @param #CONTROLLABLE self +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandDeactivateICLS(Delay) + + -- Command to deactivate + local CommandDeactivateICLS={id='DeactivateICLS', params={}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandDeactivateICLS, {self}, Delay) + else + self:SetCommand(CommandDeactivateICLS) + end + + return self +end + +--- Set callsign of the CONTROLLABLE. See [DCS command setCallsign](https://wiki.hoggitworld.com/view/DCS_command_setCallsign) +-- @param #CONTROLLABLE self +-- @param DCS#CALLSIGN CallName Number corresponding the the callsign identifier you wish this group to be called. +-- @param #number CallNumber The number value the group will be referred to as. Only valid numbers are 1-9. For example Uzi **5**-1. Default 1. +-- @param #number Delay (Optional) Delay in seconds before the callsign is set. Default is immediately. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandSetCallsign(CallName, CallNumber, Delay) + + -- Command to set the callsign. + local CommandSetCallsign={id='SetCallsign', params={callname=CallName, number=CallNumber or 1}} + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandSetCallsign, {self, CallName, CallNumber}, Delay) + else + self:SetCommand(CommandSetCallsign) + end + + return self +end + +--- Set EPLRS of the CONTROLLABLE on/off. See [DCS command EPLRS](https://wiki.hoggitworld.com/view/DCS_command_eplrs) +-- @param #CONTROLLABLE self +-- @param #boolean SwitchOnOff If true (or nil) switch EPLRS on. If false switch off. +-- @param #number Delay (Optional) Delay in seconds before the callsign is set. Default is immediately. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandEPLRS(SwitchOnOff, Delay) + + if SwitchOnOff==nil then + SwitchOnOff=true + end + + -- Command to set the callsign. + local CommandEPLRS={ + id='EPLRS', + params={ + value=SwitchOnOff, + groupId=self:GetID() + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandEPLRS, {self, SwitchOnOff}, Delay) + else + self:T(string.format("EPLRS=%s for controllable %s (id=%s)", tostring(SwitchOnOff), tostring(self:GetName()), tostring(self:GetID()))) + self:SetCommand(CommandEPLRS) + end + + return self +end + +--- Set radio frequency. See [DCS command EPLRS](https://wiki.hoggitworld.com/view/DCS_command_setFrequency) +-- @param #CONTROLLABLE self +-- @param #number Frequency Radio frequency in MHz. +-- @param #number Modulation Radio modulation. Default `radio.modulation.AM`. +-- @param #number Delay (Optional) Delay in seconds before the frequncy is set. Default is immediately. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandSetFrequency(Frequency, Modulation, Delay) + + local CommandSetFrequency = { + id = 'SetFrequency', + params = { + frequency = Frequency, + modulation = Modulation or radio.modulation.AM, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandSetFrequency, {self, Frequency, Modulation}, Delay) + else + self:SetCommand(CommandSetFrequency) + end + + return self +end + + +--- Set EPLRS data link on/off. +-- @param #CONTROLLABLE self +-- @param #boolean SwitchOnOff If true (or nil) switch EPLRS on. If false switch off. +-- @param #number idx Task index. Default 1. +-- @return #table Task wrapped action. +function CONTROLLABLE:TaskEPLRS(SwitchOnOff, idx) + + if SwitchOnOff==nil then + SwitchOnOff=true + end + + -- Command to set the callsign. + local CommandEPLRS={ + id='EPLRS', + params={ + value=SwitchOnOff, + groupId=self:GetID() + } + } + + return self:TaskWrappedAction(CommandEPLRS, idx or 1) +end + + +-- TASKS FOR AIR CONTROLLABLES --- (AIR) Attack a Controllable. -- @param #CONTROLLABLE self --- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. +-- @param Wrapper.Group#GROUP AttackGroup The Group to be attacked. -- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. -- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. -- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. -- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. -- @param DCS#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. -- @param #boolean AttackQtyLimit (optional) The flag determines how to interpret attackQty parameter. If the flag is true then attackQty is a limit on maximal attack quantity for "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. +-- @param #boolean GroupAttack (Optional) If true, attack as group. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) +function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit, GroupAttack ) + --self:F2( { self.ControllableName, AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) -- AttackGroup = { -- id = 'AttackGroup', @@ -661,74 +873,58 @@ function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, At -- } -- } - local DirectionEnabled = nil - if Direction then - DirectionEnabled = true - end - local AltitudeEnabled = nil - if Altitude then - AltitudeEnabled = true - end - - local DCSTask - DCSTask = { id = 'AttackGroup', + local DCSTask = { id = 'AttackGroup', params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - directionEnabled = DirectionEnabled, - direction = Direction, - altitudeEnabled = AltitudeEnabled, - altitude = Altitude, - attackQtyLimit = AttackQtyLimit, + groupId = AttackGroup:GetID(), + weaponType = WeaponType or 1073741822, + expend = WeaponExpend or "Auto", + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty or 1, + directionEnabled = Direction and true or false, + direction = Direction and math.rad(Direction) or 0, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + groupAttack = GroupAttack and true or false, }, - }, + } - self:T3( { DCSTask } ) return DCSTask end --- (AIR) Attack the Unit. -- @param #CONTROLLABLE self --- @param Wrapper.Unit#UNIT AttackUnit The UNIT. --- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #number Altitude (optional) The altitude from where to attack. --- @param #boolean Visible (optional) not a clue. --- @param #number WeaponType (optional) The WeaponType. +-- @param Wrapper.Unit#UNIT AttackUnit The UNIT to be attacked +-- @param #boolean GroupAttack (Optional) If true, all units in the group will attack the Unit when found. Default false. +-- @param DCS#AI.Task.WeaponExpend WeaponExpend (Optional) Determines how many weapons will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param #number AttackQty (Optional) Limits maximal quantity of attack. The aicraft/controllable will not make more attacks than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param DCS#Azimuth Direction (Optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. +-- @param #number Altitude (Optional) The (minimum) altitude in meters from where to attack. Default is altitude of unit to attack but at least 1000 m. +-- @param #number WeaponType (optional) The WeaponType. See [DCS Enumerator Weapon Type](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag) on Hoggit. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskAttackUnit( AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, WeaponType ) - self:F2( { self.ControllableName, AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, WeaponType } ) +function CONTROLLABLE:TaskAttackUnit(AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType) - local DCSTask - DCSTask = { + local DCSTask = { id = 'AttackUnit', params = { - unitId = AttackUnit:GetID(), - groupAttack = GroupAttack or false, - visible = Visible or false, - expend = WeaponExpend or "Auto", + unitId = AttackUnit:GetID(), + groupAttack = GroupAttack and GroupAttack or false, + expend = WeaponExpend or "Auto", directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude or 30, - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, - weaponType = WeaponType + direction = Direction and math.rad(Direction) or 0, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, + weaponType = WeaponType or 1073741822, } } - - self:T3( DCSTask ) return DCSTask end ---- (AIR) Delivering weapon at the point on the ground. +--- (AIR) Delivering weapon at the point on the ground. -- @param #CONTROLLABLE self -- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. -- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. @@ -740,141 +936,289 @@ end -- @param #boolean Divebomb (optional) Perform dive bombing. Default false. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, Divebomb ) - self:E( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, Divebomb } ) - - local _groupattack=false - if GroupAttack then - _groupattack=GroupAttack - end - - local _direction=0 - local _directionenabled=false - if Direction then - _direction=math.rad(Direction) - _directionenabled=true - end - - local _altitude=5000 - local _altitudeenabled=false - if Altitude then - _altitude=Altitude - _altitudeenabled=true - end - - local _attacktype=nil - if Divebomb then - _attacktype="Dive" - end - - local DCSTask - DCSTask = { + local DCSTask = { id = 'Bombing', params = { - x = Vec2.x, - y = Vec2.y, - groupAttack = _groupattack, - expend = WeaponExpend or "Auto", - attackQtyLimit = false, --AttackQty and true or false, - attackQty = AttackQty or 1, - directionEnabled = _directionenabled, - direction = _direction, - altitudeEnabled = _altitudeenabled, - altitude = _altitude, - weaponType = WeaponType, - --attackType=_attacktype, + point = Vec2, + x = Vec2.x, + y = Vec2.y, + groupAttack = GroupAttack and GroupAttack or false, + expend = WeaponExpend or "Auto", + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty or 1, + directionEnabled = Direction and true or false, + direction = Direction and math.rad(Direction) or 0, + altitudeEnabled = Altitude and true or false, + altitude = Altitude or 2000, + weaponType = WeaponType or 1073741822, + attackType = Divebomb and "Dive" or nil, }, } - self:E( { TaskBombing=DCSTask } ) return DCSTask end ---- (AIR) Attacking the map object (building, structure, e.t.c). +--- (AIR) Attacking the map object (building, structure, etc). +-- @param #CONTROLLABLE self +-- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. +-- @param #boolean GroupAttack (Optional) If true, all units in the group will attack the Unit when found. +-- @param DCS#AI.Task.WeaponExpend WeaponExpend (Optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit will choose expend on its own discretion. +-- @param #number AttackQty (Optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param DCS#Azimuth Direction (Optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param #number Altitude (Optional) The altitude [meters] from where to attack. Default 30 m. +-- @param #number WeaponType (Optional) The WeaponType. Default Auto=1073741822. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskAttackMapObject( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType ) + + local DCSTask = { + id = 'AttackMapObject', + params = { + point = Vec2, + x = Vec2.x, + y = Vec2.y, + groupAttack = GroupAttack or false, + expend = WeaponExpend or "Auto", + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, + directionEnabled = Direction and true or false, + direction = Direction and math.rad(Direction) or 0, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + weaponType = WeaponType or 1073741822, + }, + } + + return DCSTask +end + + +--- (AIR) Delivering weapon via CarpetBombing (all bombers in formation release at same time) at the point on the ground. -- @param #CONTROLLABLE self -- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. -- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit will choose expend on its own discretion. -- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. -- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. -- @param #number Altitude (optional) The altitude from where to attack. -- @param #number WeaponType (optional) The WeaponType. +-- @param #number CarpetLength (optional) default to 500 m. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskAttackMapObject( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType ) - self:F2( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType } ) +function CONTROLLABLE:TaskCarpetBombing(Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, CarpetLength) - local DCSTask - DCSTask = { - id = 'AttackMapObject', + -- Build Task Structure + local DCSTask = { + id = 'CarpetBombing', params = { - point = Vec2, - groupAttack = GroupAttack or false, - expend = WeaponExpend or "Auto", - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, + attackType = "Carpet", + x = Vec2.x, + y = Vec2.y, + groupAttack = GroupAttack and GroupAttack or false, + carpetLength = CarpetLength or 500, + weaponType = WeaponType or ENUMS.WeaponFlag.AnyBomb, + expend = WeaponExpend or "All", + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty or 1, directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude or 30, - weaponType = WeaponType, - }, - }, + direction = Direction and math.rad(Direction) or 0, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + } + } - self:T3( { DCSTask } ) return DCSTask end + +--- (AIR) Following another airborne controllable. +-- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. +-- Used to support CarpetBombing Task +-- @param #CONTROLLABLE self +-- @param #CONTROLLABLE FollowControllable The controllable to be followed. +-- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. +-- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskFollowBigFormation(FollowControllable, Vec3, LastWaypointIndex ) + + local DCSTask = { + id = 'FollowBigFormation', + params = { + groupId = FollowControllable:GetID(), + pos = Vec3, + lastWptIndexFlag = LastWaypointIndex and true or false, + lastWptIndex = LastWaypointIndex + } + } + + return DCSTask +end + + +--- (AIR HELICOPTER) Move the controllable to a Vec2 Point, wait for a defined duration and embark infantry groups. +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coordinate The point where to pickup the troops. +-- @param Core.Set#SET_GROUP GroupSetForEmbarking Set of groups to embark. +-- @param #number Duration (Optional) The maximum duration in seconds to wait until all groups have embarked. +-- @param #table Distribution (Optional) Distribution used to put the infantry groups into specific carrier units. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskEmbarking(Coordinate, GroupSetForEmbarking, Duration, Distribution) + + -- Table of group IDs for embarking. + local g4e={} + + if GroupSetForEmbarking then + for _,_group in pairs(GroupSetForEmbarking:GetSet()) do + local group=_group --Wrapper.Group#GROUP + table.insert(g4e, group:GetID()) + end + else + self:E("ERROR: No groups for embarking specified!") + return nil + end + + -- Table of group IDs for embarking. + --local Distribution={} + + -- Distribution + --local distribution={} + --distribution[id]=gids + + local groupID=self and self:GetID() + + local DCSTask = { + id = 'Embarking', + params = { + selectedTransport = groupID, + x = Coordinate.x, + y = Coordinate.z, + groupsForEmbarking = g4e, + durationFlag = Duration and true or false, + duration = Duration, + distributionFlag = Distribution and true or false, + distribution = Distribution, + } + } + + return DCSTask +end + + +--- Used in conjunction with the embarking task for a transport helicopter group. The Ground units will move to the specified location and wait to be picked up by a helicopter. +-- The helicopter will then fly them to their dropoff point defined by another task for the ground forces; DisembarkFromTransport task. +-- The controllable has to be an infantry group! +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. +-- @param #number Radius Radius in meters. Default 200 m. +-- @param #string UnitType The unit type name of the carrier, e.g. "UH-1H". Must not be specified. +-- @return DCS#Task Embark to transport task. +function CONTROLLABLE:TaskEmbarkToTransport(Coordinate, Radius, UnitType) + + local EmbarkToTransport = { + id = "EmbarkToTransport", + params={ + x = Coordinate.x, + y = Coordinate.z, + zoneRadius = Radius or 200, + selectedType = UnitType, + } + } + + return EmbarkToTransport +end + + +--- Specifies the location infantry groups that is being transported by helicopters will be unloaded at. Used in conjunction with the EmbarkToTransport task. +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up. +-- @return DCS#Task Embark to transport task. +function CONTROLLABLE:TaskDisembarking(Coordinate, GroupSetToDisembark) + + -- Table of group IDs for disembarking. + local g4e={} + + if GroupSetToDisembark then + for _,_group in pairs(GroupSetToDisembark:GetSet()) do + local group=_group --Wrapper.Group#GROUP + table.insert(g4e, group:GetID()) + end + else + self:E("ERROR: No groups for disembarking specified!") + return nil + end + + local Disembarking={ + id = "Disembarking", + params = { + x = Coordinate.x, + y = Coordinate.z, + groupsForEmbarking = g4e, -- This is no bug, the entry is really "groupsForEmbarking" even if we disembark the troops. + } + } + + return Disembarking +end + + --- (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. -- @param #CONTROLLABLE self -- @param DCS#Vec2 Point The point to hold the position. --- @param #number Altitude The altitude [m] to hold the position. +-- @param #number Altitude The altitude AGL in meters to hold the position. -- @param #number Speed The speed [m/s] flying when holding the position. -- @return #CONTROLLABLE self function CONTROLLABLE:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) self:F2( { self.ControllableName, Point, Altitude, Speed } ) - -- pattern = enum AI.Task.OribtPattern, - -- point = Vec2, - -- point2 = Vec2, - -- speed = Distance, - -- altitude = Distance - - local LandHeight = land.getHeight( Point ) - - self:T3( { LandHeight } ) - - local DCSTask = { id = 'Orbit', - params = { pattern = AI.Task.OrbitPattern.CIRCLE, - point = Point, - speed = Speed, - altitude = Altitude + LandHeight + local DCSTask = { + id = 'Orbit', + params = { + pattern = AI.Task.OrbitPattern.CIRCLE, + point = Point, + speed = Speed, + altitude = Altitude + land.getHeight( Point ) } } - - -- local AITask = { id = 'ControlledTask', - -- params = { task = { id = 'Orbit', - -- params = { pattern = AI.Task.OrbitPattern.CIRCLE, - -- point = Point, - -- speed = Speed, - -- altitude = Altitude + LandHeight - -- } - -- }, - -- stopCondition = { duration = Duration - -- } - -- } - -- } - -- ) - return DCSTask end +--- (AIR) Orbit at a position with at a given altitude and speed. Optionally, a race track pattern can be specified. +-- @param #CONTROLLABLE self +-- @param Core.Point#COORDINATE Coord Coordinate at which the CONTROLLABLE orbits. +-- @param #number Altitude Altitude in meters of the orbit pattern. Default y component of Coord. +-- @param #number Speed Speed [m/s] flying the orbit pattern. Default 128 m/s = 250 knots. +-- @param Core.Point#COORDINATE CoordRaceTrack (Optional) If this coordinate is specified, the CONTROLLABLE will fly a race-track pattern using this and the initial coordinate. +-- @return #CONTROLLABLE self +function CONTROLLABLE:TaskOrbit(Coord, Altitude, Speed, CoordRaceTrack) + + local Pattern=AI.Task.OrbitPattern.CIRCLE + + local P1=Coord:GetVec2() + local P2=nil + if CoordRaceTrack then + Pattern=AI.Task.OrbitPattern.RACE_TRACK + P2=CoordRaceTrack:GetVec2() + end + + local Task = { + id = 'Orbit', + params = { + pattern = Pattern, + point = P1, + point2 = P2, + speed = Speed or UTILS.KnotsToMps(250), + altitude = Altitude or Coord.y, + } + } + + return Task +end + --- (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. -- @param #CONTROLLABLE self -- @param #number Altitude The altitude [m] to hold the position. -- @param #number Speed The speed [m/s] flying when holding the position. --- @param Core.Point#COORDINATE Coordinate The coordinate where to orbit. +-- @param Core.Point#COORDINATE Coordinate (Optional) The coordinate where to orbit. If the coordinate is not given, then the current position of the controllable is used. -- @return #CONTROLLABLE self function CONTROLLABLE:TaskOrbitCircle( Altitude, Speed, Coordinate ) self:F2( { self.ControllableName, Altitude, Speed } ) @@ -902,47 +1246,39 @@ function CONTROLLABLE:TaskHoldPosition() end - - - - ---- (AIR) Delivering weapon on the runway. +--- (AIR) Delivering weapon on the runway. See [hoggit](https://wiki.hoggitworld.com/view/DCS_task_bombingRunway) +-- +-- Make sure the aircraft has the following role: +-- +-- * CAS +-- * Ground Attack +-- * Runway Attack +-- * Anti-Ship Strike +-- * AFAC +-- * Pinpoint Strike +-- -- @param #CONTROLLABLE self -- @param Wrapper.Airbase#AIRBASE Airbase Airbase to attack. --- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. --- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. +-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. See [DCS enum weapon flag](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag). Default 2147485694 = AnyBomb (GuidedBomb + AnyUnguidedBomb). +-- @param DCS#AI.Task.WeaponExpend WeaponExpend Enum AI.Task.WeaponExpend that defines how much munitions the AI will expend per attack run. Default "ALL". +-- @param #number AttackQty Number of times the group will attack if the target. Default 1. -- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. +-- @param #boolean GroupAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a group and not to a single aircraft. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskBombingRunway( Airbase, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack ) - self:F2( { self.ControllableName, Airbase, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack } ) +function CONTROLLABLE:TaskBombingRunway(Airbase, WeaponType, WeaponExpend, AttackQty, Direction, GroupAttack) --- BombingRunway = { --- id = 'BombingRunway', --- params = { --- runwayId = AirdromeId, --- weaponType = number, --- expend = enum AI.Task.WeaponExpend, --- attackQty = number, --- direction = Azimuth, --- controllableAttack = boolean, --- } --- } - - local DCSTask - DCSTask = { id = 'BombingRunway', + local DCSTask = { + id = 'BombingRunway', params = { - point = Airbase:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - direction = Direction, - controllableAttack = ControllableAttack, + runwayId = Airbase:GetID(), + weaponType = WeaponType or ENUMS.WeaponFlag.AnyBomb, + expend = WeaponExpend or AI.Task.WeaponExpend.ALL, + attackQty = AttackQty or 1, + direction = Direction and math.rad(Direction) or 0, + groupAttack = GroupAttack and true or false, }, - }, + } - self:T3( { DCSTask } ) return DCSTask end @@ -951,60 +1287,32 @@ end -- @param #CONTROLLABLE self -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskRefueling() - self:F2( { self.ControllableName } ) --- Refueling = { --- id = 'Refueling', --- params = {} --- } + local DCSTask={ + id='Refueling', + params={} + } - local DCSTask - DCSTask = { id = 'Refueling', - params = { - }, - }, - - self:T3( { DCSTask } ) return DCSTask end --- (AIR HELICOPTER) Landing at the ground. For helicopters only. -- @param #CONTROLLABLE self --- @param DCS#Vec2 Point The point where to land. +-- @param DCS#Vec2 Vec2 The point where to land. -- @param #number Duration The duration in seconds to stay on the ground. -- @return #CONTROLLABLE self -function CONTROLLABLE:TaskLandAtVec2( Point, Duration ) - self:F2( { self.ControllableName, Point, Duration } ) +function CONTROLLABLE:TaskLandAtVec2(Vec2, Duration) --- Land = { --- id= 'Land', --- params = { --- point = Vec2, --- durationFlag = boolean, --- duration = Time --- } --- } - - local DCSTask - if Duration and Duration > 0 then - DCSTask = { id = 'Land', - params = { - point = Point, - durationFlag = true, - duration = Duration, - }, - } - else - DCSTask = { id = 'Land', - params = { - point = Point, - durationFlag = false, - }, - } - end - - self:T3( DCSTask ) + local DCSTask = { + id = 'Land', + params = { + point = Vec2, + durationFlag = Duration and true or false, + duration = Duration, + }, + } + return DCSTask end @@ -1014,26 +1322,20 @@ end -- @param #number Duration The duration in seconds to stay on the ground. -- @return #CONTROLLABLE self function CONTROLLABLE:TaskLandAtZone( Zone, Duration, RandomPoint ) - self:F2( { self.ControllableName, Zone, Duration, RandomPoint } ) - local Point - if RandomPoint then - Point = Zone:GetRandomVec2() - else - Point = Zone:GetVec2() - end + -- Get landing point + local Point=RandomPoint and Zone:GetRandomVec2() or Zone:GetVec2() - local DCSTask = self:TaskLandAtVec2( Point, Duration ) + local DCSTask = CONTROLLABLE.TaskLandAtVec2( self, Point, Duration ) - self:T3( DCSTask ) return DCSTask end ---- (AIR) Following another airborne controllable. --- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. --- If another controllable is on land the unit / controllable will orbit around. +--- (AIR) Following another airborne controllable. +-- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. +-- If another controllable is on land the unit / controllable will orbit around. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE FollowControllable The controllable to be followed. -- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. @@ -1049,22 +1351,24 @@ function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) -- pos = Vec3, -- lastWptIndexFlag = boolean, -- lastWptIndex = number --- } +-- } -- } local LastWaypointIndexFlag = false + local lastWptIndexFlagChangedManually = false if LastWaypointIndex then LastWaypointIndexFlag = true + lastWptIndexFlagChangedManually = true end - - local DCSTask - DCSTask = { + + local DCSTask = { id = 'Follow', params = { - groupId = FollowControllable:GetID(), - pos = Vec3, - lastWptIndexFlag = LastWaypointIndexFlag, - lastWptIndex = LastWaypointIndex + groupId = FollowControllable:GetID(), + pos = Vec3, + lastWptIndexFlag = LastWaypointIndexFlag, + lastWptIndex = LastWaypointIndex, + lastWptIndexFlagChangedManually = lastWptIndexFlagChangedManually, } } @@ -1073,18 +1377,17 @@ function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) end ---- (AIR) Escort another airborne controllable. --- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. +--- (AIR) Escort another airborne controllable. +-- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. -- The unit / controllable will also protect that controllable from threats of specified types. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE FollowControllable The controllable to be escorted. -- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. -- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. --- @param #number EngagementDistance Maximal distance from escorted controllable to threat. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. --- @param DCS#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. +-- @param #number EngagementDistance Maximal distance from escorted controllable to threat. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. +-- @param DCS#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. Default {"Air"}. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes ) - self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes } ) -- Escort = { -- id = 'Escort', @@ -1095,29 +1398,22 @@ function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, E -- lastWptIndex = number, -- engagementDistMax = Distance, -- targetTypes = array of AttributeName, --- } +-- } -- } - local LastWaypointIndexFlag = false - if LastWaypointIndex then - LastWaypointIndexFlag = true - end - - TargetTypes=TargetTypes or {} - local DCSTask - DCSTask = { id = 'Escort', + DCSTask = { + id = 'Escort', params = { - groupId = FollowControllable:GetID(), - pos = Vec3, - lastWptIndexFlag = LastWaypointIndexFlag, - lastWptIndex = LastWaypointIndex, + groupId = FollowControllable:GetID(), + pos = Vec3, + lastWptIndexFlag = LastWaypointIndex and true or false, + lastWptIndex = LastWaypointIndex, engagementDistMax = EngagementDistance, - targetTypes = TargetTypes, + targetTypes = TargetTypes or {"Air"}, }, - }, + } - self:T3( { DCSTask } ) return DCSTask end @@ -1129,10 +1425,9 @@ end -- @param DCS#Vec2 Vec2 The point to fire at. -- @param DCS#Distance Radius The radius of the zone to deploy the fire at. -- @param #number AmmoCount (optional) Quantity of ammunition to expand (omit to fire until ammunition is depleted). --- @param #number WeaponType (optional) Enum for weapon type ID. This value is only required if you want the group firing to use a specific weapon, for instance using the task on a ship to force it to fire guided missiles at targets within cannon range. See http://wiki.hoggit.us/view/DCS_enum_weapon_flag +-- @param #number WeaponType (optional) Enum for weapon type ID. This value is only required if you want the group firing to use a specific weapon, for instance using the task on a ship to force it to fire guided missiles at targets within cannon range. See http://wiki.hoggit.us/view/DCS_enum_weapon_flag -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType ) - self:F2( { self.ControllableName, Vec2, Radius, AmmoCount, WeaponType } ) -- FireAtPoint = { -- id = 'FireAtPoint', @@ -1140,25 +1435,25 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType ) -- point = Vec2, -- radius = Distance, -- expendQty = number, - -- expendQtyEnabled = boolean, + -- expendQtyEnabled = boolean, -- } -- } - local DCSTask - DCSTask = { id = 'FireAtPoint', + local DCSTask = { + id = 'FireAtPoint', params = { - point = Vec2, - radius = Radius, - expendQty = 100, -- dummy value + point = Vec2, + zoneRadius = Radius, + expendQty = 100, -- dummy value expendQtyEnabled = false, } } - + if AmmoCount then DCSTask.params.expendQty = AmmoCount DCSTask.params.expendQtyEnabled = true end - + if WeaponType then DCSTask.params.weaponType=WeaponType end @@ -1171,60 +1466,42 @@ end -- @param #CONTROLLABLE self -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskHold() - self:F2( { self.ControllableName } ) - --- Hold = { --- id = 'Hold', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'Hold', - params = { - } - } - - self:T3( { DCSTask } ) + local DCSTask = {id = 'Hold', params = {}} return DCSTask end -- TASKS FOR AIRBORNE AND GROUND UNITS/CONTROLLABLES ---- (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. +--- (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. -- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. +-- If the task is assigned to the controllable lead unit will be a FAC. -- @param #CONTROLLABLE self --- @param Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. --- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param DCS#AI.Task.Designation Designation (optional) Designation type. --- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. +-- @param Wrapper.Group#GROUP AttackGroup Target GROUP object. +-- @param #number WeaponType Bitmask of weapon types, which are allowed to use. +-- @param DCS#AI.Task.Designation Designation (Optional) Designation type. +-- @param #boolean Datalink (Optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. +-- @param #number Frequency Frequency used to communicate with the FAC. +-- @param #number Modulation Modulation of radio for communication. +-- @param #number CallsignName Callsign enumerator name of the FAC. +-- @param #number CallsignNumber Callsign number, e.g. Axeman-**1**. -- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, Designation, Datalink } ) +function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink, Frequency, Modulation, CallsignName, CallsignNumber ) --- FAC_AttackGroup = { --- id = 'FAC_AttackGroup', --- params = { --- groupId = Group.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_AttackGroup', + local DCSTask = { + id = 'FAC_AttackGroup', params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, + groupId = AttackGroup:GetID(), + weaponType = WeaponType, designation = Designation, - datalink = Datalink, + datalink = Datalink, + frequency = Frequency, + modulation = Modulation, + callname = CallsignName, + number = CallsignNumber, } } - self:T3( { DCSTask } ) return DCSTask end @@ -1232,32 +1509,22 @@ end --- (AIR) Engaging targets of defined types. -- @param #CONTROLLABLE self --- @param DCS#Distance Distance Maximal distance from the target to a route leg. If the target is on a greater distance it will be ignored. --- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. --- @param #number Priority All enroute tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param DCS#Distance Distance Maximal distance from the target to a route leg. If the target is on a greater distance it will be ignored. +-- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. +-- @param #number Priority All enroute tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEngageTargets( Distance, TargetTypes, Priority ) - self:F2( { self.ControllableName, Distance, TargetTypes, Priority } ) --- EngageTargets ={ --- id = 'EngageTargets', --- params = { --- maxDist = Distance, --- targetTypes = array of AttributeName, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'EngageTargets', + local DCSTask = { + id = 'EngageTargets', params = { - maxDist = Distance, - targetTypes = TargetTypes, - priority = Priority + maxDistEnabled = Distance and true or false, + maxDist = Distance, + targetTypes = TargetTypes or {"Air"}, + priority = Priority or 0, } } - self:T3( { DCSTask } ) return DCSTask end @@ -1265,35 +1532,23 @@ end --- (AIR) Engaging a targets of defined types at circle-shaped zone. -- @param #CONTROLLABLE self --- @param DCS#Vec2 Vec2 2D-coordinates of the zone. --- @param DCS#Distance Radius Radius of the zone. --- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param DCS#Vec2 Vec2 2D-coordinates of the zone. +-- @param DCS#Distance Radius Radius of the zone. +-- @param DCS#AttributeNameArray TargetTypes (Optional) Array of target categories allowed to engage. Default {"Air"}. +-- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEngageTargetsInZone( Vec2, Radius, TargetTypes, Priority ) - self:F2( { self.ControllableName, Vec2, Radius, TargetTypes, Priority } ) --- EngageTargetsInZone = { --- id = 'EngageTargetsInZone', --- params = { --- point = Vec2, --- zoneRadius = Distance, --- targetTypes = array of AttributeName, --- priority = number --- } --- } - - local DCSTask - DCSTask = { id = 'EngageTargetsInZone', + local DCSTask = { + id = 'EngageTargetsInZone', params = { - point = Vec2, - zoneRadius = Radius, - targetTypes = TargetTypes, - priority = Priority + point = Vec2, + zoneRadius = Radius, + targetTypes = TargetTypes or {"Air"}, + priority = Priority or 0 } } - self:T3( { DCSTask } ) return DCSTask end @@ -1301,7 +1556,7 @@ end --- (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. -- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. -- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. -- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. @@ -1310,7 +1565,6 @@ end -- @param #boolean AttackQtyLimit (optional) The flag determines how to interpret attackQty parameter. If the flag is true then attackQty is a limit on maximal attack quantity for "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEngageGroup( AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit ) - self:F2( { self.ControllableName, AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } ) -- EngageControllable = { -- id = 'EngageControllable ', @@ -1328,33 +1582,22 @@ function CONTROLLABLE:EnRouteTaskEngageGroup( AttackGroup, Priority, WeaponType, -- } -- } - local DirectionEnabled = nil - if Direction then - DirectionEnabled = true - end - - local AltitudeEnabled = nil - if Altitude then - AltitudeEnabled = true - end - - local DCSTask - DCSTask = { id = 'EngageControllable', + local DCSTask = { + id = 'EngageControllable', params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, - expend = WeaponExpend, - attackQty = AttackQty, - directionEnabled = DirectionEnabled, - direction = Direction, - altitudeEnabled = AltitudeEnabled, - altitude = Altitude, - attackQtyLimit = AttackQtyLimit, - priority = Priority, + groupId = AttackGroup:GetID(), + weaponType = WeaponType, + expend = WeaponExpend or "Auto", + directionEnabled = Direction and true or false, + direction = Direction, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, + priority = Priority or 1, }, - }, + } - self:T3( { DCSTask } ) return DCSTask end @@ -1362,7 +1605,7 @@ end --- (AIR) Search and attack the Unit. -- @param #CONTROLLABLE self -- @param Wrapper.Unit#UNIT EngageUnit The UNIT. --- @param #number Priority (optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #number Priority (optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. -- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. -- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. -- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. @@ -1372,41 +1615,25 @@ end -- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEngageUnit( EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack ) - self:F2( { self.ControllableName, EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack } ) - -- EngageUnit = { - -- id = 'EngageUnit', - -- params = { - -- unitId = Unit.ID, - -- weaponType = number, - -- expend = enum AI.Task.WeaponExpend - -- attackQty = number, - -- direction = Azimuth, - -- attackQtyLimit = boolean, - -- controllableAttack = boolean, - -- priority = number, - -- } - -- } - - local DCSTask - DCSTask = { id = 'EngageUnit', + local DCSTask = { + id = 'EngageUnit', params = { - unitId = EngageUnit:GetID(), - priority = Priority or 1, - groupAttack = GroupAttack or false, - visible = Visible or false, - expend = WeaponExpend or "Auto", - directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude, - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, + unitId = EngageUnit:GetID(), + priority = Priority or 1, + groupAttack = GroupAttack and GroupAttack or false, + visible = Visible and Visible or false, + expend = WeaponExpend or "Auto", + directionEnabled = Direction and true or false, + direction = Direction and math.rad(Direction) or nil, + altitudeEnabled = Altitude and true or false, + altitude = Altitude, + attackQtyLimit = AttackQty and true or false, + attackQty = AttackQty, controllableAttack = ControllableAttack, }, - }, + } - self:T3( { DCSTask } ) return DCSTask end @@ -1416,21 +1643,12 @@ end -- @param #CONTROLLABLE self -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskAWACS( ) - self:F2( { self.ControllableName } ) --- AWACS = { --- id = 'AWACS', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'AWACS', - params = { - } + local DCSTask = { + id = 'AWACS', + params = {}, } - self:T3( { DCSTask } ) return DCSTask end @@ -1439,21 +1657,12 @@ end -- @param #CONTROLLABLE self -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskTanker( ) - self:F2( { self.ControllableName } ) --- Tanker = { --- id = 'Tanker', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'Tanker', - params = { - } + local DCSTask = { + id = 'Tanker', + params = {}, } - self:T3( { DCSTask } ) return DCSTask end @@ -1464,147 +1673,74 @@ end -- @param #CONTROLLABLE self -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEWR( ) - self:F2( { self.ControllableName } ) --- EWR = { --- id = 'EWR', --- params = { --- } --- } - - local DCSTask - DCSTask = { id = 'EWR', - params = { - } + local DCSTask = { + id = 'EWR', + params = {}, } - self:T3( { DCSTask } ) return DCSTask end --- En-route tasks for airborne and ground units/controllables +-- En-route tasks for airborne and ground units/controllables ---- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. +--- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. -- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. +-- If the task is assigned to the controllable lead unit will be a FAC. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param DCS#AI.Task.Designation Designation (optional) Designation type. --- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. +-- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default is 0. +-- @param #number WeaponType (Optional) Bitmask of weapon types those allowed to use. Default is "Auto". +-- @param DCS#AI.Task.Designation Designation (Optional) Designation type. +-- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponType, Designation, Datalink ) - self:F2( { self.ControllableName, AttackGroup, WeaponType, Priority, Designation, Datalink } ) --- FAC_EngageControllable = { --- id = 'FAC_EngageControllable', --- params = { --- groupId = Group.ID, --- weaponType = number, --- designation = enum AI.Task.Designation, --- datalink = boolean, --- priority = number, --- } --- } - - local DCSTask - DCSTask = { id = 'FAC_EngageControllable', + local DCSTask = { + id = 'FAC_EngageControllable', params = { - groupId = AttackGroup:GetID(), - weaponType = WeaponType, + groupId = AttackGroup:GetID(), + weaponType = WeaponType or "Auto", designation = Designation, - datalink = Datalink, - priority = Priority, + datalink = Datalink and Datalink or false, + priority = Priority or 0, } } - self:T3( { DCSTask } ) return DCSTask end ---- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. +--- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets. -- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. --- If the task is assigned to the controllable lead unit will be a FAC. +-- If the task is assigned to the controllable lead unit will be a FAC. -- @param #CONTROLLABLE self -- @param DCS#Distance Radius The maximal distance from the FAC to a target. --- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. +-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. -- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskFAC( Radius, Priority ) - self:F2( { self.ControllableName, Radius, Priority } ) --- FAC = { --- id = 'FAC', --- params = { +-- FAC = { +-- id = 'FAC', +-- params = { -- radius = Distance, -- priority = number --- } +-- } -- } - local DCSTask - DCSTask = { id = 'FAC', + local DCSTask = { + id = 'FAC', params = { radius = Radius, priority = Priority } } - self:T3( { DCSTask } ) return DCSTask end - - ---- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. --- @param #CONTROLLABLE self --- @param DCS#Vec2 Point The point where to wait. --- @param #number Duration The duration in seconds to wait. --- @param #CONTROLLABLE EmbarkingControllable The controllable to be embarked. --- @return DCS#Task The DCS task structure -function CONTROLLABLE:TaskEmbarking( Point, Duration, EmbarkingControllable ) - self:F2( { self.ControllableName, Point, Duration, EmbarkingControllable.DCSControllable } ) - - local DCSTask - DCSTask = { id = 'Embarking', - params = { x = Point.x, - y = Point.y, - duration = Duration, - controllablesForEmbarking = { EmbarkingControllable.ControllableID }, - durationFlag = true, - distributionFlag = false, - distribution = {}, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - ---- (GROUND) Embark to a Transport landed at a location. - ---- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. --- @param #CONTROLLABLE self --- @param DCS#Vec2 Point The point where to wait. --- @param #number Radius The radius of the embarking zone around the Point. --- @return DCS#Task The DCS task structure. -function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) - self:F2( { self.ControllableName, Point, Radius } ) - - local DCSTask --DCS#Task - DCSTask = { id = 'EmbarkToTransport', - params = { x = Point.x, - y = Point.y, - zoneRadius = Radius, - } - } - - self:T3( { DCSTask } ) - return DCSTask -end - --- This creates a Task element, with an action to call a function as part of a Wrapped Task. -- This Task can then be embedded at a Waypoint by calling the method @{#CONTROLLABLE.SetTaskWaypoint}. -- @param #CONTROLLABLE self @@ -1612,54 +1748,52 @@ end -- @param ... The variable arguments passed to the function when called! These arguments can be of any type! -- @return #CONTROLLABLE -- @usage --- --- local ZoneList = { --- ZONE:New( "ZONE1" ), --- ZONE:New( "ZONE2" ), --- ZONE:New( "ZONE3" ), --- ZONE:New( "ZONE4" ), --- ZONE:New( "ZONE5" ) +-- +-- local ZoneList = { +-- ZONE:New( "ZONE1" ), +-- ZONE:New( "ZONE2" ), +-- ZONE:New( "ZONE3" ), +-- ZONE:New( "ZONE4" ), +-- ZONE:New( "ZONE5" ) -- } --- +-- -- GroundGroup = GROUP:FindByName( "Vehicle" ) --- +-- -- --- @param Wrapper.Group#GROUP GroundGroup -- function RouteToZone( Vehicle, ZoneRoute ) --- +-- -- local Route = {} --- +-- -- Vehicle:E( { ZoneRoute = ZoneRoute } ) --- +-- -- Vehicle:MessageToAll( "Moving to zone " .. ZoneRoute:GetName(), 10 ) --- +-- -- -- Get the current coordinate of the Vehicle -- local FromCoord = Vehicle:GetCoordinate() --- +-- -- -- Select a random Zone and get the Coordinate of the new Zone. -- local RandomZone = ZoneList[ math.random( 1, #ZoneList ) ] -- Core.Zone#ZONE -- local ToCoord = RandomZone:GetCoordinate() --- +-- -- -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task -- Route[#Route+1] = FromCoord:WaypointGround( 72 ) -- Route[#Route+1] = ToCoord:WaypointGround( 60, "Vee" ) --- +-- -- local TaskRouteToZone = Vehicle:TaskFunction( "RouteToZone", RandomZone ) --- +-- -- Vehicle:SetTaskWaypoint( Route[#Route], TaskRouteToZone ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. --- +-- -- Vehicle:Route( Route, math.random( 10, 20 ) ) -- Move after a random seconds to the Route. See the Route method for details. --- +-- -- end --- +-- -- RouteToZone( GroundGroup, ZoneList[1] ) --- +-- function CONTROLLABLE:TaskFunction( FunctionString, ... ) - local DCSTask - + -- Script local DCSScript = {} DCSScript[#DCSScript+1] = "local MissionControllable = GROUP:Find( ... ) " - if arg and arg.n > 0 then local ArgumentKey = '_' .. tostring( arg ):match("table: (.*)") self:SetState( self, ArgumentKey, arg ) @@ -1669,12 +1803,10 @@ function CONTROLLABLE:TaskFunction( FunctionString, ... ) DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )" end - DCSTask = self:TaskWrappedAction(self:CommandDoScript(table.concat( DCSScript ))) - - self:T( DCSTask ) - + -- DCS task. + local DCSTask = self:TaskWrappedAction(self:CommandDoScript(table.concat( DCSScript ))) + return DCSTask - end @@ -1684,12 +1816,12 @@ end -- @param #table TaskMission A table containing the mission task. -- @return DCS#Task function CONTROLLABLE:TaskMission( TaskMission ) - self:F2( Points ) - local DCSTask - DCSTask = { id = 'Mission', params = { TaskMission, }, } + local DCSTask = { + id = 'Mission', + params = { TaskMission, }, + } - self:T3( { DCSTask } ) return DCSTask end @@ -1700,31 +1832,31 @@ do -- Patrol methods -- @param #CONTROLLABLE self -- @return #CONTROLLABLE function CONTROLLABLE:PatrolRoute() - + local PatrolGroup = self -- Wrapper.Group#GROUP - + if not self:IsInstanceOf( "GROUP" ) then PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP end - + self:F( { PatrolGroup = PatrolGroup:GetName() } ) - + if PatrolGroup:IsGround() or PatrolGroup:IsShip() then - + local Waypoints = PatrolGroup:GetTemplateRoutePoints() - + -- Calculate the new Route. local FromCoord = PatrolGroup:GetCoordinate() local From = FromCoord:WaypointGround( 120 ) - + table.insert( Waypoints, 1, From ) local TaskRoute = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRoute" ) - + self:F({Waypoints = Waypoints}) local Waypoint = Waypoints[#Waypoints] PatrolGroup:SetTaskWaypoint( Waypoint, TaskRoute ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. - + PatrolGroup:Route( Waypoints ) -- Move after a random seconds to the Route. See the Route method for details. end end @@ -1737,31 +1869,31 @@ do -- Patrol methods -- @param Core.Point#COORDINATE ToWaypoint The waypoint where the group should move to. -- @return #CONTROLLABLE function CONTROLLABLE:PatrolRouteRandom( Speed, Formation, ToWaypoint ) - + local PatrolGroup = self -- Wrapper.Group#GROUP - + if not self:IsInstanceOf( "GROUP" ) then PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP end self:F( { PatrolGroup = PatrolGroup:GetName() } ) - + if PatrolGroup:IsGround() or PatrolGroup:IsShip() then - + local Waypoints = PatrolGroup:GetTemplateRoutePoints() - + -- Calculate the new Route. local FromCoord = PatrolGroup:GetCoordinate() local FromWaypoint = 1 if ToWaypoint then FromWaypoint = ToWaypoint end - + -- Loop until a waypoint has been found that is not the same as the current waypoint. -- Otherwise the object zon't move or drive in circles and the algorithm would not do exactly -- what it is supposed to do, which is making groups drive around. local ToWaypoint - repeat + repeat -- Select a random waypoint and check if it is not the same waypoint as where the object is about. ToWaypoint = math.random( 1, #Waypoints ) until( ToWaypoint ~= FromWaypoint ) @@ -1773,12 +1905,12 @@ do -- Patrol methods local Route = {} Route[#Route+1] = FromCoord:WaypointGround( 0 ) Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation ) - - + + local TaskRouteToZone = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRouteRandom", Speed, Formation, ToWaypoint ) - + PatrolGroup:SetTaskWaypoint( Route[#Route], TaskRouteToZone ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. - + PatrolGroup:Route( Route, 1 ) -- Move after a random seconds to the Route. See the Route method for details. end end @@ -1789,44 +1921,50 @@ do -- Patrol methods -- @param #table ZoneList Table of zones. -- @param #number Speed Speed in km/h the group moves at. -- @param #string Formation (Optional) Formation the group should use. + -- @param #number DelayMin Delay in seconds before the group progresses to the next route point. Default 1 sec. + -- @param #number DelayMax Max. delay in seconds. Actual delay is randomly chosen between DelayMin and DelayMax. Default equal to DelayMin. -- @return #CONTROLLABLE - function CONTROLLABLE:PatrolZones( ZoneList, Speed, Formation ) - + function CONTROLLABLE:PatrolZones( ZoneList, Speed, Formation, DelayMin, DelayMax ) + if not type( ZoneList ) == "table" then ZoneList = { ZoneList } end - + local PatrolGroup = self -- Wrapper.Group#GROUP - + if not self:IsInstanceOf( "GROUP" ) then PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP end + DelayMin=DelayMin or 1 + if not DelayMax or DelayMax LengthDirect*10) or (LengthRoad/LengthOnRoad*100<5)) - + -- Debug info. self:T(string.format("Length on road = %.3f km", LengthOnRoad/1000)) self:T(string.format("Length directly = %.3f km", LengthDirect/1000)) @@ -2128,29 +2292,30 @@ do -- Route methods self:T(string.format("Length only road = %.3f km", LengthRoad/1000)) self:T(string.format("Length off road = %.3f km", LengthOffRoad/1000)) self:T(string.format("Percent on road = %.1f", LengthRoad/LengthOnRoad*100)) - + end - + -- Route, ground waypoints along road. local route={} local canroad=false - + -- Check if a valid path on road could be found. - if PathOnRoad and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. + if GotPath and LengthRoad and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. -- Check whether the road is very long compared to direct path. if LongRoad and Shortcut then -- Road is long ==> we take the short cut. + table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) - + else -- Create waypoints. table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) table.insert(route, PathOnRoad[2]:WaypointGround(Speed, "On Road")) table.insert(route, PathOnRoad[#PathOnRoad-1]:WaypointGround(Speed, "On Road")) - + -- Add the final coordinate because the final might not be on the road. local dist=ToCoordinate:Get2DDistance(PathOnRoad[#PathOnRoad-1]) if dist>10 then @@ -2158,16 +2323,27 @@ do -- Route methods table.insert(route, ToCoordinate:GetRandomCoordinateInRadius(10,5):WaypointGround(5, OffRoadFormation)) table.insert(route, ToCoordinate:GetRandomCoordinateInRadius(10,5):WaypointGround(5, OffRoadFormation)) end - + end - + canroad=true else - + -- No path on road could be found (can happen!) ==> Route group directly from A to B. table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) - + + end + + -- Add passing waypoint function. + if WaypointFunction then + local N=#route + for n,waypoint in pairs(route) do + waypoint.task = {} + waypoint.task.id = "ComboTask" + waypoint.task.params = {} + waypoint.task.params.tasks = {self:TaskFunction("CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack(WaypointFunctionArguments or {}))} + end end return route, canroad @@ -2177,36 +2353,59 @@ do -- Route methods -- @param #CONTROLLABLE self -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h. + -- @param #function WaypointFunction (Optional) Function called when passing a waypoint. First parameters of the function are the @{CONTROLLABLE} object, the number of the waypoint and the total number of waypoints. + -- @param #table WaypointFunctionArguments (Optional) List of parameters passed to the *WaypointFunction*. -- @return Task - function CONTROLLABLE:TaskGroundOnRailRoads(ToCoordinate, Speed) + function CONTROLLABLE:TaskGroundOnRailRoads(ToCoordinate, Speed, WaypointFunction, WaypointFunctionArguments ) self:F2({ToCoordinate=ToCoordinate, Speed=Speed}) - + -- Defaults. Speed=Speed or 20 - + -- Current coordinate. local FromCoordinate = self:GetCoordinate() - + -- Get path and path length on railroad. local PathOnRail, LengthOnRail=FromCoordinate:GetPathOnRoad(ToCoordinate, false, true) - + -- Debug info. self:T(string.format("Length on railroad = %.3f km", LengthOnRail/1000)) - + -- Route, ground waypoints along road. local route={} - + -- Check if a valid path on railroad could be found. if PathOnRail then table.insert(route, PathOnRail[1]:WaypointGround(Speed, "On Railroad")) table.insert(route, PathOnRail[2]:WaypointGround(Speed, "On Railroad")) - + end - return route + -- Add passing waypoint function. + if WaypointFunction then + local N=#route + for n,waypoint in pairs(route) do + waypoint.task = {} + waypoint.task.id = "ComboTask" + waypoint.task.params = {} + waypoint.task.params.tasks = {self:TaskFunction("CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack(WaypointFunctionArguments or {}))} + end + end + + return route end + --- Task function when controllable passes a waypoint. + -- @param #CONTROLLABLE controllable The controllable object. + -- @param #number n Current waypoint number passed. + -- @param #number N Total number of waypoints. + -- @param #function waypointfunction Function called when a waypoint is passed. + function CONTROLLABLE.___PassingWaypoint(controllable, n, N, waypointfunction, ...) + waypointfunction(controllable, n, N, ...) + end + + --- Make the AIR Controllable fly towards a specific point. -- @param #CONTROLLABLE self -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. @@ -2217,18 +2416,18 @@ do -- Route methods -- @param #number DelaySeconds Wait for the specified seconds before executing the Route. -- @return #CONTROLLABLE The CONTROLLABLE. function CONTROLLABLE:RouteAirTo( ToCoordinate, AltType, Type, Action, Speed, DelaySeconds ) - + local FromCoordinate = self:GetCoordinate() local FromWP = FromCoordinate:WaypointAir() - + local ToWP = ToCoordinate:WaypointAir( AltType, Type, Action, Speed ) - + self:Route( { FromWP, ToWP }, DelaySeconds ) - + return self end - - + + --- (AIR + GROUND) Route the controllable to a given zone. -- The controllable final destination point can be randomized. -- A speed can be given in km/h. @@ -2240,58 +2439,58 @@ do -- Route methods -- @param Base#FORMATION Formation The formation string. function CONTROLLABLE:TaskRouteToZone( Zone, Randomize, Speed, Formation ) self:F2( Zone ) - + local DCSControllable = self:GetDCSObject() - + if DCSControllable then - + local ControllablePoint = self:GetVec2() - + local PointFrom = {} PointFrom.x = ControllablePoint.x PointFrom.y = ControllablePoint.y PointFrom.type = "Turning Point" PointFrom.action = Formation or "Cone" PointFrom.speed = 20 / 3.6 - - + + local PointTo = {} local ZonePoint - + if Randomize then ZonePoint = Zone:GetRandomVec2() else ZonePoint = Zone:GetVec2() end - + PointTo.x = ZonePoint.x PointTo.y = ZonePoint.y PointTo.type = "Turning Point" - + if Formation then PointTo.action = Formation else PointTo.action = "Cone" end - + if Speed then PointTo.speed = Speed else PointTo.speed = 20 / 3.6 end - + local Points = { PointFrom, PointTo } - + self:T3( Points ) - + self:Route( Points ) - + return self end - + return nil end - + --- (GROUND) Route the controllable to a given Vec2. -- A speed can be given in km/h. -- A given formation can be given. @@ -2300,48 +2499,48 @@ do -- Route methods -- @param #number Speed The speed in m/s. Default is 5.555 m/s = 20 km/h. -- @param Base#FORMATION Formation The formation string. function CONTROLLABLE:TaskRouteToVec2( Vec2, Speed, Formation ) - + local DCSControllable = self:GetDCSObject() - + if DCSControllable then - + local ControllablePoint = self:GetVec2() - + local PointFrom = {} PointFrom.x = ControllablePoint.x PointFrom.y = ControllablePoint.y PointFrom.type = "Turning Point" PointFrom.action = Formation or "Cone" PointFrom.speed = 20 / 3.6 - - + + local PointTo = {} - + PointTo.x = Vec2.x PointTo.y = Vec2.y PointTo.type = "Turning Point" - + if Formation then PointTo.action = Formation else PointTo.action = "Cone" end - + if Speed then PointTo.speed = Speed else PointTo.speed = 20 / 3.6 end - + local Points = { PointFrom, PointTo } - + self:T3( Points ) - + self:Route( Points ) - + return self end - + return nil end @@ -2455,27 +2654,71 @@ function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRad self:F2( self.ControllableName ) local DCSControllable = self:GetDCSObject() + if DCSControllable then + local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil local DetectionOptical = ( DetectOptical and DetectOptical == true ) and Controller.Detection.OPTICAL or nil local DetectionRadar = ( DetectRadar and DetectRadar == true ) and Controller.Detection.RADAR or nil local DetectionIRST = ( DetectIRST and DetectIRST == true ) and Controller.Detection.IRST or nil local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil - + + + local Params = {} + if DetectionVisual then + Params[#Params+1] = DetectionVisual + end + if DetectionOptical then + Params[#Params+1] = DetectionOptical + end + if DetectionRadar then + Params[#Params+1] = DetectionRadar + end + if DetectionIRST then + Params[#Params+1] = DetectionIRST + end + if DetectionRWR then + Params[#Params+1] = DetectionRWR + end + if DetectionDLINK then + Params[#Params+1] = DetectionDLINK + end + + self:T2( { DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK } ) - - return self:_GetController():getDetectedTargets( DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK ) + + return self:_GetController():getDetectedTargets( Params[1], Params[2], Params[3], Params[4], Params[5], Params[6] ) end return nil end +--- Check if a target is detected. +-- The optional parametes specify the detection methods that can be applied. +-- If **no** detection method is given, the detection will use **all** the available methods by default. +-- If **at least one** detection method is specified, only the methods set to *true* will be used. +-- @param Wrapper.Controllable#CONTROLLABLE self +-- @param DCS#Object DCSObject The DCS object that is checked. +-- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +-- @return #boolean True if target is detected. +-- @return #boolean True if target is visible by line of sight. +-- @return #number Mission time when target was detected. +-- @return #boolean True if target type is known. +-- @return #boolean True if distance to target is known. +-- @return DCS#Vec3 Last known position vector of the target. +-- @return DCS#Vec3 Last known velocity vector of the target. function CONTROLLABLE:IsTargetDetected( DCSObject, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) self:F2( self.ControllableName ) local DCSControllable = self:GetDCSObject() - + if DCSControllable then local DetectionVisual = ( DetectVisual and DetectVisual == true ) and Controller.Detection.VISUAL or nil @@ -2489,15 +2732,201 @@ function CONTROLLABLE:IsTargetDetected( DCSObject, DetectVisual, DetectOptical, local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity = Controller:isTargetDetected( DCSObject, DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK ) - + return TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity end return nil end +--- Check if a certain UNIT is detected by the controllable. +-- The optional parametes specify the detection methods that can be applied. +-- If **no** detection method is given, the detection will use **all** the available methods by default. +-- If **at least one** detection method is specified, only the methods set to *true* will be used. +-- @param #CONTROLLABLE self +-- @param Wrapper.Unit#UNIT Unit The unit that is supposed to be detected. +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +-- @return #boolean True if target is detected. +-- @return #boolean True if target is visible by line of sight. +-- @return #number Mission time when target was detected. +-- @return #boolean True if target type is known. +-- @return #boolean True if distance to target is known. +-- @return DCS#Vec3 Last known position vector of the target. +-- @return DCS#Vec3 Last known velocity vector of the target. +function CONTROLLABLE:IsUnitDetected( Unit, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) + self:F2( self.ControllableName ) + + if Unit and Unit:IsAlive() then + return self:IsTargetDetected(Unit:GetDCSObject(), DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + end + + return nil +end + +--- Check if a certain GROUP is detected by the controllable. +-- The optional parametes specify the detection methods that can be applied. +-- If **no** detection method is given, the detection will use **all** the available methods by default. +-- If **at least one** detection method is specified, only the methods set to *true* will be used. +-- @param #CONTROLLABLE self +-- @param Wrapper.Group#GROUP Group The group that is supposed to be detected. +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +-- @return #boolean True if any unit of the group is detected. +function CONTROLLABLE:IsGroupDetected( Group, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK ) + self:F2( self.ControllableName ) + + if Group and Group:IsAlive() then + for _,_unit in pairs(Group:GetUnits()) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + + local isdetected=self:IsUnitDetected(unit, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + if isdetected then + return true + end + end + end + return false + end + + return nil +end + + +--- Return the detected targets of the controllable. +-- The optional parametes specify the detection methods that can be applied. +-- If **no** detection method is given, the detection will use **all** the available methods by default. +-- If **at least one** detection method is specified, only the methods set to *true* will be used. +-- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +-- @return Core.Set#SET_UNIT Set of detected units. +function CONTROLLABLE:GetDetectedUnitSet(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + -- Get detected DCS units. + local detectedtargets=self:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + local unitset=SET_UNIT:New() + + for DetectionObjectID, Detection in pairs(detectedtargets or {}) do + local DetectedObject=Detection.object -- DCS#Object + + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then + local unit=UNIT:Find(DetectedObject) + + if unit and unit:IsAlive() then + + if not unitset:FindUnit(unit:GetName()) then + unitset:AddUnit(unit) + end + + end + end + end + + return unitset +end + +--- Return the detected target groups of the controllable as a @{SET_GROUP}. +-- The optional parametes specify the detection methods that can be applied. +-- If no detection method is given, the detection will use all the available methods by default. +-- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets. +-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets. +-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar. +-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST. +-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR. +-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link. +-- @return Core.Set#SET_GROUP Set of detected groups. +function CONTROLLABLE:GetDetectedGroupSet(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + -- Get detected DCS units. + local detectedtargets=self:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK) + + local groupset=SET_GROUP:New() + + for DetectionObjectID, Detection in pairs(detectedtargets or {}) do + local DetectedObject=Detection.object -- DCS#Object + + if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then + local unit=UNIT:Find(DetectedObject) + + if unit and unit:IsAlive() then + local group=unit:GetGroup() + + if group and not groupset:FindGroup(group:GetName()) then + groupset:AddGroup(group) + end + + end + end + end + + return groupset +end + + -- Options +--- Set option. +-- @param #CONTROLLABLE self +-- @param #number OptionID ID/Type of the option. +-- @param #number OptionValue Value of the option +-- @return #CONTROLLABLE self +function CONTROLLABLE:SetOption(OptionID, OptionValue) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + Controller:setOption( OptionID, OptionValue ) + + return self + end + + return nil +end + +--- Set option for Rules of Engagement (ROE). +-- @param Wrapper.Controllable#CONTROLLABLE self +-- @param #number ROEvalue ROE value. See ENUMS.ROE. +-- @return Wrapper.Controllable#CONTROLLABLE self +function CONTROLLABLE:OptionROE(ROEvalue) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption(AI.Option.Air.id.ROE, ROEvalue ) + elseif self:IsGround() then + Controller:setOption(AI.Option.Ground.id.ROE, ROEvalue ) + elseif self:IsShip() then + Controller:setOption(AI.Option.Naval.id.ROE, ROEvalue ) + end + + return self + end + + return nil +end + --- Can the CONTROLLABLE hold their weapons? -- @param #CONTROLLABLE self -- @return #boolean @@ -2516,7 +2945,7 @@ function CONTROLLABLE:OptionROEHoldFirePossible() return nil end ---- Holding weapons. +--- Weapons Hold: AI will hold fire under all circumstances. -- @param Wrapper.Controllable#CONTROLLABLE self -- @return Wrapper.Controllable#CONTROLLABLE self function CONTROLLABLE:OptionROEHoldFire() @@ -2558,7 +2987,7 @@ function CONTROLLABLE:OptionROEReturnFirePossible() return nil end ---- Return fire. +--- Return Fire: AI will only engage threats that shoot first. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE self function CONTROLLABLE:OptionROEReturnFire() @@ -2600,7 +3029,7 @@ function CONTROLLABLE:OptionROEOpenFirePossible() return nil end ---- Openfire. +--- Open Fire (Only Designated): AI will engage only targets specified in its taskings. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE self function CONTROLLABLE:OptionROEOpenFire() @@ -2624,6 +3053,45 @@ function CONTROLLABLE:OptionROEOpenFire() return nil end +--- Can the CONTROLLABLE attack priority designated targets? Only for AIR! +-- @param #CONTROLLABLE self +-- @return #boolean +function CONTROLLABLE:OptionROEOpenFireWeaponFreePossible() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + if self:IsAir() then + return true + end + + return false + end + + return nil +end + +--- Open Fire, Weapons Free (Priority Designated): AI will engage any enemy group it detects, but will prioritize targets specified in the groups tasking. +-- **Only for AIR units!** +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionROEOpenFireWeaponFree() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE_WEAPON_FREE ) + end + + return self + end + + return nil +end + --- Can the CONTROLLABLE attack targets of opportunity? -- @param #CONTROLLABLE self -- @return #boolean @@ -2701,6 +3169,27 @@ function CONTROLLABLE:OptionROTNoReaction() return nil end +--- Set Reation On Threat behaviour. +-- @param #CONTROLLABLE self +-- @param #number ROTvalue ROT value. See ENUMS.ROT. +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionROT(ROTvalue) + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, ROTvalue ) + end + + return self + end + + return nil +end + --- Can the CONTROLLABLE evade using passive defenses? -- @param #CONTROLLABLE self -- @return #boolean @@ -2829,8 +3318,9 @@ function CONTROLLABLE:OptionAlarmStateAuto() if self:IsGround() then Controller:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.AUTO) - elseif self:IsShip() then - Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.AUTO) + elseif self:IsShip() then + --Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.AUTO) + Controller:setOption(9, 0) end return self @@ -2854,6 +3344,7 @@ function CONTROLLABLE:OptionAlarmStateGreen() elseif self:IsShip() then -- AI.Option.Naval.id.ALARM_STATE does not seem to exist! --Controller:setOption( AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.GREEN ) + Controller:setOption(9, 1) end return self @@ -2874,8 +3365,9 @@ function CONTROLLABLE:OptionAlarmStateRed() if self:IsGround() then Controller:setOption(AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED) - elseif self:IsShip() then - Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.RED) + elseif self:IsShip() then + --Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.RED) + Controller:setOption(9, 2) end return self @@ -2893,7 +3385,10 @@ end function CONTROLLABLE:OptionRTBBingoFuel( RTB ) --R2.2 self:F2( { self.ControllableName } ) - RTB = RTB or true + --RTB = RTB or true + if RTB==nil then + RTB=true + end local DCSControllable = self:GetDCSObject() if DCSControllable then @@ -2922,7 +3417,7 @@ function CONTROLLABLE:OptionRTBAmmo( WeaponsFlag ) local Controller = self:_GetController() if self:IsAir() then - Controller:setOption( AI.Option.GROUND.id.RTB_ON_OUT_OF_AMMO, WeaponsFlag ) + Controller:setOption( AI.Option.Air.id.RTB_ON_OUT_OF_AMMO, WeaponsFlag ) end return self @@ -2932,8 +3427,117 @@ function CONTROLLABLE:OptionRTBAmmo( WeaponsFlag ) end +--- Allow to Jettison of weapons upon threat. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionAllowJettisonWeaponsOnThreat() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.PROHIBIT_JETT, false ) + end + + return self + end + + return nil +end +--- Keep weapons upon threat. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionKeepWeaponsOnThreat() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.PROHIBIT_JETT, true ) + end + + return self + end + + return nil +end + +--- Prohibit Afterburner. +-- @param #CONTROLLABLE self +-- @param #boolean Prohibit If true or nil, prohibit. If false, do not prohibit. +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionProhibitAfterburner(Prohibit) + self:F2( { self.ControllableName } ) + + if Prohibit==nil then + Prohibit=true + end + + if self:IsAir() then + self:SetOption(AI.Option.Air.id.PROHIBIT_AB, Prohibit) + end + + return self +end + +--- Defines the usage of Electronic Counter Measures by airborne forces. Disables the ability for AI to use their ECM. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionECM_Never() + self:F2( { self.ControllableName } ) + + if self:IsAir() then + self:SetOption(AI.Option.Air.id.ECM_USING, 0) + end + + return self +end + +--- Defines the usage of Electronic Counter Measures by airborne forces. If the AI is actively being locked by an enemy radar they will enable their ECM jammer. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionECM_OnlyLockByRadar() + self:F2( { self.ControllableName } ) + + if self:IsAir() then + self:SetOption(AI.Option.Air.id.ECM_USING, 1) + end + + return self +end + + +--- Defines the usage of Electronic Counter Measures by airborne forces. If the AI is being detected by a radar they will enable their ECM. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionECM_DetectedLockByRadar() + self:F2( { self.ControllableName } ) + + if self:IsAir() then + self:SetOption(AI.Option.Air.id.ECM_USING, 2) + end + + return self +end + +--- Defines the usage of Electronic Counter Measures by airborne forces. AI will leave their ECM on all the time. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionECM_AlwaysOn() + self:F2( { self.ControllableName } ) + + if self:IsAir() then + self:SetOption(AI.Option.Air.id.ECM_USING, 3) + end + + return self +end --- Retrieve the controllable mission and allow to place function hooks within the mission waypoint plan. -- Use the method @{Wrapper.Controllable#CONTROLLABLE:WayPointFunction} to define the hook functions for specific waypoints. @@ -3024,6 +3628,47 @@ function CONTROLLABLE:IsAirPlane() return nil end +--- Returns if the Controllable contains Helicopters. +-- @param #CONTROLLABLE self +-- @return #boolean true if Controllable contains Helicopters. +function CONTROLLABLE:IsHelicopter() + self:F2() + local DCSObject = self:GetDCSObject() --- Message APIs \ No newline at end of file + if DCSObject then + local Category = DCSObject:getDesc().category + return Category == Unit.Category.HELICOPTER + end + + return nil +end + +--- Sets Controllable Option for Restriction of Afterburner. +-- @param #CONTROLLABLE self +-- @param #boolean RestrictBurner If true, restrict burner. If false or nil, allow (unrestrict) burner. +function CONTROLLABLE:OptionRestrictBurner(RestrictBurner) + self:F2({self.ControllableName}) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + local Controller = self:_GetController() + + if Controller then + + -- Issue https://github.com/FlightControl-Master/MOOSE/issues/1216 + if RestrictBurner == true then + if self:IsAir() then + Controller:setOption(16, true) + end + else + if self:IsAir() then + Controller:setOption(16, false) + end + end + + end + end + +end diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index e6c6648fd..9ac9211c2 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -173,6 +173,62 @@ GROUPTEMPLATE.Takeoff = { [GROUP.Takeoff.Cold] = { "TakeOffParking", "From Parking Area" } } +--- Generalized group attributes. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit. +-- @type GROUP.Attribute +-- @field #string AIR_TRANSPORTPLANE Airplane with transport capability. This can be used to transport other assets. +-- @field #string AIR_AWACS Airborne Early Warning and Control System. +-- @field #string AIR_FIGHTER Fighter, interceptor, ... airplane. +-- @field #string AIR_BOMBER Aircraft which can be used for strategic bombing. +-- @field #string AIR_TANKER Airplane which can refuel other aircraft. +-- @field #string AIR_TRANSPORTHELO Helicopter with transport capability. This can be used to transport other assets. +-- @field #string AIR_ATTACKHELO Attack helicopter. +-- @field #string AIR_UAV Unpiloted Aerial Vehicle, e.g. drones. +-- @field #string AIR_OTHER Any airborne unit that does not fall into any other airborne category. +-- @field #string GROUND_APC Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets. +-- @field #string GROUND_TRUCK Unarmed ground vehicles, which has the DCS "Truck" attribute. +-- @field #string GROUND_INFANTRY Ground infantry assets. +-- @field #string GROUND_ARTILLERY Artillery assets. +-- @field #string GROUND_TANK Tanks (modern or old). +-- @field #string GROUND_TRAIN Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently. +-- @field #string GROUND_EWR Early Warning Radar. +-- @field #string GROUND_AAA Anti-Aircraft Artillery. +-- @field #string GROUND_SAM Surface-to-Air Missile system or components. +-- @field #string GROUND_OTHER Any ground unit that does not fall into any other ground category. +-- @field #string NAVAL_AIRCRAFTCARRIER Aircraft carrier. +-- @field #string NAVAL_WARSHIP War ship, i.e. cruisers, destroyers, firgates and corvettes. +-- @field #string NAVAL_ARMEDSHIP Any armed ship that is not an aircraft carrier, a cruiser, destroyer, firgatte or corvette. +-- @field #string NAVAL_UNARMEDSHIP Any unarmed naval vessel. +-- @field #string NAVAL_OTHER Any naval unit that does not fall into any other naval category. +-- @field #string OTHER_UNKNOWN Anything that does not fall into any other category. +GROUP.Attribute = { + AIR_TRANSPORTPLANE="Air_TransportPlane", + AIR_AWACS="Air_AWACS", + AIR_FIGHTER="Air_Fighter", + AIR_BOMBER="Air_Bomber", + AIR_TANKER="Air_Tanker", + AIR_TRANSPORTHELO="Air_TransportHelo", + AIR_ATTACKHELO="Air_AttackHelo", + AIR_UAV="Air_UAV", + AIR_OTHER="Air_OtherAir", + GROUND_APC="Ground_APC", + GROUND_TRUCK="Ground_Truck", + GROUND_INFANTRY="Ground_Infantry", + GROUND_ARTILLERY="Ground_Artillery", + GROUND_TANK="Ground_Tank", + GROUND_TRAIN="Ground_Train", + GROUND_EWR="Ground_EWR", + GROUND_AAA="Ground_AAA", + GROUND_SAM="Ground_SAM", + GROUND_OTHER="Ground_OtherGround", + NAVAL_AIRCRAFTCARRIER="Naval_AircraftCarrier", + NAVAL_WARSHIP="Naval_WarShip", + NAVAL_ARMEDSHIP="Naval_ArmedShip", + NAVAL_UNARMEDSHIP="Naval_UnarmedShip", + NAVAL_OTHER="Naval_OtherNaval", + OTHER_UNKNOWN="Other_Unknown", +} + + --- Create a new GROUP from a given GroupTemplate as a parameter. -- Note that the GroupTemplate is NOT spawned into the mission. -- It is merely added to the @{Core.Database}. @@ -260,9 +316,12 @@ function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3 local DCSPositionable = self:GetDCSObject() if DCSPositionable then - local PositionablePosition = DCSPositionable:getUnits()[1]:getPosition().p + local unit = DCSPositionable:getUnits()[1] + if unit then + local PositionablePosition = unit:getPosition().p self:T3( PositionablePosition ) return PositionablePosition + end end return nil @@ -310,9 +369,11 @@ function GROUP:IsActive() local DCSGroup = self:GetDCSObject() -- DCS#Group if DCSGroup then - - local GroupIsActive = DCSGroup:getUnit(1):isActive() + local unit = DCSGroup:getUnit(1) + if unit then + local GroupIsActive = unit:isActive() return GroupIsActive + end end return nil @@ -325,7 +386,8 @@ end -- So all event listeners will catch the destroy event of this group for each unit in the group. -- To raise these events, provide the `GenerateEvent` parameter. -- @param #GROUP self --- @param #boolean GenerateEvent true if you want to generate a crash or dead event for each unit. +-- @param #boolean GenerateEvent If true, a crash or dead event for each unit is generated. If false, if no event is triggered. If nil, a RemoveUnit event is triggered. +-- @param #number delay Delay in seconds before despawning the group. -- @usage -- -- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group. -- Helicopter = GROUP:FindByName( "Helicopter" ) @@ -344,37 +406,50 @@ end -- Ship = GROUP:FindByName( "Boat" ) -- Ship:Destroy( false ) -- Don't generate an event upon destruction. -- -function GROUP:Destroy( GenerateEvent ) +function GROUP:Destroy( GenerateEvent, delay ) self:F2( self.GroupName ) + + if delay and delay>0 then + --SCHEDULER:New(nil, GROUP.Destroy, {self, GenerateEvent}, delay) + self:ScheduleOnce(delay, GROUP.Destroy, self, GenerateEvent) + else - local DCSGroup = self:GetDCSObject() - - if DCSGroup then - for Index, UnitData in pairs( DCSGroup:getUnits() ) do - if GenerateEvent and GenerateEvent == true then - if self:IsAir() then - self:CreateEventCrash( timer.getTime(), UnitData ) + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + if GenerateEvent and GenerateEvent == true then + if self:IsAir() then + self:CreateEventCrash( timer.getTime(), UnitData ) + else + self:CreateEventDead( timer.getTime(), UnitData ) + end + elseif GenerateEvent == false then + -- Do nothing! else - self:CreateEventDead( timer.getTime(), UnitData ) + self:CreateEventRemoveUnit( timer.getTime(), UnitData ) end - elseif GenerateEvent == false then - -- Do nothing! - else - self:CreateEventRemoveUnit( timer.getTime(), UnitData ) end + USERFLAG:New( self:GetName() ):Set( 100 ) + DCSGroup:destroy() + DCSGroup = nil end - USERFLAG:New( self:GetName() ):Set( 100 ) - DCSGroup:destroy() - DCSGroup = nil end - + return nil end ---- Returns category of the DCS Group. +--- Returns category of the DCS Group. Returns one of +-- +-- * Group.Category.AIRPLANE +-- * Group.Category.HELICOPTER +-- * Group.Category.GROUND +-- * Group.Category.SHIP +-- * Group.Category.TRAIN +-- -- @param #GROUP self --- @return DCS#Group.Category The category ID +-- @return DCS#Group.Category The category ID. function GROUP:GetCategory() self:F2( self.GroupName ) @@ -390,7 +465,7 @@ end --- Returns the category name of the #GROUP. -- @param #GROUP self --- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship +-- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship, Train. function GROUP:GetCategoryName() self:F2( self.GroupName ) @@ -401,6 +476,7 @@ function GROUP:GetCategoryName() [Group.Category.HELICOPTER] = "Helicopter", [Group.Category.GROUND] = "Ground Unit", [Group.Category.SHIP] = "Ship", + [Group.Category.TRAIN] = "Train", } local GroupCategory = DCSGroup:getCategory() self:T3( GroupCategory ) @@ -647,6 +723,49 @@ function GROUP:GetSize() return nil end +--- Count number of alive units in the group. +-- @param #GROUP self +-- @return #number Number of alive units. If DCS group is nil, 0 is returned. +function GROUP:CountAliveUnits() + self:F3( { self.GroupName } ) + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local units=self:GetUnits() + local n=0 + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + n=n+1 + end + end + return n + end + + return 0 +end + +--- Get the first unit of the group which is alive. +-- @param #GROUP self +-- @return Wrapper.Unit#UNIT First unit alive. +function GROUP:GetFirstUnitAlive() + self:F3({self.GroupName}) + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local units=self:GetUnits() + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + return unit + end + end + end + + return nil +end + + --- Returns the average velocity Vec3 vector. -- @param Wrapper.Group#GROUP self @@ -756,11 +875,16 @@ end --- Activates a late activated GROUP. -- @param #GROUP self +-- @param #number delay Delay in seconds, before the group is activated. -- @return #GROUP self -function GROUP:Activate() +function GROUP:Activate(delay) self:F2( { self.GroupName } ) - trigger.action.activateGroup( self:GetDCSObject() ) - return self:GetDCSObject() + if delay and delay>0 then + self:ScheduleOnce(delay, GROUP.Activate, self) + else + trigger.action.activateGroup( self:GetDCSObject() ) + end + return self end @@ -934,7 +1058,7 @@ end -- @return #number The fuel state of the unit with the least amount of fuel -- @return #Unit reference to #Unit object for further processing function GROUP:GetFuelMin() - self:F(self.ControllableName) + self:F3(self.ControllableName) if not self:GetDCSObject() then BASE:E( { "Cannot GetFuel", Group = self, Alive = self:IsAlive() } ) @@ -946,10 +1070,12 @@ function GROUP:GetFuelMin() local tmp = nil for UnitID, UnitData in pairs( self:GetUnits() ) do - tmp = UnitData:GetFuel() - if tmp < min then - min = tmp - unit = UnitData + if UnitData and UnitData:IsAlive() then + tmp = UnitData:GetFuel() + if tmp < min then + min = tmp + unit = UnitData + end end end @@ -994,6 +1120,45 @@ function GROUP:GetFuel() end +--- Get the number of shells, rockets, bombs and missiles the whole group currently has. +-- @param #GROUP self +-- @return #number Total amount of ammo the group has left. This is the sum of shells, rockets, bombs and missiles of all units. +-- @return #number Number of shells left. +-- @return #number Number of rockets left. +-- @return #number Number of bombs left. +-- @return #number Number of missiles left. +function GROUP:GetAmmunition() + self:F( self.ControllableName ) + + local DCSControllable = self:GetDCSObject() + + local Ntot=0 + local Nshells=0 + local Nrockets=0 + local Nmissiles=0 + + if DCSControllable then + + -- Loop over units. + for UnitID, UnitData in pairs( self:GetUnits() ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + + -- Get ammo of the unit + local ntot, nshells, nrockets, nmissiles = Unit:GetAmmunition() + + Ntot=Ntot+ntot + Nshells=Nshells+nshells + Nrockets=Nrockets+nrockets + Nmissiles=Nmissiles+nmissiles + + end + + end + + return Ntot, Nshells, Nrockets, Nmissiles +end + + do -- Is Zone methods --- Returns true if all units of the group are within a @{Zone}. @@ -1455,6 +1620,67 @@ function GROUP:InitRandomizePositionRadius( OuterRadius, InnerRadius ) return self end +--- Set respawn coordinate. +-- @param #GROUP self +-- @param Core.Point#COORDINATE coordinate Coordinate where the group should be respawned. +-- @return #GROUP self +function GROUP:InitCoordinate(coordinate) + self:F({coordinate=coordinate}) + self.InitCoord=coordinate + return self +end + +--- Sets the radio comms on or off when the group is respawned. Same as checking/unchecking the COMM box in the mission editor. +-- @param #GROUP self +-- @param #boolean switch If true (or nil), enables the radio comms. If false, disables the radio for the spawned group. +-- @return #GROUP self +function GROUP:InitRadioCommsOnOff(switch) + self:F({switch=switch}) + if switch==true or switch==nil then + self.InitRespawnRadio=true + else + self.InitRespawnRadio=false + end + return self +end + +--- Sets the radio frequency of the group when it is respawned. +-- @param #GROUP self +-- @param #number frequency The frequency in MHz. +-- @return #GROUP self +function GROUP:InitRadioFrequency(frequency) + self:F({frequency=frequency}) + + self.InitRespawnFreq=frequency + + return self +end + +--- Set radio modulation when the group is respawned. Default is AM. +-- @param #GROUP self +-- @param #string modulation Either "FM" or "AM". If no value is given, modulation is set to AM. +-- @return #GROUP self +function GROUP:InitRadioModulation(modulation) + self:F({modulation=modulation}) + if modulation and modulation:lower()=="fm" then + self.InitRespawnModu=radio.modulation.FM + else + self.InitRespawnModu=radio.modulation.AM + end + return self +end + +--- Sets the modex (tail number) of the first unit of the group. If more units are in the group, the number is increased with every unit. +-- @param #GROUP self +-- @param #string modex Tail number of the first unit. +-- @return #GROUP self +function GROUP:InitModex(modex) + self:F({modex=modex}) + if modex then + self.InitRespawnModex=tonumber(modex) + end + return self +end --- Respawn the @{Wrapper.Group} at a @{Point}. -- The method will setup the new group template according the Init(Respawn) settings provided for the group. @@ -1477,29 +1703,61 @@ end -- -- @param Wrapper.Group#GROUP self -- @param #table Template (optional) The template of the Group retrieved with GROUP:GetTemplate(). If the template is not provided, the template will be retrieved of the group itself. +-- @param #boolean Reset Reset positions if TRUE. +-- @return Wrapper.Group#GROUP self function GROUP:Respawn( Template, Reset ) - if not Template then - Template = self:GetTemplate() - end + -- Given template or get old. + Template = Template or self:GetTemplate() + + -- Get correct heading. + local function _Heading(course) + local h + if course<=180 then + h=math.rad(course) + else + h=-math.rad(360-course) + end + return h + end + -- First check if group is alive. if self:IsAlive() then + + -- Respawn zone. local Zone = self.InitRespawnZone -- Core.Zone#ZONE + + -- Zone position or current group position. local Vec3 = Zone and Zone:GetVec3() or self:GetVec3() + + -- From point of the template. local From = { x = Template.x, y = Template.y } + + -- X, Y Template.x = Vec3.x Template.y = Vec3.z + --Template.x = nil --Template.y = nil + -- Debug number of units. self:F( #Template.units ) + + -- Reset position etc? if Reset == true then + + -- Loop over units in group. for UnitID, UnitData in pairs( self:GetUnits() ) do local GroupUnit = UnitData -- Wrapper.Unit#UNIT - self:F( GroupUnit:GetName() ) + self:F(GroupUnit:GetName()) + if GroupUnit:IsAlive() then - self:F( "Alive" ) - local GroupUnitVec3 = GroupUnit:GetVec3() + self:I("FF Alive") + + -- Get unit position vector. + local GroupUnitVec3 = GroupUnit:GetVec3() + + -- Check if respawn zone is set. if Zone then if self.InitRespawnRandomizePositionZone then GroupUnitVec3 = Zone:GetRandomVec3() @@ -1512,17 +1770,43 @@ function GROUP:Respawn( Template, Reset ) end end + -- Coordinate where the group should be respawned. + if self.InitCoord then + GroupUnitVec3=self.InitCoord:GetVec3() + end + + -- Altitude Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y - Template.units[UnitID].x = ( Template.units[UnitID].x - From.x ) + GroupUnitVec3.x -- Keep the original x position of the template and translate to the new position. - Template.units[UnitID].y = ( Template.units[UnitID].y - From.y ) + GroupUnitVec3.z -- Keep the original z position of the template and translate to the new position. - Template.units[UnitID].heading = self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading() + + -- Unit position. Why not simply take the current positon? + if Zone then + Template.units[UnitID].x = ( Template.units[UnitID].x - From.x ) + GroupUnitVec3.x -- Keep the original x position of the template and translate to the new position. + Template.units[UnitID].y = ( Template.units[UnitID].y - From.y ) + GroupUnitVec3.z -- Keep the original z position of the template and translate to the new position. + else + Template.units[UnitID].x=GroupUnitVec3.x + Template.units[UnitID].y=GroupUnitVec3.z + end + + -- Set heading. + Template.units[UnitID].heading = _Heading(self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading()) + Template.units[UnitID].psi = -Template.units[UnitID].heading + + -- Debug. self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) end end - else + + elseif Reset==false then -- Reset=false or nil + + -- Loop over template units. for UnitID, TemplateUnitData in pairs( Template.units ) do + self:F( "Reset" ) + + -- Position from template. local GroupUnitVec3 = { x = TemplateUnitData.x, y = TemplateUnitData.alt, z = TemplateUnitData.y } + + -- Respawn zone position. if Zone then if self.InitRespawnRandomizePositionZone then GroupUnitVec3 = Zone:GetRandomVec3() @@ -1535,23 +1819,82 @@ function GROUP:Respawn( Template, Reset ) end end + -- Coordinate where the group should be respawned. + if self.InitCoord then + GroupUnitVec3=self.InitCoord:GetVec3() + end + + -- Set altitude. Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y + + -- Unit position. Template.units[UnitID].x = ( Template.units[UnitID].x - From.x ) + GroupUnitVec3.x -- Keep the original x position of the template and translate to the new position. Template.units[UnitID].y = ( Template.units[UnitID].y - From.y ) + GroupUnitVec3.z -- Keep the original z position of the template and translate to the new position. + + -- Heading Template.units[UnitID].heading = self.InitRespawnHeading and self.InitRespawnHeading or TemplateUnitData.heading + + -- Debug. self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } ) end - end + + else + + local units=self:GetUnits() + + -- Loop over template units. + for UnitID, Unit in pairs(Template.units) do + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit:GetName()==Unit.name then + local coord=unit:GetCoordinate() + local heading=unit:GetHeading() + Unit.x=coord.x + Unit.y=coord.z + Unit.alt=coord.y + Unit.heading=math.rad(heading) + Unit.psi=-Unit.heading + end + end + + end + + end end - self:Destroy() - _DATABASE:Spawn( Template ) + -- Set tail number. + if self.InitRespawnModex then + for UnitID=1,#Template.units do + Template.units[UnitID].onboard_num=string.format("%03d", self.InitRespawnModex+(UnitID-1)) + end + end + -- Set radio frequency and modulation. + if self.InitRespawnRadio then + Template.communication=self.InitRespawnRadio + end + if self.InitRespawnFreq then + Template.frequency=self.InitRespawnFreq + end + if self.InitRespawnModu then + Template.modulation=self.InitRespawnModu + end + + -- Destroy old group. Dont trigger any dead/crash events since this is a respawn. + self:Destroy(false) + + self:T({Template=Template}) + + -- Spawn new group. + _DATABASE:Spawn(Template) + + -- Reset events. self:ResetEvents() return self - end @@ -1566,98 +1909,115 @@ end function GROUP:RespawnAtCurrentAirbase(SpawnTemplate, Takeoff, Uncontrolled) -- R2.4 self:F2( { SpawnTemplate, Takeoff, Uncontrolled} ) - -- Get closest airbase. Should be the one we are currently on. - local airbase=self:GetCoordinate():GetClosestAirbase() - - if airbase then - self:F2("Closest airbase = "..airbase:GetName()) - else - self:E("ERROR: could not find closest airbase!") - return nil - end - -- Takeoff type. Default hot. - Takeoff = Takeoff or SPAWN.Takeoff.Hot - - -- Coordinate of the airbase. - local AirbaseCoord=airbase:GetCoordinate() - - -- Spawn template. - SpawnTemplate = SpawnTemplate or self:GetTemplate() + if self and self:IsAlive() then - if SpawnTemplate then - - local SpawnPoint = SpawnTemplate.route.points[1] - - -- These are only for ships. - SpawnPoint.linkUnit = nil - SpawnPoint.helipadId = nil - SpawnPoint.airdromeId = nil - - -- Aibase id and category. - local AirbaseID = airbase:GetID() - local AirbaseCategory = airbase:GetDesc().category + -- Get closest airbase. Should be the one we are currently on. + local airbase=self:GetCoordinate():GetClosestAirbase() - if AirbaseCategory == Airbase.Category.SHIP or AirbaseCategory == Airbase.Category.HELIPAD then - SpawnPoint.linkUnit = AirbaseID - SpawnPoint.helipadId = AirbaseID - elseif AirbaseCategory == Airbase.Category.AIRDROME then - SpawnPoint.airdromeId = AirbaseID + if airbase then + self:F2("Closest airbase = "..airbase:GetName()) + else + self:E("ERROR: could not find closest airbase!") + return nil end - + -- Takeoff type. Default hot. + Takeoff = Takeoff or SPAWN.Takeoff.Hot - SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type - SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + -- Coordinate of the airbase. + local AirbaseCoord=airbase:GetCoordinate() - -- Get the units of the group. - local units=self:GetUnits() - - local x - local y - for UnitID=1,#units do + -- Spawn template. + SpawnTemplate = SpawnTemplate or self:GetTemplate() + + if SpawnTemplate then + + local SpawnPoint = SpawnTemplate.route.points[1] + + -- These are only for ships. + SpawnPoint.linkUnit = nil + SpawnPoint.helipadId = nil + SpawnPoint.airdromeId = nil + + -- Aibase id and category. + local AirbaseID = airbase:GetID() + local AirbaseCategory = airbase:GetAirbaseCategory() + + if AirbaseCategory == Airbase.Category.SHIP or AirbaseCategory == Airbase.Category.HELIPAD then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.AIRDROME then + SpawnPoint.airdromeId = AirbaseID + end + + + SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + + -- Get the units of the group. + local units=self:GetUnits() + + local x + local y + for UnitID=1,#units do + + local unit=units[UnitID] --Wrapper.Unit#UNIT + + -- Get closest parking spot of current unit. Note that we look for occupied spots since the unit is currently sitting on it! + local Parkingspot, TermialID, Distance=unit:GetCoordinate():GetClosestParkingSpot(airbase) - local unit=units[UnitID] --Wrapper.Unit#UNIT - - -- Get closest parking spot of current unit. Note that we look for occupied spots since the unit is currently sitting on it! - local Parkingspot, TermialID, Distance=unit:GetCoordinate():GetClosestParkingSpot(airbase) - - --Parkingspot:MarkToAll("parking spot") - self:T2(string.format("Closest parking spot distance = %s, terminal ID=%s", tostring(Distance), tostring(TermialID))) - - -- Get unit coordinates for respawning position. - local uc=unit:GetCoordinate() - --uc:MarkToAll(string.format("re-spawnplace %s terminal %d", unit:GetName(), TermialID)) - - SpawnTemplate.units[UnitID].x = uc.x --Parkingspot.x - SpawnTemplate.units[UnitID].y = uc.z --Parkingspot.z - SpawnTemplate.units[UnitID].alt = uc.y --Parkingspot.y - - SpawnTemplate.units[UnitID].parking = TermialID - SpawnTemplate.units[UnitID].parking_id = nil - - --SpawnTemplate.units[UnitID].unitId=nil - end - - --SpawnTemplate.groupId=nil - - SpawnPoint.x = SpawnTemplate.units[1].x --x --AirbaseCoord.x - SpawnPoint.y = SpawnTemplate.units[1].y --y --AirbaseCoord.z - SpawnPoint.alt = SpawnTemplate.units[1].alt --AirbaseCoord:GetLandHeight() - - SpawnTemplate.x = SpawnTemplate.units[1].x --x --AirbaseCoord.x - SpawnTemplate.y = SpawnTemplate.units[1].y --y --AirbaseCoord.z - - -- Set uncontrolled state. - SpawnTemplate.uncontrolled=Uncontrolled - - -- Destroy old group. - self:Destroy(false) - - _DATABASE:Spawn( SpawnTemplate ) + --Parkingspot:MarkToAll("parking spot") + self:T2(string.format("Closest parking spot distance = %s, terminal ID=%s", tostring(Distance), tostring(TermialID))) - -- Reset events. - self:ResetEvents() - - return self + -- Get unit coordinates for respawning position. + local uc=unit:GetCoordinate() + --uc:MarkToAll(string.format("re-spawnplace %s terminal %d", unit:GetName(), TermialID)) + + SpawnTemplate.units[UnitID].x = uc.x --Parkingspot.x + SpawnTemplate.units[UnitID].y = uc.z --Parkingspot.z + SpawnTemplate.units[UnitID].alt = uc.y --Parkingspot.y + + SpawnTemplate.units[UnitID].parking = TermialID + SpawnTemplate.units[UnitID].parking_id = nil + + --SpawnTemplate.units[UnitID].unitId=nil + end + + --SpawnTemplate.groupId=nil + + SpawnPoint.x = SpawnTemplate.units[1].x --x --AirbaseCoord.x + SpawnPoint.y = SpawnTemplate.units[1].y --y --AirbaseCoord.z + SpawnPoint.alt = SpawnTemplate.units[1].alt --AirbaseCoord:GetLandHeight() + + SpawnTemplate.x = SpawnTemplate.units[1].x --x --AirbaseCoord.x + SpawnTemplate.y = SpawnTemplate.units[1].y --y --AirbaseCoord.z + + -- Set uncontrolled state. + SpawnTemplate.uncontrolled=Uncontrolled + + -- Set radio frequency and modulation. + if self.InitRespawnRadio then + SpawnTemplate.communication=self.InitRespawnRadio + end + if self.InitRespawnFreq then + SpawnTemplate.frequency=self.InitRespawnFreq + end + if self.InitRespawnModu then + SpawnTemplate.modulation=self.InitRespawnModu + end + + -- Destroy old group. + self:Destroy(false) + + -- Spawn new group. + _DATABASE:Spawn(SpawnTemplate) + + -- Reset events. + self:ResetEvents() + + return self + end + else + self:E("WARNING: GROUP is not alive!") end return nil @@ -1770,6 +2130,47 @@ function GROUP:InAir() return nil end +--- Checks whether any unit (or optionally) all units of a group is(are) airbore or not. +-- @param Wrapper.Group#GROUP self +-- @param #boolean AllUnits (Optional) If true, check whether all units of the group are airborne. +-- @return #boolean True if at least one (optionally all) unit(s) is(are) airborne or false otherwise. Nil if no unit exists or is alive. +function GROUP:IsAirborne(AllUnits) + self:F2( self.GroupName ) + + -- Get all units of the group. + local units=self:GetUnits() + + if units then + + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit then + + -- Unit in air or not. + local inair=unit:InAir() + + -- Unit is not in air and we wanted to know whether ALL units are ==> return false + if inair==false and AllUnits==true then + return false + end + + -- At least one unit is in are and we did not care which one. + if inair==true and not AllUnits then + return true + end + + end + -- At least one unit is in the air. + return true + end + end + + return nil +end + + + --- Returns the DCS descriptor table of the nth unit of the group. -- @param #GROUP self -- @param #number n (Optional) The number of the unit for which the dscriptor is returned. @@ -1787,6 +2188,117 @@ function GROUP:GetDCSDesc(n) return nil end + +--- Get the generalized attribute of a self. +-- Note that for a heterogenious self, the attribute is determined from the attribute of the first unit! +-- @param #GROUP self +-- @return #string Generalized attribute of the self. +function GROUP:GetAttribute() + + -- Default + local attribute=GROUP.Attribute.OTHER_UNKNOWN --#GROUP.Attribute + + if self then + + ----------- + --- Air --- + ----------- + -- Planes + local transportplane=self:HasAttribute("Transports") and self:HasAttribute("Planes") + local awacs=self:HasAttribute("AWACS") + local fighter=self:HasAttribute("Fighters") or self:HasAttribute("Interceptors") or self:HasAttribute("Multirole fighters") or (self:HasAttribute("Bombers") and not self:HasAttribute("Strategic bombers")) + local bomber=self:HasAttribute("Strategic bombers") + local tanker=self:HasAttribute("Tankers") + local uav=self:HasAttribute("UAVs") + -- Helicopters + local transporthelo=self:HasAttribute("Transport helicopters") + local attackhelicopter=self:HasAttribute("Attack helicopters") + + -------------- + --- Ground --- + -------------- + -- Ground + local apc=self:HasAttribute("Infantry carriers") + local truck=self:HasAttribute("Trucks") and self:GetCategory()==Group.Category.GROUND + local infantry=self:HasAttribute("Infantry") + local artillery=self:HasAttribute("Artillery") + local tank=self:HasAttribute("Old Tanks") or self:HasAttribute("Modern Tanks") + local aaa=self:HasAttribute("AAA") + local ewr=self:HasAttribute("EWR") + local sam=self:HasAttribute("SAM elements") and (not self:HasAttribute("AAA")) + -- Train + local train=self:GetCategory()==Group.Category.TRAIN + + ------------- + --- Naval --- + ------------- + -- Ships + local aircraftcarrier=self:HasAttribute("Aircraft Carriers") + local warship=self:HasAttribute("Heavy armed ships") + local armedship=self:HasAttribute("Armed ships") + local unarmedship=self:HasAttribute("Unarmed ships") + + + -- Define attribute. Order is important. + if transportplane then + attribute=GROUP.Attribute.AIR_TRANSPORTPLANE + elseif awacs then + attribute=GROUP.Attribute.AIR_AWACS + elseif fighter then + attribute=GROUP.Attribute.AIR_FIGHTER + elseif bomber then + attribute=GROUP.Attribute.AIR_BOMBER + elseif tanker then + attribute=GROUP.Attribute.AIR_TANKER + elseif transporthelo then + attribute=GROUP.Attribute.AIR_TRANSPORTHELO + elseif attackhelicopter then + attribute=GROUP.Attribute.AIR_ATTACKHELO + elseif uav then + attribute=GROUP.Attribute.AIR_UAV + elseif apc then + attribute=GROUP.Attribute.GROUND_APC + elseif infantry then + attribute=GROUP.Attribute.GROUND_INFANTRY + elseif artillery then + attribute=GROUP.Attribute.GROUND_ARTILLERY + elseif tank then + attribute=GROUP.Attribute.GROUND_TANK + elseif aaa then + attribute=GROUP.Attribute.GROUND_AAA + elseif ewr then + attribute=GROUP.Attribute.GROUND_EWR + elseif sam then + attribute=GROUP.Attribute.GROUND_SAM + elseif truck then + attribute=GROUP.Attribute.GROUND_TRUCK + elseif train then + attribute=GROUP.Attribute.GROUND_TRAIN + elseif aircraftcarrier then + attribute=GROUP.Attribute.NAVAL_AIRCRAFTCARRIER + elseif warship then + attribute=GROUP.Attribute.NAVAL_WARSHIP + elseif armedship then + attribute=GROUP.Attribute.NAVAL_ARMEDSHIP + elseif unarmedship then + attribute=GROUP.Attribute.NAVAL_UNARMEDSHIP + else + if self:IsGround() then + attribute=GROUP.Attribute.GROUND_OTHER + elseif self:IsShip() then + attribute=GROUP.Attribute.NAVAL_OTHER + elseif self:IsAir() then + attribute=GROUP.Attribute.AIR_OTHER + else + attribute=GROUP.Attribute.OTHER_UNKNOWN + end + end + end + + return attribute +end + + do -- Route methods --- (AIR) Return the Group to an @{Wrapper.Airbase#AIRBASE}. @@ -1800,8 +2312,8 @@ do -- Route methods -- -- @param #GROUP self -- @param Wrapper.Airbase#AIRBASE RTBAirbase (optional) The @{Wrapper.Airbase} to return to. If blank, the controllable will return to the nearest friendly airbase. - -- @param #number Speed (optional) The Speed, if no Speed is given, the maximum Speed of the first unit is selected. - -- @return #GROUP + -- @param #number Speed (optional) The Speed, if no Speed is given, 80% of maximum Speed of the group is selected. + -- @return #GROUP self function GROUP:RouteRTB( RTBAirbase, Speed ) self:F( { RTBAirbase:GetName(), Speed } ) @@ -1811,42 +2323,42 @@ do -- Route methods if RTBAirbase then - local GroupPoint = self:GetVec2() - local GroupVelocity = self:GetUnit(1):GetDesc().speedMax - - local PointFrom = {} - PointFrom.x = GroupPoint.x - PointFrom.y = GroupPoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = GroupVelocity - - - local PointTo = {} - local AirbasePointVec2 = RTBAirbase:GetPointVec2() - local AirbaseAirPoint = AirbasePointVec2:WaypointAir( - POINT_VEC3.RoutePointAltType.BARO, - "Land", - "Landing", - Speed or self:GetUnit(1):GetDesc().speedMax - ) + -- If speed is not given take 80% of max speed. + local Speed=Speed or self:GetSpeedMax()*0.8 - AirbaseAirPoint["airdromeId"] = RTBAirbase:GetID() - AirbaseAirPoint["speed_locked"] = true, + -- Curent (from) waypoint. + local coord=self:GetCoordinate() + local PointFrom=coord:WaypointAirTurningPoint(nil, Speed) + + -- Airbase coordinate. + --local PointAirbase=RTBAirbase:GetCoordinate():SetAltitude(coord.y):WaypointAirTurningPoint(nil ,Speed) + + -- Landing waypoint. More general than prev version since it should also work with FAPRS and ships. + local PointLanding=RTBAirbase:GetCoordinate():WaypointAirLanding(Speed, RTBAirbase) + + -- Waypoint table. + local Points={PointFrom, PointLanding} + --local Points={PointFrom, PointAirbase, PointLanding} - self:F(AirbaseAirPoint ) - - local Points = { PointFrom, AirbaseAirPoint } - - self:T3( Points ) + -- Debug info. + self:T3(Points) - local Template = self:GetTemplate() - Template.route.points = Points - self:Respawn( Template ) - - --self:Route( Points ) + -- Get group template. + local Template=self:GetTemplate() + + -- Set route points. + Template.route.points=Points + + -- Respawn the group. + self:Respawn(Template, true) + + -- Route the group or this will not work. + self:Route(Points) else + + -- Clear all tasks. self:ClearTasks() + end end diff --git a/Moose Development/Moose/Wrapper/Identifiable.lua b/Moose Development/Moose/Wrapper/Identifiable.lua index 13a5b3234..5b340aec8 100644 --- a/Moose Development/Moose/Wrapper/Identifiable.lua +++ b/Moose Development/Moose/Wrapper/Identifiable.lua @@ -90,7 +90,6 @@ end --- Returns the type name of the DCS Identifiable. -- @param #IDENTIFIABLE self -- @return #string The type name of the DCS Identifiable. --- @return #nil The DCS Identifiable is not existing or alive. function IDENTIFIABLE:GetTypeName() self:F2( self.IdentifiableName ) @@ -107,9 +106,17 @@ function IDENTIFIABLE:GetTypeName() end ---- Returns category of the DCS Identifiable. +--- Returns object category of the DCS Identifiable. One of +-- +-- * Object.Category.UNIT = 1 +-- * Object.Category.WEAPON = 2 +-- * Object.Category.STATIC = 3 +-- * Object.Category.BASE = 4 +-- * Object.Category.SCENERY = 5 +-- * Object.Category.Cargo = 6 +-- -- @param #IDENTIFIABLE self --- @return DCS#Object.Category The category ID +-- @return DCS#Object.Category The category ID, i.e. a number. function IDENTIFIABLE:GetCategory() self:F2( self.ObjectName ) @@ -168,20 +175,13 @@ function IDENTIFIABLE:GetCoalitionName() local DCSIdentifiable = self:GetDCSObject() if DCSIdentifiable then + + -- Get coaliton ID. local IdentifiableCoalition = DCSIdentifiable:getCoalition() self:T3( IdentifiableCoalition ) - if IdentifiableCoalition == coalition.side.BLUE then - return "Blue" - end + return UTILS.GetCoalitionName(IdentifiableCoalition) - if IdentifiableCoalition == coalition.side.RED then - return "Red" - end - - if IdentifiableCoalition == coalition.side.NEUTRAL then - return "Neutral" - end end self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index fef546f38..be85454a3 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -4,7 +4,7 @@ -- -- ### Author: **FlightControl** -- --- ### Contributions: +-- ### Contributions: **Hardcard**, **funkyfranky** -- -- === -- @@ -63,7 +63,7 @@ POSITIONABLE.__.Cargo = {} -- @param #string PositionableName The POSITIONABLE name -- @return #POSITIONABLE self function POSITIONABLE:New( PositionableName ) - local self = BASE:Inherit( self, IDENTIFIABLE:New( PositionableName ) ) + local self = BASE:Inherit( self, IDENTIFIABLE:New( PositionableName ) ) -- #POSITIONABLE self.PositionableName = PositionableName return self @@ -310,6 +310,44 @@ function POSITIONABLE:GetCoordinate() return nil end +--- Returns a COORDINATE object, which is offset with respect to the orientation of the POSITIONABLE. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @param #number x Offset in the direction "the nose" of the unit is pointing in meters. Default 0 m. +-- @param #number y Offset "above" the unit in meters. Default 0 m. +-- @param #number z Offset in the direction "the wing" of the unit is pointing in meters. z>0 starboard, z<0 port. Default 0 m. +-- @return Core.Point#COORDINATE The COORDINATE of the offset with respect to the orientation of the POSITIONABLE. +function POSITIONABLE:GetOffsetCoordinate(x,y,z) + + -- Default if nil. + x=x or 0 + y=y or 0 + z=z or 0 + + -- Vectors making up the coordinate system. + local X=self:GetOrientationX() + local Y=self:GetOrientationY() + local Z=self:GetOrientationZ() + + -- Offset vector: x meters ahead, z meters starboard, y meters above. + local A={x=x, y=y, z=z} + + -- Scale components of orthonormal coordinate vectors. + local x={x=X.x*A.x, y=X.y*A.x, z=X.z*A.x} + local y={x=Y.x*A.y, y=Y.y*A.y, z=Y.z*A.y} + local z={x=Z.x*A.z, y=Z.y*A.z, z=Z.z*A.z} + + -- Add up vectors in the unit coordinate system ==> this gives the offset vector relative the the origin of the map. + local a={x=x.x+y.x+z.x, y=x.y+y.y+z.y, z=x.z+y.z+z.z} + + -- Vector from the origin of the map to the unit. + local u=self:GetVec3() + + -- Translate offset vector from map origin to the unit: v=u+a. + local v={x=a.x+u.x, y=a.y+u.y, z=a.z+u.z} + + -- Return the offset coordinate. + return COORDINATE:NewFromVec3(v) +end --- Returns a random @{DCS#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission. -- @param Wrapper.Positionable#POSITIONABLE self @@ -390,6 +428,27 @@ function POSITIONABLE:GetBoundingBox() --R2.1 end +--- Get the object size. +-- @param #POSITIONABLE self +-- @return DCS#Distance Max size of object in x, z or 0 if bounding box could not be obtained. +-- @return DCS#Distance Length x or 0 if bounding box could not be obtained. +-- @return DCS#Distance Height y or 0 if bounding box could not be obtained. +-- @return DCS#Distance Width z or 0 if bounding box could not be obtained. +function POSITIONABLE:GetObjectSize() + + -- Get bounding box. + local box=self:GetBoundingBox() + + if box then + local x=box.max.x+math.abs(box.min.x) --length + local y=box.max.y+math.abs(box.min.y) --height + local z=box.max.z+math.abs(box.min.z) --width + return math.max(x,z), x , y, z + end + + return 0,0,0,0 +end + --- Get the bounding radius of the underlying POSITIONABLE DCS Object. -- @param #POSITIONABLE self -- @param #number mindist (Optional) If bounding box is smaller than this value, mindist is returned. @@ -543,6 +602,26 @@ function POSITIONABLE:IsGround() end +--- Returns if the unit is of ship category. +-- @param #POSITIONABLE self +-- @return #boolean Ship category evaluation result. +function POSITIONABLE:IsShip() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + + local IsShip = ( UnitDescriptor.category == Unit.Category.SHIP ) + + return IsShip + end + + return nil +end + + --- Returns true if the POSITIONABLE is in the air. -- Polymorphic, is overridden in GROUP and UNIT. -- @param Wrapper.Positionable#POSITIONABLE self @@ -596,6 +675,21 @@ function POSITIONABLE:GetVelocityVec3() return nil end +--- Get relative velocity with respect to another POSITIONABLE. +-- @param #POSITIONABLE self +-- @param #POSITIONABLE positionable Other positionable. +-- @return #number Relative velocity in m/s. +function POSITIONABLE:GetRelativeVelocity(positionable) + self:F2( self.PositionableName ) + + local v1=self:GetVelocityVec3() + local v2=positionable:GetVelocityVec3() + + local vtot=UTILS.VecAdd(v1,v2) + + return UTILS.VecNorm(vtot) +end + --- Returns the POSITIONABLE height in meters. -- @param Wrapper.Positionable#POSITIONABLE self @@ -656,6 +750,14 @@ function POSITIONABLE:GetVelocityMPS() return 0 end +--- Returns the POSITIONABLE velocity in knots. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number The velocity in knots. +function POSITIONABLE:GetVelocityKNOTS() + self:F2( self.PositionableName ) + return UTILS.MpsToKnots(self:GetVelocityMPS()) +end + --- Returns the Angle of Attack of a positionable. -- @param Wrapper.Positionable#POSITIONABLE self -- @return #number Angle of attack in degrees. @@ -706,8 +808,8 @@ end --- Returns the unit's climb or descent angle. -- @param Wrapper.Positionable#POSITIONABLE self --- @return #number Climb or descent angle in degrees. -function POSITIONABLE:GetClimbAnge() +-- @return #number Climb or descent angle in degrees. Or 0 if velocity vector norm is zero (or nil). Or nil, if the position of the POSITIONABLE returns nil. +function POSITIONABLE:GetClimbAngle() -- Get position of the unit. local unitpos = self:GetPosition() @@ -719,10 +821,17 @@ function POSITIONABLE:GetClimbAnge() if unitvel and UTILS.VecNorm(unitvel)~=0 then - return math.asin(unitvel.y/UTILS.VecNorm(unitvel)) - + -- Calculate climb angle. + local angle=math.asin(unitvel.y/UTILS.VecNorm(unitvel)) + + -- Return angle in degrees. + return math.deg(angle) + else + return 0 end end + + return nil end --- Returns the pitch angle of a unit. @@ -1027,7 +1136,7 @@ function POSITIONABLE:MessageToSetGroup( Message, Duration, MessageSetGroup, Nam local DCSObject = self:GetDCSObject() if DCSObject then if DCSObject:isExist() then - MessageSetGroup:ForEachGroup( + MessageSetGroup:ForEachGroupAlive( function( MessageGroup ) self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup ) end @@ -1074,9 +1183,9 @@ end --- Start Lasing a POSITIONABLE -- @param #POSITIONABLE self --- @param #POSITIONABLE Target --- @param #number LaserCode --- @param #number Duration +-- @param #POSITIONABLE Target The target to lase. +-- @param #number LaserCode Laser code or random number in [1000, 9999]. +-- @param #number Duration Duration of lasing in seconds. -- @return Core.Spot#SPOT function POSITIONABLE:LaseUnit( Target, LaserCode, Duration ) --R2.1 self:F2() @@ -1095,6 +1204,24 @@ function POSITIONABLE:LaseUnit( Target, LaserCode, Duration ) --R2.1 end +--- Start Lasing a COORDINATE. +-- @param #POSITIONABLE self +-- @param Core.Point#COORDIUNATE Coordinate The coordinate where the lase is pointing at. +-- @param #number LaserCode Laser code or random number in [1000, 9999]. +-- @param #number Duration Duration of lasing in seconds. +-- @return Core.Spot#SPOT +function POSITIONABLE:LaseCoordinate(Coordinate, LaserCode, Duration) + self:F2() + + LaserCode = LaserCode or math.random(1000, 9999) + + self.Spot = SPOT:New(self) -- Core.Spot#SPOT + self.Spot:LaseOnCoordinate(Coordinate, LaserCode, Duration) + self.LaserCode = LaserCode + + return self.Spot +end + --- Stop Lasing a POSITIONABLE -- @param #POSITIONABLE self -- @return #POSITIONABLE @@ -1405,3 +1532,33 @@ function POSITIONABLE:SmokeBlue() end +--- Returns true if the unit is within a @{Zone}. +-- @param #STPOSITIONABLEATIC self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the unit is within the @{Core.Zone#ZONE_BASE} +function POSITIONABLE:IsInZone( Zone ) + self:F2( { self.PositionableName, Zone } ) + + if self:IsAlive() then + local IsInZone = Zone:IsVec3InZone( self:GetVec3() ) + + return IsInZone + end + return false +end + +--- Returns true if the unit is not within a @{Zone}. +-- @param #POSITIONABLE self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the unit is not within the @{Core.Zone#ZONE_BASE} +function POSITIONABLE:IsNotInZone( Zone ) + self:F2( { self.PositionableName, Zone } ) + + if self:IsAlive() then + local IsNotInZone = not Zone:IsVec3InZone( self:GetVec3() ) + + return IsNotInZone + else + return false + end +end \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Scenery.lua b/Moose Development/Moose/Wrapper/Scenery.lua index 01d9af957..9eccde422 100644 --- a/Moose Development/Moose/Wrapper/Scenery.lua +++ b/Moose Development/Moose/Wrapper/Scenery.lua @@ -31,6 +31,11 @@ SCENERY = { } +--- Register scenery object as POSITIONABLE. +--@param #SCENERY self +--@param #string SceneryName Scenery name. +--@param #DCS.Object SceneryObject DCS scenery object. +--@return #SCENERY Scenery object. function SCENERY:Register( SceneryName, SceneryObject ) local self = BASE:Inherit( self, POSITIONABLE:New( SceneryName ) ) self.SceneryName = SceneryName @@ -38,11 +43,17 @@ function SCENERY:Register( SceneryName, SceneryObject ) return self end +--- Register scenery object as POSITIONABLE. +--@param #SCENERY self +--@return #DCS.Object DCS scenery object. function SCENERY:GetDCSObject() return self.SceneryObject end +--- Register scenery object as POSITIONABLE. +--@param #SCENERY self +--@return #number Threat level 0. +--@return #string "Scenery". function SCENERY:GetThreatLevel() - return 0, "Scenery" end diff --git a/Moose Development/Moose/Wrapper/Static.lua b/Moose Development/Moose/Wrapper/Static.lua index fb3d73296..9df2fd594 100644 --- a/Moose Development/Moose/Wrapper/Static.lua +++ b/Moose Development/Moose/Wrapper/Static.lua @@ -4,7 +4,7 @@ -- -- ### Author: **FlightControl** -- --- ### Contributions: +-- ### Contributions: **funkyfranky** -- -- === -- @@ -48,6 +48,10 @@ STATIC = { } +--- Register a static object. +-- @param #STATIC self +-- @param #string StaticName Name of the static object. +-- @return #STATIC self function STATIC:Register( StaticName ) local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) ) self.StaticName = StaticName @@ -71,19 +75,21 @@ end -- @param #STATIC self -- @param #string StaticName Name of the DCS **Static** as defined within the Mission Editor. -- @param #boolean RaiseError Raise an error if not found. --- @return #STATIC +-- @return #STATIC self or *nil* function STATIC:FindByName( StaticName, RaiseError ) + + -- Find static in DB. local StaticFound = _DATABASE:FindStatic( StaticName ) + -- Set static name. self.StaticName = StaticName if StaticFound then - StaticFound:F3( { StaticName } ) return StaticFound end - - if RaiseError == nil or RaiseError == true then - error( "STATIC not found for: " .. StaticName ) + + if RaiseError == nil or RaiseError == true then + error( "STATIC not found for: " .. StaticName ) end return nil @@ -136,13 +142,16 @@ function STATIC:Destroy( GenerateEvent ) end DCSObject:destroy() + return true end return nil end - +--- Get DCS object of static of static. +-- @param #STATIC self +-- @return DCS static object function STATIC:GetDCSObject() local DCSStatic = StaticObject.getByName( self.StaticName ) @@ -172,77 +181,72 @@ function STATIC:GetUnits() end - - +--- Get threat level of static. +-- @param #STATIC self +-- @return #number Threat level 1. +-- @return #string "Static" function STATIC:GetThreatLevel() - return 1, "Static" end ---- Respawn the @{Wrapper.Unit} using a (tweaked) template of the parent Group. +--- Spawn the @{Wrapper.Static} at a specific coordinate and heading. -- @param #STATIC self -- @param Core.Point#COORDINATE Coordinate The coordinate where to spawn the new Static. --- @param #number Heading The heading of the unit respawn. -function STATIC:SpawnAt( Coordinate, Heading ) +-- @param #number Heading The heading of the static respawn in degrees. Default is 0 deg. +-- @param #number Delay Delay in seconds before the static is spawned. +function STATIC:SpawnAt( Coordinate, Heading, Delay ) - local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName ) + Heading=Heading or 0 + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.SpawnAt, {self, Coordinate, Heading}, Delay) + else + + local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName ) - SpawnStatic:SpawnFromPointVec2( Coordinate, Heading, self.StaticName ) + SpawnStatic:SpawnFromPointVec2( Coordinate, Heading, self.StaticName ) + + end end --- Respawn the @{Wrapper.Unit} at the same location with the same properties. -- This is useful to respawn a cargo after it has been destroyed. -- @param #STATIC self --- @param DCS#country.id countryid The country ID used for spawning the new static. -function STATIC:ReSpawn(countryid) +-- @param DCS#country.id countryid The country ID used for spawning the new static. Default is same as currently. +-- @param #number Delay Delay in seconds before static is respawned. +function STATIC:ReSpawn(countryid, Delay) - local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName, countryid ) - - SpawnStatic:ReSpawn() + countryid=countryid or self:GetCountry() + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.ReSpawn, {self, countryid}, Delay) + else + + local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName, countryid ) + + SpawnStatic:ReSpawn() + + end end --- Respawn the @{Wrapper.Unit} at a defined Coordinate with an optional heading. -- @param #STATIC self -- @param Core.Point#COORDINATE Coordinate The coordinate where to spawn the new Static. --- @param #number Heading The heading of the unit respawn. -function STATIC:ReSpawnAt( Coordinate, Heading ) +-- @param #number Heading The heading of the static respawn in degrees. Default is 0 deg. +-- @param #number Delay Delay in seconds before static is respawned. +function STATIC:ReSpawnAt( Coordinate, Heading, Delay ) - local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName ) - - SpawnStatic:ReSpawnAt( Coordinate, Heading ) -end + Heading=Heading or 0 - ---- Returns true if the unit is within a @{Zone}. --- @param #STATIC self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is within the @{Core.Zone#ZONE_BASE} -function STATIC:IsInZone( Zone ) - self:F2( { self.StaticName, Zone } ) - - if self:IsAlive() then - local IsInZone = Zone:IsVec3InZone( self:GetVec3() ) - - return IsInZone - end - return false -end - ---- Returns true if the unit is not within a @{Zone}. --- @param #STATIC self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is not within the @{Core.Zone#ZONE_BASE} -function STATIC:IsNotInZone( Zone ) - self:F2( { self.StaticName, Zone } ) - - if self:IsAlive() then - local IsInZone = not Zone:IsVec3InZone( self:GetVec3() ) - - self:T( { IsInZone } ) - return IsInZone + if Delay and Delay>0 then + SCHEDULER:New(nil, self.ReSpawnAt, {self, Coordinate, Heading}, Delay) else - return false + + local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName ) + + SpawnStatic:ReSpawnAt( Coordinate, Heading ) end end + diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index d24a0b0b0..a7b2316b9 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -318,13 +318,37 @@ function UNIT:GetCallsign() return nil end +--- Check if an (air) unit is a client or player slot. Information is retrieved from the group template. +-- @param #UNIT self +-- @return #boolean If true, unit is associated with a client or player slot. +function UNIT:IsPlayer() + + -- Get group. + local group=self:GetGroup() + + -- Units of template group. + local units=group:GetTemplate().units + + -- Get numbers. + for _,unit in pairs(units) do + + -- Check if unit name matach and skill is Client or Player. + if unit.name==self:GetName() and (unit.skill=="Client" or unit.skill=="Player") then + return true + end + + end + + return false +end + --- Returns name of the player that control the unit or nil if the unit is controlled by A.I. -- @param #UNIT self -- @return #string Player Name -- @return #nil The DCS Unit is not existing or alive. function UNIT:GetPlayerName() - self:F2( self.UnitName ) + self:F( self.UnitName ) local DCSUnit = self:GetDCSObject() -- DCS#Unit @@ -400,9 +424,9 @@ function UNIT:GetRange() local Desc = self:GetDesc() if Desc then - local Range = Desc.range --This is in nautical miles for some reason. But should check again! + local Range = Desc.range --This is in kilometers (not meters) for some reason. But should check again! if Range then - Range=UTILS.NMToMeters(Range) + Range=Range*1000 -- convert to meters. else Range=10000000 --10.000 km if no range end @@ -412,6 +436,64 @@ function UNIT:GetRange() return nil end +--- Check if the unit is refuelable. Also retrieves the refuelling system (boom or probe) if applicable. +-- @param #UNIT self +-- @return #boolean If true, unit is refuelable (checks for the attribute "Refuelable"). +-- @return #number Refueling system (if any): 0=boom, 1=probe. +function UNIT:IsRefuelable() + self:F2( self.UnitName ) + + local refuelable=self:HasAttribute("Refuelable") + + local system=nil + + local Desc=self:GetDesc() + if Desc and Desc.tankerType then + system=Desc.tankerType + end + + return refuelable, system +end + +--- Check if the unit is a tanker. Also retrieves the refuelling system (boom or probe) if applicable. +-- @param #UNIT self +-- @return #boolean If true, unit is refuelable (checks for the attribute "Refuelable"). +-- @return #number Refueling system (if any): 0=boom, 1=probe. +function UNIT:IsTanker() + self:F2( self.UnitName ) + + local tanker=self:HasAttribute("Tankers") + + local system=nil + + if tanker then + + local Desc=self:GetDesc() + if Desc and Desc.tankerType then + system=Desc.tankerType + end + + local typename=self:GetTypeName() + + -- Some hard coded data as this is not in the descriptors... + if typename=="IL-78M" then + system=1 --probe + elseif typename=="KC130" then + system=1 --probe + elseif typename=="KC135BDA" then + system=1 --probe + elseif typename=="KC135MPRS" then + system=1 --probe + elseif typename=="S-3B Tanker" then + system=1 --probe + end + + end + + return tanker, system +end + + --- Returns the unit's group if it exist and nil otherwise. -- @param Wrapper.Unit#UNIT self -- @return Wrapper.Group#GROUP The Group of the Unit. @@ -454,8 +536,7 @@ end --- Returns the Unit's ammunition. -- @param #UNIT self --- @return DCS#Unit.Ammo --- @return #nil The DCS Unit is not existing or alive. +-- @return DCS#Unit.Ammo Table with ammuntion of the unit (or nil). This can be a complex table! function UNIT:GetAmmo() self:F2( self.UnitName ) @@ -469,6 +550,94 @@ function UNIT:GetAmmo() return nil end +--- Get the number of ammunition and in particular the number of shells, rockets, bombs and missiles a unit currently has. +-- @param #UNIT self +-- @return #number Total amount of ammo the unit has left. This is the sum of shells, rockets, bombs and missiles. +-- @return #number Number of shells left. +-- @return #number Number of rockets left. +-- @return #number Number of bombs left. +-- @return #number Number of missiles left. +function UNIT:GetAmmunition() + + -- Init counter. + local nammo=0 + local nshells=0 + local nrockets=0 + local nmissiles=0 + local nbombs=0 + + local unit=self + + -- Get ammo table. + local ammotable=unit:GetAmmo() + + if ammotable then + + local weapons=#ammotable + + -- Loop over all weapons. + for w=1,weapons do + + -- Number of current weapon. + local Nammo=ammotable[w]["count"] + + -- Type name of current weapon. + local Tammo=ammotable[w]["desc"]["typeName"] + + local _weaponString = UTILS.Split(Tammo,"%.") + local _weaponName = _weaponString[#_weaponString] + + -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3 + local Category=ammotable[w].desc.category + + -- Get missile category: Weapon.MissileCategory AAM=1, SAM=2, BM=3, ANTI_SHIP=4, CRUISE=5, OTHER=6 + local MissileCategory=nil + if Category==Weapon.Category.MISSILE then + MissileCategory=ammotable[w].desc.missileCategory + end + + -- We are specifically looking for shells or rockets here. + if Category==Weapon.Category.SHELL then + + -- Add up all shells. + nshells=nshells+Nammo + + elseif Category==Weapon.Category.ROCKET then + + -- Add up all rockets. + nrockets=nrockets+Nammo + + elseif Category==Weapon.Category.BOMB then + + -- Add up all rockets. + nbombs=nbombs+Nammo + + elseif Category==Weapon.Category.MISSILE then + + -- Add up all cruise missiles (category 5) + if MissileCategory==Weapon.MissileCategory.AAM then + nmissiles=nmissiles+Nammo + elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then + nmissiles=nmissiles+Nammo + elseif MissileCategory==Weapon.MissileCategory.BM then + nmissiles=nmissiles+Nammo + elseif MissileCategory==Weapon.MissileCategory.OTHER then + nmissiles=nmissiles+Nammo + end + + end + + end + end + + -- Total amount of ammunition. + nammo=nshells+nrockets+nmissiles+nbombs + + return nammo, nshells, nrockets, nbombs, nmissiles +end + + + --- Returns the unit sensors. -- @param #UNIT self -- @return DCS#Unit.Sensors @@ -552,10 +721,9 @@ end --- Returns relative amount of fuel (from 0.0 to 1.0) the UNIT has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. -- @param #UNIT self --- @return #number The relative amount of fuel (from 0.0 to 1.0). --- @return #nil The DCS Unit is not existing or alive. +-- @return #number The relative amount of fuel (from 0.0 to 1.0) or *nil* if the DCS Unit is not existing or alive. function UNIT:GetFuel() - self:F( self.UnitName ) + self:F3( self.UnitName ) local DCSUnit = self:GetDCSObject() @@ -571,7 +739,7 @@ end -- @param #UNIT self -- @return #list A list of one @{Wrapper.Unit}. function UNIT:GetUnits() - self:F2( { self.UnitName } ) + self:F3( { self.UnitName } ) local DCSUnit = self:GetDCSObject() local Units = {} @@ -588,8 +756,7 @@ end --- Returns the unit's health. Dead units has health <= 1.0. -- @param #UNIT self --- @return #number The Unit's health value. --- @return #nil The DCS Unit is not existing or alive. +-- @return #number The Unit's health value or -1 if unit does not exist any more. function UNIT:GetLife() self:F2( self.UnitName ) @@ -605,8 +772,7 @@ end --- Returns the Unit's initial health. -- @param #UNIT self --- @return #number The Unit's initial health value. --- @return #nil The DCS Unit is not existing or alive. +-- @return #number The Unit's initial health value or 0 if unit does not exist any more. function UNIT:GetLife0() self:F2( self.UnitName ) @@ -620,6 +786,55 @@ function UNIT:GetLife0() return 0 end +--- Returns the unit's relative health. +-- @param #UNIT self +-- @return #number The Unit's relative health value, i.e. a number in [0,1] or -1 if unit does not exist any more. +function UNIT:GetLifeRelative() + self:F2(self.UnitName) + + if self and self:IsAlive() then + local life0=self:GetLife0() + local lifeN=self:GetLife() + return lifeN/life0 + end + + return -1 +end + +--- Returns the unit's relative damage, i.e. 1-life. +-- @param #UNIT self +-- @return #number The Unit's relative health value, i.e. a number in [0,1] or 1 if unit does not exist any more. +function UNIT:GetDamageRelative() + self:F2(self.UnitName) + + if self and self:IsAlive() then + return 1-self:GetLifeRelative() + end + + return 1 +end + +--- Returns the category of the #UNIT from descriptor. Returns one of +-- +-- * Unit.Category.AIRPLANE +-- * Unit.Category.HELICOPTER +-- * Unit.Category.GROUND_UNIT +-- * Unit.Category.SHIP +-- * Unit.Category.STRUCTURE +-- +-- @param #UNIT self +-- @return #number Unit category from `getDesc().category`. +function UNIT:GetUnitCategory() + self:F3( self.UnitName ) + + local DCSUnit = self:GetDCSObject() + if DCSUnit then + return DCSUnit:getDesc().category + end + + return nil +end + --- Returns the category name of the #UNIT. -- @param #UNIT self -- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship @@ -646,7 +861,9 @@ end --- Returns the Unit's A2G threat level on a scale from 1 to 10 ... --- The following threat levels are foreseen: +-- Depending on the era and the type of unit, the following threat levels are foreseen: +-- +-- **Modern**: -- -- * Threat level 0: Unit is unarmed. -- * Threat level 1: Unit is infantry. @@ -659,13 +876,51 @@ end -- * 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. --- @param #UNIT self +-- +-- **Cold**: +-- +-- * Threat level 0: Unit is unarmed. +-- * Threat level 1: Unit is infantry. +-- * Threat level 2: Unit is an infantry vehicle. +-- * Threat level 3: Unit is ground artillery. +-- * Threat level 4: Unit is a tank. +-- * Threat level 5: Unit is a modern tank or ifv with ATGM. +-- * Threat level 6: Unit is a AAA. +-- * Threat level 7: Unit is a SAM or manpad, IR guided. +-- * Threat level 8: Unit is a Short Range SAM, radar guided. +-- * Threat level 10: Unit is a Medium Range SAM, radar guided. +-- +-- **Korea**: +-- +-- * Threat level 0: Unit is unarmed. +-- * Threat level 1: Unit is infantry. +-- * Threat level 2: Unit is an infantry vehicle. +-- * Threat level 3: Unit is ground artillery. +-- * Threat level 5: Unit is a tank. +-- * Threat level 6: Unit is a AAA. +-- * Threat level 7: Unit is a SAM or manpad, IR guided. +-- * Threat level 10: Unit is a Short Range SAM, radar guided. +-- +-- **WWII**: +-- +-- * Threat level 0: Unit is unarmed. +-- * Threat level 1: Unit is infantry. +-- * Threat level 2: Unit is an infantry vehicle. +-- * Threat level 3: Unit is ground artillery. +-- * Threat level 5: Unit is a tank. +-- * Threat level 7: Unit is FLAK. +-- * Threat level 10: Unit is AAA. +-- +-- +-- @param #UNIT self +-- @return #number Number between 0 (low threat level) and 10 (high threat level). +-- @return #string Some text. function UNIT:GetThreatLevel() local ThreatLevel = 0 local ThreatText = "" - + local Descriptor = self:GetDesc() if Descriptor then @@ -783,40 +1038,36 @@ function UNIT:GetThreatLevel() end +--- Triggers an explosion at the coordinates of the unit. +-- @param #UNIT self +-- @param #number power Power of the explosion in kg TNT. Default 100 kg TNT. +-- @param #number delay (Optional) Delay of explosion in seconds. +-- @return #UNIT self +function UNIT:Explode(power, delay) + + -- Default. + power=power or 100 + + local DCSUnit = self:GetDCSObject() + if DCSUnit then + + -- Check if delay or not. + if delay and delay>0 then + -- Delayed call. + SCHEDULER:New(nil, self.Explode, {self, power}, delay) + else + -- Create an explotion at the coordinate of the unit. + self:GetCoordinate():Explosion(power) + end + + return self + end + + return nil +end -- Is functions ---- Returns true if the unit is within a @{Zone}. --- @param #UNIT self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is within the @{Core.Zone#ZONE_BASE} -function UNIT:IsInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = Zone:IsVec3InZone( self:GetVec3() ) - - return IsInZone - end - return false -end - ---- Returns true if the unit is not within a @{Zone}. --- @param #UNIT self --- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the unit is not within the @{Core.Zone#ZONE_BASE} -function UNIT:IsNotInZone( Zone ) - self:F2( { self.UnitName, Zone } ) - - if self:IsAlive() then - local IsInZone = not Zone:IsVec3InZone( self:GetVec3() ) - - self:T( { IsInZone } ) - return IsInZone - else - return false - end -end --- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit. @@ -897,34 +1148,35 @@ end --- Returns true if the UNIT is in the air. -- @param #UNIT self --- @return #boolean true if in the air. --- @return #nil The UNIT is not existing or alive. +-- @return #boolean Return true if in the air or #nil if the UNIT is not existing or alive. function UNIT:InAir() self:F2( self.UnitName ) + -- Get DCS unit object. local DCSUnit = self:GetDCSObject() --DCS#Unit if DCSUnit then --- Implementation of workaround. The original code is below. --- This to simulate the landing on buildings. - - local UnitInAir = true + -- Get DCS result of whether unit is in air or not. + local UnitInAir = DCSUnit:inAir() + + -- Get unit category. local UnitCategory = DCSUnit:getDesc().category - if UnitCategory == Unit.Category.HELICOPTER then + + -- If DCS says that it is in air, check if this is really the case, since we might have landed on a building where inAir()=true but actually is not. + -- This is a workaround since DCS currently does not acknoledge that helos land on buildings. + -- Note however, that the velocity check will fail if the ground is moving, e.g. on an aircraft carrier! + if UnitInAir==true and UnitCategory == Unit.Category.HELICOPTER then local VelocityVec3 = DCSUnit:getVelocity() - local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec + local Velocity = UTILS.VecNorm(VelocityVec3) local Coordinate = DCSUnit:getPoint() local LandHeight = land.getHeight( { x = Coordinate.x, y = Coordinate.z } ) local Height = Coordinate.y - LandHeight if Velocity < 1 and Height <= 60 then UnitInAir = false end - else - UnitInAir = DCSUnit:inAir() end - - + self:T3( UnitInAir ) return UnitInAir end @@ -992,6 +1244,122 @@ do -- Detection return IsLOS end - -end \ No newline at end of file + --- Forces the unit to become aware of the specified target, without the unit manually detecting the other unit itself. + -- Applies only to a Unit Controller. Cannot be used at the group level. + -- @param #UNIT self + -- @param #UNIT TargetUnit The unit to be known. + -- @param #boolean TypeKnown The target type is known. If *false*, the type is not known. + -- @param #boolean DistanceKnown The distance to the target is known. If *false*, distance is unknown. + function UNIT:KnowUnit(TargetUnit, TypeKnown, DistanceKnown) + + -- Defaults. + if TypeKnown~=false then + TypeKnown=true + end + if DistanceKnown~=false then + DistanceKnown=true + end + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local Controller = DCSControllable:getController() --self:_GetController() + + if Controller then + + local object=TargetUnit:GetDCSObject() + + if object then + + self:I(string.format("Unit %s now knows target unit %s. Type known=%s, distance known=%s", self:GetName(), TargetUnit:GetName(), tostring(TypeKnown), tostring(DistanceKnown))) + + Controller:knowTarget(object, TypeKnown, DistanceKnown) + + end + + end + + end + + end + +end + +--- Get the unit table from a unit's template. +-- @param #UNIT self +-- @return #table Table of the unit template (deep copy) or #nil. +function UNIT:GetTemplate() + + local group=self:GetGroup() + + local name=self:GetName() + + if group then + local template=group:GetTemplate() + + if template then + + for _,unit in pairs(template.units) do + + if unit.name==name then + return UTILS.DeepCopy(unit) + end + end + + end + end + + return nil +end + + +--- Get the payload table from a unit's template. +-- The payload table has elements: +-- +-- * pylons +-- * fuel +-- * chaff +-- * gun +-- +-- @param #UNIT self +-- @return #table Payload table (deep copy) or #nil. +function UNIT:GetTemplatePayload() + + local unit=self:GetTemplate() + + if unit then + return unit.payload + end + + return nil +end + +--- Get the pylons table from a unit's template. This can be a complex table depending on the weapons the unit is carrying. +-- @param #UNIT self +-- @return #table Table of pylons (deepcopy) or #nil. +function UNIT:GetTemplatePylons() + + local payload=self:GetTemplatePayload() + + if payload then + return payload.pylons + end + + return nil +end + +--- Get the fuel of the unit from its template. +-- @param #UNIT self +-- @return #number Fuel of unit in kg. +function UNIT:GetTemplateFuel() + + local payload=self:GetTemplatePayload() + + if payload then + return payload.fuel + end + + return nil +end diff --git a/Moose Setup/Eclipse/Moose Loader Dynamic.launch b/Moose Setup/Eclipse/Moose Loader Dynamic.launch new file mode 100644 index 000000000..5efaf2485 --- /dev/null +++ b/Moose Setup/Eclipse/Moose Loader Dynamic.launch @@ -0,0 +1,6 @@ + + + + + + diff --git a/Moose Setup/Eclipse/Moose Loader Static.launch b/Moose Setup/Eclipse/Moose Loader Static.launch new file mode 100644 index 000000000..4ee25e04a --- /dev/null +++ b/Moose Setup/Eclipse/Moose Loader Static.launch @@ -0,0 +1,6 @@ + + + + + + diff --git a/Moose Setup/Moose Templates/Moose_Dynamic_Loader.lua b/Moose Setup/Moose Templates/Moose_Dynamic_Loader.lua index 24ba37689..d63555297 100644 --- a/Moose Setup/Moose Templates/Moose_Dynamic_Loader.lua +++ b/Moose Setup/Moose Templates/Moose_Dynamic_Loader.lua @@ -18,3 +18,5 @@ __Moose.Include = function( IncludeFile ) end __Moose.Includes = {} + +__Moose.Include( 'Scripts/Moose/Modules.lua' ) diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 9ef0e3f57..4852f9346 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -58,13 +58,27 @@ Functional/Artillery.lua Functional/Suppression.lua Functional/PseudoATC.lua Functional/Warehouse.lua +Functional/Fox.lua + +Ops/Airboss.lua +Ops/RecoveryTanker.lua +Ops/RescueHelo.lua +Ops/ATIS.lua AI/AI_Balancer.lua +AI/AI_Air.lua AI/AI_A2A.lua AI/AI_A2A_Patrol.lua AI/AI_A2A_Cap.lua AI/AI_A2A_Gci.lua AI/AI_A2A_Dispatcher.lua +AI/AI_A2G.lua +AI/AI_A2G_Engage.lua +AI/AI_A2G_BAI.lua +AI/AI_A2G_CAS.lua +AI/AI_A2G_SEAD.lua +AI/AI_A2G_Patrol.lua +AI/AI_A2G_Dispatcher.lua AI/AI_Patrol.lua AI/AI_Cap.lua AI/AI_Cas.lua @@ -100,4 +114,4 @@ Tasking/Task_Cargo_CSAR.lua Tasking/Task_Cargo_Dispatcher.lua Tasking/TaskZoneCapture.lua -Moose.lua +Globals.lua diff --git a/Moose Setup/Moose_Create.lua b/Moose Setup/Moose_Create.lua index 500a56965..42a3213aa 100644 --- a/Moose Setup/Moose_Create.lua +++ b/Moose Setup/Moose_Create.lua @@ -12,15 +12,15 @@ print( "Moose development path : " .. MooseDevelopmentPath ) print( "Moose setup path : " .. MooseSetupPath ) print( "Moose target path : " .. MooseTargetPath ) -local MooseSourcesFilePath = MooseSetupPath .. "/Moose.files" -local MooseFilePath = MooseTargetPath.."/Moose.lua" +local MooseModulesFilePath = MooseDevelopmentPath .. "/Modules.lua" +local LoaderFilePath = MooseTargetPath .. "/Moose.lua" -print( "Reading Moose source list : " .. MooseSourcesFilePath ) +print( "Reading Moose source list : " .. MooseModulesFilePath ) -local MooseFile = io.open( MooseFilePath, "w" ) +local LoaderFile = io.open( LoaderFilePath, "w" ) if MooseDynamicStatic == "S" then - MooseFile:write( "env.info( '*** MOOSE GITHUB Commit Hash ID: " .. MooseCommitHash .. " ***' )\n" ) + LoaderFile:write( "env.info( '*** MOOSE GITHUB Commit Hash ID: " .. MooseCommitHash .. " ***' )\n" ) end local MooseLoaderPath @@ -35,27 +35,26 @@ local MooseLoader = io.open( MooseLoaderPath, "r" ) local MooseLoaderText = MooseLoader:read( "*a" ) MooseLoader:close() -MooseFile:write( MooseLoaderText ) +LoaderFile:write( MooseLoaderText ) - -local MooseSourcesFile = io.open( MooseSourcesFilePath, "r" ) +local MooseSourcesFile = io.open( MooseModulesFilePath, "r" ) local MooseSource = MooseSourcesFile:read("*l") while( MooseSource ) do if MooseSource ~= "" then + MooseSource = string.match( MooseSource, "Scripts/Moose/(.+)'" ) local MooseFilePath = MooseDevelopmentPath .. "/" .. MooseSource if MooseDynamicStatic == "D" then - print( "Load dynamic: " .. MooseSource ) - MooseFile:write( "__Moose.Include( 'Scripts/Moose/" .. MooseSource .. "' )\n" ) + print( "Load dynamic: " .. MooseFilePath ) end if MooseDynamicStatic == "S" then - print( "Load static: " .. MooseSource ) + print( "Load static: " .. MooseFilePath ) local MooseSourceFile = io.open( MooseFilePath, "r" ) local MooseSourceFileText = MooseSourceFile:read( "*a" ) MooseSourceFile:close() - MooseFile:write( MooseSourceFileText ) + LoaderFile:write( MooseSourceFileText ) end end @@ -63,13 +62,13 @@ while( MooseSource ) do end if MooseDynamicStatic == "D" then - MooseFile:write( "BASE:TraceOnOff( true )\n" ) + LoaderFile:write( "BASE:TraceOnOff( true )\n" ) end if MooseDynamicStatic == "S" then - MooseFile:write( "BASE:TraceOnOff( false )\n" ) + LoaderFile:write( "BASE:TraceOnOff( false )\n" ) end -MooseFile:write( "env.info( '*** MOOSE INCLUDE END *** ' )\n" ) +LoaderFile:write( "env.info( '*** MOOSE INCLUDE END *** ' )\n" ) MooseSourcesFile:close() -MooseFile:close() +LoaderFile:close()