diff --git a/.appveyor/appveyor.yml b/.appveyor/appveyor.yml index ce7a2f7b6..7fcd96a10 100644 --- a/.appveyor/appveyor.yml +++ b/.appveyor/appveyor.yml @@ -1,4 +1,4 @@ -version: 3.9.1.{build} +version: 2.4.a.{build} shallow_clone: true skip_branch_with_pr: false skip_commits: @@ -17,6 +17,7 @@ environment: platform: - x64 + init: - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` @@ -61,7 +62,7 @@ build_script: $project = Invoke-RestMethod -method Post -Uri "$apiUrl/builds" -Headers $headers -Body $RequestBody } - ps: | - if( $env:appveyor_repo_branch -eq 'master' ) + if( $env:appveyor_repo_branch -eq 'master' -or $env:appveyor_repo_branch -eq 'develop' ) { $apiUrl = 'https://ci.appveyor.com/api' $token = 'qts80b5kpq0ooj4x6vvw' @@ -69,13 +70,12 @@ build_script: "Authorization" = "Bearer $token" "Content-type" = "application/json" } - $RequestBody = @{ accountName = 'FlightControl-Master'; projectSlug = 'moose-docs'; branch = 'master'; environmentVariables = @{} } | ConvertTo-Json + $RequestBody = @{ accountName = 'FlightControl-Master'; projectSlug = 'moose-docs'; branch = "$env:appveyor_repo_branch"; environmentVariables = @{} } | ConvertTo-Json # get project with last build details $project = Invoke-RestMethod -method Post -Uri "$apiUrl/builds" -Headers $headers -Body $RequestBody } - test: off # test_script: # - cmd: luacheck "Moose Development\Moose\moose.lua" "Moose Mission Setup\moose.lua" diff --git a/Moose Development/Moose/AI/AI_A2A.lua b/Moose Development/Moose/AI/AI_A2A.lua index 1309b52bc..14c137dc2 100644 --- a/Moose Development/Moose/AI/AI_A2A.lua +++ b/Moose Development/Moose/AI/AI_A2A.lua @@ -1,14 +1,13 @@ --- **AI** -- (R2.2) - Models the process of air operations for airplanes. -- --- This is a class used in the @{AI_A2A_Dispatcher}. --- -- === -- -- ### Author: **FlightControl** -- -- === -- --- @module AI_A2A +-- @module AI.AI_A2A +-- @image AI_Air_To_Air_Dispatching.JPG --BASE:TraceClass("AI_A2A") @@ -16,9 +15,7 @@ --- @type AI_A2A -- @extends Core.Fsm#FSM_CONTROLLABLE ---- # AI_A2A class, extends @{Fsm#FSM_CONTROLLABLE} --- --- The AI_A2A class implements the core functions to operate an AI @{Group} A2A tasking. +--- The AI_A2A class implements the core functions to operate an AI @{Wrapper.Group} A2A tasking. -- -- -- ## AI_A2A constructor @@ -295,8 +292,8 @@ end --- Sets (modifies) the minimum and maximum speed of the patrol. -- @param #AI_A2A self --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. +-- @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 ) self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) @@ -308,8 +305,8 @@ end --- Sets the floor and ceiling altitude of the patrol. -- @param #AI_A2A self --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @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 ) self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) @@ -370,7 +367,6 @@ end -- @return #AI_A2A self function AI_A2A:SetFuelThreshold( PatrolFuelThresholdPercentage, PatrolOutOfFuelOrbitTime ) - self.PatrolManageFuel = true self.PatrolFuelThresholdPercentage = PatrolFuelThresholdPercentage self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime @@ -404,7 +400,6 @@ end -- @param #string Event The Event string. -- @param #string To The To State string. function AI_A2A:onafterStart( Controllable, From, Event, To ) - self:F2() self:__Status( 10 ) -- Check status status every 30 seconds. @@ -427,8 +422,6 @@ end --- @param #AI_A2A self function AI_A2A:onafterStatus() - self:F( " Checking Status" ) - if self.Controllable and self.Controllable:IsAlive() then local RTB = false @@ -445,18 +438,19 @@ function AI_A2A:onafterStatus() RTB = false end end - - if self:Is( "Fuel" ) or self:Is( "Damaged" ) or self:Is( "LostControl" ) then - if DistanceFromHomeBase < 5000 then - self:E( self.Controllable:GetName() .. " is too far from home base, RTB!" ) - self:Home( "Destroy" ) - end - end + +-- I think this code is not requirement anymore after release 2.5. +-- if self:Is( "Fuel" ) or self:Is( "Damaged" ) or self:Is( "LostControl" ) then +-- if DistanceFromHomeBase < 5000 then +-- self:E( self.Controllable:GetName() .. " is near the home base, RTB!" ) +-- self:Home( "Destroy" ) +-- end +-- end if not self:Is( "Fuel" ) and not self:Is( "Home" ) then - local Fuel = self.Controllable:GetFuel() - self:F({Fuel=Fuel}) + local Fuel = self.Controllable:GetFuelMin() + self:F({Fuel=Fuel, PatrolFuelThresholdPercentage=self.PatrolFuelThresholdPercentage}) if Fuel < self.PatrolFuelThresholdPercentage then if self.TankerName then self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" ) @@ -488,11 +482,14 @@ function AI_A2A:onafterStatus() end -- Check if planes went RTB and are out of control. + -- We only check if planes are out of control, when they are in duty. if self.Controllable:HasTask() == false then if not self:Is( "Started" ) and not self:Is( "Stopped" ) and + not self:Is( "Fuel" ) and + not self:Is( "Damaged" ) and not self:Is( "Home" ) then - if self.IdleCount >= 2 then + if self.IdleCount >= 3 then if Damage ~= InitialLife then self:Damaged() else @@ -510,8 +507,11 @@ function AI_A2A:onafterStatus() if RTB == true then self:__RTB( 0.5 ) end + + if not self:Is("Home") then + self:__Status( 10 ) + end - self:__Status( 10 ) end end @@ -642,7 +642,7 @@ end --- @param Wrapper.Group#GROUP AIGroup function AI_A2A.Resume( AIGroup, Fsm ) - AIGroup:F( { "AI_A2A.Resume:", AIGroup:GetName() } ) + AIGroup:I( { "AI_A2A.Resume:", AIGroup:GetName() } ) if AIGroup:IsAlive() then Fsm:__RTB( 0.5 ) end diff --git a/Moose Development/Moose/AI/AI_A2A_Cap.lua b/Moose Development/Moose/AI/AI_A2A_Cap.lua index 9b90b489e..de9e184da 100644 --- a/Moose Development/Moose/AI/AI_A2A_Cap.lua +++ b/Moose Development/Moose/AI/AI_A2A_Cap.lua @@ -1,29 +1,24 @@ --- **AI** -- (R2.2) - Models the process of Combat Air Patrol (CAP) for airplanes. -- --- This is a class used in the @{AI_A2A_Dispatcher}. --- -- === -- -- ### Author: **FlightControl** -- -- === -- --- @module AI_A2A_Cap - ---BASE:TraceClass("AI_A2A_CAP") +-- @module AI.AI_A2A_Cap +-- @image AI_Combat_Air_Patrol.JPG --- @type AI_A2A_CAP -- @extends AI.AI_A2A_Patrol#AI_A2A_PATROL ---- # AI_A2A_CAP class, extends @{AI_CAP#AI_PATROL_ZONE} --- --- The AI_A2A_CAP class implements the core functions to patrol a @{Zone} by an AI @{Group} or @{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 @{Group} and this must be done before the AI_A2A_CAP process can be started using the **Start** event. +-- 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) -- @@ -66,15 +61,15 @@ -- -- ### 2.2 AI_A2A_CAP Events -- --- * **@{AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. --- * **@{AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. -- * **@{#AI_A2A_CAP.Engage}**: Let the AI engage the bogeys. -- * **@{#AI_A2A_CAP.Abort}**: Aborts the engagement and return patrolling in the patrol zone. --- * **@{AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. --- * **@{#AI_A2A_CAP.Destroy}**: The AI has destroyed a bogey @{Unit}. --- * **@{#AI_A2A_CAP.Destroyed}**: The AI has destroyed all bogeys @{Unit}s assigned in the CAS task. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. +-- * **@{#AI_A2A_CAP.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. +-- * **@{#AI_A2A_CAP.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. -- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. -- -- ## 3. Set the Range of Engagement @@ -85,7 +80,7 @@ -- 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_CAP#AI_A2A_CAP.SetEngageRange}() to define that range. +-- Use the method @{AI.AI_CAP#AI_A2A_CAP.SetEngageRange}() to define that range. -- -- ## 4. Set the Zone of Engagement -- @@ -93,7 +88,7 @@ -- -- An optional @{Zone} can be set, -- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI_Cap#AI_A2A_CAP.SetEngageZone}() to define that Zone. +-- Use the method @{AI.AI_Cap#AI_A2A_CAP.SetEngageZone}() to define that Zone. -- -- === -- @@ -106,13 +101,13 @@ AI_A2A_CAP = { -- @param #AI_A2A_CAP self -- @param Wrapper.Group#GROUP AICap -- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Group} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Group} in km/h. --- @param Dcs.DCSTypes#Speed EngageMinSpeed The minimum speed of the @{Group} in km/h when engaging a target. --- @param Dcs.DCSTypes#Speed EngageMaxSpeed The maximum speed of the @{Group} in km/h when engaging a target. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#Speed EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO -- @return #AI_A2A_CAP function AI_A2A_CAP:New( AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, PatrolAltType ) @@ -287,13 +282,14 @@ function AI_A2A_CAP:New( AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAl end --- onafter State Transition for Event Patrol. --- @param #AI_A2A_GCI self +-- @param #AI_A2A_CAP self -- @param Wrapper.Group#GROUP AICap The AI Group managed by the FSM. -- @param #string From The From State string. -- @param #string Event The Event string. -- @param #string To The To State string. function AI_A2A_CAP:onafterStart( AICap, From, Event, To ) + self:GetParent( self ).onafterStart( self, AICap, From, Event, To ) AICap:HandleEvent( EVENTS.Takeoff, nil, self ) end @@ -480,13 +476,12 @@ function AI_A2A_CAP:OnEventDead( EventData ) end --- @param Wrapper.Group#GROUP AICap -function AI_A2A_CAP.Resume( AICap ) +function AI_A2A_CAP.Resume( AICap, Fsm ) - AICap:F( { "AI_A2A_CAP.Resume:", AICap:GetName() } ) + AICap:I( { "AI_A2A_CAP.Resume:", AICap:GetName() } ) if AICap:IsAlive() then - local _AI_A2A = AICap:GetState( AICap, "AI_A2A" ) -- #AI_A2A - _AI_A2A:__Reset( 1 ) - _AI_A2A:__Route( 5 ) + Fsm:__Reset( 1 ) + Fsm:__Route( 5 ) end end diff --git a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua index 96fc731d1..9b9370bb3 100644 --- a/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua +++ b/Moose Development/Moose/AI/AI_A2A_Dispatcher.lua @@ -2,10 +2,37 @@ -- -- === -- --- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia1.JPG) +-- Features: +-- +-- * Setup quickly an A2A defense system for a coalition. +-- * Setup (CAP) Control Air Patrols at defined zones to enhance your A2A defenses. +-- * Setup (GCI) Ground Control Intercept at defined airbases to enhance your A2A defenses. +-- * Define and use an EWR (Early Warning Radar) network. +-- * Define squadrons at airbases. +-- * Enable airbases for A2A defenses. +-- * Add different plane types to different squadrons. +-- * Add multiple squadrons to different airbases. +-- * Define different ranges to engage upon intruders. +-- * Establish an automatic in air refuel process for CAP using refuel tankers. +-- * Setup default settings for all squadrons and A2A defenses. +-- * Setup specific settings for specific squadrons. +-- * Quickly setup an A2A defense system using @{#AI_A2A_GCICAP}. +-- * Setup a more advanced defense system using @{#AI_A2A_DISPATCHER}. -- -- === --- +-- +-- ## Missions: +-- +-- [AID-A2A - AI A2A Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching/AID-A2A%20-%20AI%20A2A%20Dispatching) +-- +-- === +-- +-- ## YouTube Channel: +-- +-- [DCS WORLD - MOOSE - A2A GCICAP - Build an automatic A2A Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) +-- +-- === +-- -- # QUICK START GUIDE -- -- There are basically two classes available to model an A2A defense system. @@ -155,7 +182,8 @@ -- ### 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_A2A_Dispatcher +-- @module AI.AI_A2A_Dispatcher +-- @image AI_Air_To_Air_Dispatching.JPG @@ -165,23 +193,7 @@ do -- AI_A2A_DISPATCHER -- @type AI_A2A_DISPATCHER -- @extends Tasking.DetectionManager#DETECTION_MANAGER - --- # AI\_A2A\_DISPATCHER class, extends @{Tasking#DETECTION_MANAGER} - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia1.JPG) - -- - -- The @{#AI_A2A_DISPATCHER} class is designed to create an automatic air defence system for a coalition. - -- - -- === - -- - -- # Demo Missions - -- - -- ### [AI\_A2A\_DISPATCHER Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching) - -- - -- === - -- - -- # YouTube Channel - -- - -- ### [DCS WORLD - MOOSE - A2A GCICAP - Build an automatic A2A Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) + --- Create an automatic air defence system for a coalition. -- -- === -- @@ -229,7 +241,7 @@ do -- AI_A2A_DISPATCHER -- 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_BASE} object that is given as the input parameter of the AI\_A2A\_DISPATCHER class. + -- EWR networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection#DETECTION_BASE} object that is given as the input parameter of the AI\_A2A\_DISPATCHER class. -- By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, -- increasing or decreasing the radar coverage of the Early Warning System. -- @@ -262,7 +274,7 @@ do -- AI_A2A_DISPATCHER -- 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. @@ -346,7 +358,7 @@ do -- AI_A2A_DISPATCHER -- -- ![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 @{Zone#ZONE_BASE}. + -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Core.Zone#ZONE_BASE}. -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than -- it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. -- In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. @@ -544,18 +556,18 @@ do -- AI_A2A_DISPATCHER -- * 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 @{Zone#ZONE_BASE} class. Zones can be @{Zone#ZONE}, @{Zone#ZONE_POLYGON}, @{Zone#ZONE_UNIT}, @{Zone#ZONE_GROUP}, etc. + -- * 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. + -- * 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. -- @@ -740,7 +752,7 @@ do -- AI_A2A_DISPATCHER -- -- 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.SetDefaultFuelTreshold}() to set the %-tage left in the defender airplane tanks when a refuel action is needed. + -- 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. -- @@ -786,20 +798,27 @@ do -- AI_A2A_DISPATCHER -- -- Use the method @{#AI_A2A_DISPATCHER.SetDisengageRadius}() to modify the default Disengage Radius to another distance setting. -- + -- ## 11. Airbase capture: -- - -- ## 11. Q & A: + -- 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. -- - -- ### 11.1. Which countries will be selected for each coalition? + -- ## 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. + -- Therefore if F4s are wanted as a coalition's CAP or GCI aircraft Germany will need to be assigned to that coalition. -- - -- ### 11.2. Country, type, load out, skill and skins for CAP and GCI aircraft? + -- ### 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”. + -- * Note these can be from any countries within the coalition but must be an aircraft with one of the main tasks being "CAP". -- * Obviously skins which are selected must be available to all players that join the mission otherwise they will see a default skin. -- * Load outs should be appropriate to a CAP mission eg perhaps drop tanks for CAP flights and extra missiles for GCI flights. -- * These decisions will eventually lead to template aircraft units being placed as late activation units that the script will use as templates for spawning CAP and GCI flights. Up to 4 different aircraft configurations can be chosen for each coalition. The spawned aircraft will inherit the characteristics of the template aircraft. @@ -830,7 +849,7 @@ do -- AI_A2A_DISPATCHER --- AI_A2A_DISPATCHER constructor. -- This is defining the A2A DISPATCHER for one coaliton. - -- The Dispatcher works with a @{Functional#Detection} object that is taking of the detection of targets using the EWR units. + -- The Dispatcher works with a @{Functional.Detection#DETECTION_BASE} object that is taking of the detection of targets using the EWR units. -- The Detection object is polymorphic, depending on the type of detection object choosen, the detection will work differently. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network. @@ -983,17 +1002,77 @@ do -- AI_A2A_DISPATCHER 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 + 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 = {} + if DefenderSquadron.ResourceCount then + for Resource = 1, DefenderSquadron.ResourceCount do + self:ParkDefender( DefenderSquadron ) + end + end + end + end + + + --- @param #AI_A2A_DISPATCHER self + 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 + 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_A2A_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function AI_A2A_DISPATCHER:OnEventBaseCaptured( EventData ) + + local AirbaseName = EventData.PlaceName -- The name of the airbase that was captured. + + self:I( "Captured " .. AirbaseName ) + + -- Now search for all squadrons located at the airbase, and sanatize them. + for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do + if Squadron.AirbaseName == AirbaseName then + Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning. + Squadron.Captured = true + self:I( "Squadron " .. SquadronName .. " captured." ) + end + end + end + --- @param #AI_A2A_DISPATCHER self -- @param Core.Event#EVENTDATA EventData function AI_A2A_DISPATCHER:OnEventCrashOrDead( EventData ) @@ -1016,6 +1095,7 @@ do -- AI_A2A_DISPATCHER self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() + self:ParkDefender( Squadron, Defender ) return end if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then @@ -1023,10 +1103,6 @@ do -- AI_A2A_DISPATCHER DefenderUnit:Destroy() return end - if DefenderUnit:GetFuel() <= self.DefenderDefault.FuelThreshold then - DefenderUnit:Destroy() - return - end end end @@ -1039,12 +1115,14 @@ do -- AI_A2A_DISPATCHER if Squadron then self:F( { SquadronName = Squadron.Name } ) local LandingMethod = self:GetSquadronLanding( Squadron.Name ) - if LandingMethod == AI_A2A_DISPATCHER.Landing.AtEngineShutdown then + if LandingMethod == AI_A2A_DISPATCHER.Landing.AtEngineShutdown and + not DefenderUnit:InAir() then local DefenderSize = Defender:GetSize() if DefenderSize == 1 then self:RemoveDefenderFromSquadron( Squadron, Defender ) end DefenderUnit:Destroy() + self:ParkDefender( Squadron, Defender ) end end end @@ -1142,7 +1220,7 @@ do -- AI_A2A_DISPATCHER --- Define a border area to simulate a **cold war** scenario. -- A **cold war** is one where CAP aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border. -- A **hot war** is one where CAP aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send CAP and GCI aircraft to attack it. - -- If it’s a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Zone#ZONE_BASE}. This method needs to be used for this. + -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{zone} object derived from @{Core.Zone#ZONE_BASE}. This method needs to be used for this. -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. Set the noborders parameter to 1 -- @param #AI_A2A_DISPATCHER self -- @param Core.Zone#ZONE_BASE BorderZone An object derived from ZONE_BASE, or a list of objects derived from ZONE_BASE. @@ -1276,7 +1354,7 @@ do -- AI_A2A_DISPATCHER --- Calculates which AI friendlies are nearby the area -- @param #AI_A2A_DISPATCHER self -- @param DetectedItem - -- @return #number, Core.CommandCenter#REPORT + -- @return #table A list of the friendlies nearby. function AI_A2A_DISPATCHER:GetAIFriendliesNearBy( DetectedItem ) local FriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem ) @@ -1423,18 +1501,18 @@ do -- AI_A2A_DISPATCHER -- 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 @{Airbase#AIRBASE} class contains enumerations of the airbases of each map. + -- 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: @{Airbase#AIRBASE.Caucaus} - -- * Nevada or NTTR: @{Airbase#AIRBASE.Nevada} - -- * Normandy: @{Airbase#AIRBASE.Normandy} + -- * 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 Resources (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available. + -- @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. @@ -1457,13 +1535,13 @@ do -- AI_A2A_DISPATCHER -- -- @usage -- -- This is an example like the previous, but now with infinite resources. - -- -- The Resources parameter is not given in the SetSquadron method. + -- -- 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 - function AI_A2A_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, Resources ) + function AI_A2A_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount ) self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} @@ -1472,6 +1550,7 @@ do -- AI_A2A_DISPATCHER 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 @@ -1487,10 +1566,11 @@ do -- AI_A2A_DISPATCHER DefenderSquadron.Spawn[#DefenderSquadron.Spawn+1] = self.DefenderSpawns[SpawnTemplate] end end - DefenderSquadron.Resources = Resources + 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:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, Resources } } ) + self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } ) return self end @@ -1509,10 +1589,58 @@ do -- AI_A2A_DISPATCHER 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 + -- @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 ) + + DefenderSquadron.Uncontrolled = true + + 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_A2A_DISPATCHER self + -- @param #string SquadronName The squadron name. + -- @return #bool 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 ) + + 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 Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed. + -- @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 PatrolMinSpeed The minimum speed at which the cap can be executed. @@ -1655,16 +1783,19 @@ do -- AI_A2A_DISPATCHER local DefenderSquadron = self:GetSquadron( SquadronName ) - if ( not DefenderSquadron.Resources ) or ( DefenderSquadron.Resources and DefenderSquadron.Resources > 0 ) then - - local Cap = DefenderSquadron.Cap - if Cap then - local CapCount = self:CountCapAirborne( SquadronName ) - self:F( { CapCount = CapCount } ) - if CapCount < Cap.CapLimit then - local Probability = math.random() - if Probability <= Cap.Probability then - return DefenderSquadron + if DefenderSquadron.Captured == false then -- We can only spawn new CAP if the base has not been captured. + + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + + local Cap = DefenderSquadron.Cap + if Cap then + local CapCount = self:CountCapAirborne( SquadronName ) + self:F( { CapCount = CapCount } ) + if CapCount < Cap.CapLimit then + local Probability = math.random() + if Probability <= Cap.Probability then + return DefenderSquadron + end end end end @@ -1685,10 +1816,13 @@ do -- AI_A2A_DISPATCHER local DefenderSquadron = self:GetSquadron( SquadronName ) - if ( not DefenderSquadron.Resources ) or ( DefenderSquadron.Resources and DefenderSquadron.Resources > 0 ) then - local Gci = DefenderSquadron.Gci - if Gci then - return DefenderSquadron + if DefenderSquadron.Captured == false then -- We can only spawn new CAP if the base has not been captured. + + if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0 ) then -- And, if there are sufficient resources. + local Gci = DefenderSquadron.Gci + if Gci then + return DefenderSquadron + end end end return nil @@ -2357,7 +2491,7 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- -- -- Now Setup the default fuel treshold. - -- A2ADispatcher:SetDefaultRefuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- A2ADispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. -- function AI_A2A_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold ) @@ -2391,7 +2525,7 @@ do -- AI_A2A_DISPATCHER --- Set the default tanker where defenders will Refuel in the air. -- @param #AI_A2A_DISPATCHER self - -- @param #strig TankerName A string defining the group name of the Tanker as defined within the Mission Editor. + -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. -- @return #AI_A2A_DISPATCHER -- @usage -- @@ -2399,7 +2533,7 @@ do -- AI_A2A_DISPATCHER -- A2ADispatcher = AI_A2A_DISPATCHER:New( Detection ) -- -- -- Now Setup the default fuel treshold. - -- A2ADispatcher:SetDefaultRefuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank. + -- 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. @@ -2414,7 +2548,7 @@ do -- AI_A2A_DISPATCHER --- Set the squadron tanker where defenders will Refuel in the air. -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The name of the squadron. - -- @param #strig TankerName A string defining the group name of the Tanker as defined within the Mission Editor. + -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor. -- @return #AI_A2A_DISPATCHER -- @usage -- @@ -2442,21 +2576,21 @@ do -- AI_A2A_DISPATCHER self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() self.Defenders[ DefenderName ] = Squadron - if Squadron.Resources then - Squadron.Resources = Squadron.Resources - Size + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount - Size end - self:F( { DefenderName = DefenderName, SquadronResources = Squadron.Resources } ) + self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) end --- @param #AI_A2A_DISPATCHER self function AI_A2A_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender ) self.Defenders = self.Defenders or {} local DefenderName = Defender:GetName() - if Squadron.Resources then - Squadron.Resources = Squadron.Resources + Defender:GetSize() + if Squadron.ResourceCount then + Squadron.ResourceCount = Squadron.ResourceCount + Defender:GetSize() end self.Defenders[ DefenderName ] = nil - self:F( { DefenderName = DefenderName, SquadronResources = Squadron.Resources } ) + self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } ) end function AI_A2A_DISPATCHER:GetSquadronFromDefender( Defender ) @@ -2470,7 +2604,7 @@ do -- AI_A2A_DISPATCHER --- Creates an SWEEP task when there are targets for it. -- @param #AI_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. + -- @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 } ) @@ -2541,8 +2675,12 @@ do -- AI_A2A_DISPATCHER local SquadronOverhead = Squadron.Overhead or self.DefenderDefault.Overhead local DefenderSize = Defender:GetInitialSize() - DefenderCount = DefenderCount + DefenderSize / SquadronOverhead - self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) + if DefenderSize then + DefenderCount = DefenderCount + DefenderSize / SquadronOverhead + self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize ) + else + DefenderCount = 0 + end end end @@ -2594,7 +2732,80 @@ do -- AI_A2A_DISPATCHER return Friendlies end + + --- + -- @param #AI_A2A_DISPATCHER self + 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 + + if self:IsSquadronVisible( SquadronName ) then + + -- Here we CAP the new planes. + -- The Resources table is filled in advance. + local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) -- Choose the template. + + -- We determine the grouping based on the parameters set. + self:F( { DefenderGrouping = DefenderGrouping } ) + + -- New we will form the group to spawn in. + -- We search for the first free resource matching the template. + local DefenderUnitIndex = 1 + local DefenderCAPTemplate = nil + local DefenderName = nil + for GroupName, DefenderGroup in pairs( DefenderSquadron.Resources[TemplateID] or {} ) do + self:F( { GroupName = GroupName } ) + local DefenderTemplate = _DATABASE:GetGroupTemplate( GroupName ) + if DefenderUnitIndex == 1 then + DefenderCAPTemplate = UTILS.DeepCopy( DefenderTemplate ) + self.DefenderCAPIndex = self.DefenderCAPIndex + 1 + DefenderCAPTemplate.name = SquadronName .. "#" .. self.DefenderCAPIndex .. "#" .. GroupName + DefenderName = DefenderCAPTemplate.name + else + -- Add the unit in the template to the DefenderCAPTemplate. + local DefenderUnitTemplate = DefenderTemplate.units[1] + DefenderCAPTemplate.units[DefenderUnitIndex] = DefenderUnitTemplate + end + DefenderUnitIndex = DefenderUnitIndex + 1 + DefenderSquadron.Resources[TemplateID][GroupName] = nil + if DefenderUnitIndex > DefenderGrouping then + break + end + + end + + if DefenderCAPTemplate then + local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) + local SpawnGroup = GROUP:Register( DefenderName ) + DefenderCAPTemplate.lateActivation = nil + DefenderCAPTemplate.uncontrolled = nil + local Takeoff = self:GetSquadronTakeoff( SquadronName ) + DefenderCAPTemplate.route.points[1].type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + DefenderCAPTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + local Defender = _DATABASE:Spawn( DefenderCAPTemplate ) + + self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping ) + return Defender, DefenderGrouping + end + else + 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_A2A_DISPATCHER self @@ -2611,15 +2822,9 @@ do -- AI_A2A_DISPATCHER local Cap = DefenderSquadron.Cap if Cap then - - local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN - local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping - Spawn:InitGrouping( DefenderGrouping ) - local TakeoffMethod = self:GetSquadronTakeoff( SquadronName ) - local DefenderCAP = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) - self:AddDefenderToSquadron( DefenderSquadron, DefenderCAP, DefenderGrouping ) - + 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 ) @@ -2634,7 +2839,7 @@ do -- AI_A2A_DISPATCHER self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", Fsm ) function Fsm:onafterTakeoff( Defender, From, Event, To ) - self:F({"GCI Birth", Defender:GetName()}) + self:F({"CAP Birth", Defender:GetName()}) --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER @@ -2668,9 +2873,9 @@ do -- AI_A2A_DISPATCHER if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) Defender:Destroy() + self:ParkDefender( Squadron, Defender ) end end - end end end @@ -2776,31 +2981,19 @@ do -- AI_A2A_DISPATCHER self:F( { Grouping = DefenderGrouping, SquadronGrouping = DefenderSquadron.Grouping, DefaultGrouping = self.DefenderDefault.Grouping } ) self:F( { DefendersCount = DefenderCount, DefendersNeeded = DefendersNeeded } ) - -- DefenderSquadron.Resources can have the value nil, which expresses unlimited resources. - -- DefendersNeeded cannot exceed DefenderSquadron.Resources! - if DefenderSquadron.Resources and DefendersNeeded > DefenderSquadron.Resources then - DefendersNeeded = DefenderSquadron.Resources + -- 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 Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN - local DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded - if DefenderGrouping then - Spawn:InitGrouping( DefenderGrouping ) - else - Spawn:InitGrouping() - end - - local TakeoffMethod = self:GetSquadronTakeoff( ClosestDefenderSquadronName ) - local DefenderGCI = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP - self:F( { GCIDefender = DefenderGCI:GetName() } ) + local DefenderGCI, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) DefendersNeeded = DefendersNeeded - DefenderGrouping - self:AddDefenderToSquadron( DefenderSquadron, DefenderGCI, DefenderGrouping ) - if DefenderGCI then DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead @@ -2867,6 +3060,7 @@ do -- AI_A2A_DISPATCHER if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) Defender:Destroy() + self:ParkDefender( Squadron, Defender ) end end end -- if DefenderGCI then @@ -2890,8 +3084,8 @@ 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 - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. + -- @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. function AI_A2A_DISPATCHER:EvaluateENGAGE( DetectedItem ) self:F( { DetectedItem.ItemID } ) @@ -2917,8 +3111,8 @@ do -- AI_A2A_DISPATCHER --- Creates an GCI task when there are targets for it. -- @param #AI_A2A_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. + -- @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. function AI_A2A_DISPATCHER:EvaluateGCI( DetectedItem ) self:F( { DetectedItem.ItemID } ) @@ -2944,7 +3138,7 @@ do -- AI_A2A_DISPATCHER --- 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 @{Detection#DETECTION_BASE} derived object. + -- @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 ) @@ -2984,6 +3178,8 @@ do -- AI_A2A_DISPATCHER local Report = REPORT:New( "\nTactical Overview" ) + local DefenderGroupCount = 0 + -- Now that all obsolete tasks are removed, loop through the detected targets. for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do @@ -3021,16 +3217,19 @@ do -- AI_A2A_DISPATCHER for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do local Defender = Defender -- Wrapper.Group#GROUP if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then - local Fuel = Defender:GetFuel() * 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" ) ) + 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 @@ -3043,20 +3242,23 @@ do -- AI_A2A_DISPATCHER TaskCount = TaskCount + 1 local Defender = Defender -- Wrapper.Group#GROUP if not DefenderTask.Target then - local DefenderHasTask = Defender:HasTask() - local Fuel = Defender:GetFuel() * 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" ) ) + 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", TaskCount ) ) + Report:Add( string.format( "\n - %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) ) self:F( Report:Text( "\n" ) ) trigger.action.outText( Report:Text( "\n" ), 25 ) @@ -3069,10 +3271,10 @@ end do - --- Calculates which HUMAN friendlies are nearby the area + --- Calculates which HUMAN friendlies are nearby the area. -- @param #AI_A2A_DISPATCHER self - -- @param DetectedItem - -- @return #number, Core.CommandCenter#REPORT + -- @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_A2A_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem ) local DetectedSet = DetectedItem.Set @@ -3115,14 +3317,14 @@ do return PlayersCount, PlayerTypesReport end - --- Calculates which friendlies are nearby the area + --- Calculates which friendlies are nearby the area. -- @param #AI_A2A_DISPATCHER self - -- @param DetectedItem - -- @return #number, Core.CommandCenter#REPORT - function AI_A2A_DISPATCHER:GetFriendliesNearBy( Target ) + -- @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_A2A_DISPATCHER:GetFriendliesNearBy( DetectedItem ) - local DetectedSet = Target.Set - local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( Target ) + local DetectedSet = DetectedItem.Set + local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem ) local FriendlyTypes = {} local FriendliesCount = 0 @@ -3159,8 +3361,8 @@ do return FriendliesCount, FriendlyTypesReport end - --- - -- @param AI_A2A_DISPATCHER + --- Schedules a new CAP for the given SquadronName. + -- @param #AI_A2A_DISPATCHER self -- @param #string SquadronName The squadron name. function AI_A2A_DISPATCHER:SchedulerCAP( SquadronName ) self:CAP( SquadronName ) @@ -3173,12 +3375,8 @@ do --- @type AI_A2A_GCICAP -- @extends #AI_A2A_DISPATCHER - --- # AI\_A2A\_GCICAP class, extends @{AI_A2A_Dispatcher#AI_A2A_DISPATCHER} - -- - -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia1.JPG) - -- - -- The AI_A2A_GCICAP class is designed to create an automatic air defence system for a coalition setting up GCI and CAP air defenses. - -- The class derives from @{AI#AI_A2A_DISPATCHER} and thus, all the methods that are defined in the @{AI#AI_A2A_DISPATCHER} class, can be used also in AI\_A2A\_GCICAP. + --- 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. -- -- === -- @@ -3281,7 +3479,7 @@ do -- -- **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#AI_A2A_DISPATCHER}: + -- ## 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. -- @@ -3444,7 +3642,7 @@ do -- 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 Resources The amount of resources that will be allocated to each squadron. + -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. -- @return #AI_A2A_GCICAP -- @usage -- @@ -3519,7 +3717,7 @@ do -- -- 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, Resources ) + function AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) local EWRSetGroup = SET_GROUP:New() EWRSetGroup:FilterPrefixes( EWRPrefixes ) @@ -3553,27 +3751,27 @@ do -- Setup squadrons - self:F( { Airbases = AirbaseNames } ) + self:I( { Airbases = AirbaseNames } ) - self:F( "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:F( { 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() if AirbaseZone:IsVec2InZone( TemplateCoord:GetVec2() ) then Templates = Templates or {} table.insert( Templates, Template:GetName() ) - self:F( { Template = Template:GetName() } ) + self:I( { Template = Template:GetName() } ) end end if Templates then - self:SetSquadron( AirbaseName, AirbaseName, Templates, Resources ) + self:SetSquadron( AirbaseName, AirbaseName, Templates, ResourceCount ) end end @@ -3585,13 +3783,13 @@ do self.CAPTemplates:FilterPrefixes( CapPrefixes ) self.CAPTemplates:FilterOnce() - self:F( "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:F( { 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() @@ -3599,7 +3797,7 @@ do local Squadron = self.DefenderSquadrons[AirbaseName] if Squadron then local Distance = AirbaseCoord:Get2DDistance( CAPZone:GetCoordinate() ) - self:F( { AirbaseDistance = Distance } ) + self:I( { AirbaseDistance = Distance } ) if Distance < AirbaseDistance then AirbaseDistance = Distance AirbaseClosest = Airbase @@ -3607,7 +3805,7 @@ do end end if AirbaseClosest then - self:F( { 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 @@ -3615,14 +3813,14 @@ do -- Setup GCI. -- GCI is setup for all Squadrons. - self:F( "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 } ) if Squadron then - self:F( { GCIAirbase = AirbaseName } ) + self:I( { GCIAirbase = AirbaseName } ) self:SetSquadronGci( AirbaseName, 800, 1200 ) end end @@ -3631,6 +3829,7 @@ do 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 ) @@ -3649,7 +3848,7 @@ do -- 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 Resources The amount of resources that will be allocated to each squadron. + -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. -- @return #AI_A2A_GCICAP -- @usage -- @@ -3733,9 +3932,9 @@ do -- -- 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, Resources ) + 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, Resources ) + local self = AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) if BorderPrefix then self:SetBorderZone( ZONE_POLYGON:New( BorderPrefix, GROUP:FindByName( BorderPrefix ) ) ) diff --git a/Moose Development/Moose/AI/AI_A2A_Gci.lua b/Moose Development/Moose/AI/AI_A2A_Gci.lua index e714fcfe6..53b7141ab 100644 --- a/Moose Development/Moose/AI/AI_A2A_Gci.lua +++ b/Moose Development/Moose/AI/AI_A2A_Gci.lua @@ -8,7 +8,8 @@ -- -- === -- --- @module AI_A2A_GCI +-- @module AI.AI_A2A_GCI +-- @image AI_Ground_Control_Intercept.JPG @@ -16,13 +17,11 @@ -- @extends AI.AI_A2A#AI_A2A ---- # AI_A2A_GCI class, extends @{AI_A2A#AI_A2A} --- --- The AI_A2A_GCI class implements the core functions to intercept intruders. The Engage function will intercept intruders. +--- 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 @{Group} and this must be done before the AI_A2A_GCI process can be started using the **Start** event. +-- 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) -- @@ -65,15 +64,15 @@ -- -- ### 2.2 AI_A2A_GCI Events -- --- * **@{AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. --- * **@{AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. -- * **@{#AI_A2A_GCI.Engage}**: Let the AI engage the bogeys. -- * **@{#AI_A2A_GCI.Abort}**: Aborts the engagement and return patrolling in the patrol zone. --- * **@{AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. --- * **@{#AI_A2A_GCI.Destroy}**: The AI has destroyed a bogey @{Unit}. --- * **@{#AI_A2A_GCI.Destroyed}**: The AI has destroyed all bogeys @{Unit}s assigned in the CAS task. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. +-- * **@{#AI_A2A_GCI.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. +-- * **@{#AI_A2A_GCI.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. -- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. -- -- ## 3. Set the Range of Engagement @@ -84,7 +83,7 @@ -- 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_GCI#AI_A2A_GCI.SetEngageRange}() to define that range. +-- Use the method @{AI.AI_GCI#AI_A2A_GCI.SetEngageRange}() to define that range. -- -- ## 4. Set the Zone of Engagement -- @@ -92,7 +91,7 @@ -- -- An optional @{Zone} can be set, -- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI_Cap#AI_A2A_GCI.SetEngageZone}() to define that Zone. +-- Use the method @{AI.AI_Cap#AI_A2A_GCI.SetEngageZone}() to define that Zone. -- -- === -- @@ -291,6 +290,7 @@ 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 ) end diff --git a/Moose Development/Moose/AI/AI_A2A_Patrol.lua b/Moose Development/Moose/AI/AI_A2A_Patrol.lua index 927c5fd32..7b44c14fb 100644 --- a/Moose Development/Moose/AI/AI_A2A_Patrol.lua +++ b/Moose Development/Moose/AI/AI_A2A_Patrol.lua @@ -1,26 +1,23 @@ --- **AI** -- (R2.2) - Models the process of air patrol of airplanes. -- --- This is a class used in the @{AI_A2A_Dispatcher}. --- -- === -- -- ### Author: **FlightControl** -- -- === -- --- @module AI_A2A_Patrol +-- @module AI.AI_A2A_Patrol +-- @image AI_Air_Patrolling.JPG --- @type AI_A2A_PATROL -- @extends AI.AI_A2A#AI_A2A ---- # AI_A2A_PATROL class, extends @{Fsm#FSM_CONTROLLABLE} --- --- The AI_A2A_PATROL class implements the core functions to patrol a @{Zone} by an AI @{Group} or @{Group}. +--- Implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group}. -- -- ![Process](..\Presentations\AI_PATROL\Dia3.JPG) -- --- The AI_A2A_PATROL is assigned a @{Group} and this must be done before the AI_A2A_PATROL process can be started using the **Start** event. +-- The AI_A2A_PATROL is assigned a @{Wrapper.Group} and this must be done before the AI_A2A_PATROL process can be started using the **Start** event. -- -- ![Process](..\Presentations\AI_PATROL\Dia4.JPG) -- @@ -93,7 +90,7 @@ -- * @{#AI_A2A_PATROL.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. -- -- The detection frequency can be set with @{#AI_A2A_PATROL.SetRefreshTimeInterval}( seconds ), where the amount of seconds specify how much seconds will be waited before the next detection. --- Use the method @{#AI_A2A_PATROL.GetDetectedUnits}() to obtain a list of the @{Unit}s detected by the AI. +-- Use the method @{#AI_A2A_PATROL.GetDetectedUnits}() to obtain a list of the @{Wrapper.Unit}s detected by the AI. -- -- The detection can be filtered to potential targets in a specific zone. -- Use the method @{#AI_A2A_PATROL.SetDetectionZone}() to set the zone where targets need to be detected. @@ -126,11 +123,11 @@ AI_A2A_PATROL = { -- @param #AI_A2A_PATROL self -- @param Wrapper.Group#GROUP AIPatrol -- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Group} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Group} in km/h. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @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_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. @@ -236,8 +233,8 @@ end --- Sets (modifies) the minimum and maximum speed of the patrol. -- @param #AI_A2A_PATROL self --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Group} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Group} in km/h. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h. -- @return #AI_A2A_PATROL self function AI_A2A_PATROL:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) @@ -250,8 +247,8 @@ end --- Sets the floor and ceiling altitude of the patrol. -- @param #AI_A2A_PATROL self --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @return #AI_A2A_PATROL self function AI_A2A_PATROL:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) @@ -354,13 +351,12 @@ function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To ) end --- @param Wrapper.Group#GROUP AIPatrol -function AI_A2A_PATROL.Resume( AIPatrol ) +function AI_A2A_PATROL.Resume( AIPatrol, Fsm ) - AIPatrol:F( { "AI_A2A_PATROL.Resume:", AIPatrol:GetName() } ) + AIPatrol:I( { "AI_A2A_PATROL.Resume:", AIPatrol:GetName() } ) if AIPatrol:IsAlive() then - local _AI_A2A = AIPatrol:GetState( AIPatrol, "AI_A2A" ) -- #AI_A2A - _AI_A2A:__Reset( 1 ) - _AI_A2A:__Route( 5 ) + Fsm:__Reset( 1 ) + Fsm:__Route( 5 ) end end diff --git a/Moose Development/Moose/AI/AI_A2G.lua b/Moose Development/Moose/AI/AI_A2G.lua new file mode 100644 index 000000000..b80eb8796 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G.lua @@ -0,0 +1,69 @@ +--- **AI** -- Models the process of air to ground operations for airplanes and helicopters. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G +-- @image AI_Air_To_Ground_Dispatching.JPG + +--- @type AI_A2G +-- @extends AI.AI_Air#AI_AIR + +--- The AI_A2G class implements the core functions to operate an AI @{Wrapper.Group} A2G tasking. +-- +-- +-- # 1) AI_A2G constructor +-- +-- * @{#AI_A2G.New}(): Creates a new AI_A2G object. +-- +-- # 2) AI_A2G is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- +-- ## 2.1) AI_A2G States. +-- +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_A2G Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- @field #AI_A2G +AI_A2G = { + ClassName = "AI_A2G", +} + +--- Creates a new AI_A2G process. +-- @param #AI_A2G self +-- @param Wrapper.Group#GROUP AIGroup The group object to receive the A2G Process. +-- @return #AI_A2G +function AI_A2G:New( AIGroup ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_AIR:New( AIGroup ) ) -- #AI_A2G + + self:SetFuelThreshold( .2, 60 ) + self:SetDamageThreshold( 0.95 ) + self:SetDisengageRadius( 70000 ) + + return self +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..59da48bd8 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_BAI.lua @@ -0,0 +1,149 @@ +--- **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 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 ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_BAI + + return self +end + + +--- @param #AI_A2G_BAI 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_A2G_BAI:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + + local DefenderGroupName = DefenderGroup:GetName() + + self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT + + local AttackCount = self.AttackSetUnit:Count() + + if AttackCount > 0 then + + if DefenderGroup:IsAlive() then + + -- 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( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. + + local TargetCoord = self.AttackSetUnit:GetFirst():GetPointVec3() + TargetCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. + + local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + + local EngageRoute = {} + + local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + + --- Calculate the target route point. + + local FromWP = DefenderCoord:WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = FromWP + + local ToCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + self:SetTargetDistance( ToCoord ) -- For RTB status check + + local FromEngageAngle = ToCoord:GetAngleDegrees( ToCoord:GetDirectionVec3( DefenderCoord ) ) + + --- Create a route point of type air. + local ToWP = ToCoord:Translate( 10000, FromEngageAngle ):WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + self:F( { Angle = FromEngageAngle, ToTargetSpeed = ToTargetSpeed } ) + self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) + + EngageRoute[#EngageRoute+1] = ToWP + + local AttackTasks = {} + + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "Engage Unit evaluation:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) + self:T( { "Eliminating Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) + end + end + + if #AttackTasks == 0 then + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTEvadeFire() + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + end + + DefenderGroup:Route( EngageRoute, 0.5 ) + end + else + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + end +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..63562487b --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_CAS.lua @@ -0,0 +1,154 @@ +--- **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_A2G_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 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 ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_CAS + + local RTBSpeedMax = AIGroup:GetSpeedMax() + + self:SetRTBSpeed( RTBSpeedMax * 0.50, RTBSpeedMax * 0.75 ) + + return self +end + + +--- @param #AI_A2G_CAS 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_A2G_CAS:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + + local DefenderGroupName = DefenderGroup:GetName() + + self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT + + local AttackCount = self.AttackSetUnit:Count() + + if AttackCount > 0 then + + if DefenderGroup:IsAlive() then + + -- 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( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. + + local TargetCoord = self.AttackSetUnit:GetFirst():GetPointVec3() + TargetCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. + + local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + + local EngageRoute = {} + + local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + + --- Calculate the target route point. + + local FromWP = DefenderCoord:WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = FromWP + + self:SetTargetDistance( TargetCoord ) -- For RTB status check + + local FromEngageAngle = TargetCoord:GetAngleDegrees( TargetCoord:GetDirectionVec3( DefenderCoord ) ) + + local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 ) + + --- Create a route point of type air. + local ToWP = TargetCoord:Translate( EngageDistance, FromEngageAngle ):WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + self:F( { Angle = FromEngageAngle, ToTargetSpeed = ToTargetSpeed } ) + self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) + + EngageRoute[#EngageRoute+1] = ToWP + + local AttackTasks = {} + + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "Eliminating Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) + end + end + + if #AttackTasks == 0 then + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTEvadeFire() + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + end + + DefenderGroup:Route( EngageRoute, 0.5 ) + end + else + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + end +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..facc45a66 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_Dispatcher.lua @@ -0,0 +1,4181 @@ +--- **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 specif points in your battlefield. +-- * Setup (SEAD) suppression of air defenses to enhance the control of enemy airspace. +-- * Setup (CAS) Controlled Air Support to attack approach enemy ground units. +-- * Setup (BAI) Battleground Air Interdiction to attack detected remote enemy ground units and targets. +-- * Define and use a detection network setup by recce. +-- * Define defense squadrons at airbases, farps and carriers. +-- * Enable airbases for A2G defenses. +-- * Add different planes and helicopter templates to different 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 or also Airborne? +-- +-- 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. +-- +-- +-- ## 4. How do defenses decide to engage on approaching enemy units? +-- +-- The A2G dispacher needs you to setup defense coordinates, which are specific coordinates that are strategic positions in the battle field +-- to be defended. Any ground based enemy approaching to such a defense point, will be engaged for defense by A2G defense units. +-- The A2G dispatcher provides parameters to setup the defensiveness, meaning, when actually A2G units will engage with the approaching enemy. +-- For this, a probability distribution model has been created, which models an increased probability that a defense will engage an attacker, +-- depending on the distance of the attacker to the defense coordinate. There are 3 levels of defense reactivity setup, which are Low, Medium and High. +-- Defenses will start to consider defensive action when an enemy ground unit is within 60km from a defense point, by default. +-- But you can change this maximum distance using on of the available methods. The close 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 are at the same distance from a defense coordinate. +-- This will ensure optimal defenses, SEAD tasks will be much more quicker launched agains radar emitters, to ensure air superiority. +-- Approaching main battle tanks will be much faster defended upon, 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. +-- +-- 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 as the "home base" on an airfield, carrier or farp. +-- Carefully plan where each Squadron will be located as part of the defense system. +-- 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. +-- +-- +-- ## 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 provide through an instance of a @{Functional.Detection} network. + -- The most effective reconnaissance for the A2G dispatcher would be to use the @{Functional.Detection#DETECTION_AREAS} object. + -- + -- An 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_A2A_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**. + -- + -- There are two ways how targets can be engaged: directly upon 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" ) + -- + -- @field #AI_A2G_DISPATCHER + AI_A2G_DISPATCHER = { + ClassName = "AI_A2G_DISPATCHER", + Detection = nil, + } + + + --- 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 Landing + AI_A2G_DISPATCHER.Landing = { + NearAirbase = 1, + AtRunway = 2, + AtEngineShutdown = 3, + } + + --- 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: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:__Start( 5 ) + + 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:ParkDefender( DefenderSquadron ) + end + end + end + + + --- @param #AI_A2G_DISPATCHER self + function AI_A2G_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 + 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:ParkDefender( 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:ParkDefender( 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 engage any target by airborne friendlies, which are executing cap or returning from an defense mission. + -- If there is a target area detected and reported, then any friendlies that are airborne near this target area, + -- will be commanded to (re-)engage that target when available (if no other tasks were commanded). + -- + -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, + -- will be considered to receive the command to engage that target area. + -- + -- You need to evaluate the value of this parameter carefully: + -- + -- * If too small, more defense 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_A2G_DISPATCHER.SetEngageRadius}() to modify the default Engage Radius for ALL squadrons.** + -- + -- Demonstration Mission: [AID-019 - AI_A2G - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-019%20-%20AI_A2G%20-%20Engage%20Range%20Test) + -- + -- @param #AI_A2G_DISPATCHER self + -- @param #number EngageRadius (Optional, Default = 100000) The radius to report friendlies near the target. + -- @return #AI_A2G_DISPATCHER + -- @usage + -- + -- -- Set 50km as the radius to engage any target by airborne friendlies. + -- A2GDispatcher:SetEngageRadius( 50000 ) + -- + -- -- Set 100km as the radius to engage any target by airborne friendlies. + -- A2GDispatcher:SetEngageRadius() -- 100000 is the default value. + -- + function AI_A2G_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_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: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" ) + -- + 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 + + + --- 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 + + + + + --- + -- @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 ) + -- A2GDispatcher:SetSquadronSead( "Novo", 900, 2100 ) + -- A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronSead( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + 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 + Sead.EngageCeilingAltitude = EngageCeilingAltitude + Sead.Defend = true + + self:F( { Sead = Sead } ) + 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 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 ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.SEAD = DefenderSquadron.SEAD or {} + + local SeadPatrol = DefenderSquadron.SEAD + SeadPatrol.Name = SquadronName + SeadPatrol.Zone = Zone + SeadPatrol.PatrolFloorAltitude = FloorAltitude + SeadPatrol.PatrolCeilingAltitude = CeilingAltitude + SeadPatrol.EngageFloorAltitude = FloorAltitude + SeadPatrol.EngageCeilingAltitude = CeilingAltitude + SeadPatrol.PatrolMinSpeed = PatrolMinSpeed + SeadPatrol.PatrolMaxSpeed = PatrolMaxSpeed + SeadPatrol.EngageMinSpeed = EngageMinSpeed + SeadPatrol.EngageMaxSpeed = EngageMaxSpeed + SeadPatrol.AltType = AltType + SeadPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "SEAD" ) + + self:F( { Sead = SeadPatrol } ) + end + + + --- + -- @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 ) + -- A2GDispatcher:SetSquadronCas( "Novo", 900, 2100 ) + -- A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronCas( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + 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 + Cas.EngageCeilingAltitude = EngageCeilingAltitude + Cas.Defend = true + + self:F( { Cas = Cas } ) + 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 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 ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.CAS = DefenderSquadron.CAS or {} + + local CasPatrol = DefenderSquadron.CAS + CasPatrol.Name = SquadronName + CasPatrol.Zone = Zone + CasPatrol.PatrolFloorAltitude = FloorAltitude + CasPatrol.PatrolCeilingAltitude = CeilingAltitude + CasPatrol.EngageFloorAltitude = FloorAltitude + CasPatrol.EngageCeilingAltitude = CeilingAltitude + CasPatrol.PatrolMinSpeed = PatrolMinSpeed + CasPatrol.PatrolMaxSpeed = PatrolMaxSpeed + CasPatrol.EngageMinSpeed = EngageMinSpeed + CasPatrol.EngageMaxSpeed = EngageMaxSpeed + CasPatrol.AltType = AltType + CasPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "CAS" ) + + self:F( { Cas = CasPatrol } ) + end + + + --- + -- @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 ) + -- A2GDispatcher:SetSquadronBai( "Novo", 900, 2100 ) + -- A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200 ) + -- + -- @return #AI_A2G_DISPATCHER + function AI_A2G_DISPATCHER:SetSquadronBai( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + 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 + Bai.EngageCeilingAltitude = EngageCeilingAltitude + Bai.Defend = true + + self:F( { Bai = Bai } ) + 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 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 ) + + local DefenderSquadron = self:GetSquadron( SquadronName ) + + DefenderSquadron.BAI = DefenderSquadron.BAI or {} + + local BaiPatrol = DefenderSquadron.BAI + BaiPatrol.Name = SquadronName + BaiPatrol.Zone = Zone + BaiPatrol.PatrolFloorAltitude = FloorAltitude + BaiPatrol.PatrolCeilingAltitude = CeilingAltitude + BaiPatrol.EngageFloorAltitude = FloorAltitude + BaiPatrol.EngageCeilingAltitude = CeilingAltitude + BaiPatrol.PatrolMinSpeed = PatrolMinSpeed + BaiPatrol.PatrolMaxSpeed = PatrolMaxSpeed + BaiPatrol.EngageMinSpeed = EngageMinSpeed + BaiPatrol.EngageMaxSpeed = EngageMaxSpeed + BaiPatrol.AltType = AltType + BaiPatrol.Patrol = true + + self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "BAI" ) + + self:F( { Bai = BaiPatrol } ) + 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 + + + --- 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 + + + + + --- @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 ) + + -- First, count the active AIGroups Units, targetting the DetectedSet + local DefendersEngaged = 0 + local DefendersTotal = 0 + + local AttackerSet = AttackerDetection.Set + local AttackerCount = AttackerSet:Count() + 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 ) + 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 + + 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 ) + + self:F({SquadronName = SquadronName}) + + local DefenderSquadron, Patrol = self:CanPatrol( SquadronName, DefenseTaskType ) + + if Patrol then + + local DefenderPatrol, DefenderGrouping = self:ResourceActivate( DefenderSquadron ) + + if DefenderPatrol then + + local AI_A2G_PATROL = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } + + local Fsm = AI_A2G_PATROL[DefenseTaskType]:New( DefenderPatrol, Patrol.EngageMinSpeed, Patrol.EngageMaxSpeed, Patrol.EngageFloorAltitude, Patrol.EngageCeilingAltitude, Patrol.Zone, Patrol.PatrolFloorAltitude, Patrol.PatrolCeilingAltitude, Patrol.PatrolMinSpeed, Patrol.PatrolMaxSpeed, Patrol.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, DefenderPatrol, DefenseTaskType, Fsm, nil, DefenderGrouping ) + + function Fsm:onafterTakeoff( Defender, From, Event, To ) + self:F({"Patrol Birth", Defender:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + + if Squadron then + Fsm:__Patrol( 2 ) -- Start Patrolling + end + end + + function Fsm:onafterRTB( Defender, From, Event, To ) + self:F({"Patrol RTB", Defender:GetName()}) + self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + Dispatcher:ClearDefenderTaskTarget( Defender ) + end + + --- @param #AI_A2G_DISPATCHER self + function Fsm:onafterHome( Defender, From, Event, To, Action ) + self:F({"Patrol Home", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local Dispatcher = self:GetDispatcher() -- #AI_A2G_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_A2G_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + Dispatcher:ParkDefender( Squadron, Defender ) + end + 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( 1, AttackerDetection.Set ) -- Engage on the TargetSetUnit + + self:SetDefenderTaskTarget( Defender, AttackerDetection ) + + end + end + end + + --- + -- @param #AI_A2G_DISPATCHER self + function AI_A2G_DISPATCHER:onafterDefend( From, Event, To, AttackerDetection, DefendersTotal, DefendersEngaged, DefendersMissing, DefenderFriendlies, DefenseTaskType ) + + self:F( { From, Event, To, AttackerDetection.Index, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing, DefenderFriendlies = DefenderFriendlies } ) + + AttackerDetection.Type = DefenseTaskType -- This is set to report the task type in the status panel. + + 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 SquadronName = self:GetDefenderTask( DefenderGroup ).SquadronName + local SquadronOverhead = self:GetSquadronOverhead( SquadronName ) + + local Fsm = self:GetDefenderTaskFsm( DefenderGroup ) + Fsm:__Engage( 1, AttackerSet ) -- Engage on the TargetSetUnit + + self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection ) + + local DefenderGroupSize = DefenderGroup:GetSize() + DefendersMissing = DefendersMissing - DefenderGroupSize / SquadronOverhead + DefendersTotal = DefendersTotal + DefenderGroupSize / SquadronOverhead + + if DefendersMissing <= 0 then + break + end + 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 + + if DefenderSquadron[DefenseTaskType] then + + local SpawnCoord = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE + local AttackerCoord = AttackerUnit:GetCoordinate() + local InterceptCoord = AttackerDetection.InterceptCoord + self:F( { InterceptCoord = InterceptCoord } ) + if InterceptCoord then + local InterceptDistance = SpawnCoord:Get2DDistance( InterceptCoord ) + local AirbaseDistance = SpawnCoord:Get2DDistance( AttackerCoord ) + self:F( { InterceptDistance = InterceptDistance, AirbaseDistance = AirbaseDistance, InterceptCoord = InterceptCoord } ) + + if ClosestDistance == 0 or InterceptDistance < ClosestDistance then + + -- Only intercept if the distance to target is smaller or equal to the GciRadius limit. + if AirbaseDistance <= self.DefenseRadius then + ClosestDistance = InterceptDistance + ClosestDefenderSquadronName = SquadronName + end + end + end + end + end + + if ClosestDefenderSquadronName then + + local DefenderSquadron, Defense = self:CanDefend( ClosestDefenderSquadronName, 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 + + local DefenderGroup, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded ) + + DefendersNeeded = DefendersNeeded - DefenderGrouping + + if DefenderGroup then + + DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead + + local AI_A2G_ENGAGE = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS } + + local Fsm = AI_A2G_ENGAGE[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude ) -- AI.AI_A2G_ENGAGE + 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, DefenderGroup, DefenseTaskType, Fsm, AttackerDetection, DefenderGrouping ) + + function Fsm:onafterTakeoff( Defender, From, Event, To ) + self:F({"Defender Birth", Defender:GetName()}) + --self:GetParent(self).onafterBirth( self, Defender, From, Event, To ) + + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender ) + + self:F( { DefenderTarget = DefenderTarget } ) + + if DefenderTarget then + Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit + end + end + + function Fsm:onafterRTB( Defender, From, Event, To ) + self:F({"Defender RTB", Defender:GetName()}) + self:GetParent(self).onafterRTB( self, Defender, From, Event, To ) + + local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER + Dispatcher:ClearDefenderTaskTarget( Defender ) + end + + --- @param #AI_A2G_DISPATCHER self + function Fsm:onafterLostControl( Defender, From, Event, To ) + self:F({"Defender LostControl", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local Dispatcher = Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER + local Squadron = Dispatcher:GetSquadronFromDefender( Defender ) + --if Defender:IsAboveRunway() then + --Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + --Defender:Destroy() + --end + end + + --- @param #AI_A2G_DISPATCHER self + function Fsm:onafterHome( Defender, From, Event, To, Action ) + self:F({"Defender Home", Defender:GetName()}) + self:GetParent(self).onafterHome( self, Defender, From, Event, To ) + + local Dispatcher = self:GetDispatcher() -- #AI_A2G_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_A2G_DISPATCHER.Landing.NearAirbase then + Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender ) + Defender:Destroy() + Dispatcher:ParkDefender( Squadron, Defender ) + end + end + end -- if DefenderGCI then + 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:Count() + local IsSEAD = AttackerSet:HasSEAD() -- Is the AttackerSet a SEAD group? + + if ( IsSEAD > 0 ) then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) + + 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 nil, nil, nil + 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? + + self:F( { Friendlies = self.Detection:GetFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) } ) + + if IsCas == true then + + -- First, count the active defenders, engaging the DetectedItem. + local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem ) + + 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 nil, nil, nil + 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 ) + + 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 nil, nil, nil + 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() + + + 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 + if DefenderTask.Target then + local AttackerItem = Detection:GetDetectedItemByIndex( DefenderTask.Target.Index ) + if not AttackerItem then + self:F( { "Removing obsolete Target:", DefenderTask.Target.Index } ) + self:ClearDefenderTaskTarget( 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 + + local Report = REPORT:New( "\nTactical Overview" ) + + local DefenderGroupCount = 0 + local Delay = 0 -- We need to implement a delay for each action because the spawning on airbases get confused if done too quick. + + local DefendersTotal = 0 + + -- Now that all obsolete tasks are removed, loop through the detected targets. + for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do + + local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem + local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT + local DetectedCount = DetectedSet:Count() + local DetectedZone = DetectedItem.Zone + + self:F( { "Target ID", DetectedItem.ItemID } ) + DetectedSet:Flush( self ) + + local DetectedID = DetectedItem.ID + local DetectionIndex = DetectedItem.Index + local DetectedItemChanged = DetectedItem.Changed + + local AttackerCoordinate = 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 DefenseCoordinate = nil + + for DefenseCoordinateName, EvaluateCoordinate in pairs( self.DefenseCoordinates ) do + + local EvaluateDistance = AttackerCoordinate:Get2DDistance( EvaluateCoordinate ) + + if EvaluateDistance <= self.DefenseRadius then + + local DistanceProbability = ( self.DefenseRadius / EvaluateDistance * self.DefenseReactivity ) + local DefenseProbability = math.random() + + self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } ) + + if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then + DefenseCoordinate = EvaluateCoordinate + break + end + end + end + + if DefenseCoordinate then + do + local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_SEAD( DetectedItem ) -- Returns a SET_UNIT with the SEAD targets to be engaged... + if DefendersMissing and DefendersMissing > 0 then + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "SEAD", DefenseCoordinate ) + Delay = Delay + 1 + 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 and DefendersMissing > 0 then + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "CAS", DefenseCoordinate ) + Delay = Delay + 1 + 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 and DefendersMissing > 0 then + self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } ) + self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "BAI", DefenseCoordinate ) + Delay = Delay + 1 + end + end + end + +-- do +-- local DefendersMissing, Friendlies = self:Evaluate_CAS( DetectedItem ) +-- if DefendersMissing and DefendersMissing > 0 then +-- self:F( { DefendersMissing = DefendersMissing } ) +-- self:CAS( DetectedItem, DefendersMissing, Friendlies ) +-- end +-- end + + if self.TacticalDisplay then + -- Show tactical situation + Report:Add( string.format( "\n - %4s %s ( %s ): ( #%d ) %s" , DetectedItem.Type or " --- ", 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 ) + 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 + +do + + --- @type AI_A2G_GCICAP + -- @extends #AI_A2G_DISPATCHER + + --- Create an automatic air defence system for a coalition setting up GCI and CAP air defenses. + -- The class derives from @{#AI_A2G_DISPATCHER} and thus, all the methods that are defined in the @{#AI_A2G_DISPATCHER} class, can be used also in AI\_A2G\_GCICAP. + -- + -- === + -- + -- # Demo Missions + -- + -- ### [AI\_A2G\_GCICAP for Caucasus](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-200%20-%20AI_A2G%20-%20GCICAP%20Demonstration) + -- ### [AI\_A2G\_GCICAP for NTTR](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-210%20-%20NTTR%20AI_A2G_GCICAP%20Demonstration) + -- ### [AI\_A2G\_GCICAP for Normandy](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-220%20-%20NORMANDY%20AI_A2G_GCICAP%20Demonstration) + -- + -- ### [AI\_A2G\_GCICAP for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching) + -- + -- === + -- + -- # YouTube Channel + -- + -- ### [DCS WORLD - MOOSE - A2G GCICAP - Build an automatic A2G Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx) + -- + -- === + -- + -- ![Banner Image](..\Presentations\AI_A2G_DISPATCHER\Dia3.JPG) + -- + -- AI\_A2G\_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_A2G_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 A2G defense system for a coalition using the DCS Mission Editor available functions. + -- Using the DCS Mission Editor, you define borders of the coalition which are guarded by GCICAP, + -- configure airbases to belong to the coalition, define squadrons flying certain types of planes or payloads per airbase, and define CAP zones. + -- **Very little lua needs to be applied, a one liner**, which is fully explained below, which can be embedded + -- right in a DO SCRIPT trigger action or in a larger DO SCRIPT FILE trigger action. + -- + -- CAP flights will take off and proceed to designated CAP zones where they will remain on station until the ground radars direct them to intercept + -- detected enemy aircraft or they run short of fuel and must return to base (RTB). + -- + -- When a CAP flight leaves their zone to perform a GCI or return to base a new CAP flight will spawn to take its place. + -- If all CAP flights are engaged or RTB then additional GCI interceptors will scramble to intercept unengaged enemy aircraft under ground radar control. + -- + -- In short it is a plug in very flexible and configurable air defence module for DCS World. + -- + -- === + -- + -- # The following actions need to be followed when using AI\_A2G\_GCICAP in your mission: + -- + -- ## 1) Configure a working AI\_A2G\_GCICAP defense system for ONE coalition. + -- + -- ### 1.1) Define which airbases are for which coalition. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_1.JPG) + -- + -- Color the airbases red or blue. You can do this by selecting the airbase on the map, and select the coalition blue or red. + -- + -- ### 1.2) Place groups of units given a name starting with a **EWR prefix** of your choice to build your EWR network. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_2.JPG) + -- + -- **All EWR groups starting with the EWR prefix (text) will be included in the detection system.** + -- + -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy. + -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. + -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US). + -- Additionally, ANY other radar capable unit can be part of the EWR network! + -- Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar. + -- The position of these units is very important as they need to provide enough coverage + -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them. + -- + -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates. + -- For example if they are a long way forward and can detect enemy planes on the ground and taking off + -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition. + -- Having the radars further back will mean a slower escalation because fewer targets will be detected and + -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map. + -- It all depends on what the desired effect is. + -- + -- EWR networks are **dynamically maintained**. By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, + -- increasing or decreasing the radar coverage of the Early Warning System. + -- + -- ### 1.3) Place Airplane or Helicopter Groups with late activation switched on above the airbases to define Squadrons. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_3.JPG) + -- + -- These are **templates**, with a given name starting with a **Template prefix** above each airbase that you wanna have a squadron. + -- These **templates** need to be within 1.5km from the airbase center. They don't need to have a slot at the airplane, they can just be positioned above the airbase, + -- without a route, and should only have ONE unit. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_4.JPG) + -- + -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** + -- + -- ### 1.4) Place floating helicopters to create the CAP zones defined by its route points. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_GCICAP-ME_5.JPG) + -- + -- **All airplane or helicopter groups that are starting with any of the choosen Template Prefixes will result in a squadron created at the airbase.** + -- + -- The helicopter indicates the start of the CAP zone. + -- The route points define the form of the CAP zone polygon. + -- + -- ![Mission Editor Action](..\Presentations\AI_A2G_DISPATCHER\AI_A2G_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_A2G_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\_A2G\_DISPATCHER: + -- + -- * @{#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. + -- + -- 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\_A2G\_DISPATCHER: + -- + -- * @{#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 + -- A2G 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. + -- + -- ### 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\_A2G\_DISPATCHER: + -- + -- The method @{#AI_A2G_DISPATCHER.SetSquadronPatrol}() defines a CAP execution for a squadron. + -- + -- Setting-up a CAP zone also requires specific parameters: + -- + -- * The minimum and maximum altitude + -- * The minimum speed and maximum patrol speed + -- * The minimum and maximum engage speed + -- * The type of altitude measurement + -- + -- These define how the squadron will perform the CAP while partrolling. Different terrain types requires different types of CAP. + -- + -- The @{#AI_A2G_DISPATCHER.SetSquadronPatrolInterval}() method specifies **how much** and **when** CAP flights will takeoff. + -- + -- It is recommended not to overload the air defense with CAP flights, as these will decrease the performance of the overall system. + -- + -- For example, the following setup will create a CAP for squadron "Sochi": + -- + -- A2GDispatcher:SetSquadronPatrol( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" ) + -- A2GDispatcher:SetSquadronPatrolInterval( "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\_A2G\_DISPATCHER: + -- + -- The method @{#AI_A2G_DISPATCHER.SetSquadronGci}() defines a GCI execution for a squadron. + -- + -- Setting-up a GCI readiness also requires specific parameters: + -- + -- * The minimum speed and maximum patrol speed + -- + -- Essentially this controls how many flights of GCI aircraft can be active at any time. + -- Note allowing large numbers of active GCI flights can adversely impact mission performance on low or medium specification hosts/servers. + -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders, + -- too short will mean that the intruders may have alraedy passed the ideal interception point! + -- + -- For example, the following setup will create a GCI for squadron "Sochi": + -- + -- A2GDispatcher:SetSquadronGci( "Mozdok", 900, 1200 ) + -- + -- ### 2.5) Grouping or detected targets. + -- + -- Detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate + -- group being detected. + -- + -- Targets will be grouped within a radius of 30km by default. + -- + -- The radius indicates that detected targets need to be grouped within a radius of 30km. + -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation. + -- Fast planes like in the 80s, need a larger radius than WWII planes. + -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft. + -- + -- ## 3) Additional notes: + -- + -- In order to create a two way A2G defense system, **two AI\_A2G\_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\_A2G\_GCICAP class: + -- + -- ### 4.1) An easy setup: + -- + -- -- Setup the AI_A2G_GCICAP dispatcher for one coalition, and initialize it. + -- GCI_Red = AI_A2G_GCICAP:New( "EWR CCCP", "SQUADRON CCCP", "CAP CCCP", 2 ) + -- -- + -- The following parameters were given to the :New method of AI_A2G_GCICAP, and mean the following: + -- + -- * `"EWR CCCP"`: Groups of the blue coalition are placed that define the EWR network. These groups start with the name `EWR CCCP`. + -- * `"SQUADRON CCCP"`: Late activated Groups objects of the red coalition are placed above the relevant airbases that will contain these templates in the squadron. + -- These late activated Groups start with the name `SQUADRON CCCP`. Each Group object contains only one Unit, and defines the weapon payload, skin and skill level. + -- * `"CAP CCCP"`: CAP Zones are defined using floating, late activated Helicopter Group objects, where the route points define the route of the polygon of the CAP Zone. + -- These Helicopter Group objects start with the name `CAP CCCP`, and will be the locations wherein CAP will be performed. + -- * `2` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. + -- + -- + -- ### 4.2) A more advanced setup: + -- + -- -- Setup the AI_A2G_GCICAP dispatcher for the blue coalition. + -- + -- A2G_GCICAP_Blue = AI_A2G_GCICAP:New( { "BLUE EWR" }, { "104th", "105th", "106th" }, { "104th CAP" }, 4 ) + -- + -- The following parameters for the :New method have the following meaning: + -- + -- * `{ "BLUE EWR" }`: An array of the group name prefixes of the groups of the blue coalition are placed that define the EWR network. These groups start with the name `BLUE EWR`. + -- * `{ "104th", "105th", "106th" } `: An array of the group name prefixes of the Late activated Groups objects of the blue coalition are + -- placed above the relevant airbases that will contain these templates in the squadron. + -- These late activated Groups start with the name `104th` or `105th` or `106th`. + -- * `{ "104th CAP" }`: An array of the names of the CAP zones are defined using floating, late activated helicopter group objects, + -- where the route points define the route of the polygon of the CAP Zone. + -- These Helicopter Group objects start with the name `104th CAP`, and will be the locations wherein CAP will be performed. + -- * `4` Defines how many CAP airplanes are patrolling in each CAP zone defined simulateneously. + -- + -- @field #AI_A2G_GCICAP + AI_A2G_GCICAP = { + ClassName = "AI_A2G_GCICAP", + Detection = nil, + } + + + --- AI_A2G_GCICAP constructor. + -- @param #AI_A2G_GCICAP self + -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network. + -- @param #string TemplatePrefixes A list of template prefixes. + -- @param #string PatrolPrefixes A list of CAP zone prefixes (polygon zones). + -- @param #number PatrolLimit A number of how many CAP maximum will be spawned. + -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. + -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. + -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. + -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. + -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. + -- @return #AI_A2G_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. + -- A2GDispatcher = AI_A2G_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. + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources. + -- -- The EWR network group prefix is DF CCCP. All groups starting with DF CCCP will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The CAP Zone prefix is nil. No CAP is created. + -- -- The CAP Limit is nil. + -- -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets. + -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. + -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2GDispatcher = AI_A2G_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, nil, nil, nil, nil, nil, 30 ) + -- + function AI_A2G_GCICAP:New( EWRPrefixes, TemplatePrefixes, PatrolPrefixes, PatrolLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + + local EWRSetGroup = SET_GROUP:New() + EWRSetGroup:FilterPrefixes( EWRPrefixes ) + EWRSetGroup:FilterStart() + + local Detection = DETECTION_AREAS:New( EWRSetGroup, GroupingRadius or 30000 ) + + local self = BASE:Inherit( self, AI_A2G_DISPATCHER:New( Detection ) ) -- #AI_A2G_GCICAP + + self:SetGciRadius( GciRadius ) + + -- Determine the coalition of the EWRNetwork, this will be the coalition of the GCICAP. + local EWRFirst = EWRSetGroup:GetFirst() -- Wrapper.Group#GROUP + local EWRCoalition = EWRFirst:GetCoalition() + + -- Determine the airbases belonging to the coalition. + local AirbaseNames = {} -- #list<#string> + for AirbaseID, AirbaseData in pairs( _DATABASE.AIRBASES ) do + local Airbase = AirbaseData -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + if Airbase:GetCoalition() == EWRCoalition then + table.insert( AirbaseNames, AirbaseName ) + end + end + + self.Templates = SET_GROUP + :New() + :FilterPrefixes( TemplatePrefixes ) + :FilterOnce() + + -- Setup squadrons + + self:I( { Airbases = AirbaseNames } ) + + self:I( "Defining Templates for Airbases ..." ) + for AirbaseID, AirbaseName in pairs( AirbaseNames ) do + local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + local AirbaseCoord = Airbase:GetCoordinate() + local AirbaseZone = ZONE_RADIUS:New( "Airbase", AirbaseCoord:GetVec2(), 3000 ) + local Templates = nil + self:I( { Airbase = AirbaseName } ) + for TemplateID, Template in pairs( self.Templates:GetSet() ) do + local Template = Template -- Wrapper.Group#GROUP + local TemplateCoord = Template:GetCoordinate() + if AirbaseZone:IsVec2InZone( TemplateCoord:GetVec2() ) then + Templates = Templates or {} + table.insert( Templates, Template:GetName() ) + self:I( { Template = Template:GetName() } ) + end + end + if Templates then + self:SetSquadron( AirbaseName, AirbaseName, Templates, ResourceCount ) + end + end + + -- Setup CAP. + -- Find for each CAP the nearest airbase to the (start or center) of the zone. + -- CAP will be launched from there. + + self.CAPTemplates = SET_GROUP:New() + self.CAPTemplates:FilterPrefixes( PatrolPrefixes ) + self.CAPTemplates:FilterOnce() + + self:I( "Setting up CAP ..." ) + for CAPID, CAPTemplate in pairs( self.CAPTemplates:GetSet() ) do + local CAPZone = ZONE_POLYGON:New( CAPTemplate:GetName(), CAPTemplate ) + -- Now find the closest airbase from the ZONE (start or center) + local AirbaseDistance = 99999999 + local AirbaseClosest = nil -- Wrapper.Airbase#AIRBASE + self:I( { CAPZoneGroup = CAPID } ) + for AirbaseID, AirbaseName in pairs( AirbaseNames ) do + local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + local AirbaseCoord = Airbase:GetCoordinate() + local Squadron = self.DefenderSquadrons[AirbaseName] + if Squadron then + local Distance = AirbaseCoord:Get2DDistance( CAPZone:GetCoordinate() ) + self:I( { AirbaseDistance = Distance } ) + if Distance < AirbaseDistance then + AirbaseDistance = Distance + AirbaseClosest = Airbase + end + end + end + if AirbaseClosest then + self:I( { CAPAirbase = AirbaseClosest:GetName() } ) + self:SetSquadronPatrol( AirbaseClosest:GetName(), CAPZone, 6000, 10000, 500, 800, 800, 1200, "RADIO" ) + self:SetSquadronPatrolInterval( AirbaseClosest:GetName(), PatrolLimit, 300, 600, 1 ) + end + end + + -- Setup GCI. + -- GCI is setup for all Squadrons. + self:I( "Setting up GCI ..." ) + for AirbaseID, AirbaseName in pairs( AirbaseNames ) do + local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE + local AirbaseName = Airbase:GetName() + local Squadron = self.DefenderSquadrons[AirbaseName] + self:F( { Airbase = AirbaseName } ) + if Squadron then + self:I( { GCIAirbase = AirbaseName } ) + self:SetSquadronGci( AirbaseName, 800, 1200 ) + end + end + + self:__Start( 5 ) + + self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead ) + self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead ) + + self:HandleEvent( EVENTS.Land ) + self:HandleEvent( EVENTS.EngineShutdown ) + + return self + end + + --- AI_A2G_GCICAP constructor with border. + -- @param #AI_A2G_GCICAP self + -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network. + -- @param #string TemplatePrefixes A list of template prefixes. + -- @param #string BorderPrefix A Border Zone Prefix. + -- @param #string PatrolPrefixes A list of CAP zone prefixes (polygon zones). + -- @param #number PatrolLimit A number of how many CAP maximum will be spawned. + -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area. + -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter. + -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task. + -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI. + -- @param #number ResourceCount The amount of resources that will be allocated to each squadron. + -- @return #AI_A2G_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. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is "CAP Zone". + -- -- The CAP Limit is 2. + -- -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets. + -- -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition, + -- -- will be considered a defense task if the target is within 60km from the defender. + -- -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000, 30 ) + -- + -- @usage + -- + -- -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources. + -- -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network. + -- -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates. + -- -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border. + -- -- The CAP Zone prefix is nil. No CAP is created. + -- -- The CAP Limit is nil. + -- -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets. + -- -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defenser being assigned to a task. + -- -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement. + -- -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created. + -- + -- A2GDispatcher = AI_A2G_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", nil, nil, nil, nil, nil, 30 ) + -- + function AI_A2G_GCICAP:NewWithBorder( EWRPrefixes, TemplatePrefixes, BorderPrefix, PatrolPrefixes, PatrolLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + + local self = AI_A2G_GCICAP:New( EWRPrefixes, TemplatePrefixes, PatrolPrefixes, PatrolLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount ) + + 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_A2G_Engage.lua b/Moose Development/Moose/AI/AI_A2G_Engage.lua new file mode 100644 index 000000000..e6be845c3 --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_Engage.lua @@ -0,0 +1,378 @@ +--- **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_Engage +-- @image AI_Air_To_Ground_Engage.JPG + + + +--- @type AI_A2G_ENGAGE +-- @extends AI.AI_A2G#AI_A2G + + +--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders. +-- +-- ![Process](..\Presentations\AI_GCI\Dia3.JPG) +-- +-- The AI_A2G_ENGAGE is assigned a @{Wrapper.Group} and this must be done before the AI_A2G_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_A2G_ENGAGE constructor +-- +-- * @{#AI_A2G_ENGAGE.New}(): Creates a new AI_A2G_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_A2G_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_A2G_ENGAGE.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_A2G_ENGAGE +AI_A2G_ENGAGE = { + ClassName = "AI_A2G_ENGAGE", +} + + + +--- Creates a new AI_A2G_ENGAGE object +-- @param #AI_A2G_ENGAGE self +-- @param Wrapper.Group#GROUP AIGroup +-- @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. +-- @return #AI_A2G_ENGAGE +function AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2G:New( AIGroup ) ) -- #AI_A2G_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:AddTransition( { "Started", "Engaging", "Returning", "Airborne", "Patrolling" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Engage. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeEngage + -- @param #AI_A2G_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_A2G_ENGAGE] OnAfterEngage + -- @param #AI_A2G_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_A2G_ENGAGE] Engage + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Engage. + -- @function [parent=#AI_A2G_ENGAGE] __Engage + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Engaging. +-- @function [parent=#AI_A2G_ENGAGE] OnLeaveEngaging +-- @param #AI_A2G_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_A2G_ENGAGE] OnEnterEngaging +-- @param #AI_A2G_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_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Fired. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeFired + -- @param #AI_A2G_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_A2G_ENGAGE] OnAfterFired + -- @param #AI_A2G_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_A2G_ENGAGE] Fired + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Fired. + -- @function [parent=#AI_A2G_ENGAGE] __Fired + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Destroy. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeDestroy + -- @param #AI_A2G_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_A2G_ENGAGE] OnAfterDestroy + -- @param #AI_A2G_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_A2G_ENGAGE] Destroy + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Destroy. + -- @function [parent=#AI_A2G_ENGAGE] __Destroy + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Abort. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeAbort + -- @param #AI_A2G_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_A2G_ENGAGE] OnAfterAbort + -- @param #AI_A2G_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_A2G_ENGAGE] Abort + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Abort. + -- @function [parent=#AI_A2G_ENGAGE] __Abort + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_ENGAGE. + + --- OnBefore Transition Handler for Event Accomplish. + -- @function [parent=#AI_A2G_ENGAGE] OnBeforeAccomplish + -- @param #AI_A2G_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_A2G_ENGAGE] OnAfterAccomplish + -- @param #AI_A2G_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_A2G_ENGAGE] Accomplish + -- @param #AI_A2G_ENGAGE self + + --- Asynchronous Event Trigger for Event Accomplish. + -- @function [parent=#AI_A2G_ENGAGE] __Accomplish + -- @param #AI_A2G_ENGAGE self + -- @param #number Delay The delay in seconds. + + return self +end + +--- onafter event handler for Start event. +-- @param #AI_A2G_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_A2G_ENGAGE:onafterStart( AIGroup, From, Event, To ) + + self:GetParent( self, AI_A2G_ENGAGE ).onafterStart( self, AIGroup, From, Event, To ) + + AIGroup:HandleEvent( EVENTS.Takeoff, nil, self ) + +end + + + +--- onafter event handler for Engage event. +-- @param #AI_A2G_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_A2G_ENGAGE:onafterEngage( AIGroup, From, Event, To ) + + self:HandleEvent( EVENTS.Dead ) + +end + +-- todo: need to fix this global function + +--- @param Wrapper.Group#GROUP AIControllable +function AI_A2G_ENGAGE.EngageRoute( AIGroup, Fsm ) + + AIGroup:F( { "AI_A2G_ENGAGE.EngageRoute:", AIGroup:GetName() } ) + + if AIGroup:IsAlive() then + Fsm:__Engage( 0.5 ) + + --local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) + --AIGroup:SetTask( Task ) + end +end + +--- onbefore event handler for Engage event. +-- @param #AI_A2G_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_A2G_ENGAGE:onbeforeEngage( AIGroup, From, Event, To ) + + if self.Accomplished == true then + return false + end +end + +--- onafter event handler for Abort event. +-- @param #AI_A2G_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_A2G_ENGAGE:onafterAbort( AIGroup, From, Event, To ) + AIGroup:ClearTasks() + self:Return() + self:__RTB( 0.5 ) +end + + +--- @param #AI_A2G_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_A2G_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + +end + +--- @param #AI_A2G_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_A2G_ENGAGE:onafterAccomplish( AIGroup, From, Event, To ) + self.Accomplished = true + self:SetDetectionOff() +end + +--- @param #AI_A2G_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_A2G_ENGAGE:onafterDestroy( AIGroup, From, Event, To, EventData ) + + if EventData.IniUnit then + self.AttackUnits[EventData.IniUnit] = nil + end +end + +--- @param #AI_A2G_ENGAGE self +-- @param Core.Event#EVENTDATA EventData +function AI_A2G_ENGAGE: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 diff --git a/Moose Development/Moose/AI/AI_A2G_Patrol.lua b/Moose Development/Moose/AI/AI_A2G_Patrol.lua new file mode 100644 index 000000000..dd9e0342a --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_Patrol.lua @@ -0,0 +1,323 @@ +--- **AI** -- Models the process of A2G patrolling and engaging ground targets for airplanes and helicopters. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_A2G_Patrol +-- @image AI_Air_To_Ground_Patrol.JPG + +--- @type AI_A2G_PATROL +-- @extends AI.AI_A2G_Engage#AI_A2G_ENGAGE + + +--- The AI_A2G_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_A2G_PATROL is assigned a @{Wrapper.Group} and this must be done before the AI_A2G_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_A2G_PATROL constructor +-- +-- * @{#AI_A2G_PATROL.New}(): Creates a new AI_A2G_PATROL object. +-- +-- ## 2. AI_A2G_PATROL is a FSM +-- +-- ![Process](..\Presentations\AI_CAP\Dia2.JPG) +-- +-- ### 2.1 AI_A2G_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_A2G_PATROL 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_A2G_PATROL.Engage}**: Let the AI engage the bogeys. +-- * **@{#AI_A2G_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_A2G_PATROL.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. +-- * **@{#AI_A2G_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_A2G_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_A2G_PATROL.SetEngageZone}() to define that Zone. +-- +-- === +-- +-- @field #AI_A2G_PATROL +AI_A2G_PATROL = { + ClassName = "AI_A2G_PATROL", +} + +--- Creates a new AI_A2G_PATROL object +-- @param #AI_A2G_PATROL self +-- @param Wrapper.Group#GROUP AIGroup +-- @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 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_A2G_PATROL +function AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2G_ENGAGE:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude ) ) -- #AI_A2G_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_A2G_PATROL] OnBeforePatrol + -- @param #AI_A2G_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_A2G_PATROL] OnAfterPatrol + -- @param #AI_A2G_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_A2G_PATROL] Patrol + -- @param #AI_A2G_PATROL self + + --- Asynchronous Event Trigger for Event Patrol. + -- @function [parent=#AI_A2G_PATROL] __Patrol + -- @param #AI_A2G_PATROL self + -- @param #number Delay The delay in seconds. + + --- OnLeave Transition Handler for State Patrolling. + -- @function [parent=#AI_A2G_PATROL] OnLeavePatrolling + -- @param #AI_A2G_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_A2G_PATROL] OnEnterPatrolling + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + + --- OnBefore Transition Handler for Event Route. + -- @function [parent=#AI_A2G_PATROL] OnBeforeRoute + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @return #boolean Return false to cancel Transition. + + --- OnAfter Transition Handler for Event Route. + -- @function [parent=#AI_A2G_PATROL] OnAfterRoute + -- @param #AI_A2G_PATROL self + -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM. + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + + --- Synchronous Event Trigger for Event Route. + -- @function [parent=#AI_A2G_PATROL] Route + -- @param #AI_A2G_PATROL self + + --- Asynchronous Event Trigger for Event Route. + -- @function [parent=#AI_A2G_PATROL] __Route + -- @param #AI_A2G_PATROL self + -- @param #number Delay The delay in seconds. + + + self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2G_PATROL. + + return self +end + + +--- Set the Engage Range when the AI will engage with airborne enemies. +-- @param #AI_A2G_PATROL self +-- @param #number EngageRange The Engage Range. +-- @return #AI_A2G_PATROL self +function AI_A2G_PATROL:SetEngageRange( EngageRange ) + self:F2() + + if EngageRange then + self.EngageRange = EngageRange + else + self.EngageRange = nil + end +end + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_A2G_PATROL self +-- @return #AI_A2G_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_A2G_PATROL:onafterPatrol( AIPatrol, From, Event, To ) + self:F2() + + self:ClearTargetDistance() + + self:__Route( 1 ) + + AIPatrol:OnReSpawn( + function( PatrolGroup ) + self:__Reset( 1 ) + self:__Route( 5 ) + end + ) +end + +--- @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. +-- Note that this method is required, as triggers the next route when patrolling for the AIPatrol. +function AI_A2G_PATROL.PatrolRoute( AIPatrol, Fsm ) + + AIPatrol:F( { "AI_A2G_PATROL.PatrolRoute:", AIPatrol:GetName() } ) + + if AIPatrol:IsAlive() then + Fsm:Route() + end + +end + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_A2G_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_A2G_PATROL:onafterRoute( AIPatrol, From, Event, To ) + + self:F2() + + -- When RTB, don't allow anymore the routing. + if From == "RTB" then + return + end + + + if AIPatrol:IsAlive() then + + local PatrolRoute = {} + + --- Calculate the target route point. + + local CurrentCoord = AIPatrol:GetCoordinate() + + local ToTargetCoord = self.PatrolZone:GetRandomPointVec2() + ToTargetCoord:SetAlt( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) ) + self:SetTargetDistance( ToTargetCoord ) -- For RTB status check + + local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed ) + + --- Create a route point of type air. + local ToPatrolRoutePoint = ToTargetCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint + PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint + + local Tasks = {} + Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_A2G_PATROL.PatrolRoute", self ) + PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks ) + + AIPatrol:OptionROEReturnFire() + AIPatrol:OptionROTEvadeFire() + + AIPatrol:Route( PatrolRoute, 0.5 ) + end + +end + +--- @param Wrapper.Group#GROUP AIPatrol +function AI_A2G_PATROL.Resume( AIPatrol, Fsm ) + + AIPatrol:I( { "AI_A2G_PATROL.Resume:", AIPatrol:GetName() } ) + if AIPatrol:IsAlive() then + Fsm:__Reset( 1 ) + Fsm:__Route( 5 ) + 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..2da77818a --- /dev/null +++ b/Moose Development/Moose/AI/AI_A2G_SEAD.lua @@ -0,0 +1,231 @@ +--- **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_A2G_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 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 ) + + -- Inherits from BASE + local self = BASE:Inherit( self, AI_A2G_PATROL:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2G_SEAD + + return self +end + + + +--- @param #AI_A2G_SEAD 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_A2G_SEAD:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit ) + + self:F( { DefenderGroup, From, Event, To, AttackSetUnit} ) + + local DefenderGroupName = DefenderGroup:GetName() + + self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT + + local AttackCount = self.AttackSetUnit:Count() + + if AttackCount > 0 then + + if DefenderGroup:IsAlive() then + + -- Determine the distance to the target. + -- If it is less than 50km, then attack without a route. + -- Otherwise perform a route attack. + + local DefenderCoord = DefenderGroup:GetPointVec3() + DefenderCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. + + local TargetCoord = self.AttackSetUnit:GetFirst():GetPointVec3() + TargetCoord:SetY( math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude ) ) -- Ground targets don't have an altitude. + + local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord ) + +-- if TargetDistance >= 50000 then + + local EngageRoute = {} + + local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed ) + + --- Calculate the target route point. + + local FromWP = DefenderCoord:WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = FromWP + + local ToCoord = self.AttackSetUnit:GetFirst():GetCoordinate() + self:SetTargetDistance( ToCoord ) -- For RTB status check + + local FromEngageAngle = ToCoord:GetAngleDegrees( ToCoord:GetDirectionVec3( DefenderCoord ) ) + + --- Create a route point of type air, 50km from the center of the attack point. + local ToWP = ToCoord:Translate( 50000, FromEngageAngle ):WaypointAir( + self.PatrolAltType or "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + self:F( { Angle = FromEngageAngle, ToTargetSpeed = ToTargetSpeed } ) + self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } ) + + EngageRoute[#EngageRoute+1] = ToWP + + local AttackTasks = {} + + for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do + if AttackUnit:IsAlive() and AttackUnit:IsGround() then + self:T( { "Engage Unit evaluation:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) + local HasRadar = AttackUnit:HasSEAD() + if HasRadar then + self:T( { "Eliminating Unit:", AttackUnit:GetName() } ) + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) + end + end + end + + if #AttackTasks == 0 then + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + else + DefenderGroup:OptionROEOpenFire() + DefenderGroup:OptionROTVertical() + DefenderGroup:OptionKeepWeaponsOnThreat() + --DefenderGroup:OptionRTBAmmo( Weapon.flag.AnyASM ) + + AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) + EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks ) + end + + DefenderGroup:Route( EngageRoute, 2 ) + +-- else +-- local AttackTasks = {} +-- --local AttackUnit = self.AttackSetUnit:GetRandom() -- Wrapper.Unit#UNIT +-- for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do +-- if AttackUnit:IsAlive() and AttackUnit:IsGround() then +-- local HasRadar = AttackUnit:HasSEAD() +-- if HasRadar then +-- self:T( { "Eliminating Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsGround() } ) +-- AttackTasks[#AttackTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit ) +-- AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_A2G_ENGAGE.EngageRoute", self ) +-- end +-- end +-- end +-- local DefenderTask = DefenderGroup:TaskCombo( AttackTasks ) +-- +-- DefenderGroup:OptionROEOpenFire() +-- DefenderGroup:OptionROTVertical() +-- DefenderGroup:OptionKeepWeaponsOnThreat() +-- DefenderGroup:OptionRTBAmmo( Weapon.flag.AnyASM ) +-- +-- DefenderGroup:SetTask( DefenderTask, 0 ) +-- end + end + else + self:E( DefenderGroupName .. ": No targets found -> Going RTB") + self:Return() + self:__RTB( 0.5 ) + end +end + diff --git a/Moose Development/Moose/AI/AI_Air.lua b/Moose Development/Moose/AI/AI_Air.lua new file mode 100644 index 000000000..bbfadc5a0 --- /dev/null +++ b/Moose Development/Moose/AI/AI_Air.lua @@ -0,0 +1,749 @@ +--- **AI** -- Models the process of AI air operations. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Air +-- @image AI_Air_Operations.JPG + +--- @type AI_AIR +-- @extends Core.Fsm#FSM_CONTROLLABLE + +--- The AI_AIR class implements the core functions to operate an AI @{Wrapper.Group}. +-- +-- +-- # 1) AI_AIR constructor +-- +-- * @{#AI_AIR.New}(): Creates a new AI_AIR object. +-- +-- # 2) AI_AIR is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- +-- ## 2.1) AI_AIR States. +-- +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_AIR Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- @field #AI_AIR +AI_AIR = { + ClassName = "AI_AIR", +} + +--- Creates a new AI_AIR process. +-- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup The group object to receive the A2G Process. +-- @return #AI_AIR +function AI_AIR:New( AIGroup ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_AIR + + self:SetControllable( AIGroup ) + + self:SetStartState( "Stopped" ) + + self:AddTransition( "*", "Start", "Started" ) + + --- Start Handler OnBefore for AI_AIR + -- @function [parent=#AI_AIR] OnBeforeStart + -- @param #AI_AIR self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Start Handler OnAfter for AI_AIR + -- @function [parent=#AI_AIR] OnAfterStart + -- @param #AI_AIR self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Start Trigger for AI_AIR + -- @function [parent=#AI_AIR] Start + -- @param #AI_AIR self + + --- Start Asynchronous Trigger for AI_AIR + -- @function [parent=#AI_AIR] __Start + -- @param #AI_AIR self + -- @param #number Delay + + self:AddTransition( "*", "Stop", "Stopped" ) + +--- OnLeave Transition Handler for State Stopped. +-- @function [parent=#AI_AIR] OnLeaveStopped +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Stopped. +-- @function [parent=#AI_AIR] OnEnterStopped +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- OnBefore Transition Handler for Event Stop. +-- @function [parent=#AI_AIR] OnBeforeStop +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Stop. +-- @function [parent=#AI_AIR] OnAfterStop +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Stop. +-- @function [parent=#AI_AIR] Stop +-- @param #AI_AIR self + +--- Asynchronous Event Trigger for Event Stop. +-- @function [parent=#AI_AIR] __Stop +-- @param #AI_AIR self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR. + +--- OnBefore Transition Handler for Event Status. +-- @function [parent=#AI_AIR] OnBeforeStatus +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event Status. +-- @function [parent=#AI_AIR] OnAfterStatus +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event Status. +-- @function [parent=#AI_AIR] Status +-- @param #AI_AIR self + +--- Asynchronous Event Trigger for Event Status. +-- @function [parent=#AI_AIR] __Status +-- @param #AI_AIR self +-- @param #number Delay The delay in seconds. + + self:AddTransition( "*", "RTB", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR. + +--- OnBefore Transition Handler for Event RTB. +-- @function [parent=#AI_AIR] OnBeforeRTB +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnAfter Transition Handler for Event RTB. +-- @function [parent=#AI_AIR] OnAfterRTB +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + +--- Synchronous Event Trigger for Event RTB. +-- @function [parent=#AI_AIR] RTB +-- @param #AI_AIR self + +--- Asynchronous Event Trigger for Event RTB. +-- @function [parent=#AI_AIR] __RTB +-- @param #AI_AIR self +-- @param #number Delay The delay in seconds. + +--- OnLeave Transition Handler for State Returning. +-- @function [parent=#AI_AIR] OnLeaveReturning +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +-- @return #boolean Return false to cancel Transition. + +--- OnEnter Transition Handler for State Returning. +-- @function [parent=#AI_AIR] OnEnterReturning +-- @param #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. + + self:AddTransition( "Patrolling", "Refuel", "Refuelling" ) + + --- Refuel Handler OnBefore for AI_AIR + -- @function [parent=#AI_AIR] OnBeforeRefuel + -- @param #AI_AIR self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- Refuel Handler OnAfter for AI_AIR + -- @function [parent=#AI_AIR] OnAfterRefuel + -- @param #AI_AIR self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Refuel Trigger for AI_AIR + -- @function [parent=#AI_AIR] Refuel + -- @param #AI_AIR self + + --- Refuel Asynchronous Trigger for AI_AIR + -- @function [parent=#AI_AIR] __Refuel + -- @param #AI_AIR self + -- @param #number Delay + + self:AddTransition( "*", "Takeoff", "Airborne" ) + self:AddTransition( "*", "Return", "Returning" ) + self:AddTransition( "*", "Hold", "Holding" ) + self:AddTransition( "*", "Home", "Home" ) + self:AddTransition( "*", "LostControl", "LostControl" ) + self:AddTransition( "*", "Fuel", "Fuel" ) + self:AddTransition( "*", "Damaged", "Damaged" ) + self:AddTransition( "*", "Eject", "*" ) + self:AddTransition( "*", "Crash", "Crashed" ) + self:AddTransition( "*", "PilotDead", "*" ) + + self.IdleCount = 0 + + return self +end + +--- @param Wrapper.Group#GROUP self +-- @param Core.Event#EVENTDATA EventData +function GROUP:OnEventTakeoff( EventData, Fsm ) + Fsm:Takeoff() + self:UnHandleEvent( EVENTS.Takeoff ) +end + + + +function AI_AIR:SetDispatcher( Dispatcher ) + self.Dispatcher = Dispatcher +end + +function AI_AIR:GetDispatcher() + return self.Dispatcher +end + +function AI_AIR:SetTargetDistance( Coordinate ) + + local CurrentCoord = self.Controllable:GetCoordinate() + self.TargetDistance = CurrentCoord:Get2DDistance( Coordinate ) + + self.ClosestTargetDistance = ( not self.ClosestTargetDistance or self.ClosestTargetDistance > self.TargetDistance ) and self.TargetDistance or self.ClosestTargetDistance +end + + +function AI_AIR:ClearTargetDistance() + + self.TargetDistance = nil + self.ClosestTargetDistance = nil +end + + +--- Sets (modifies) the minimum and maximum speed of the patrol. +-- @param #AI_AIR self +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @return #AI_AIR self +function AI_AIR:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) + self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) + + self.PatrolMinSpeed = PatrolMinSpeed + self.PatrolMaxSpeed = PatrolMaxSpeed +end + + +--- Sets (modifies) the minimum and maximum RTB speed of the patrol. +-- @param #AI_AIR self +-- @param DCS#Speed RTBMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed RTBMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @return #AI_AIR self +function AI_AIR:SetRTBSpeed( RTBMinSpeed, RTBMaxSpeed ) + self:F2( { RTBMinSpeed, RTBMaxSpeed } ) + + self.RTBMinSpeed = RTBMinSpeed + self.RTBMaxSpeed = RTBMaxSpeed +end + + +--- Sets the floor and ceiling altitude of the patrol. +-- @param #AI_AIR self +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @return #AI_AIR self +function AI_AIR:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) + self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) + + self.PatrolFloorAltitude = PatrolFloorAltitude + self.PatrolCeilingAltitude = PatrolCeilingAltitude +end + + +--- Sets the home airbase. +-- @param #AI_AIR self +-- @param Wrapper.Airbase#AIRBASE HomeAirbase +-- @return #AI_AIR self +function AI_AIR:SetHomeAirbase( HomeAirbase ) + self:F2( { HomeAirbase } ) + + self.HomeAirbase = HomeAirbase +end + +--- Sets to refuel at the given tanker. +-- @param #AI_AIR self +-- @param Wrapper.Group#GROUP TankerName The group name of the tanker as defined within the Mission Editor or spawned. +-- @return #AI_AIR self +function AI_AIR:SetTanker( TankerName ) + self:F2( { TankerName } ) + + self.TankerName = TankerName +end + + +--- Sets the disengage range, that when engaging a target beyond the specified range, the engagement will be cancelled and the plane will RTB. +-- @param #AI_AIR self +-- @param #number DisengageRadius The disengage range. +-- @return #AI_AIR self +function AI_AIR:SetDisengageRadius( DisengageRadius ) + self:F2( { DisengageRadius } ) + + self.DisengageRadius = DisengageRadius +end + +--- Set the status checking off. +-- @param #AI_AIR self +-- @return #AI_AIR self +function AI_AIR:SetStatusOff() + self:F2() + + self.CheckStatus = false +end + + +--- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base. +-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel treshold is calculated. +-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_AIR. +-- Once the time is finished, the old AI will return to the base. +-- @param #AI_AIR self +-- @param #number FuelThresholdPercentage The treshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel. +-- @param #number OutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base. +-- @return #AI_AIR self +function AI_AIR:SetFuelThreshold( FuelThresholdPercentage, OutOfFuelOrbitTime ) + + self.FuelThresholdPercentage = FuelThresholdPercentage + self.OutOfFuelOrbitTime = OutOfFuelOrbitTime + + self.Controllable:OptionRTBBingoFuel( false ) + + return self +end + +--- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base. +-- However, damage cannot be foreseen early on. +-- Therefore, when the damage treshold is reached, +-- the AI will return immediately to the home base (RTB). +-- Note that for groups, the average damage of the complete group will be calculated. +-- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage treshold will be 0.25. +-- @param #AI_AIR self +-- @param #number PatrolDamageThreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged. +-- @return #AI_AIR self +function AI_AIR:SetDamageThreshold( PatrolDamageThreshold ) + + self.PatrolManageDamage = true + self.PatrolDamageThreshold = PatrolDamageThreshold + + return self +end + +--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings. +-- @param #AI_AIR self +-- @return #AI_AIR self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM. +-- @param #string From The From State string. +-- @param #string Event The Event string. +-- @param #string To The To State string. +function AI_AIR:onafterStart( Controllable, From, Event, To ) + + self:__Status( 10 ) -- Check status status every 30 seconds. + + self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead ) + self:HandleEvent( EVENTS.Crash, self.OnCrash ) + self:HandleEvent( EVENTS.Ejection, self.OnEjection ) + + Controllable:OptionROEHoldFire() + Controllable:OptionROTVertical() +end + + + +--- @param #AI_AIR self +function AI_AIR:onbeforeStatus() + + return self.CheckStatus +end + +--- @param #AI_AIR self +function AI_AIR:onafterStatus() + + if self.Controllable and self.Controllable:IsAlive() then + + local RTB = false + + local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) + + if not self:Is( "Holding" ) and not self:Is( "Returning" ) then + local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() ) + + if DistanceFromHomeBase > self.DisengageRadius then + self:E( self.Controllable:GetName() .. " is too far from home base, RTB!" ) + self:Hold( 300 ) + RTB = false + end + end + +-- I think this code is not requirement anymore after release 2.5. +-- if self:Is( "Fuel" ) or self:Is( "Damaged" ) or self:Is( "LostControl" ) then +-- if DistanceFromHomeBase < 5000 then +-- self:E( self.Controllable:GetName() .. " is near the home base, RTB!" ) +-- self:Home( "Destroy" ) +-- end +-- end + + + if not self:Is( "Fuel" ) and not self:Is( "Home" ) then + + local Fuel = self.Controllable:GetFuelMin() + + -- If the fuel in the controllable is below the treshold percentage, + -- then send for refuel in case of a tanker, otherwise RTB. + if Fuel < self.FuelThresholdPercentage then + + if self.TankerName then + self:E( 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!" ) + local OldAIControllable = self.Controllable + + local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) + local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.OutOfFuelOrbitTime,nil ) ) + OldAIControllable:SetTask( TimedOrbitTask, 10 ) + + self:Fuel() + RTB = true + end + else + end + end + + -- TODO: Check GROUP damage function. + local Damage = self.Controllable:GetLife() + local InitialLife = self.Controllable:GetLife0() + + -- If the group is damaged, then RTB. + -- Note that a group can consist of more units, so if one unit is damaged of a group, the mission may continue. + -- The damaged unit will RTB due to DCS logic, and the others will continue to engage. + if ( Damage / InitialLife ) < self.PatrolDamageThreshold then + self:E( self.Controllable:GetName() .. " is damaged: " .. Damage .. " ... RTB!" ) + self:Damaged() + RTB = true + self:SetStatusOff() + end + + -- Check if planes went RTB and are out of control. + -- We only check if planes are out of control, when they are in duty. + if self.Controllable:HasTask() == false then + if not self:Is( "Started" ) and + not self:Is( "Stopped" ) and + not self:Is( "Fuel" ) and + not self:Is( "Damaged" ) and + not self:Is( "Home" ) then + if self.IdleCount >= 10 then + if Damage ~= InitialLife then + self:Damaged() + else + self:E( self.Controllable:GetName() .. " control lost! " ) + + self:LostControl() + end + else + self.IdleCount = self.IdleCount + 1 + end + end + else + self.IdleCount = 0 + end + + if RTB == true then + self:__RTB( 0.5 ) + end + + if not self:Is("Home") then + self:__Status( 10 ) + end + + end +end + + +--- @param Wrapper.Group#GROUP AIGroup +function AI_AIR.RTBRoute( AIGroup, Fsm ) + + AIGroup:F( { "AI_AIR.RTBRoute:", AIGroup:GetName() } ) + + if AIGroup:IsAlive() then + Fsm:__RTB( 0.5 ) + end + +end + +--- @param Wrapper.Group#GROUP AIGroup +function AI_AIR.RTBHold( AIGroup, Fsm ) + + AIGroup:F( { "AI_AIR.RTBHold:", AIGroup:GetName() } ) + if AIGroup:IsAlive() then + Fsm:__RTB( 0.5 ) + Fsm:Return() + local Task = AIGroup:TaskOrbitCircle( 4000, 400 ) + AIGroup:SetTask( Task ) + end + +end + + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterRTB( AIGroup, From, Event, To ) + self:F( { AIGroup, From, Event, To } ) + + + if AIGroup and AIGroup:IsAlive() then + + self:E( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" ) + + self:ClearTargetDistance() + --AIGroup:ClearTasks() + + local EngageRoute = {} + + --- Calculate the target route point. + + local CurrentCoord = AIGroup:GetCoordinate() + local ToTargetCoord = self.HomeAirbase:GetCoordinate() + local ToTargetSpeed = math.random( self.RTBMinSpeed, self.RTBMaxSpeed ) + local ToAirbaseAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) ) + + local Distance = CurrentCoord:Get2DDistance( ToTargetCoord ) + + local ToAirbaseCoord = CurrentCoord:Translate( 5000, ToAirbaseAngle ) + if Distance < 5000 then + self:E( "RTB and near the airbase!" ) + self:Home() + return + end + --- Create a route point of type air. + local ToRTBRoutePoint = ToAirbaseCoord:WaypointAir( + self.PatrolAltType, + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + ToTargetSpeed, + true + ) + + EngageRoute[#EngageRoute+1] = ToRTBRoutePoint + EngageRoute[#EngageRoute+1] = ToRTBRoutePoint + + 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_AIR.RTBRoute", self ) + EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks ) + + --- NOW ROUTE THE GROUP! + AIGroup:Route( EngageRoute, 0.5 ) + + end + +end + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterHome( AIGroup, From, Event, To ) + self:F( { AIGroup, From, Event, To } ) + + self:E( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" ) + + if AIGroup and AIGroup:IsAlive() then + end + +end + + + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterHold( AIGroup, From, Event, To, HoldTime ) + self:F( { AIGroup, From, Event, To } ) + + self:E( "Group " .. self.Controllable:GetName() .. " ... Holding! ( " .. self:GetState() .. " )" ) + + if AIGroup and AIGroup:IsAlive() then + local OrbitTask = AIGroup:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed ) + local TimedOrbitTask = AIGroup:TaskControlled( OrbitTask, AIGroup:TaskCondition( nil, nil, nil, nil, HoldTime , nil ) ) + + local RTBTask = AIGroup:TaskFunction( "AI_AIR.RTBHold", self ) + + local OrbitHoldTask = AIGroup:TaskOrbitCircle( 4000, self.PatrolMinSpeed ) + + --AIGroup:SetState( AIGroup, "AI_AIR", self ) + + AIGroup:SetTask( AIGroup:TaskCombo( { TimedOrbitTask, RTBTask, OrbitHoldTask } ), 1 ) + end + +end + +--- @param Wrapper.Group#GROUP AIGroup +function AI_AIR.Resume( AIGroup, Fsm ) + + AIGroup:I( { "AI_AIR.Resume:", AIGroup:GetName() } ) + if AIGroup:IsAlive() then + Fsm:__RTB( 0.5 ) + end + +end + +--- @param #AI_AIR self +-- @param Wrapper.Group#GROUP AIGroup +function AI_AIR:onafterRefuel( AIGroup, From, Event, To ) + self:F( { AIGroup, From, Event, To } ) + + self:E( "Group " .. self.Controllable:GetName() .. " ... Refuelling! ( " .. self:GetState() .. " )" ) + + if AIGroup and AIGroup:IsAlive() then + local Tanker = GROUP:FindByName( self.TankerName ) + if Tanker:IsAlive() and Tanker:IsAirPlane() then + + local RefuelRoute = {} + + --- Calculate the target route point. + + local CurrentCoord = 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 + ) + + self:F( { ToRefuelSpeed = ToRefuelSpeed } ) + + RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint + RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint + + AIGroup:OptionROEHoldFire() + AIGroup:OptionROTEvadeFire() + + local Tasks = {} + Tasks[#Tasks+1] = AIGroup:TaskRefueling() + Tasks[#Tasks+1] = AIGroup:TaskFunction( self:GetClassName() .. ".Resume", self ) + RefuelRoute[#RefuelRoute].task = AIGroup:TaskCombo( Tasks ) + + AIGroup:Route( RefuelRoute, 0.5 ) + else + self:RTB() + end + end + +end + + + +--- @param #AI_AIR self +function AI_AIR:onafterDead() + self:SetStatusOff() +end + + +--- @param #AI_AIR self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR:OnCrash( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:E( self.Controllable:GetUnits() ) + if #self.Controllable:GetUnits() == 1 then + self:__Crash( 1, EventData ) + end + end +end + +--- @param #AI_AIR self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR:OnEjection( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__Eject( 1, EventData ) + end +end + +--- @param #AI_AIR self +-- @param Core.Event#EVENTDATA EventData +function AI_AIR:OnPilotDead( EventData ) + + if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then + self:__PilotDead( 1, EventData ) + end +end diff --git a/Moose Development/Moose/AI/AI_BAI.lua b/Moose Development/Moose/AI/AI_BAI.lua index ccb0603bc..42bccbf06 100644 --- a/Moose Development/Moose/AI/AI_BAI.lua +++ b/Moose Development/Moose/AI/AI_BAI.lua @@ -1,9 +1,14 @@ ---- **AI** -- (R2.1) - Manages the independent process of Battlefield Air Interdiction (bombing) for airplanes. +--- **AI** -- Peform Battlefield Area Interdiction (BAI) within an engagement zone. -- --- === --- --- ![Banner Image](..\Presentations\AI_BAI\Dia1.JPG) +-- **Features:** -- +-- * Hold and standby within a patrol zone. +-- * Engage upon command the assigned targets within an engagement zone. +-- * Loop the zone until all targets are eliminated. +-- * Trigger different events upon the results achieved. +-- * After combat, return to the patrol zone and hold. +-- * RTB when commanded or after out of fuel. +-- -- === -- -- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/BAI%20-%20Battlefield%20Air%20Interdiction) @@ -21,25 +26,23 @@ -- -- === -- --- @module AI_Bai +-- @module AI.AI_Bai +-- @image AI_Battlefield_Air_Interdiction.JPG --- AI_BAI_ZONE class -- @type AI_BAI_ZONE --- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. +-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling. -- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. -- @extends AI.AI_Patrol#AI_PATROL_ZONE ---- # AI_BAI_ZONE class, extends @{AI_Patrol#AI_PATROL_ZONE} +--- Implements the core functions to provide BattleGround Air Interdiction in an Engage @{Zone} by an AIR @{Wrapper.Controllable} or @{Wrapper.Group}. -- --- AI_BAI_ZONE derives from the @{AI_Patrol#AI_PATROL_ZONE}, inheriting its methods and behaviour. --- --- The AI_BAI_ZONE class implements the core functions to provide BattleGround Air Interdiction in an Engage @{Zone} by an AIR @{Controllable} or @{Group}. -- The AI_BAI_ZONE runs a process. It holds an AI in a Patrol Zone and when the AI is commanded to engage, it will fly to an Engage Zone. -- -- ![HoldAndEngage](..\Presentations\AI_BAI\Dia3.JPG) -- --- The AI_BAI_ZONE is assigned a @{Group} and this must be done before the AI_BAI_ZONE process can be started through the **Start** event. +-- The AI_BAI_ZONE is assigned a @{Wrapper.Group} and this must be done before the AI_BAI_ZONE process can be started through the **Start** event. -- -- ![Start Event](..\Presentations\AI_BAI\Dia4.JPG) -- @@ -105,15 +108,15 @@ -- -- ### 2.2. AI_BAI_ZONE Events -- --- * **@{AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. --- * **@{AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. -- * **@{#AI_BAI_ZONE.Engage}**: Engage the AI to provide BOMB in the Engage Zone, destroying any target it finds. -- * **@{#AI_BAI_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone. --- * **@{AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. --- * **@{#AI_BAI_ZONE.Destroy}**: The AI has destroyed a target @{Unit}. --- * **@{#AI_BAI_ZONE.Destroyed}**: The AI has destroyed all target @{Unit}s assigned in the BOMB task. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. +-- * **@{#AI_BAI_ZONE.Destroy}**: The AI has destroyed a target @{Wrapper.Unit}. +-- * **@{#AI_BAI_ZONE.Destroyed}**: The AI has destroyed all target @{Wrapper.Unit}s assigned in the BOMB task. -- * **Status**: The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. -- -- ## 3. Modify the Engage Zone behaviour to pinpoint a **map object** or **scenery object** @@ -140,12 +143,12 @@ AI_BAI_ZONE = { --- Creates a new AI_BAI_ZONE object -- @param #AI_BAI_ZONE self -- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} 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#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. -- @param Core.Zone#ZONE_BASE EngageZone The zone where the engage will happen. --- @param Dcs.DCSTypes#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 RADIO -- @return #AI_BAI_ZONE self function AI_BAI_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone, PatrolAltType ) @@ -182,24 +185,24 @@ function AI_BAI_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude -- @function [parent=#AI_BAI_ZONE] Engage -- @param #AI_BAI_ZONE self -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. + -- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. + -- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. -- If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- Use the structure @{DCSTypes#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. + -- Use the structure @{DCS#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. + -- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- Asynchronous Event Trigger for Event Engage. -- @function [parent=#AI_BAI_ZONE] __Engage -- @param #AI_BAI_ZONE self -- @param #number Delay The delay in seconds. -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. + -- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. + -- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. -- If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- Use the structure @{DCSTypes#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. + -- Use the structure @{DCS#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. + -- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- OnLeave Transition Handler for State Engaging. -- @function [parent=#AI_BAI_ZONE] OnLeaveEngaging @@ -486,10 +489,10 @@ end -- @param #string Event The Event string. -- @param #string To The To State string. -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. --- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. +-- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. function AI_BAI_ZONE:onafterEngage( Controllable, From, Event, To, EngageSpeed, EngageAltitude, diff --git a/Moose Development/Moose/AI/AI_Balancer.lua b/Moose Development/Moose/AI/AI_Balancer.lua index 18bb5fa65..1b18bf79d 100644 --- a/Moose Development/Moose/AI/AI_Balancer.lua +++ b/Moose Development/Moose/AI/AI_Balancer.lua @@ -1,8 +1,11 @@ ---- **AI** -- (2.1) - Balance player slots with AI to create an engaging simulation environment, independent of the amount of players. +--- **AI** -- Balance player slots with AI to create an engaging simulation environment, independent of the amount of players. -- --- === +-- **Features:** -- --- ![Banner Image](..\Presentations\AI_Balancer\Dia1.JPG) +-- * Automatically spawn AI as a replacement of free player slots for a coalition. +-- * Make the AI to perform tasks. +-- * Define a maximum amount of AI to be active at the same time. +-- * Configure the behaviour of AI when a human joins a slot for which an AI is active. -- -- === -- @@ -21,7 +24,8 @@ -- -- === -- --- @module AI_Balancer +-- @module AI.AI_Balancer +-- @image AI_Balancing.JPG --- @type AI_BALANCER -- @field Core.Set#SET_CLIENT SetClient @@ -30,13 +34,11 @@ -- @extends Core.Fsm#FSM_SET ---- # AI_BALANCER class, extends @{Fsm#FSM_SET} --- --- The AI_BALANCER class monitors and manages as many replacement AI groups as there are --- CLIENTS in a SET_CLIENT collection, which are not occupied by human players. +--- Monitors and manages as many replacement AI groups as there are +-- CLIENTS in a SET\_CLIENT collection, which are not occupied by human players. -- In other words, use AI_BALANCER to simulate human behaviour by spawning in replacement AI in multi player missions. -- --- The parent class @{Fsm#FSM_SET} manages the functionality to control the Finite State Machine (FSM). +-- The parent class @{Core.Fsm#FSM_SET} manages the functionality to control the Finite State Machine (FSM). -- The mission designer can tailor the behaviour of the AI_BALANCER, by defining event and state transition methods. -- An explanation about state and event transition methods can be found in the @{FSM} module documentation. -- @@ -78,8 +80,8 @@ -- However, there are 2 additional options that you can use to customize the destroy behaviour. -- When a human player joins a slot, you can configure to let the AI return to: -- --- * @{#AI_BALANCER.ReturnToHomeAirbase}: Returns the AI to the **home** @{Airbase#AIRBASE}. --- * @{#AI_BALANCER.ReturnToNearestAirbases}: Returns the AI to the **nearest friendly** @{Airbase#AIRBASE}. +-- * @{#AI_BALANCER.ReturnToHomeAirbase}: Returns the AI to the **home** @{Wrapper.Airbase#AIRBASE}. +-- * @{#AI_BALANCER.ReturnToNearestAirbases}: Returns the AI to the **nearest friendly** @{Wrapper.Airbase#AIRBASE}. -- -- Note that when AI returns to an airbase, the AI_BALANCER will trigger the **Return** event and the AI will return, -- otherwise the AI_BALANCER will trigger a **Destroy** event, and the AI will be destroyed. @@ -141,10 +143,10 @@ function AI_BALANCER:InitSpawnInterval( Earliest, Latest ) return self end ---- Returns the AI to the nearest friendly @{Airbase#AIRBASE}. +--- Returns the AI to the nearest friendly @{Wrapper.Airbase#AIRBASE}. -- @param #AI_BALANCER self --- @param Dcs.DCSTypes#Distance ReturnThresholdRange If there is an enemy @{Client#CLIENT} within the ReturnThresholdRange given in meters, the AI will not return to the nearest @{Airbase#AIRBASE}. --- @param Core.Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Set#SET_AIRBASE}s to evaluate where to return to. +-- @param DCS#Distance ReturnThresholdRange If there is an enemy @{Wrapper.Client#CLIENT} within the ReturnThresholdRange given in meters, the AI will not return to the nearest @{Wrapper.Airbase#AIRBASE}. +-- @param Core.Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Core.Set#SET_AIRBASE}s to evaluate where to return to. function AI_BALANCER:ReturnToNearestAirbases( ReturnThresholdRange, ReturnAirbaseSet ) self.ToNearestAirbase = true @@ -152,9 +154,9 @@ function AI_BALANCER:ReturnToNearestAirbases( ReturnThresholdRange, ReturnAirbas self.ReturnAirbaseSet = ReturnAirbaseSet end ---- Returns the AI to the home @{Airbase#AIRBASE}. +--- Returns the AI to the home @{Wrapper.Airbase#AIRBASE}. -- @param #AI_BALANCER self --- @param Dcs.DCSTypes#Distance ReturnThresholdRange If there is an enemy @{Client#CLIENT} within the ReturnThresholdRange given in meters, the AI will not return to the nearest @{Airbase#AIRBASE}. +-- @param DCS#Distance ReturnThresholdRange If there is an enemy @{Wrapper.Client#CLIENT} within the ReturnThresholdRange given in meters, the AI will not return to the nearest @{Wrapper.Airbase#AIRBASE}. function AI_BALANCER:ReturnToHomeAirbase( ReturnThresholdRange ) self.ToHomeAirbase = true diff --git a/Moose Development/Moose/AI/AI_CAP.lua b/Moose Development/Moose/AI/AI_CAP.lua index 8dc70fd40..1ffdbc23b 100644 --- a/Moose Development/Moose/AI/AI_CAP.lua +++ b/Moose Development/Moose/AI/AI_CAP.lua @@ -1,9 +1,13 @@ ---- **AI** -- (R2.1) - Manages the independent process of Combat Air Patrol (CAP) for airplanes. +--- **AI** -- Perform Combat Air Patrolling (CAP) for airplanes. -- --- === --- --- ![Banner Image](..\Presentations\AI_CAP\Dia1.JPG) +-- **Features:** -- +-- * Patrol AI airplanes within a given zone. +-- * Trigger detected events when enemy airplanes are detected. +-- * Manage a fuel treshold to RTB on time. +-- * Engage the enemy when detected. +-- +-- -- === -- -- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/CAP%20-%20Combat%20Air%20Patrol) @@ -25,23 +29,22 @@ -- -- === -- --- @module AI_Cap +-- @module AI.AI_Cap +-- @image AI_Combat_Air_Patrol.JPG --- @type AI_CAP_ZONE --- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. +-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling. -- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. -- @extends AI.AI_Patrol#AI_PATROL_ZONE ---- # AI_CAP_ZONE class, extends @{AI_CAP#AI_PATROL_ZONE} --- --- The AI_CAP_ZONE class implements the core functions to patrol a @{Zone} by an AI @{Controllable} or @{Group} +--- Implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Controllable} or @{Wrapper.Group} -- and automatically engage any airborne enemies that are within a certain range or within a certain zone. -- -- ![Process](..\Presentations\AI_CAP\Dia3.JPG) -- --- The AI_CAP_ZONE is assigned a @{Group} and this must be done before the AI_CAP_ZONE process can be started using the **Start** event. +-- The AI_CAP_ZONE is assigned a @{Wrapper.Group} and this must be done before the AI_CAP_ZONE process can be started using the **Start** event. -- -- ![Process](..\Presentations\AI_CAP\Dia4.JPG) -- @@ -84,15 +87,15 @@ -- -- ### 2.2 AI_CAP_ZONE Events -- --- * **@{AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. --- * **@{AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. -- * **@{#AI_CAP_ZONE.Engage}**: Let the AI engage the bogeys. -- * **@{#AI_CAP_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone. --- * **@{AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. --- * **@{#AI_CAP_ZONE.Destroy}**: The AI has destroyed a bogey @{Unit}. --- * **@{#AI_CAP_ZONE.Destroyed}**: The AI has destroyed all bogeys @{Unit}s assigned in the CAS task. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. +-- * **@{#AI_CAP_ZONE.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}. +-- * **@{#AI_CAP_ZONE.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task. -- * **Status** ( Group ): The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. -- -- ## 3. Set the Range of Engagement @@ -103,7 +106,7 @@ -- 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_CAP#AI_CAP_ZONE.SetEngageRange}() to define that range. +-- Use the method @{AI.AI_CAP#AI_CAP_ZONE.SetEngageRange}() to define that range. -- -- ## 4. Set the Zone of Engagement -- @@ -111,7 +114,7 @@ -- -- An optional @{Zone} can be set, -- that will define when the AI will engage with the detected airborne enemy targets. --- Use the method @{AI_Cap#AI_CAP_ZONE.SetEngageZone}() to define that Zone. +-- Use the method @{AI.AI_Cap#AI_CAP_ZONE.SetEngageZone}() to define that Zone. -- -- === -- @@ -125,11 +128,11 @@ AI_CAP_ZONE = { --- Creates a new AI_CAP_ZONE object -- @param #AI_CAP_ZONE self -- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO -- @return #AI_CAP_ZONE self function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) diff --git a/Moose Development/Moose/AI/AI_CAS.lua b/Moose Development/Moose/AI/AI_CAS.lua index 2450d02ff..33e07849d 100644 --- a/Moose Development/Moose/AI/AI_CAS.lua +++ b/Moose Development/Moose/AI/AI_CAS.lua @@ -1,9 +1,14 @@ ---- **AI** -- (R2.1) - Manages the independent process of Close Air Support for airplanes. +--- **AI** -- Perform Close Air Support (CAS) near friendlies. -- --- === --- --- ![Banner Image](..\Presentations\AI_CAS\Dia1.JPG) +-- **Features:** -- +-- * Hold and standby within a patrol zone. +-- * Engage upon command the enemies within an engagement zone. +-- * Loop the zone until all enemies are eliminated. +-- * Trigger different events upon the results achieved. +-- * After combat, return to the patrol zone and hold. +-- * RTB when commanded or after fuel. +-- -- === -- -- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/CAS%20-%20Close%20Air%20Support) @@ -23,25 +28,21 @@ -- -- === -- --- @module AI_Cas - +-- @module AI.AI_Cas +-- @image AI_Close_Air_Support.JPG --- AI_CAS_ZONE class -- @type AI_CAS_ZONE --- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. +-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling. -- @field Core.Zone#ZONE_BASE TargetZone The @{Zone} where the patrol needs to be executed. -- @extends AI.AI_Patrol#AI_PATROL_ZONE ---- # AI_CAS_ZONE class, extends @{AI_Patrol#AI_PATROL_ZONE} --- --- AI_CAS_ZONE derives from the @{AI_Patrol#AI_PATROL_ZONE}, inheriting its methods and behaviour. --- --- The AI_CAS_ZONE class implements the core functions to provide Close Air Support in an Engage @{Zone} by an AIR @{Controllable} or @{Group}. +--- Implements the core functions to provide Close Air Support in an Engage @{Zone} by an AIR @{Wrapper.Controllable} or @{Wrapper.Group}. -- The AI_CAS_ZONE runs a process. It holds an AI in a Patrol Zone and when the AI is commanded to engage, it will fly to an Engage Zone. -- -- ![HoldAndEngage](..\Presentations\AI_CAS\Dia3.JPG) -- --- The AI_CAS_ZONE is assigned a @{Group} and this must be done before the AI_CAS_ZONE process can be started through the **Start** event. +-- The AI_CAS_ZONE is assigned a @{Wrapper.Group} and this must be done before the AI_CAS_ZONE process can be started through the **Start** event. -- -- ![Start Event](..\Presentations\AI_CAS\Dia4.JPG) -- @@ -107,15 +108,15 @@ -- -- ### 2.2. AI_CAS_ZONE Events -- --- * **@{AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. --- * **@{AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone. -- * **@{#AI_CAS_ZONE.Engage}**: Engage the AI to provide CAS in the Engage Zone, destroying any target it finds. -- * **@{#AI_CAS_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone. --- * **@{AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. --- * **@{AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. --- * **@{#AI_CAS_ZONE.Destroy}**: The AI has destroyed a target @{Unit}. --- * **@{#AI_CAS_ZONE.Destroyed}**: The AI has destroyed all target @{Unit}s assigned in the CAS task. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets. +-- * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets. +-- * **@{#AI_CAS_ZONE.Destroy}**: The AI has destroyed a target @{Wrapper.Unit}. +-- * **@{#AI_CAS_ZONE.Destroyed}**: The AI has destroyed all target @{Wrapper.Unit}s assigned in the CAS task. -- * **Status**: The AI is checking status (fuel and damage). When the tresholds have been reached, the AI will RTB. -- -- === @@ -130,12 +131,12 @@ AI_CAS_ZONE = { --- Creates a new AI_CAS_ZONE object -- @param #AI_CAS_ZONE self -- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} 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#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. -- @param Core.Zone#ZONE_BASE EngageZone The zone where the engage will happen. --- @param Dcs.DCSTypes#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 RADIO -- @return #AI_CAS_ZONE self function AI_CAS_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone, PatrolAltType ) @@ -171,24 +172,24 @@ function AI_CAS_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude -- @function [parent=#AI_CAS_ZONE] Engage -- @param #AI_CAS_ZONE self -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. + -- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. + -- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. -- If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- Use the structure @{DCSTypes#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. + -- Use the structure @{DCS#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. + -- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- Asynchronous Event Trigger for Event Engage. -- @function [parent=#AI_CAS_ZONE] __Engage -- @param #AI_CAS_ZONE self -- @param #number Delay The delay in seconds. -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. - -- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. - -- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. + -- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. + -- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. -- If parameter is not defined the unit / controllable will choose expend on its own discretion. - -- Use the structure @{DCSTypes#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. + -- Use the structure @{DCS#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack. -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. - -- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. + -- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. --- OnLeave Transition Handler for State Engaging. -- @function [parent=#AI_CAS_ZONE] OnLeaveEngaging @@ -430,10 +431,10 @@ end -- @param #string Event The Event string. -- @param #string To The To State string. -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone. --- @param Dcs.DCSTypes#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. --- @param Dcs.DCSTypes#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. +-- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement. +-- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion. -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo. --- @param Dcs.DCSTypes#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To, EngageSpeed, EngageAltitude, diff --git a/Moose Development/Moose/AI/AI_Cargo.lua b/Moose Development/Moose/AI/AI_Cargo.lua new file mode 100644 index 000000000..ed76eca7b --- /dev/null +++ b/Moose Development/Moose/AI/AI_Cargo.lua @@ -0,0 +1,568 @@ +--- **AI** -- (R2.4) - Models the intelligent transportation of infantry and other cargo. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo +-- @image Cargo.JPG + +--- @type AI_CARGO +-- @extends Core.Fsm#FSM_CONTROLLABLE + + +--- Base class for the dynamic cargo handling capability for AI groups. +-- +-- Carriers can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- The AI_CARGO module uses the @{Cargo.Cargo} capabilities within the MOOSE framework. +-- CARGO derived objects must be declared within the mission to make the AI_CARGO object recognize the cargo. +-- Please consult the @{Cargo.Cargo} module for more information. +-- +-- The derived classes from this module are: +-- +-- * @{AI.AI_Cargo_APC} - Cargo transportation using APCs and other vehicles between zones. +-- * @{AI.AI_Cargo_Helicopter} - Cargo transportation using helicopters between zones. +-- * @{AI.AI_Cargo_Airplane} - Cargo transportation using airplanes to and from airbases. +-- +-- @field #AI_CARGO +AI_CARGO = { + ClassName = "AI_CARGO", + Coordinate = nil, -- Core.Point#COORDINATE, + Carrier_Cargo = {}, +} + +--- Creates a new AI_CARGO object. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param Core.Set#SET_CARGO CargoSet +-- @param #number CombatRadius +-- @return #AI_CARGO +function AI_CARGO:New( Carrier, CargoSet ) + + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New( Carrier ) ) -- #AI_CARGO + + self.CargoSet = CargoSet -- Core.Set#SET_CARGO + self.CargoCarrier = Carrier -- Wrapper.Group#GROUP + + self:SetStartState( "Unloaded" ) + + self:AddTransition( "Unloaded", "Pickup", "*" ) + self:AddTransition( "Loaded", "Deploy", "*" ) + + self:AddTransition( "*", "Load", "Boarding" ) + self:AddTransition( { "Boarding", "Loaded" }, "Board", "Boarding" ) + self:AddTransition( "Boarding", "Loaded", "Boarding" ) + self:AddTransition( "Boarding", "PickedUp", "Loaded" ) + + self:AddTransition( "Loaded", "Unload", "Unboarding" ) + self:AddTransition( "Unboarding", "Unboard", "Unboarding" ) + self:AddTransition( "Unboarding", "Unloaded", "Unboarding" ) + self:AddTransition( "Unboarding", "Deployed", "Unloaded" ) + + --- Pickup Handler OnBefore for AI_CARGO + -- @function [parent=#AI_CARGO] OnBeforePickup + -- @param #AI_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + -- @return #boolean + + --- Pickup Handler OnAfter for AI_CARGO + -- @function [parent=#AI_CARGO] OnAfterPickup + -- @param #AI_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + + --- Pickup Trigger for AI_CARGO + -- @function [parent=#AI_CARGO] Pickup + -- @param #AI_CARGO self + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + + --- Pickup Asynchronous Trigger for AI_CARGO + -- @function [parent=#AI_CARGO] __Pickup + -- @param #AI_CARGO self + -- @param #number Delay + -- @param Core.Point#COORDINATE Coordinate Pickup place. If not given, loading starts at the current location. + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + + --- Deploy Handler OnBefore for AI_CARGO + -- @function [parent=#AI_CARGO] OnBeforeDeploy + -- @param #AI_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + -- @return #boolean + + --- Deploy Handler OnAfter for AI_CARGO + -- @function [parent=#AI_CARGO] OnAfterDeploy + -- @param #AI_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + + --- Deploy Trigger for AI_CARGO + -- @function [parent=#AI_CARGO] Deploy + -- @param #AI_CARGO self + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + + --- Deploy Asynchronous Trigger for AI_CARGO + -- @function [parent=#AI_CARGO] __Deploy + -- @param #AI_CARGO self + -- @param #number Delay + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. + + + --- Loaded Handler OnAfter for AI_CARGO + -- @function [parent=#AI_CARGO] OnAfterLoaded + -- @param #AI_CARGO self + -- @param Wrapper.Group#GROUP Carrier + -- @param #string From + -- @param #string Event + -- @param #string To + + --- Unloaded Handler OnAfter for AI_CARGO + -- @function [parent=#AI_CARGO] OnAfterUnloaded + -- @param #AI_CARGO self + -- @param Wrapper.Group#GROUP Carrier + -- @param #string From + -- @param #string Event + -- @param #string To + + for _, CarrierUnit in pairs( Carrier:GetUnits() ) do + local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT + CarrierUnit:SetCargoBayWeightLimit() + end + + self.Transporting = false + self.Relocating = false + + return self +end + + + +function AI_CARGO:IsTransporting() + + return self.Transporting == true +end + +function AI_CARGO:IsRelocating() + + return self.Relocating == true +end + + +--- On after Pickup event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP APC +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate of the pickup point. +-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the home coordinate. +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO:onafterPickup( APC, From, Event, To, Coordinate, Speed, Height, PickupZone ) + + self.Transporting = false + self.Relocating = true + +end + + +--- On after Deploy event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP APC +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate Deploy place. +-- @param #number Speed Speed in km/h to drive to the depoly coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the deploy coordinate. +-- @param Core.Zone#ZONE DeployZone The zone where the cargo will be deployed. +function AI_CARGO:onafterDeploy( APC, From, Event, To, Coordinate, Speed, Height, DeployZone ) + + self.Relocating = false + self.Transporting = true + +end + +--- On before Load event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO:onbeforeLoad( Carrier, From, Event, To, PickupZone ) + self:F( { Carrier, From, Event, To } ) + + local Boarding = false + + local LoadInterval = 2 + local LoadDelay = 1 + local Carrier_List = {} + local Carrier_Weight = {} + + if Carrier and Carrier:IsAlive() then + self.Carrier_Cargo = {} + for _, CarrierUnit in pairs( Carrier:GetUnits() ) do + local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT + + local CargoBayFreeWeight = CarrierUnit:GetCargoBayFreeWeight() + self:F({CargoBayFreeWeight=CargoBayFreeWeight}) + + Carrier_List[#Carrier_List+1] = CarrierUnit + Carrier_Weight[CarrierUnit] = CargoBayFreeWeight + end + + local Carrier_Count = #Carrier_List + local Carrier_Index = 1 + + local Loaded = false + + for _, Cargo in UTILS.spairs( self.CargoSet:GetSet(), function( t, a, b ) return t[a]:GetWeight() > t[b]:GetWeight() end ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + + self:F( { IsUnLoaded = Cargo:IsUnLoaded(), IsDeployed = Cargo:IsDeployed(), Cargo:GetName(), Carrier:GetName() } ) + + -- Try all Carriers, but start from the one according the Carrier_Index + for Carrier_Loop = 1, #Carrier_List do + + local CarrierUnit = Carrier_List[Carrier_Index] -- Wrapper.Unit#UNIT + + -- This counters loop through the available Carriers. + Carrier_Index = Carrier_Index + 1 + if Carrier_Index > Carrier_Count then + Carrier_Index = 1 + end + + if Cargo:IsUnLoaded() and not Cargo:IsDeployed() then + if Cargo:IsInLoadRadius( CarrierUnit:GetCoordinate() ) then + self:F( { "In radius", CarrierUnit:GetName() } ) + + local CargoWeight = Cargo:GetWeight() + + -- Only when there is space within the bay to load the next cargo item! + if Carrier_Weight[CarrierUnit] > CargoWeight then --and CargoBayFreeVolume > CargoVolume then + Carrier:RouteStop() + --Cargo:Ungroup() + Cargo:__Board( -LoadDelay, CarrierUnit ) + self:__Board( LoadDelay, Cargo, CarrierUnit, PickupZone ) + + LoadDelay = LoadDelay + Cargo:GetCount() * LoadInterval + + -- So now this CarrierUnit has Cargo that is being loaded. + -- This will be used further in the logic to follow and to check cargo status. + self.Carrier_Cargo[Cargo] = CarrierUnit + Boarding = true + Carrier_Weight[CarrierUnit] = Carrier_Weight[CarrierUnit] - CargoWeight + Loaded = true + + -- Ok, we loaded a cargo, now we can stop the loop. + break + end + end + end + + end + + end + + if not Loaded == true then + -- No loading happened, so we need to pickup something else. + self.Relocating = false + end + end + + return Boarding + +end + + +--- On before Reload event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO:onbeforeReload( Carrier, From, Event, To ) + self:F( { Carrier, From, Event, To } ) + + local Boarding = false + + local LoadInterval = 2 + local LoadDelay = 1 + local Carrier_List = {} + local Carrier_Weight = {} + + if Carrier and Carrier:IsAlive() then + for _, CarrierUnit in pairs( Carrier:GetUnits() ) do + local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT + + Carrier_List[#Carrier_List+1] = CarrierUnit + end + + local Carrier_Count = #Carrier_List + local Carrier_Index = 1 + + local Loaded = false + + for Cargo, CarrierUnit in pairs( self.Carrier_Cargo ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + + self:F( { IsUnLoaded = Cargo:IsUnLoaded(), IsDeployed = Cargo:IsDeployed(), Cargo:GetName(), Carrier:GetName() } ) + + -- Try all Carriers, but start from the one according the Carrier_Index + for Carrier_Loop = 1, #Carrier_List do + + local CarrierUnit = Carrier_List[Carrier_Index] -- Wrapper.Unit#UNIT + + -- This counters loop through the available Carriers. + Carrier_Index = Carrier_Index + 1 + if Carrier_Index > Carrier_Count then + Carrier_Index = 1 + end + + if Cargo:IsUnLoaded() and not Cargo:IsDeployed() then + Carrier:RouteStop() + Cargo:__Board( -LoadDelay, CarrierUnit ) + self:__Board( LoadDelay, Cargo, CarrierUnit ) + + LoadDelay = LoadDelay + Cargo:GetCount() * LoadInterval + + -- So now this CarrierUnit has Cargo that is being loaded. + -- This will be used further in the logic to follow and to check cargo status. + self.Carrier_Cargo[Cargo] = CarrierUnit + Boarding = true + Loaded = true + end + + end + + end + + if not Loaded == true then + -- No loading happened, so we need to pickup something else. + self.Relocating = false + end + end + + return Boarding + +end + +--- On after Board event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Cargo.Cargo#CARGO Cargo Cargo object. +-- @param Wrapper.Unit#UNIT CarrierUnit +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO:onafterBoard( Carrier, From, Event, To, Cargo, CarrierUnit, PickupZone ) + self:F( { Carrier, From, Event, To, Cargo, CarrierUnit:GetName() } ) + + if Carrier and Carrier:IsAlive() then + self:F({ IsLoaded = Cargo:IsLoaded(), Cargo:GetName(), Carrier:GetName() } ) + if not Cargo:IsLoaded() and not Cargo:IsDestroyed() then + self:__Board( -10, Cargo, CarrierUnit, PickupZone ) + return + end + end + + self:__Loaded( 0.1, Cargo, CarrierUnit, PickupZone ) + +end + +--- On after Loaded event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean Cargo loaded. +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO:onafterLoaded( Carrier, From, Event, To, Cargo, PickupZone ) + self:F( { Carrier, From, Event, To } ) + + local Loaded = true + + if Carrier and Carrier:IsAlive() then + for Cargo, CarrierUnit in pairs( self.Carrier_Cargo ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + self:F( { IsLoaded = Cargo:IsLoaded(), IsDestroyed = Cargo:IsDestroyed(), Cargo:GetName(), Carrier:GetName() } ) + if not Cargo:IsLoaded() and not Cargo:IsDestroyed() then + Loaded = false + end + end + end + + if Loaded then + self:__PickedUp( 0.1, PickupZone ) + end + +end + +--- On after PickedUp event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO:onafterPickedUp( Carrier, From, Event, To, PickupZone ) + self:F( { Carrier, From, Event, To } ) + + Carrier:RouteResume() + + local HasCargo = false + if Carrier and Carrier:IsAlive() then + for Cargo, CarrierUnit in pairs( self.Carrier_Cargo ) do + HasCargo = true + break + end + end + + self.Relocating = false + if HasCargo then + self:F( "Transporting" ) + self.Transporting = true + end + +end + + + + +--- On after Unload event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +function AI_CARGO:onafterUnload( Carrier, From, Event, To, DeployZone, Defend ) + self:F( { Carrier, From, Event, To, DeployZone, Defend = Defend } ) + + local UnboardInterval = 5 + local UnboardDelay = 5 + + if Carrier and Carrier:IsAlive() then + for _, CarrierUnit in pairs( Carrier:GetUnits() ) do + local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT + Carrier:RouteStop() + for _, Cargo in pairs( CarrierUnit:GetCargo() ) do + self:F( { Cargo = Cargo:GetName(), Isloaded = Cargo:IsLoaded() } ) + if Cargo:IsLoaded() then + Cargo:__UnBoard( UnboardDelay ) + UnboardDelay = UnboardDelay + Cargo:GetCount() * UnboardInterval + self:__Unboard( UnboardDelay, Cargo, CarrierUnit, DeployZone, Defend ) + if not Defend == true then + Cargo:SetDeployed( true ) + end + end + end + end + end + +end + +--- On after Unboard event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Cargo.Cargo#CARGO Cargo Cargo object. +-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +function AI_CARGO:onafterUnboard( Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone, Defend ) + self:F( { Carrier, From, Event, To, Cargo:GetName(), DeployZone = DeployZone, Defend = Defend } ) + + if Carrier and Carrier:IsAlive() then + if not Cargo:IsUnLoaded() then + self:__Unboard( 10, Cargo, CarrierUnit, DeployZone, Defend ) + return + end + end + + self:Unloaded( Cargo, CarrierUnit, DeployZone, Defend ) + +end + +--- On after Unloaded event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Cargo.Cargo#CARGO Cargo Cargo object. +-- @param #boolean Deployed Cargo is deployed. +-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +function AI_CARGO:onafterUnloaded( Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone, Defend ) + self:F( { Carrier, From, Event, To, Cargo:GetName(), DeployZone = DeployZone, Defend = Defend } ) + + local AllUnloaded = true + + --Cargo:Regroup() + + if Carrier and Carrier:IsAlive() then + for _, CarrierUnit in pairs( Carrier:GetUnits() ) do + local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT + local IsEmpty = CarrierUnit:IsCargoEmpty() + self:I({ IsEmpty = IsEmpty }) + if not IsEmpty then + AllUnloaded = false + break + end + end + + if AllUnloaded == true then + if DeployZone == true then + self.Carrier_Cargo = {} + end + self.CargoCarrier = Carrier + end + end + + if AllUnloaded == true then + self:__Deployed( 5, DeployZone, Defend ) + end + +end + +--- On after Deployed event. +-- @param #AI_CARGO self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +function AI_CARGO:onafterDeployed( Carrier, From, Event, To, DeployZone, Defend ) + self:F( { Carrier, From, Event, To, DeployZone = DeployZone, Defend = Defend } ) + + if not Defend == true then + self.Transporting = false + else + self:F( "Defending" ) + + end + +end + diff --git a/Moose Development/Moose/AI/AI_Cargo_APC.lua b/Moose Development/Moose/AI/AI_Cargo_APC.lua new file mode 100644 index 000000000..c20475711 --- /dev/null +++ b/Moose Development/Moose/AI/AI_Cargo_APC.lua @@ -0,0 +1,531 @@ +--- **AI** -- (R2.4) - Models the intelligent transportation of infantry and other cargo. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo_APC +-- @image AI_Cargo_Dispatching_For_APC.JPG + +--- @type AI_CARGO_APC +-- @extends AI.AI_Cargo#AI_CARGO + + +--- Brings a dynamic cargo handling capability for an AI vehicle group. +-- +-- Armoured Personnel Carriers (APC), Trucks, Jeeps and other ground based carrier equipment can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- +-- The AI_CARGO_APC class uses the @{Cargo.Cargo} capabilities within the MOOSE framework. +-- @{Cargo.Cargo} must be declared within the mission to make the AI_CARGO_APC object recognize the cargo. +-- Please consult the @{Cargo.Cargo} module for more information. +-- +-- ## Cargo loading. +-- +-- The module will load automatically cargo when the APCs are within boarding or loading radius. +-- The boarding or loading radius is specified when the cargo is created in the simulation, and therefore, this radius depends on the type of cargo +-- and the specified boarding radius. +-- +-- ## **Defending** the APCs when enemies nearby. +-- +-- Cargo will defend the carrier with its available arms, and to avoid cargo being lost within the battlefield. +-- +-- When the APCs are approaching enemy units, something special is happening. +-- The APCs will stop moving, and the loaded infantry will unboard and follow the APCs and will help to defend the group. +-- The carrier will hold the route once the unboarded infantry is further than 50 meters from the APCs, +-- to ensure that the APCs are not too far away from the following running infantry. +-- Once all enemies are cleared, the infantry will board again automatically into the APCs. Once boarded, the APCs will follow its pre-defined route. +-- +-- A combat radius needs to be specified in meters at the @{#AI_CARGO_APC.New}() method. +-- This combat radius will trigger the unboarding of troops when enemies are within the combat radius around the APCs. +-- During my tests, I've noticed that there is a balance between ensuring that the infantry is within sufficient hit radius (effectiveness) versus +-- vulnerability of the infantry. It all depends on the kind of enemies that are expected to be encountered. +-- A combat radius of 350 meters to 500 meters has been proven to be the most effective and efficient. +-- +-- However, when the defense of the carrier, is not required, it must be switched off. +-- This is done by disabling the defense of the carrier using the method @{#AI_CARGO_APC.SetCombatRadius}(), and providing a combat radius of 0 meters. +-- It can be switched on later when required by reenabling the defense using the method and providing a combat radius larger than 0. +-- +-- ## Infantry or cargo **health**. +-- +-- When infantry is unboarded from the APCs, the infantry is actually respawned into the battlefield. +-- As a result, the unboarding infantry is very _healthy_ every time it unboards. +-- This is due to the limitation of the DCS simulator, which is not able to specify the health of new spawned units as a parameter. +-- However, infantry that was destroyed when unboarded and following the APCs, won't be respawned again. Destroyed is destroyed. +-- As a result, there is some additional strength that is gained when an unboarding action happens, but in terms of simulation balance this has +-- marginal impact on the overall battlefield simulation. Fortunately, the firing strength of infantry is limited, and thus, respacing healthy infantry every +-- time is not so much of an issue ... +-- +-- ## Control the APCs on the map. +-- +-- It is possible also as a human ground commander to influence the path of the APCs, by pointing a new path using the DCS user interface on the map. +-- In this case, the APCs will change the direction towards its new indicated route. However, there is a catch! +-- Once the APCs are near the enemy, and infantry is unboarded, the APCs won't be able to hold the route until the infantry could catch up. +-- The APCs will simply drive on and won't stop! This is a limitation in ED that prevents user actions being controlled by the scripting engine. +-- No workaround is possible on this. +-- +-- ## Cargo deployment. +-- +-- Using the @{#AI_CARGO_APC.Deploy}() method, you are able to direct the APCs towards a point on the battlefield to unboard/unload the cargo at the specific coordinate. +-- The APCs will follow nearby roads as much as possible, to ensure fast and clean cargo transportation between the objects and villages in the simulation environment. +-- +-- ## Cargo pickup. +-- +-- Using the @{#AI_CARGO_APC.Pickup}() method, you are able to direct the APCs towards a point on the battlefield to board/load the cargo at the specific coordinate. +-- The APCs will follow nearby roads as much as possible, to ensure fast and clean cargo transportation between the objects and villages in the simulation environment. +-- +-- +-- +-- @field #AI_CARGO_APC +AI_CARGO_APC = { + ClassName = "AI_CARGO_APC", + Coordinate = nil, -- Core.Point#COORDINATE, +} + +--- Creates a new AI_CARGO_APC object. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP APC The carrier APC group. +-- @param Core.Set#SET_CARGO CargoSet The set of cargo to be transported. +-- @param #number CombatRadius Provide the combat radius to defend the carrier by unboarding the cargo when enemies are nearby. When the combat radius is 0, no defense will happen of the carrier. +-- @return #AI_CARGO_APC +function AI_CARGO_APC:New( APC, CargoSet, CombatRadius ) + + local self = BASE:Inherit( self, AI_CARGO:New( APC, CargoSet ) ) -- #AI_CARGO_APC + + self:AddTransition( "*", "Monitor", "*" ) + self:AddTransition( "*", "Follow", "Following" ) + self:AddTransition( "*", "Guard", "Unloaded" ) + self:AddTransition( "*", "Home", "*" ) + self:AddTransition( "*", "Reload", "Boarding" ) + + self:AddTransition( "*", "Destroyed", "Destroyed" ) + + self:SetCombatRadius( CombatRadius ) + + self:SetCarrier( APC ) + + return self +end + + +--- Set the Carrier. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP CargoCarrier +-- @return #AI_CARGO_APC +function AI_CARGO_APC:SetCarrier( CargoCarrier ) + + self.CargoCarrier = CargoCarrier -- Wrapper.Group#GROUP + self.CargoCarrier:SetState( self.CargoCarrier, "AI_CARGO_APC", self ) + + CargoCarrier:HandleEvent( EVENTS.Dead ) + + function CargoCarrier:OnEventDead( EventData ) + self:F({"dead"}) + local AICargoTroops = self:GetState( self, "AI_CARGO_APC" ) + self:F({AICargoTroops=AICargoTroops}) + if AICargoTroops then + self:F({}) + if not AICargoTroops:Is( "Loaded" ) then + -- There are enemies within combat radius. Unload the CargoCarrier. + AICargoTroops:Destroyed() + end + end + end + +-- CargoCarrier:HandleEvent( EVENTS.Hit ) +-- +-- function CargoCarrier:OnEventHit( EventData ) +-- self:F({"hit"}) +-- local AICargoTroops = self:GetState( self, "AI_CARGO_APC" ) +-- if AICargoTroops then +-- self:F( { OnHitLoaded = AICargoTroops:Is( "Loaded" ) } ) +-- if AICargoTroops:Is( "Loaded" ) or AICargoTroops:Is( "Boarding" ) then +-- -- There are enemies within combat radius. Unload the CargoCarrier. +-- AICargoTroops:Unload( false ) +-- end +-- end +-- end + + self.Zone = ZONE_UNIT:New( self.CargoCarrier:GetName() .. "-Zone", self.CargoCarrier, self.CombatRadius ) + self.Coalition = self.CargoCarrier:GetCoalition() + + self:SetControllable( CargoCarrier ) + + self:Guard() + + return self +end + + +--- Find a free Carrier within a radius. +-- @param #AI_CARGO_APC self +-- @param Core.Point#COORDINATE Coordinate +-- @param #number Radius +-- @return Wrapper.Group#GROUP NewCarrier +function AI_CARGO_APC:FindCarrier( Coordinate, Radius ) + + local CoordinateZone = ZONE_RADIUS:New( "Zone" , Coordinate:GetVec2(), Radius ) + CoordinateZone:Scan( { Object.Category.UNIT } ) + for _, DCSUnit in pairs( CoordinateZone:GetScannedUnits() ) do + local NearUnit = UNIT:Find( DCSUnit ) + self:F({NearUnit=NearUnit}) + if not NearUnit:GetState( NearUnit, "AI_CARGO_APC" ) then + local Attributes = NearUnit:GetDesc() + self:F({Desc=Attributes}) + if NearUnit:HasAttribute( "Trucks" ) then + return NearUnit:GetGroup() + end + end + end + + return nil + +end + +--- Enable/Disable unboarding of cargo (infantry) when enemies are nearby (to help defend the carrier). +-- This is only valid for APCs and trucks etc, thus ground vehicles. +-- @param #AI_CARGO_APC self +-- @param #number CombatRadius Provide the combat radius to defend the carrier by unboarding the cargo when enemies are nearby. +-- When the combat radius is 0, no defense will happen of the carrier. +-- When the combat radius is not provided, no defense will happen! +-- @return #AI_CARGO_APC +-- @usage +-- +-- -- Disembark the infantry when the carrier is under attack. +-- AICargoAPC:SetCombatRadius( true ) +-- +-- -- Keep the cargo in the carrier when the carrier is under attack. +-- AICargoAPC:SetCombatRadius( false ) +function AI_CARGO_APC:SetCombatRadius( CombatRadius ) + + self.CombatRadius = CombatRadius or 0 + + if self.CombatRadius > 0 then + self:__Monitor( -5 ) + end + + return self +end + + +--- Follow Infantry to the Carrier. +-- @param #AI_CARGO_APC self +-- @param #AI_CARGO_APC Me +-- @param Wrapper.Unit#UNIT APCUnit +-- @param Cargo.CargoGroup#CARGO_GROUP Cargo +-- @return #AI_CARGO_APC +function AI_CARGO_APC:FollowToCarrier( Me, APCUnit, CargoGroup ) + + local InfantryGroup = CargoGroup:GetGroup() + + self:F( { self = self:GetClassNameAndID(), InfantryGroup = InfantryGroup:GetName() } ) + + --if self:Is( "Following" ) then + + if APCUnit:IsAlive() then + -- We check if the Cargo is near to the CargoCarrier. + if InfantryGroup:IsPartlyInZone( ZONE_UNIT:New( "Radius", APCUnit, 25 ) ) then + + -- The Cargo does not need to follow the Carrier. + Me:Guard() + + else + + self:F( { InfantryGroup = InfantryGroup:GetName() } ) + + if InfantryGroup:IsAlive() then + + self:F( { InfantryGroup = InfantryGroup:GetName() } ) + + local Waypoints = {} + + -- Calculate the new Route. + local FromCoord = InfantryGroup:GetCoordinate() + local FromGround = FromCoord:WaypointGround( 10, "Diamond" ) + self:F({FromGround=FromGround}) + table.insert( Waypoints, FromGround ) + + local ToCoord = APCUnit:GetCoordinate():GetRandomCoordinateInRadius( 10, 5 ) + local ToGround = ToCoord:WaypointGround( 10, "Diamond" ) + self:F({ToGround=ToGround}) + table.insert( Waypoints, ToGround ) + + local TaskRoute = InfantryGroup:TaskFunction( "AI_CARGO_APC.FollowToCarrier", Me, APCUnit, CargoGroup ) + + self:F({Waypoints = Waypoints}) + local Waypoint = Waypoints[#Waypoints] + InfantryGroup:SetTaskWaypoint( Waypoint, TaskRoute ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. + + InfantryGroup:Route( Waypoints, 1 ) -- Move after a random seconds to the Route. See the Route method for details. + end + end + end +end + + +--- On after Monitor event. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP APC +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AI_CARGO_APC:onafterMonitor( APC, From, Event, To ) + self:F( { APC, From, Event, To, IsTransporting = self:IsTransporting() } ) + + if self.CombatRadius > 0 then + if APC and APC:IsAlive() then + if self.CarrierCoordinate then + if self:IsTransporting() == true then + local Coordinate = APC:GetCoordinate() + if self:Is( "Unloaded" ) or self:Is( "Loaded" ) then + self.Zone:Scan( { Object.Category.UNIT } ) + if self.Zone:IsAllInZoneOfCoalition( self.Coalition ) then + if self:Is( "Unloaded" ) then + -- There are no enemies within combat radius. Reload the CargoCarrier. + self:Reload() + end + else + if self:Is( "Loaded" ) then + -- There are enemies within combat radius. Unload the CargoCarrier. + self:__Unload( 1, nil, true ) -- The 2nd parameter is true, which means that the unload is for defending the carrier, not to deploy! + else + if self:Is( "Unloaded" ) then + --self:Follow() + end + self:F( "I am here" .. self:GetCurrentState() ) + if self:Is( "Following" ) then + for Cargo, APCUnit in pairs( self.Carrier_Cargo ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + local APCUnit = APCUnit -- Wrapper.Unit#UNIT + if Cargo:IsAlive() then + if not Cargo:IsNear( APCUnit, 40 ) then + APCUnit:RouteStop() + self.CarrierStopped = true + else + if self.CarrierStopped then + if Cargo:IsNear( APCUnit, 25 ) then + APCUnit:RouteResume() + self.CarrierStopped = nil + end + end + end + end + end + end + end + end + end + end + + end + self.CarrierCoordinate = APC:GetCoordinate() + end + + self:__Monitor( -5 ) + end + +end + + +--- On after Follow event. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP APC +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AI_CARGO_APC:onafterFollow( APC, From, Event, To ) + self:F( { APC, From, Event, To } ) + + self:F( "Follow" ) + if APC and APC:IsAlive() then + for Cargo, APCUnit in pairs( self.Carrier_Cargo ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + if Cargo:IsUnLoaded() then + self:FollowToCarrier( self, APCUnit, Cargo ) + APCUnit:RouteResume() + end + end + end + +end + + +--- @param #AI_CARGO_APC +-- @param Wrapper.Group#GROUP APC +function AI_CARGO_APC._Pickup( APC, self, Coordinate, Speed, PickupZone ) + + APC:F( { "AI_CARGO_APC._Pickup:", APC:GetName() } ) + + if APC:IsAlive() then + self:Load( PickupZone ) + end +end + + +function AI_CARGO_APC._Deploy( APC, self, Coordinate, DeployZone ) + + APC:F( { "AI_CARGO_APC._Deploy:", APC } ) + + if APC:IsAlive() then + self:Unload( DeployZone ) + end +end + + + +--- On after Pickup event. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP APC +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate of the pickup point. +-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the pickup coordinate. This parameter is ignored for APCs. +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO_APC:onafterPickup( APC, From, Event, To, Coordinate, Speed, Height, PickupZone ) + + if APC and APC:IsAlive() then + + if Coordinate then + self.RoutePickup = true + + local _speed=Speed or APC:GetSpeedMax()*0.5 + + local Waypoints = APC:TaskGroundOnRoad( Coordinate, _speed, "Line abreast", true ) + + local TaskFunction = APC:TaskFunction( "AI_CARGO_APC._Pickup", self, Coordinate, Speed, PickupZone ) + + self:F({Waypoints = Waypoints}) + local Waypoint = Waypoints[#Waypoints] + APC:SetTaskWaypoint( Waypoint, TaskFunction ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. + + APC:Route( Waypoints, 1 ) -- Move after a random seconds to the Route. See the Route method for details. + else + AI_CARGO_APC._Pickup( APC, self, Coordinate, Speed, PickupZone ) + end + + self:GetParent( self, AI_CARGO_APC ).onafterPickup( self, APC, From, Event, To, Coordinate, Speed, Height, PickupZone ) + end + +end + + +--- On after Deploy event. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP APC +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate Deploy place. +-- @param #number Speed Speed in km/h to drive to the depoly coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the deploy coordinate. This parameter is ignored for APCs. +-- @param Core.Zone#ZONE DeployZone The zone where the cargo will be deployed. +function AI_CARGO_APC:onafterDeploy( APC, From, Event, To, Coordinate, Speed, Height, DeployZone ) + + if APC and APC:IsAlive() then + + self.RouteDeploy = true + + local _speed=Speed or APC:GetSpeedMax()*0.5 + + local Waypoints = APC:TaskGroundOnRoad( Coordinate, _speed, "Line abreast", true ) + + local TaskFunction = APC:TaskFunction( "AI_CARGO_APC._Deploy", self, Coordinate, DeployZone ) + + self:F({Waypoints = Waypoints}) + local Waypoint = Waypoints[#Waypoints] + APC:SetTaskWaypoint( Waypoint, TaskFunction ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone. + + APC:Route( Waypoints, 1 ) -- Move after a random seconds to the Route. See the Route method for details. + + self:GetParent( self, AI_CARGO_APC ).onafterDeploy( self, APC, From, Event, To, Coordinate, Speed, Height, DeployZone ) + + end + +end + +--- On after Unloaded event. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string Cargo.Cargo#CARGO Cargo Cargo object. +-- @param #boolean Deployed Cargo is deployed. +-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +function AI_CARGO_APC:onafterUnloaded( Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone, Defend ) + self:F( { Carrier, From, Event, To, DeployZone = DeployZone, Defend = Defend } ) + + + self:GetParent( self, AI_CARGO_APC ).onafterUnloaded( self, Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone, Defend ) + + -- If Defend == true then we need to scan for possible enemies within combat zone and engage only ground forces. + if Defend == true then + self.Zone:Scan( { Object.Category.UNIT } ) + if not self.Zone:IsAllInZoneOfCoalition( self.Coalition ) then + -- OK, enemies nearby, now find the enemies and attack them. + local AttackUnits = self.Zone:GetScannedUnits() -- #list + local Move = {} + local CargoGroup = Cargo.CargoObject -- Wrapper.Group#GROUP + Move[#Move+1] = CargoGroup:GetCoordinate():WaypointGround( 70, "Custom" ) + for UnitId, AttackUnit in pairs( AttackUnits ) do + local MooseUnit = UNIT:Find( AttackUnit ) + if MooseUnit:GetCoalition() ~= CargoGroup:GetCoalition() then + Move[#Move+1] = MooseUnit:GetCoordinate():WaypointGround( 70, "Line abreast" ) + --MoveTo.Task = CargoGroup:TaskCombo( CargoGroup:TaskAttackUnit( MooseUnit, true ) ) + self:F( { MooseUnit = MooseUnit:GetName(), CargoGroup = CargoGroup:GetName() } ) + end + end + CargoGroup:RoutePush( Move, 0.1 ) + end + + end + +end + +--- On after Deployed event. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP Carrier +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +function AI_CARGO_APC:onafterDeployed( APC, From, Event, To, DeployZone, Defend ) + self:F( { APC, From, Event, To, DeployZone = DeployZone, Defend = Defend } ) + + self:__Guard( 0.1 ) + + self:GetParent( self, AI_CARGO_APC ).onafterDeployed( self, APC, From, Event, To, DeployZone, Defend ) + +end + + +--- On after Home event. +-- @param #AI_CARGO_APC self +-- @param Wrapper.Group#GROUP APC +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate Home place. +-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the home coordinate. This parameter is ignored for APCs. +function AI_CARGO_APC:onafterHome( APC, From, Event, To, Coordinate, Speed, Height, HomeZone ) + + if APC and APC:IsAlive() ~= nil then + + self.RouteHome = true + + Speed = Speed or APC:GetSpeedMax()*0.5 + + local Waypoints = APC:TaskGroundOnRoad( Coordinate, Speed, "Line abreast", true ) + + self:F({Waypoints = Waypoints}) + local Waypoint = Waypoints[#Waypoints] + + APC:Route( Waypoints, 1 ) -- Move after a random seconds to the Route. See the Route method for details. + + end + +end diff --git a/Moose Development/Moose/AI/AI_Cargo_Airplane.lua b/Moose Development/Moose/AI/AI_Cargo_Airplane.lua new file mode 100644 index 000000000..947d7ec5b --- /dev/null +++ b/Moose Development/Moose/AI/AI_Cargo_Airplane.lua @@ -0,0 +1,484 @@ +--- **AI** -- (R2.4) - Models the intelligent transportation of infantry (cargo). +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo_Airplane +-- @image AI_Cargo_Dispatching_For_Airplanes.JPG + +--- @type AI_CARGO_AIRPLANE +-- @extends Core.Fsm#FSM_CONTROLLABLE + + +--- Brings a dynamic cargo handling capability for an AI airplane group. +-- +-- Airplane carrier equipment can be mobilized to intelligently transport infantry and other cargo within the simulation between airbases. +-- +-- The AI_CARGO_AIRPLANE module uses the @{Cargo.Cargo} capabilities within the MOOSE framework. +-- @{Cargo.Cargo} must be declared within the mission to make AI_CARGO_AIRPLANE recognize the cargo. +-- Please consult the @{Cargo.Cargo} module for more information. +-- +-- ## Cargo pickup. +-- +-- Using the @{#AI_CARGO_AIRPLANE.Pickup}() method, you are able to direct the helicopters towards a point on the battlefield to board/load the cargo at the specific coordinate. +-- Ensure that the landing zone is horizontally flat, and that trees cannot be found in the landing vicinity, or the helicopters won't land or will even crash! +-- +-- ## Cargo deployment. +-- +-- Using the @{#AI_CARGO_AIRPLANE.Deploy}() method, you are able to direct the helicopters towards a point on the battlefield to unboard/unload the cargo at the specific coordinate. +-- Ensure that the landing zone is horizontally flat, and that trees cannot be found in the landing vicinity, or the helicopters won't land or will even crash! +-- +-- ## Infantry health. +-- +-- When infantry is unboarded from the APCs, the infantry is actually respawned into the battlefield. +-- As a result, the unboarding infantry is very _healthy_ every time it unboards. +-- This is due to the limitation of the DCS simulator, which is not able to specify the health of new spawned units as a parameter. +-- However, infantry that was destroyed when unboarded, won't be respawned again. Destroyed is destroyed. +-- As a result, there is some additional strength that is gained when an unboarding action happens, but in terms of simulation balance this has +-- marginal impact on the overall battlefield simulation. Fortunately, the firing strength of infantry is limited, and thus, respacing healthy infantry every +-- time is not so much of an issue ... +-- +-- +-- @field #AI_CARGO_AIRPLANE +AI_CARGO_AIRPLANE = { + ClassName = "AI_CARGO_AIRPLANE", + Coordinate = nil, -- Core.Point#COORDINATE +} + +--- Creates a new AI_CARGO_AIRPLANE object. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane Plane used for transportation of cargo. +-- @param Core.Set#SET_CARGO CargoSet Cargo set to be transported. +-- @return #AI_CARGO_AIRPLANE +function AI_CARGO_AIRPLANE:New( Airplane, CargoSet ) + + local self = BASE:Inherit( self, AI_CARGO:New( Airplane, CargoSet ) ) -- #AI_CARGO_AIRPLANE + + self:AddTransition( "*", "Landed", "*" ) + self:AddTransition( "*", "Home" , "*" ) + + self:AddTransition( "*", "Destroyed", "Destroyed" ) + + --- Pickup Handler OnBefore for AI_CARGO_AIRPLANE + -- @function [parent=#AI_CARGO_AIRPLANE] OnBeforePickup + -- @param #AI_CARGO_AIRPLANE self + -- @param Wrapper.Group#GROUP Airplane Cargo transport plane. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where troops are picked up. + -- @param #number Speed in km/h for travelling to pickup base. + -- @return #boolean + + --- Pickup Handler OnAfter for AI_CARGO_AIRPLANE + -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterPickup + -- @param #AI_CARGO_AIRPLANE self + -- @param Wrapper.Group#GROUP Airplane Cargo 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. + + --- 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. + + --- 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. + + --- Deploy Handler OnBefore for AI_CARGO_AIRPLANE + -- @function [parent=#AI_CARGO_AIRPLANE] OnBeforeDeploy + -- @param #AI_CARGO_AIRPLANE self + -- @param Wrapper.Group#GROUP Airplane Cargo plane. + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Wrapper.Airbase#AIRBASE Airbase Destination airbase where troops are deployed. + -- @param #number Speed Speed in km/h for travelling to deploy base. + -- @return #boolean + + --- Deploy Handler OnAfter for AI_CARGO_AIRPLANE + -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterDeploy + -- @param #AI_CARGO_AIRPLANE self + -- @param Wrapper.Group#GROUP Airplane Cargo plane. + -- @param #string From + -- @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. + + --- 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. + + --- 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. + + --- On after Loaded event, i.e. triggered when the cargo is inside the carrier. + -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterLoaded + -- @param #AI_CARGO_AIRPLANE self + -- @param Wrapper.Group#GROUP Airplane Cargo plane. + -- @param From + -- @param Event + -- @param To + + -- Set carrier. + self:SetCarrier( Airplane ) + + return self +end + + +--- Set the Carrier (controllable). Also initializes events for carrier and defines the coalition. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane Transport plane. +-- @return #AI_CARGO_AIRPLANE self +function AI_CARGO_AIRPLANE:SetCarrier( Airplane ) + + local AICargo = self + + self.Airplane = Airplane -- Wrapper.Group#GROUP + self.Airplane:SetState( self.Airplane, "AI_CARGO_AIRPLANE", self ) + + self.RoutePickup = false + self.RouteDeploy = false + + Airplane:HandleEvent( EVENTS.Dead ) + Airplane:HandleEvent( EVENTS.Hit ) + Airplane:HandleEvent( EVENTS.EngineShutdown ) + + function Airplane:OnEventDead( EventData ) + local AICargoTroops = self:GetState( self, "AI_CARGO_AIRPLANE" ) + self:F({AICargoTroops=AICargoTroops}) + if AICargoTroops then + self:F({}) + if not AICargoTroops:Is( "Loaded" ) then + -- There are enemies within combat range. Unload the Airplane. + AICargoTroops:Destroyed() + end + end + end + + + function Airplane:OnEventHit( EventData ) + local AICargoTroops = self:GetState( self, "AI_CARGO_AIRPLANE" ) + if AICargoTroops then + self:F( { OnHitLoaded = AICargoTroops:Is( "Loaded" ) } ) + if AICargoTroops:Is( "Loaded" ) or AICargoTroops:Is( "Boarding" ) then + -- There are enemies within combat range. Unload the Airplane. + AICargoTroops:Unload() + end + end + end + + + function Airplane:OnEventEngineShutdown( EventData ) + AICargo.Relocating = false + AICargo:Landed( self.Airplane ) + end + + self.Coalition = self.Airplane:GetCoalition() + + self:SetControllable( Airplane ) + + return self +end + + +--- Find a free Carrier within a range. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Airbase#AIRBASE Airbase +-- @param #number Radius +-- @return Wrapper.Group#GROUP NewCarrier +function AI_CARGO_AIRPLANE:FindCarrier( Coordinate, Radius ) + + local CoordinateZone = ZONE_RADIUS:New( "Zone" , Coordinate:GetVec2(), Radius ) + CoordinateZone:Scan( { Object.Category.UNIT } ) + for _, DCSUnit in pairs( CoordinateZone:GetScannedUnits() ) do + local NearUnit = UNIT:Find( DCSUnit ) + self:F({NearUnit=NearUnit}) + if not NearUnit:GetState( NearUnit, "AI_CARGO_AIRPLANE" ) then + local Attributes = NearUnit:GetDesc() + self:F({Desc=Attributes}) + if NearUnit:HasAttribute( "Trucks" ) then + self:SetCarrier( NearUnit ) + break + end + end + end + +end + +--- On after "Landed" event. Called on engine shutdown and initiates the pickup mission or unloading event. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane Cargo transport plane. +-- @param From +-- @param Event +-- @param To +function AI_CARGO_AIRPLANE:onafterLanded( Airplane, From, Event, To ) + + self:F({Airplane, From, Event, To}) + + if Airplane and Airplane:IsAlive()~=nil then + + -- Aircraft was sent to this airbase to pickup troops. Initiate loadling. + if self.RoutePickup == true then + self:Load( self.PickupZone ) + end + + -- Aircraft was send to this airbase to deploy troops. Initiate unloading. + if self.RouteDeploy == true then + self:Unload() + self.RouteDeploy = false + end + + end + +end + + +--- On after "Pickup" event. Routes transport to pickup airbase. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane Cargo transport plane. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate +-- @param #number 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. +function AI_CARGO_AIRPLANE:onafterPickup( Airplane, From, Event, To, Coordinate, Speed, Height, PickupZone ) + + if Airplane and Airplane:IsAlive() then + + self.PickupZone = PickupZone + + -- Get closest airbase of current position. + local ClosestAirbase, DistToAirbase=Airplane:GetCoordinate():GetClosestAirbase() + + -- Two cases. Aircraft spawned in air or at an airbase. + if Airplane:InAir() then + self.Airbase=nil --> route will start in air + else + self.Airbase=ClosestAirbase + end + + -- Set pickup airbase. + local Airbase = 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 + + -- Route aircraft to pickup airbase. + self:Route( Airplane, Airbase, Speed, Height ) + + -- Set airbase as starting point in the next Route() call. + self.Airbase = Airbase + + -- Aircraft is on a pickup mission. + self.RoutePickup = true + + else + + -- We are already at the right airbase ==> Landed ==> triggers loading of troops. Is usually called at engine shutdown event. + self.RoutePickup=true + self:Landed() + + end + + self:GetParent( self, AI_CARGO_AIRPLANE ).onafterPickup( self, Airplane, From, Event, To, Coordinate, Speed, Height, PickupZone ) + + end + + +end + +--- On after Depoly event. Routes plane to the airbase where the troops are deployed. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane Cargo transport plane. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Coordinate +-- @param #number Speed in km/h for travelling to pickup 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. +function AI_CARGO_AIRPLANE:onafterDeploy( Airplane, From, Event, To, Coordinate, Speed, Height, DeployZone ) + + if Airplane and Airplane:IsAlive()~=nil then + + local Airbase = DeployZone:GetAirbase() + + -- Activate uncontrolled airplane. + if Airplane:IsAlive()==false then + Airplane:SetCommand({id = 'Start', params = {}}) + end + + -- Route to destination airbase. + self:Route( Airplane, Airbase, Speed, Height ) + + -- Aircraft is on a depoly mission. + self.RouteDeploy = true + + -- Set destination airbase for next :Route() command. + self.Airbase = Airbase + + self:GetParent( self, AI_CARGO_AIRPLANE ).onafterDeploy( self, Airplane, From, Event, To, Coordinate, Speed, Height, DeployZone ) + end + +end + + +--- On after Unload event. Cargo is beeing unloaded, i.e. the unboarding process is started. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane Cargo transport plane. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AI_CARGO_AIRPLANE:onafterUnload( Airplane, From, Event, To, DeployZone ) + + local UnboardInterval = 10 + local UnboardDelay = 10 + + if Airplane and Airplane:IsAlive() then + for _, AirplaneUnit in pairs( Airplane:GetUnits() ) do + local Cargos = AirplaneUnit:GetCargo() + for CargoID, Cargo in pairs( Cargos ) do + + local Angle = 180 + local CargoCarrierHeading = Airplane:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + self:T( { CargoCarrierHeading, CargoDeployHeading } ) + local CargoDeployCoordinate = Airplane:GetPointVec2():Translate( 150, CargoDeployHeading ) + + Cargo:__UnBoard( UnboardDelay, CargoDeployCoordinate ) + UnboardDelay = UnboardDelay + UnboardInterval + Cargo:SetDeployed( true ) + self:__Unboard( UnboardDelay, Cargo, AirplaneUnit, DeployZone ) + end + end + end + +end + + + + +--- Route the airplane from one airport or it's current position to another airbase. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane Airplane group to be routed. +-- @param Wrapper.Airbase#AIRBASE Airbase Destination airbase. +-- @param #number Speed Speed in km/h. Default is 80% of max possible speed the group can do. +-- @param #number Height Height in meters to move to the Airbase. +-- @param #boolean Uncontrolled If true, spawn group in uncontrolled state. +function AI_CARGO_AIRPLANE:Route( Airplane, Airbase, Speed, Height, Uncontrolled ) + + if Airplane and Airplane:IsAlive() then + + -- Set takeoff type. + local Takeoff = SPAWN.Takeoff.Cold + + -- Get template of group. + local Template = Airplane:GetTemplate() + + -- Nil check + if Template==nil then + return + end + + -- Waypoints of the route. + local Points={} + + -- To point. + local AirbasePointVec2 = Airbase:GetPointVec2() + local ToWaypoint = AirbasePointVec2:WaypointAir( + POINT_VEC3.RoutePointAltType.BARO, + "Land", + "Landing", + Speed or Airplane:GetSpeedMax()*0.8 + ) + ToWaypoint["airdromeId"] = Airbase:GetID() + ToWaypoint["speed_locked"] = true + + + -- If self.Airbase~=nil then group is currently at an airbase, where it should be respawned. + if self.Airbase then + + -- Second point of the route. First point is done in RespawnAtCurrentAirbase() routine. + Template.route.points[2] = ToWaypoint + + -- Respawn group at the current airbase. + Airplane:RespawnAtCurrentAirbase(Template, Takeoff, Uncontrolled) + + else + + -- From point. + local GroupPoint = Airplane:GetVec2() + local FromWaypoint = {} + FromWaypoint.x = GroupPoint.x + FromWaypoint.y = GroupPoint.y + FromWaypoint.type = "Turning Point" + FromWaypoint.action = "Turning Point" + FromWaypoint.speed = Airplane:GetSpeedMax()*0.8 + + -- The two route points. + Points[1] = FromWaypoint + Points[2] = ToWaypoint + + local PointVec3 = Airplane:GetPointVec3() + Template.x = PointVec3.x + Template.y = PointVec3.z + + Template.route.points = Points + + local GroupSpawned = Airplane:Respawn(Template) + + end + end +end + +--- On after Home event. Aircraft will be routed to their home base. +-- @param #AI_CARGO_AIRPLANE self +-- @param Wrapper.Group#GROUP Airplane The cargo plane. +-- @param From From state. +-- @param Event Event. +-- @param To To State. +-- @param Core.Point#COORDINATE Coordinate Home place (not used). +-- @param #number Speed Speed in km/h to fly to the home airbase (zone). Default is 80% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the home coordinate. +-- @param Core.Zone#ZONE_AIRBASE HomeZone The home airbase (zone) where the plane should return to. +function AI_CARGO_AIRPLANE:onafterHome(Airplane, From, Event, To, Coordinate, Speed, Height, HomeZone ) + if Airplane and Airplane:IsAlive() then + + -- We are going home! + self.RouteHome = true + + -- Home Base. + local HomeBase=HomeZone:GetAirbase() + self.Airbase=HomeBase + + -- Now route the airplane home + self:Route( Airplane, HomeBase, Speed, Height ) + + end + +end diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua new file mode 100644 index 000000000..311a07b59 --- /dev/null +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher.lua @@ -0,0 +1,1227 @@ +--- **AI** -- (R2.4) - Models the intelligent transportation of infantry and other cargo. +-- +-- ## Features: +-- +-- * AI_CARGO_DISPATCHER is the **base class** for: +-- +-- * @{AI.AI_Cargo_Dispatcher_APC#AI_CARGO_DISPATCHER_APC} +-- * @{AI.AI_Cargo_Dispatcher_Helicopter#AI_CARGO_DISPATCHER_HELICOPTER} +-- * @{AI.AI_Cargo_Dispatcher_Airplane#AI_CARGO_DISPATCHER_AIRPLANE} +-- +-- * Provides the facilities to transport cargo over the battle field for the above classes. +-- * Dispatches transport tasks to a common set of cargo transporting groups. +-- * Different options can be setup to tweak the cargo transporation behaviour. +-- +-- === +-- +-- ## Test Missions: +-- +-- Test missions can be located on the main GITHUB site. +-- +-- [FlightControl-Master/MOOSE_MISSIONS/AID - AI Dispatching/AID-CGO - AI Cargo Dispatching/](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/AID%20-%20AI%20Dispatching/AID-CGO%20-%20AI%20Cargo%20Dispatching) +-- +-- === +-- +-- # The dispatcher concept. +-- +-- Carrier equipment can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- The AI_CARGO_DISPATCHER module uses the @{Cargo.Cargo} capabilities within the MOOSE framework, to enable Carrier GROUP objects +-- to transport @{Cargo.Cargo} towards several deploy zones. +-- @{Cargo.Cargo} must be declared within the mission to make the AI_CARGO_DISPATCHER object recognize the cargo. +-- Please consult the @{Cargo.Cargo} module for more information. +-- +-- +-- ## Why cargo dispatching? +-- +-- It provides a realistic way of distributing your army forces around the battlefield, and to provide a quick means of cargo transportation. +-- Instead of having troops or cargo to "appear" suddenly at certain locations, the dispatchers will pickup the cargo and transport it. +-- It also allows to enforce or retreat your army from certain zones when needed, using helicopters or APCs. +-- Airplanes can transport cargo over larger distances between the airfields. +-- +-- +-- ## What is a cargo object then? +-- +-- In order to make use of the MOOSE cargo system, you need to **declare** the DCS objects as MOOSE cargo objects! +-- This sounds complicated, but it is actually quite simple. +-- +-- See here an example: +-- +-- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) +-- +-- The above code declares a MOOSE cargo object called `EngineerCargoGroup`. +-- It actually just refers to an infantry group created within the sim called `"Engineers"`. +-- The infantry group now becomes controlled by the MOOSE cargo object `EngineerCargoGroup`. +-- A MOOSE cargo object also has properties, like the type of cargo, the logical name, and the reporting range. +-- +-- For more information, please consult the @{Cargo.Cargo} module documentation. Please read through it, because it will explain how to setup the cargo objects for use +-- within your dispatchers. +-- +-- +-- ## Do I need to do a lot of coding to setup a dispatcher? +-- +-- No! It requires a bit of studying to set it up, but once you understand the different components that use the cargo dispatcher, it becomes very easy. +-- Also, the dispatchers work in a true dynamic environment. The carriers and cargo, pickup and deploy zones can be created dynamically in your mission, +-- and will automatically be recognized by the dispatcher. +-- +-- +-- ## Is the dispatcher causing a lot of CPU overhead? +-- +-- A little yes, but once the cargo is properly loaded into the carrier, the CPU consumption is very little. +-- When infantry or vehicles board into a carrier, or unboard from a carrier, you may perceive certain performance lags. +-- We are working to minimize the impact of those. +-- That being said, the DCS simulator is limited. It is just impossible to deploy hundreds of cargo over the battlefield, hundreds of helicopters transporting, +-- without any performance impact. The amount of helicopters that are active and flying in your simulation influences more the performance than the dispatchers. +-- It really comes down to trying it out and getting experienced with what is possible and what is not (or too much). +-- +-- +-- ## Are the dispatchers a "black box" in terms of the logic? +-- +-- No. You can tailor the dispatcher mechanisms using event handlers, and create additional logic to enhance the behaviour and dynamism in your own mission. +-- The events are listed below, and so are the options, but here are a couple of examples of what is possible: +-- +-- * You could handle the **Deployed** event, when all the cargo is unloaded from a carrier in the dispatcher. +-- Adding your own code to the event handler, you could move the deployed cargo (infantry) to specific points to engage in the battlefield. +-- +-- * When a carrier is picking up cargo, the *Pickup** event is triggered, and you can inform the coalition of this event, +-- because it is an indication that troops are planned to join. +-- +-- +-- ## Are there options that you can set to modify the behaviour of the carries? +-- +-- Yes, there are options to configure: +-- +-- * the location where carriers will park or land near the cargo for pickup. +-- * the location where carriers will park or land in the deploy zone for cargo deployment. +-- * the height for airborne carriers when they fly to and from pickup and deploy zones. +-- * the speed of the carriers. This is an important parameter, because depending on the tactication situation, speed will influence the detection by radars. +-- +-- +-- ## Can the zones be of any zone type? +-- +-- Yes, please ensure that the zones are declared using the @{Core.Zone} classes. +-- Possible zones that function at the moment are ZONE, ZONE_GROUP, ZONE_UNIT, ZONE_POLYGON. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo_Dispatcher +-- @image AI_Cargo_Dispatcher.JPG + + +--- @type AI_CARGO_DISPATCHER +-- @field Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers that will transport the cargo. +-- @field Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects. +-- @field Core.Zone#SET_ZONE PickupZoneSet The set of pickup zones, which are used to where the cargo can be picked up by the carriers. If nil, then cargo can be picked up everywhere. +-- @field Core.Zone#SET_ZONE DeployZoneSet The set of deploy zones, which are used to where the cargo will be deployed by the carriers. +-- @field #number PickupMaxSpeed The maximum speed to move to the cargo pickup location. +-- @field #number PickupMinSpeed The minimum speed to move to the cargo pickup location. +-- @field #number DeployMaxSpeed The maximum speed to move to the cargo deploy location. +-- @field #number DeployMinSpeed The minimum speed to move to the cargo deploy location. +-- @field #number PickupMaxHeight The maximum height to fly to the cargo pickup location. +-- @field #number PickupMinHeight The minimum height to fly to the cargo pickup location. +-- @field #number DeployMaxHeight The maximum height to fly to the cargo deploy location. +-- @field #number DeployMinHeight The minimum height to fly to the cargo deploy location. +-- @field #number PickupOuterRadius The outer radius in meters around the cargo coordinate to pickup the cargo. +-- @field #number PickupInnerRadius The inner radius in meters around the cargo coordinate to pickup the cargo. +-- @field #number DeployOuterRadius The outer radius in meters around the cargo coordinate to deploy the cargo. +-- @field #number DeployInnerRadius The inner radius in meters around the cargo coordinate to deploy the cargo. +-- @field Core.Zone#ZONE_BASE HomeZone The home zone where the carriers will return when there is no more cargo to pickup. +-- @field #number MonitorTimeInterval The interval in seconds when the cargo dispatcher will search for new cargo to be picked up. +-- @extends Core.Fsm#FSM + + +--- A dynamic cargo handling capability for AI groups. +-- +-- --- +-- +-- Carrier equipment can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- The AI_CARGO_DISPATCHER module uses the @{Cargo.Cargo} capabilities within the MOOSE framework, to enable Carrier GROUP objects +-- to transport @{Cargo.Cargo} towards several deploy zones. +-- @{Cargo.Cargo} must be declared within the mission to make the AI_CARGO_DISPATCHER object recognize the cargo. +-- Please consult the @{Cargo.Cargo} module for more information. +-- +-- # 1) AI_CARGO_DISPATCHER constructor. +-- +-- * @{#AI_CARGO_DISPATCHER.New}(): Creates a new AI_CARGO_DISPATCHER object. +-- +-- Find below some examples of AI cargo dispatcher objects created. +-- +-- ### An AI dispatcher object for a helicopter squadron, moving infantry from pickup zones to deploy zones. +-- +-- local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local SetHelicopter = SET_GROUP:New():FilterPrefixes( "Helicopter" ):FilterStart() +-- local SetPickupZones = SET_ZONE:New():FilterPrefixes( "Pickup" ):FilterStart() +-- local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart() +-- +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- AICargoDispatcherHelicopter:SetHomeZone( ZONE:FindByName( "Home" ) ) +-- +-- ### An AI dispatcher object for a vehicle squadron, moving infantry from pickup zones to deploy zones. +-- +-- local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local SetAPC = SET_GROUP:New():FilterPrefixes( "APC" ):FilterStart() +-- local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart() +-- +-- AICargoDispatcherAPC = AI_CARGO_DISPATCHER_APC:New( SetAPC, SetCargoInfantry, nil, SetDeployZones ) +-- AICargoDispatcherAPC:Start() +-- +-- ### An AI dispatcher object for an airplane squadron, moving infantry and vehicles from pickup airbases to deploy airbases. +-- +-- local CargoInfantrySet = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local AirplanesSet = SET_GROUP:New():FilterPrefixes( "Airplane" ):FilterStart() +-- local PickupZoneSet = SET_ZONE:New() +-- local DeployZoneSet = SET_ZONE:New() +-- +-- PickupZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Gudauta ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Sochi_Adler ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Maykop_Khanskaya ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Mineralnye_Vody ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Vaziani ) ) +-- +-- AICargoDispatcherAirplanes = AI_CARGO_DISPATCHER_AIRPLANE:New( AirplanesSet, CargoInfantrySet, PickupZoneSet, DeployZoneSet ) +-- AICargoDispatcherAirplanes:SetHomeZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Kobuleti ) ) +-- +-- --- +-- +-- # 2) AI_CARGO_DISPATCHER is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- * Monitoring => Monitor => Monitoring +-- * Monitoring => Stop => Idle +-- +-- * Monitoring => Pickup => Monitoring +-- * Monitoring => Load => Monitoring +-- * Monitoring => Loading => Monitoring +-- * Monitoring => Loaded => Monitoring +-- * Monitoring => PickedUp => Monitoring +-- * Monitoring => Deploy => Monitoring +-- * Monitoring => Unload => Monitoring +-- * Monitoring => Unloaded => Monitoring +-- * Monitoring => Deployed => Monitoring +-- * Monitoring => Home => Monitoring +-- +-- ## 2.1) AI_CARGO_DISPATCHER States. +-- +-- * **Monitoring**: The process is dispatching. +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_CARGO_DISPATCHER Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- * **Pickup**: Pickup cargo. +-- * **Load**: Load the cargo. +-- * **Loading**: The dispatcher is coordinating the loading of a cargo. +-- * **Loaded**: Flag that the cargo is loaded. +-- * **PickedUp**: The dispatcher has loaded all requested cargo into the CarrierGroup. +-- * **Deploy**: Deploy cargo to a location. +-- * **Unload**: Unload the cargo. +-- * **Unloaded**: Flag that the cargo is unloaded. +-- * **Deployed**: All cargo is unloaded from the carriers in the group. +-- * **Home**: A Carrier is going home. +-- +-- --- +-- +-- # 3) Enhance your mission scripts with **Tailored** Event Handling! +-- +-- Use these methods to capture the events and tailor the events with your own code! +-- All classes derived from AI_CARGO_DISPATCHER can capture these events, and you can write your own code. +-- +-- In order to properly capture the events, it is mandatory that you execute the following actions using your script: +-- +-- * Copy / Paste the code section into your script. +-- * Change the CLASS literal to the object name you have in your script. +-- * Within the function, you can now write your own code! +-- * IntelliSense will recognize the type of the variables provided by the function. Note: the From, Event and To variables can be safely ignored, +-- but you need to declare them as they are automatically provided by the event handling system of MOOSE. +-- +-- You can send messages or fire off any other events within the code section. The sky is the limit! +-- +-- Mission AID-CGO-140, AID-CGO-240 and AID-CGO-340 contain examples how these events can be tailored. +-- +-- For those who don't have the time to check the test missions, find the underlying example of a Deployed event that is tailored. +-- +-- --- Deployed Handler OnAfter for AI_CARGO_DISPATCHER. +-- -- Use this event handler to tailor the event when a carrier has deployed all cargo objects from the CarrierGroup. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- @function OnAfterDeployed +-- -- @param #AICargoDispatcherHelicopter self +-- -- @param #string From A string that contains the "*from state name*" when the event was fired. +-- -- @param #string Event A string that contains the "*event name*" when the event was fired. +-- -- @param #string To A string that contains the "*to state name*" when the event was fired. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- function AICargoDispatcherHelicopter:OnAfterDeployed( From, Event, To, CarrierGroup, DeployZone ) +-- +-- MESSAGE:NewType( "Group " .. CarrierGroup:GetName() .. " deployed all cargo in zone " .. DeployZone:GetName(), MESSAGE.Type.Information ):ToAll() +-- +-- end +-- +-- +-- ## 3.1) Tailor the **Pickup** event +-- +-- Use this event handler to tailor the event when a CarrierGroup is routed towards a new pickup Coordinate and a specified Speed. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- +-- --- Pickup event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierGroup is routed towards a new pickup Coordinate and a specified Speed. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Point#COORDINATE Coordinate The coordinate of the pickup location. +-- -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the pickup Coordinate. +-- -- @param #number Height Height in meters to move to the pickup coordinate. +-- -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. +-- function CLASS:OnAfterPickup( From, Event, To, CarrierGroup, Coordinate, Speed, Height, PickupZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.2) Tailor the **Load** event +-- +-- Use this event handler to tailor the event when a CarrierGroup has initiated the loading or boarding of cargo within reporting or near range. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- +-- --- Load event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierGroup has initiated the loading or boarding of cargo within reporting or near range. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. +-- function CLASS:OnAfterLoad( From, Event, To, CarrierGroup, PickupZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.3) Tailor the **Loading** event +-- +-- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup is in the process of loading or boarding of a cargo object. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- +-- --- Loading event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup is in the process of loading or boarding of a cargo object. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- Note that this event is triggered repeatedly until all cargo (units) have been boarded into the carrier. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Cargo.Cargo#CARGO Cargo The cargo object. +-- -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo loading operation. +-- -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. +-- function CLASS:OnAfterLoading( From, Event, To, CarrierGroup, Cargo, CarrierUnit, PickupZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.4) Tailor the **Loaded** event +-- +-- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has loaded a cargo object. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- Note that if more cargo objects were loading or boarding into the CarrierUnit, then this event can be triggered multiple times for each different Cargo/CarrierUnit. +-- +-- The function provides the CarrierGroup, which is the main group that was loading the Cargo into the CarrierUnit. +-- A CarrierUnit is part of the larger CarrierGroup. +-- +-- +-- --- Loaded event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has loaded a cargo object. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- Note that if more cargo objects were loading or boarding into the CarrierUnit, then this event can be triggered multiple times for each different Cargo/CarrierUnit. +-- -- A CarrierUnit can be part of the larger CarrierGroup. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Cargo.Cargo#CARGO Cargo The cargo object. +-- -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo loading operation. +-- -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. +-- function CLASS:OnAfterLoaded( From, Event, To, CarrierGroup, Cargo, CarrierUnit, PickupZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.5) Tailor the **PickedUp** event +-- +-- Use this event handler to tailor the event when a carrier has picked up all cargo objects into the CarrierGroup. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- +-- --- PickedUp event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a carrier has picked up all cargo objects into the CarrierGroup. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. +-- function CLASS:OnAfterPickedUp( From, Event, To, CarrierGroup, PickupZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.6) Tailor the **Deploy** event +-- +-- Use this event handler to tailor the event when a CarrierGroup is routed to a deploy coordinate, to Unload all cargo objects in each CarrierUnit. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- +-- --- Deploy event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierGroup is routed to a deploy coordinate, to Unload all cargo objects in each CarrierUnit. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Point#COORDINATE Coordinate The deploy coordinate. +-- -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the deploy Coordinate. +-- -- @param #number Height Height in meters to move to the deploy coordinate. +-- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- function CLASS:OnAfterDeploy( From, Event, To, CarrierGroup, Coordinate, Speed, Height, DeployZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.7) Tailor the **Unload** event +-- +-- Use this event handler to tailor the event when a CarrierGroup has initiated the unloading or unboarding of cargo. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- +-- --- Unload event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierGroup has initiated the unloading or unboarding of cargo. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- function CLASS:OnAfterUnload( From, Event, To, CarrierGroup, DeployZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.8) Tailor the **Unloading** event +-- +-- +-- --- UnLoading event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup is in the process of unloading or unboarding of a cargo object. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- Note that this event is triggered repeatedly until all cargo (units) have been unboarded from the CarrierUnit. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Cargo.Cargo#CARGO Cargo The cargo object. +-- -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo unloading operation. +-- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- function CLASS:OnAfterUnload( From, Event, To, CarrierGroup, Cargo, CarrierUnit, DeployZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.9) Tailor the **Unloaded** event +-- +-- +-- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has unloaded a cargo object. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- --- Unloaded event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has unloaded a cargo object. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- Note that if more cargo objects were unloading or unboarding from the CarrierUnit, then this event can be triggered multiple times for each different Cargo/CarrierUnit. +-- -- A CarrierUnit can be part of the larger CarrierGroup. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Cargo.Cargo#CARGO Cargo The cargo object. +-- -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo unloading operation. +-- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- function CLASS:OnAfterUnloaded( From, Event, To, CarrierGroup, Cargo, CarrierUnit, DeployZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- +-- ## 3.10) Tailor the **Deployed** event +-- +-- Use this event handler to tailor the event when a carrier has deployed all cargo objects from the CarrierGroup. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- +-- --- Deployed event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a carrier has deployed all cargo objects from the CarrierGroup. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- function CLASS:OnAfterDeployed( From, Event, To, CarrierGroup, DeployZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- ## 3.11) Tailor the **Home** event +-- +-- Use this event handler to tailor the event when a CarrierGroup is returning to the HomeZone, after it has deployed all cargo objects from the CarrierGroup. +-- You can use this event handler to post messages to players, or provide status updates etc. +-- +-- --- Home event handler OnAfter for CLASS. +-- -- Use this event handler to tailor the event when a CarrierGroup is returning to the HomeZone, after it has deployed all cargo objects from the CarrierGroup. +-- -- You can use this event handler to post messages to players, or provide status updates etc. +-- -- If there is no HomeZone is specified, the CarrierGroup will stay at the current location after having deployed all cargo and this event won't be triggered. +-- -- @param #CLASS self +-- -- @param #string From A string that contains the "*from state name*" when the event was triggered. +-- -- @param #string Event A string that contains the "*event name*" when the event was triggered. +-- -- @param #string To A string that contains the "*to state name*" when the event was triggered. +-- -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. +-- -- @param Core.Point#COORDINATE Coordinate The home coordinate the Carrier will arrive and stop it's activities. +-- -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the home Coordinate. +-- -- @param #number Height Height in meters to move to the home coordinate. +-- -- @param Core.Zone#ZONE HomeZone The zone wherein the carrier will return when all cargo has been transported. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +-- function CLASS:OnAfterHome( From, Event, To, CarrierGroup, Coordinate, Speed, Height, HomeZone ) +-- +-- -- Write here your own code. +-- +-- end +-- +-- --- +-- +-- # 4) Set the pickup parameters. +-- +-- Several parameters can be set to pickup cargo: +-- +-- * @{#AI_CARGO_DISPATCHER.SetPickupRadius}(): Sets or randomizes the pickup location for the carrier around the cargo coordinate in a radius defined an outer and optional inner radius. +-- * @{#AI_CARGO_DISPATCHER.SetPickupSpeed}(): Set the speed or randomizes the speed in km/h to pickup the cargo. +-- * @{#AI_CARGO_DISPATCHER.SetPickupHeight}(): Set the height or randomizes the height in meters to pickup the cargo. +-- +-- --- +-- +-- # 5) Set the deploy parameters. +-- +-- Several parameters can be set to deploy cargo: +-- +-- * @{#AI_CARGO_DISPATCHER.SetDeployRadius}(): Sets or randomizes the deploy location for the carrier around the cargo coordinate in a radius defined an outer and an optional inner radius. +-- * @{#AI_CARGO_DISPATCHER.SetDeploySpeed}(): Set the speed or randomizes the speed in km/h to deploy the cargo. +-- * @{#AI_CARGO_DISPATCHER.SetDeployHeight}(): Set the height or randomizes the height in meters to deploy the cargo. +-- +-- --- +-- +-- # 6) Set the home zone when there isn't any more cargo to pickup. +-- +-- A home zone can be specified to where the Carriers will move when there isn't any cargo left for pickup. +-- Use @{#AI_CARGO_DISPATCHER.SetHomeZone}() to specify the home zone. +-- +-- If no home zone is specified, the carriers will wait near the deploy zone for a new pickup command. +-- +-- === +-- +-- @field #AI_CARGO_DISPATCHER +AI_CARGO_DISPATCHER = { + ClassName = "AI_CARGO_DISPATCHER", + AI_Cargo = {}, + PickupCargo = {} +} + +--- @field #list +AI_CARGO_DISPATCHER.AI_Cargo = {} + +--- @field #list +AI_CARGO_DISPATCHER.PickupCargo = {} + + +--- Creates a new AI_CARGO_DISPATCHER object. +-- @param #AI_CARGO_DISPATCHER self +-- @param Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers that will transport the cargo. +-- @param Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects. +-- @param Core.Set#SET_ZONE PickupZoneSet (optional) The set of pickup zones, which are used to where the cargo can be picked up by the carriers. If nil, then cargo can be picked up everywhere. +-- @param Core.Set#SET_ZONE DeployZoneSet The set of deploy zones, which are used to where the cargo will be deployed by the carriers. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- An AI dispatcher object for a helicopter squadron, moving infantry from pickup zones to deploy zones. +-- +-- local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local SetHelicopter = SET_GROUP:New():FilterPrefixes( "Helicopter" ):FilterStart() +-- local SetPickupZones = SET_ZONE:New():FilterPrefixes( "Pickup" ):FilterStart() +-- local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart() +-- +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- AICargoDispatcherHelicopter:Start() +-- +-- @usage +-- +-- -- An AI dispatcher object for a vehicle squadron, moving infantry from pickup zones to deploy zones. +-- +-- local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local SetAPC = SET_GROUP:New():FilterPrefixes( "APC" ):FilterStart() +-- local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart() +-- +-- AICargoDispatcherAPC = AI_CARGO_DISPATCHER_APC:New( SetAPC, SetCargoInfantry, nil, SetDeployZones ) +-- AICargoDispatcherAPC:Start() +-- +-- @usage +-- +-- -- An AI dispatcher object for an airplane squadron, moving infantry and vehicles from pickup airbases to deploy airbases. +-- +-- local CargoInfantrySet = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local AirplanesSet = SET_GROUP:New():FilterPrefixes( "Airplane" ):FilterStart() +-- local PickupZoneSet = SET_ZONE:New() +-- local DeployZoneSet = SET_ZONE:New() +-- +-- PickupZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Gudauta ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Sochi_Adler ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Maykop_Khanskaya ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Mineralnye_Vody ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Vaziani ) ) +-- +-- AICargoDispatcherAirplanes = AI_CARGO_DISPATCHER_AIRPLANE:New( AirplanesSet, CargoInfantrySet, PickupZoneSet, DeployZoneSet ) +-- AICargoDispatcherAirplanes:Start() +-- +function AI_CARGO_DISPATCHER:New( CarrierSet, CargoSet, PickupZoneSet, DeployZoneSet ) + + local self = BASE:Inherit( self, FSM:New() ) -- #AI_CARGO_DISPATCHER + + self.SetCarrier = CarrierSet -- Core.Set#SET_GROUP + self.SetCargo = CargoSet -- Core.Set#SET_CARGO + + + self.PickupZoneSet=PickupZoneSet + self.DeployZoneSet=DeployZoneSet + + self:SetStartState( "Idle" ) + + self:AddTransition( "Monitoring", "Monitor", "Monitoring" ) + + self:AddTransition( "Idle", "Start", "Monitoring" ) + self:AddTransition( "Monitoring", "Stop", "Idle" ) + + + self:AddTransition( "Monitoring", "Pickup", "Monitoring" ) + self:AddTransition( "Monitoring", "Load", "Monitoring" ) + self:AddTransition( "Monitoring", "Loading", "Monitoring" ) + self:AddTransition( "Monitoring", "Loaded", "Monitoring" ) + self:AddTransition( "Monitoring", "PickedUp", "Monitoring" ) + + self:AddTransition( "Monitoring", "Transport", "Monitoring" ) + + self:AddTransition( "Monitoring", "Deploy", "Monitoring" ) + self:AddTransition( "Monitoring", "Unload", "Monitoring" ) + self:AddTransition( "Monitoring", "Unloading", "Monitoring" ) + self:AddTransition( "Monitoring", "Unloaded", "Monitoring" ) + self:AddTransition( "Monitoring", "Deployed", "Monitoring" ) + + self:AddTransition( "Monitoring", "Home", "Monitoring" ) + + self:SetMonitorTimeInterval( 30 ) + + self:SetDeployRadius( 500, 200 ) + + self.PickupCargo = {} + self.CarrierHome = {} + + -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset. + function self.SetCarrier.OnAfterRemoved( SetCarrier, From, Event, To, CarrierName, Carrier ) + self:F( { Carrier = Carrier:GetName() } ) + self.PickupCargo[Carrier] = nil + self.CarrierHome[Carrier] = nil + end + + return self +end + + +--- Set the monitor time interval. +-- @param #AI_CARGO_DISPATCHER self +-- @param #number MonitorTimeInterval The interval in seconds when the cargo dispatcher will search for new cargo to be picked up. +-- @return #AI_CARGO_DISPATCHER +function AI_CARGO_DISPATCHER:SetMonitorTimeInterval( MonitorTimeInterval ) + + self.MonitorTimeInterval = MonitorTimeInterval + + return self +end + + +--- Set the home zone. +-- When there is nothing anymore to pickup, the carriers will go to a random coordinate in this zone. +-- They will await here new orders. +-- @param #AI_CARGO_DISPATCHER self +-- @param Core.Zone#ZONE_BASE HomeZone The home zone where the carriers will return when there is no more cargo to pickup. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- Create a new cargo dispatcher +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- +-- -- Set the home coordinate +-- local HomeZone = ZONE:New( "Home" ) +-- AICargoDispatcherHelicopter:SetHomeZone( HomeZone ) +-- +function AI_CARGO_DISPATCHER:SetHomeZone( HomeZone ) + + self.HomeZone = HomeZone + + return self +end + + +--- Sets or randomizes the pickup location for the carrier around the cargo coordinate in a radius defined an outer and optional inner radius. +-- This radius is influencing the location where the carrier will land to pickup the cargo. +-- There are two aspects that are very important to remember and take into account: +-- +-- - Ensure that the outer and inner radius are within reporting radius set by the cargo. +-- For example, if the cargo has a reporting radius of 400 meters, and the outer and inner radius is set to 500 and 450 respectively, +-- then no cargo will be loaded!!! +-- - Also take care of the potential cargo position and possible reasons to crash the carrier. This is especially important +-- for locations which are crowded with other objects, like in the middle of villages or cities. +-- So, for the best operation of cargo operations, always ensure that the cargo is located at open spaces. +-- +-- The default radius is 0, so the center. In case of a polygon zone, a random location will be selected as the center in the zone. +-- @param #AI_CARGO_DISPATCHER self +-- @param #number OuterRadius The outer radius in meters around the cargo coordinate. +-- @param #number InnerRadius (optional) The inner radius in meters around the cargo coordinate. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- Create a new cargo dispatcher +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- +-- -- Set the carrier to land within a band around the cargo coordinate between 500 and 300 meters! +-- AICargoDispatcherHelicopter:SetPickupRadius( 500, 300 ) +-- +function AI_CARGO_DISPATCHER:SetPickupRadius( OuterRadius, InnerRadius ) + + OuterRadius = OuterRadius or 0 + InnerRadius = InnerRadius or OuterRadius + + self.PickupOuterRadius = OuterRadius + self.PickupInnerRadius = InnerRadius + + return self +end + + +--- Set the speed or randomizes the speed in km/h to pickup the cargo. +-- @param #AI_CARGO_DISPATCHER self +-- @param #number MaxSpeed (optional) The maximum speed to move to the cargo pickup location. +-- @param #number MinSpeed The minimum speed to move to the cargo pickup location. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- Create a new cargo dispatcher +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- +-- -- Set the minimum pickup speed to be 100 km/h and the maximum speed to be 200 km/h. +-- AICargoDispatcherHelicopter:SetPickupSpeed( 200, 100 ) +-- +function AI_CARGO_DISPATCHER:SetPickupSpeed( MaxSpeed, MinSpeed ) + + MaxSpeed = MaxSpeed or 999 + MinSpeed = MinSpeed or MaxSpeed + + self.PickupMinSpeed = MinSpeed + self.PickupMaxSpeed = MaxSpeed + + return self +end + + +--- Sets or randomizes the deploy location for the carrier around the cargo coordinate in a radius defined an outer and an optional inner radius. +-- This radius is influencing the location where the carrier will land to deploy the cargo. +-- There is an aspect that is very important to remember and take into account: +-- +-- - Take care of the potential cargo position and possible reasons to crash the carrier. This is especially important +-- for locations which are crowded with other objects, like in the middle of villages or cities. +-- So, for the best operation of cargo operations, always ensure that the cargo is located at open spaces. +-- +-- The default radius is 0, so the center. In case of a polygon zone, a random location will be selected as the center in the zone. +-- @param #AI_CARGO_DISPATCHER self +-- @param #number OuterRadius The outer radius in meters around the cargo coordinate. +-- @param #number InnerRadius (optional) The inner radius in meters around the cargo coordinate. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- Create a new cargo dispatcher +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- +-- -- Set the carrier to land within a band around the cargo coordinate between 500 and 300 meters! +-- AICargoDispatcherHelicopter:SetDeployRadius( 500, 300 ) +-- +function AI_CARGO_DISPATCHER:SetDeployRadius( OuterRadius, InnerRadius ) + + OuterRadius = OuterRadius or 0 + InnerRadius = InnerRadius or OuterRadius + + self.DeployOuterRadius = OuterRadius + self.DeployInnerRadius = InnerRadius + + return self +end + + +--- Sets or randomizes the speed in km/h to deploy the cargo. +-- @param #AI_CARGO_DISPATCHER self +-- @param #number MaxSpeed The maximum speed to move to the cargo deploy location. +-- @param #number MinSpeed (optional) The minimum speed to move to the cargo deploy location. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- Create a new cargo dispatcher +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- +-- -- Set the minimum deploy speed to be 100 km/h and the maximum speed to be 200 km/h. +-- AICargoDispatcherHelicopter:SetDeploySpeed( 200, 100 ) +-- +function AI_CARGO_DISPATCHER:SetDeploySpeed( MaxSpeed, MinSpeed ) + + MaxSpeed = MaxSpeed or 999 + MinSpeed = MinSpeed or MaxSpeed + + self.DeployMinSpeed = MinSpeed + self.DeployMaxSpeed = MaxSpeed + + return self +end + + +--- Set the height or randomizes the height in meters to fly and pickup the cargo. The default height is 200 meters. +-- @param #AI_CARGO_DISPATCHER self +-- @param #number MaxHeight (optional) The maximum height to fly to the cargo pickup location. +-- @param #number MinHeight (optional) The minimum height to fly to the cargo pickup location. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- Create a new cargo dispatcher +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- +-- -- Set the minimum pickup fly height to be 50 meters and the maximum height to be 200 meters. +-- AICargoDispatcherHelicopter:SetPickupHeight( 200, 50 ) +-- +function AI_CARGO_DISPATCHER:SetPickupHeight( MaxHeight, MinHeight ) + + MaxHeight = MaxHeight or 200 + MinHeight = MinHeight or MaxHeight + + self.PickupMinHeight = MinHeight + self.PickupMaxHeight = MaxHeight + + return self +end + + +--- Set the height or randomizes the height in meters to fly and deploy the cargo. The default height is 200 meters. +-- @param #AI_CARGO_DISPATCHER self +-- @param #number MaxHeight (optional) The maximum height to fly to the cargo deploy location. +-- @param #number MinHeight (optional) The minimum height to fly to the cargo deploy location. +-- @return #AI_CARGO_DISPATCHER +-- @usage +-- +-- -- Create a new cargo dispatcher +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- +-- -- Set the minimum deploy fly height to be 50 meters and the maximum height to be 200 meters. +-- AICargoDispatcherHelicopter:SetDeployHeight( 200, 50 ) +-- +function AI_CARGO_DISPATCHER:SetDeployHeight( MaxHeight, MinHeight ) + + MaxHeight = MaxHeight or 200 + MinHeight = MinHeight or MaxHeight + + self.DeployMinHeight = MinHeight + self.DeployMaxHeight = MaxHeight + + return self +end + + +--- The Start trigger event, which actually takes action at the specified time interval. +-- @param #AI_CARGO_DISPATCHER self +function AI_CARGO_DISPATCHER:onafterMonitor() + + self:F("Carriers") + self.SetCarrier:Flush() + + for CarrierGroupName, Carrier in pairs( self.SetCarrier:GetSet() ) do + local Carrier = Carrier -- Wrapper.Group#GROUP + if Carrier:IsAlive() ~= nil then + local AI_Cargo = self.AI_Cargo[Carrier] + if not AI_Cargo then + + -- ok, so this Carrier does not have yet an AI_CARGO handling object... + -- let's create one and also declare the Loaded and UnLoaded handlers. + self.AI_Cargo[Carrier] = self:AICargo( Carrier, self.SetCargo, self.CombatRadius ) + AI_Cargo = self.AI_Cargo[Carrier] + + --- Pickup event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierGroup is routed towards a new pickup Coordinate and a specified Speed. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterPickup + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Core.Point#COORDINATE Coordinate The coordinate of the pickup location. + -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the pickup Coordinate. + -- @param #number Height Height in meters to move to the pickup coordinate. + -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. + function AI_Cargo.OnAfterPickup( AI_Cargo, CarrierGroup, From, Event, To, Coordinate, Speed, Height, PickupZone ) + self:Pickup( CarrierGroup, Coordinate, Speed, Height, PickupZone ) + end + + --- Load event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierGroup has initiated the loading or boarding of cargo within reporting or near range. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterLoad + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. + + function AI_Cargo.OnAfterLoad( AI_Cargo, CarrierGroup, From, Event, To, PickupZone ) + self:Load( CarrierGroup, PickupZone ) + end + + --- Loading event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup is in the process of loading or boarding of a cargo object. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- Note that this event is triggered repeatedly until all cargo (units) have been boarded into the carrier. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterLoading + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Cargo.Cargo#CARGO Cargo The cargo object. + -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo loading operation. + -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. + + function AI_Cargo.OnAfterBoard( AI_Cargo, CarrierGroup, From, Event, To, Cargo, CarrierUnit, PickupZone ) + self:Loading( CarrierGroup, Cargo, CarrierUnit, PickupZone ) + end + + --- Loaded event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has loaded a cargo object. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- Note that if more cargo objects were loading or boarding into the CarrierUnit, then this event can be triggered multiple times for each different Cargo/CarrierUnit. + -- A CarrierUnit can be part of the larger CarrierGroup. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterLoaded + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Cargo.Cargo#CARGO Cargo The cargo object. + -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo loading operation. + -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. + + function AI_Cargo.OnAfterLoaded( AI_Cargo, CarrierGroup, From, Event, To, Cargo, CarrierUnit, PickupZone ) + self:Loaded( CarrierGroup, Cargo, CarrierUnit, PickupZone ) + end + + --- PickedUp event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a carrier has picked up all cargo objects into the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterPickedUp + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone. + + function AI_Cargo.OnAfterPickedUp( AI_Cargo, CarrierGroup, From, Event, To, PickupZone ) + self:PickedUp( CarrierGroup, PickupZone ) + self:Transport( CarrierGroup ) + end + + + --- Deploy event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierGroup is routed to a deploy coordinate, to Unload all cargo objects in each CarrierUnit. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterDeploy + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Core.Point#COORDINATE Coordinate The deploy coordinate. + -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the deploy Coordinate. + -- @param #number Height Height in meters to move to the deploy coordinate. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + + function AI_Cargo.OnAfterDeploy( AI_Cargo, CarrierGroup, From, Event, To, Coordinate, Speed, Height, DeployZone ) + self:Deploy( CarrierGroup, Coordinate, Speed, Height, DeployZone ) + end + + + --- Unload event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierGroup has initiated the unloading or unboarding of cargo. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterUnload + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + + function AI_Cargo.OnAfterUnload( AI_Cargo, Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone ) + self:Unloading( Carrier, Cargo, CarrierUnit, DeployZone ) + end + + --- UnLoading event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup is in the process of unloading or unboarding of a cargo object. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- Note that this event is triggered repeatedly until all cargo (units) have been unboarded from the CarrierUnit. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterUnloading + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Cargo.Cargo#CARGO Cargo The cargo object. + -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo unloading operation. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + + function AI_Cargo.OnAfterUnboard( AI_Cargo, CarrierGroup, From, Event, To, Cargo, CarrierUnit, DeployZone ) + self:Unloading( CarrierGroup, Cargo, CarrierUnit, DeployZone ) + end + + + --- Unloaded event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has unloaded a cargo object. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- Note that if more cargo objects were unloading or unboarding from the CarrierUnit, then this event can be triggered multiple times for each different Cargo/CarrierUnit. + -- A CarrierUnit can be part of the larger CarrierGroup. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterUnloaded + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Cargo.Cargo#CARGO Cargo The cargo object. + -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo unloading operation. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + + function AI_Cargo.OnAfterUnloaded( AI_Cargo, Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone ) + self:Unloaded( Carrier, Cargo, CarrierUnit, DeployZone ) + end + + --- Deployed event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a carrier has deployed all cargo objects from the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterDeployed + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + + function AI_Cargo.OnAfterDeployed( AI_Cargo, Carrier, From, Event, To, DeployZone ) + self:Deployed( Carrier, DeployZone ) + end + + --- Home event handler OnAfter for AI_CARGO_DISPATCHER. + -- Use this event handler to tailor the event when a CarrierGroup is returning to the HomeZone, after it has deployed all cargo objects from the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- If there is no HomeZone is specified, the CarrierGroup will stay at the current location after having deployed all cargo. + -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterHome + -- @param #AI_CARGO_DISPATCHER self + -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits. + -- @param Core.Point#COORDINATE Coordinate The home coordinate the Carrier will arrive and stop it's activities. + -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the home Coordinate. + -- @param #number Height Height in meters to move to the home coordinate. + -- @param Core.Zone#ZONE HomeZone The zone wherein the carrier will return when all cargo has been transported. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + + function AI_Cargo.OnAfterHome( AI_Cargo, Carrier, From, Event, To, Coordinate, Speed, Height, HomeZone ) + self:Home( Carrier, Coordinate, Speed, Height, HomeZone ) + end + end + + -- The Pickup sequence ... + -- Check if this Carrier need to go and Pickup something... + -- So, if the cargo bay is not full yet with cargo to be loaded ... + self:I( { Carrier = CarrierGroupName, IsRelocating = AI_Cargo:IsRelocating(), IsTransporting = AI_Cargo:IsTransporting() } ) + if AI_Cargo:IsRelocating() == false and AI_Cargo:IsTransporting() == false then + -- ok, so there is a free Carrier + -- now find the first cargo that is Unloaded + + local PickupCargo = nil + local PickupZone = nil + + self.SetCargo:Flush() + for CargoName, Cargo in UTILS.spairs( self.SetCargo:GetSet(), function( t, a, b ) return t[a]:GetWeight() < t[b]:GetWeight() end ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + self:F( { Cargo = Cargo:GetName(), UnLoaded = Cargo:IsUnLoaded(), Deployed = Cargo:IsDeployed(), PickupCargo = self.PickupCargo[Carrier] ~= nil } ) + if Cargo:IsUnLoaded() == true and Cargo:IsDeployed() == false then + local CargoCoordinate = Cargo:GetCoordinate() + local CoordinateFree = true + --self.PickupZoneSet:Flush() + --PickupZone = self.PickupZoneSet:GetRandomZone() + PickupZone = self.PickupZoneSet and self.PickupZoneSet:IsCoordinateInZone( CargoCoordinate ) + if not self.PickupZoneSet or PickupZone then + for CarrierPickup, Coordinate in pairs( self.PickupCargo ) do + if CarrierPickup:IsAlive() == true then + if CargoCoordinate:Get2DDistance( Coordinate ) <= 25 then + self:F( { "Coordinate not free for ", Cargo = Cargo:GetName(), Carrier:GetName(), PickupCargo = self.PickupCargo[Carrier] ~= nil } ) + CoordinateFree = false + break + end + else + self.PickupCargo[CarrierPickup] = nil + end + end + if CoordinateFree == true then + -- Check if this cargo can be picked-up by at least one carrier unit of AI_Cargo. + local LargestLoadCapacity = 0 + for _, Carrier in pairs( Carrier:GetUnits() ) do + local LoadCapacity = Carrier:GetCargoBayFreeWeight() + if LargestLoadCapacity < LoadCapacity then + LargestLoadCapacity = LoadCapacity + end + end + -- So if there is a carrier that has the required load capacity to load the total weight of the cargo, dispatch the carrier. + -- Otherwise break and go to the next carrier. + -- This will skip cargo which is too large to be able to be loaded by carriers + -- and will secure an efficient dispatching scheme. + if LargestLoadCapacity >= Cargo:GetWeight() then + self.PickupCargo[Carrier] = CargoCoordinate + PickupCargo = Cargo + break + end + end + end + end + end + + if PickupCargo then + self.CarrierHome[Carrier] = nil + local PickupCoordinate = PickupCargo:GetCoordinate():GetRandomCoordinateInRadius( self.PickupOuterRadius, self.PickupInnerRadius ) + AI_Cargo:Pickup( PickupCoordinate, math.random( self.PickupMinSpeed, self.PickupMaxSpeed ), math.random( self.PickupMinHeight, self.PickupMaxHeight ), PickupZone ) + break + else + if self.HomeZone then + if not self.CarrierHome[Carrier] then + self.CarrierHome[Carrier] = true + AI_Cargo:Home( self.HomeZone:GetRandomPointVec2(), math.random( self.PickupMinSpeed, self.PickupMaxSpeed ), math.random( self.PickupMinHeight, self.PickupMaxHeight ), self.HomeZone ) + end + end + end + end + end + end + + self:__Monitor( self.MonitorTimeInterval ) +end + + +--- Start Trigger for AI_CARGO_DISPATCHER +-- @function [parent=#AI_CARGO_DISPATCHER] Start +-- @param #AI_CARGO_DISPATCHER self + +--- Start Asynchronous Trigger for AI_CARGO_DISPATCHER +-- @function [parent=#AI_CARGO_DISPATCHER] __Start +-- @param #AI_CARGO_DISPATCHER self +-- @param #number Delay + +function AI_CARGO_DISPATCHER:onafterStart( From, Event, To ) + self:__Monitor( -1 ) +end + + +--- Stop Trigger for AI_CARGO_DISPATCHER +-- @function [parent=#AI_CARGO_DISPATCHER] Stop +-- @param #AI_CARGO_DISPATCHER self + +--- Stop Asynchronous Trigger for AI_CARGO_DISPATCHER +-- @function [parent=#AI_CARGO_DISPATCHER] __Stop +-- @param #AI_CARGO_DISPATCHER self +-- @param #number Delay + + +--- Make a Carrier run for a cargo deploy action after the cargo has been loaded, by default. +-- @param #AI_CARGO_DISPATCHER self +-- @param From +-- @param Event +-- @param To +-- @param Wrapper.Group#GROUP Carrier +-- @param Cargo.Cargo#CARGO Cargo +-- @return #AI_CARGO_DISPATCHER +function AI_CARGO_DISPATCHER:onafterTransport( From, Event, To, Carrier, Cargo ) + + if self.DeployZoneSet then + if self.AI_Cargo[Carrier]:IsTransporting() == true then + local DeployZone = self.DeployZoneSet:GetRandomZone() + + local DeployCoordinate = DeployZone:GetCoordinate():GetRandomCoordinateInRadius( self.DeployOuterRadius, self.DeployInnerRadius ) + self.AI_Cargo[Carrier]:__Deploy( 0.1, DeployCoordinate, math.random( self.DeployMinSpeed, self.DeployMaxSpeed ), math.random( self.DeployMinHeight, self.DeployMaxHeight ), DeployZone ) + end + end + + self:F( { Carrier = Carrier:GetName(), PickupCargo = self.PickupCargo } ) + self.PickupCargo[Carrier] = nil +end + diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_APC.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_APC.lua new file mode 100644 index 000000000..b4bef21bc --- /dev/null +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_APC.lua @@ -0,0 +1,209 @@ +--- **AI** -- (2.4) - Models the intelligent transportation of infantry and other cargo using APCs. +-- +-- ## Features: +-- +-- * Quickly transport cargo to various deploy zones using ground vehicles (APCs, trucks ...). +-- * Various @{Cargo.Cargo#CARGO} types can be transported. These are infantry groups and crates. +-- * Define a list of deploy zones of various types to transport the cargo to. +-- * The vehicles follow the roads to ensure the fastest possible cargo transportation over the ground. +-- * Multiple vehicles can transport multiple cargo as one vehicle group. +-- * Multiple vehicle groups can be enabled as one collaborating transportation process. +-- * Infantry loaded as cargo, will unboard in case enemies are nearby and will help defending the vehicles. +-- * Different ranges can be setup for enemy defenses. +-- * Different options can be setup to tweak the cargo transporation behaviour. +-- +-- === +-- +-- ## Test Missions: +-- +-- Test missions can be located on the main GITHUB site. +-- +-- [FlightControl-Master/MOOSE_MISSIONS/AID - AI Dispatching/AID-CGO - AI Cargo Dispatching/] +-- (https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/AID%20-%20AI%20Dispatching/AID-CGO%20-%20AI%20Cargo%20Dispatching) +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo_Dispatcher_APC +-- @image AI_Cargo_Dispatching_For_APC.JPG + +--- @type AI_CARGO_DISPATCHER_APC +-- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER + + +--- A dynamic cargo transportation capability for AI groups. +-- +-- Armoured Personnel APCs (APC), Trucks, Jeeps and other carrier equipment can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- +-- The AI_CARGO_DISPATCHER_APC module is derived from the AI_CARGO_DISPATCHER module. +-- +-- ## Note! In order to fully understand the mechanisms of the AI_CARGO_DISPATCHER_APC class, it is recommended that you first consult and READ the documentation of the @{AI.AI_Cargo_Dispatcher} module!!! +-- +-- Especially to learn how to **Tailor the different cargo handling events**, this will be very useful! +-- +-- On top, the AI_CARGO_DISPATCHER_APC class uses the @{Cargo.Cargo} capabilities within the MOOSE framework. +-- Also ensure that you fully understand how to declare and setup Cargo objects within the MOOSE framework before using this class. +-- CARGO derived objects must be declared within the mission to make the AI_CARGO_DISPATCHER_HELICOPTER object recognize the cargo. +-- +-- +-- # 1) AI_CARGO_DISPATCHER_APC constructor. +-- +-- * @{#AI_CARGO_DISPATCHER_APC.New}(): Creates a new AI_CARGO_DISPATCHER_APC object. +-- +-- --- +-- +-- # 2) AI_CARGO_DISPATCHER_APC is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- * Monitoring => Monitor => Monitoring +-- * Monitoring => Stop => Idle +-- +-- * Monitoring => Pickup => Monitoring +-- * Monitoring => Load => Monitoring +-- * Monitoring => Loading => Monitoring +-- * Monitoring => Loaded => Monitoring +-- * Monitoring => PickedUp => Monitoring +-- * Monitoring => Deploy => Monitoring +-- * Monitoring => Unload => Monitoring +-- * Monitoring => Unloaded => Monitoring +-- * Monitoring => Deployed => Monitoring +-- * Monitoring => Home => Monitoring +-- +-- +-- ## 2.1) AI_CARGO_DISPATCHER States. +-- +-- * **Monitoring**: The process is dispatching. +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_CARGO_DISPATCHER Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- * **Pickup**: Pickup cargo. +-- * **Load**: Load the cargo. +-- * **Loading**: The dispatcher is coordinating the loading of a cargo. +-- * **Loaded**: Flag that the cargo is loaded. +-- * **PickedUp**: The dispatcher has loaded all requested cargo into the CarrierGroup. +-- * **Deploy**: Deploy cargo to a location. +-- * **Unload**: Unload the cargo. +-- * **Unloaded**: Flag that the cargo is unloaded. +-- * **Deployed**: All cargo is unloaded from the carriers in the group. +-- * **Home**: A Carrier is going home. +-- +-- ## 2.3) Enhance your mission scripts with **Tailored** Event Handling! +-- +-- Within your mission, you can capture these events when triggered, and tailor the events with your own code! +-- Check out the @{AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER} class at chapter 3 for details on the different event handlers that are available and how to use them. +-- +-- **There are a lot of templates available that allows you to quickly setup an event handler for a specific event type!** +-- +-- --- +-- +-- # 3) Set the pickup parameters. +-- +-- Several parameters can be set to pickup cargo: +-- +-- * @{#AI_CARGO_DISPATCHER_APC.SetPickupRadius}(): Sets or randomizes the pickup location for the APC around the cargo coordinate in a radius defined an outer and optional inner radius. +-- * @{#AI_CARGO_DISPATCHER_APC.SetPickupSpeed}(): Set the speed or randomizes the speed in km/h to pickup the cargo. +-- +-- # 4) Set the deploy parameters. +-- +-- Several parameters can be set to deploy cargo: +-- +-- * @{#AI_CARGO_DISPATCHER_APC.SetDeployRadius}(): Sets or randomizes the deploy location for the APC around the cargo coordinate in a radius defined an outer and an optional inner radius. +-- * @{#AI_CARGO_DISPATCHER_APC.SetDeploySpeed}(): Set the speed or randomizes the speed in km/h to deploy the cargo. +-- +-- # 5) Set the home zone when there isn't any more cargo to pickup. +-- +-- A home zone can be specified to where the APCs will move when there isn't any cargo left for pickup. +-- Use @{#AI_CARGO_DISPATCHER_APC.SetHomeZone}() to specify the home zone. +-- +-- If no home zone is specified, the APCs will wait near the deploy zone for a new pickup command. +-- +-- === +-- +-- @field #AI_CARGO_DISPATCHER_APC +AI_CARGO_DISPATCHER_APC = { + ClassName = "AI_CARGO_DISPATCHER_APC", +} + +--- Creates a new AI_CARGO_DISPATCHER_APC object. +-- @param #AI_CARGO_DISPATCHER_APC self +-- @param Core.Set#SET_GROUP APCSet The set of @{Wrapper.Group#GROUP} objects of vehicles, trucks, APCs that will transport the cargo. +-- @param Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects. +-- @param Core.Set#SET_ZONE PickupZoneSet (optional) The set of pickup zones, which are used to where the cargo can be picked up by the APCs. If nil, then cargo can be picked up everywhere. +-- @param Core.Set#SET_ZONE DeployZoneSet The set of deploy zones, which are used to where the cargo will be deployed by the APCs. +-- @param DCS#Distance CombatRadius The cargo will be unloaded from the APC and engage the enemy if the enemy is within CombatRadius range. The radius is in meters, the default value is 500 meters. +-- @return #AI_CARGO_DISPATCHER_APC +-- @usage +-- +-- -- An AI dispatcher object for a vehicle squadron, moving infantry from pickup zones to deploy zones. +-- +-- local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local SetAPC = SET_GROUP:New():FilterPrefixes( "APC" ):FilterStart() +-- local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart() +-- +-- AICargoDispatcherAPC = AI_CARGO_DISPATCHER_APC:New( SetAPC, SetCargoInfantry, nil, SetDeployZones ) +-- AICargoDispatcherAPC:Start() +-- +function AI_CARGO_DISPATCHER_APC:New( APCSet, CargoSet, PickupZoneSet, DeployZoneSet, CombatRadius ) + + local self = BASE:Inherit( self, AI_CARGO_DISPATCHER:New( APCSet, CargoSet, PickupZoneSet, DeployZoneSet ) ) -- #AI_CARGO_DISPATCHER_APC + + self:SetDeploySpeed( 120, 70 ) + self:SetPickupSpeed( 120, 70 ) + self:SetPickupRadius( 0, 0 ) + self:SetDeployRadius( 0, 0 ) + + self:SetPickupHeight() + self:SetDeployHeight() + + self:SetCombatRadius( CombatRadius ) + + return self +end + +function AI_CARGO_DISPATCHER_APC:AICargo( APC, CargoSet ) + + return AI_CARGO_APC:New( APC, CargoSet, self.CombatRadius ) +end + +--- Enable/Disable unboarding of cargo (infantry) when enemies are nearby (to help defend the carrier). +-- This is only valid for APCs and trucks etc, thus ground vehicles. +-- @param #AI_CARGO_DISPATCHER_APC self +-- @param #number CombatRadius Provide the combat radius to defend the carrier by unboarding the cargo when enemies are nearby. +-- When the combat radius is 0, no defense will happen of the carrier. +-- When the combat radius is not provided, no defense will happen! +-- @return #AI_CARGO_DISPATCHER_APC +-- @usage +-- +-- -- Disembark the infantry when the carrier is under attack. +-- AICargoDispatcher:SetCombatRadius( true ) +-- +-- -- Keep the cargo in the carrier when the carrier is under attack. +-- AICargoDispatcher:SetCombatRadius( false ) +function AI_CARGO_DISPATCHER_APC:SetCombatRadius( CombatRadius ) + + self.CombatRadius = CombatRadius or 0 + + return self +end + diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Airplane.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Airplane.lua new file mode 100644 index 000000000..11df8c188 --- /dev/null +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Airplane.lua @@ -0,0 +1,164 @@ +--- **AI** -- (R2.4) - Models the intelligent transportation of infantry and other cargo using Planes. +-- +-- ## Features: +-- +-- * The airplanes will fly towards the pickup airbases to pickup the cargo. +-- * The airplanes will fly towards the deploy airbases to deploy the cargo. +-- +-- === +-- +-- ## Test Missions: +-- +-- Test missions can be located on the main GITHUB site. +-- +-- [FlightControl-Master/MOOSE_MISSIONS/AID - AI Dispatching/AID-CGO - AI Cargo Dispatching/] +-- (https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/AID%20-%20AI%20Dispatching/AID-CGO%20-%20AI%20Cargo%20Dispatching) +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo_Dispatcher_Airplane +-- @image AI_Cargo_Dispatching_For_Airplanes.JPG + + +--- @type AI_CARGO_DISPATCHER_AIRPLANE +-- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER + + +--- Brings a dynamic cargo handling capability for AI groups. +-- +-- Airplanes can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- +-- The AI_CARGO_DISPATCHER_AIRPLANE module is derived from the AI_CARGO_DISPATCHER module. +-- +-- ## Note! In order to fully understand the mechanisms of the AI_CARGO_DISPATCHER_AIRPLANE class, it is recommended that you first consult and READ the documentation of the @{AI.AI_Cargo_Dispatcher} module!!!** +-- +-- Especially to learn how to **Tailor the different cargo handling events**, this will be very useful! +-- +-- On top, the AI_CARGO_DISPATCHER_AIRPLANE class uses the @{Cargo.Cargo} capabilities within the MOOSE framework. +-- Also ensure that you fully understand how to declare and setup Cargo objects within the MOOSE framework before using this class. +-- CARGO derived objects must be declared within the mission to make the AI_CARGO_DISPATCHER_HELICOPTER object recognize the cargo. +-- +-- # 1) AI_CARGO_DISPATCHER_AIRPLANE constructor. +-- +-- * @{#AI_CARGO_DISPATCHER_AIRPLANE.New}(): Creates a new AI_CARGO_DISPATCHER_AIRPLANE object. +-- +-- --- +-- +-- # 2) AI_CARGO_DISPATCHER_AIRPLANE is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- * Monitoring => Monitor => Monitoring +-- * Monitoring => Stop => Idle +-- +-- * Monitoring => Pickup => Monitoring +-- * Monitoring => Load => Monitoring +-- * Monitoring => Loading => Monitoring +-- * Monitoring => Loaded => Monitoring +-- * Monitoring => PickedUp => Monitoring +-- * Monitoring => Deploy => Monitoring +-- * Monitoring => Unload => Monitoring +-- * Monitoring => Unloaded => Monitoring +-- * Monitoring => Deployed => Monitoring +-- * Monitoring => Home => Monitoring +-- +-- +-- ## 2.1) AI_CARGO_DISPATCHER States. +-- +-- * **Monitoring**: The process is dispatching. +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_CARGO_DISPATCHER Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- * **Pickup**: Pickup cargo. +-- * **Load**: Load the cargo. +-- * **Loading**: The dispatcher is coordinating the loading of a cargo. +-- * **Loaded**: Flag that the cargo is loaded. +-- * **PickedUp**: The dispatcher has loaded all requested cargo into the CarrierGroup. +-- * **Deploy**: Deploy cargo to a location. +-- * **Unload**: Unload the cargo. +-- * **Unloaded**: Flag that the cargo is unloaded. +-- * **Deployed**: All cargo is unloaded from the carriers in the group. +-- * **Home**: A Carrier is going home. +-- +-- ## 2.3) Enhance your mission scripts with **Tailored** Event Handling! +-- +-- Within your mission, you can capture these events when triggered, and tailor the events with your own code! +-- Check out the @{AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER} class at chapter 3 for details on the different event handlers that are available and how to use them. +-- +-- **There are a lot of templates available that allows you to quickly setup an event handler for a specific event type!** +-- +-- +-- +-- @field #AI_CARGO_DISPATCHER_AIRPLANE +AI_CARGO_DISPATCHER_AIRPLANE = { + ClassName = "AI_CARGO_DISPATCHER_AIRPLANE", +} + +--- Creates a new AI_CARGO_DISPATCHER_AIRPLANE object. +-- @param #AI_CARGO_DISPATCHER_AIRPLANE self +-- @param Core.Set#SET_GROUP AirplaneSet The set of @{Wrapper.Group#GROUP} objects of airplanes that will transport the cargo. +-- @param Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects. +-- @param Core.Zone#SET_ZONE PickupZoneSet The set of zone airbases where the cargo has to be picked up. +-- @param Core.Zone#SET_ZONE DeployZoneSet The set of zone airbases where the cargo is deployed. Choice for each cargo is random. +-- @return #AI_CARGO_DISPATCHER_AIRPLANE self +-- @usage +-- +-- -- An AI dispatcher object for an airplane squadron, moving infantry and vehicles from pickup airbases to deploy airbases. +-- +-- local CargoInfantrySet = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local AirplanesSet = SET_GROUP:New():FilterPrefixes( "Airplane" ):FilterStart() +-- local PickupZoneSet = SET_ZONE:New() +-- local DeployZoneSet = SET_ZONE:New() +-- +-- PickupZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Gudauta ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Sochi_Adler ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Maykop_Khanskaya ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Mineralnye_Vody ) ) +-- DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Vaziani ) ) +-- +-- AICargoDispatcherAirplanes = AI_CARGO_DISPATCHER_AIRPLANE:New( AirplanesSet, CargoInfantrySet, PickupZoneSet, DeployZoneSet ) +-- AICargoDispatcherAirplanes:Start() +-- +function AI_CARGO_DISPATCHER_AIRPLANE:New( AirplaneSet, CargoSet, PickupZoneSet, DeployZoneSet ) + + local self = BASE:Inherit( self, AI_CARGO_DISPATCHER:New( AirplaneSet, CargoSet, PickupZoneSet, DeployZoneSet ) ) -- #AI_CARGO_DISPATCHER_AIRPLANE + + self:SetPickupSpeed( 1200, 600 ) + self:SetDeploySpeed( 1200, 600 ) + + self:SetPickupRadius( 0, 0 ) + self:SetDeployRadius( 0, 0 ) + + self:SetPickupHeight( 8000, 6000 ) + self:SetDeployHeight( 8000, 6000 ) + + self:SetMonitorTimeInterval( 600 ) + + return self +end + +function AI_CARGO_DISPATCHER_AIRPLANE:AICargo( Airplane, CargoSet ) + + return AI_CARGO_AIRPLANE:New( Airplane, CargoSet ) +end diff --git a/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua new file mode 100644 index 000000000..127c67dbb --- /dev/null +++ b/Moose Development/Moose/AI/AI_Cargo_Dispatcher_Helicopter.lua @@ -0,0 +1,191 @@ +--- **AI** -- (2.4) - Models the intelligent transportation of infantry and other cargo using Helicopters. +-- +-- ## Features: +-- +-- * The helicopters will fly towards the pickup locations to pickup the cargo. +-- * The helicopters will fly towards the deploy zones to deploy the cargo. +-- * Precision deployment as well as randomized deployment within the deploy zones are possible. +-- * Helicopters will orbit the deploy zones when there is no space for landing until the deploy zone is free. +-- +-- === +-- +-- ## Test Missions: +-- +-- Test missions can be located on the main GITHUB site. +-- +-- [FlightControl-Master/MOOSE_MISSIONS/AID - AI Dispatching/AID-CGO - AI Cargo Dispatching/] +-- (https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/AID%20-%20AI%20Dispatching/AID-CGO%20-%20AI%20Cargo%20Dispatching) +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo_Dispatcher_Helicopter +-- @image AI_Cargo_Dispatching_For_Helicopters.JPG + +--- @type AI_CARGO_DISPATCHER_HELICOPTER +-- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER + + +--- A dynamic cargo handling capability for AI helicopter groups. +-- +-- Helicopters can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- +-- +-- The AI_CARGO_DISPATCHER_HELICOPTER module is derived from the AI_CARGO_DISPATCHER module. +-- +-- ## Note! In order to fully understand the mechanisms of the AI_CARGO_DISPATCHER_HELICOPTER class, it is recommended that you first consult and READ the documentation of the @{AI.AI_Cargo_Dispatcher} module!!!** +-- +-- Especially to learn how to **Tailor the different cargo handling events**, this will be very useful! +-- +-- On top, the AI_CARGO_DISPATCHER_HELICOPTER class uses the @{Cargo.Cargo} capabilities within the MOOSE framework. +-- Also ensure that you fully understand how to declare and setup Cargo objects within the MOOSE framework before using this class. +-- CARGO derived objects must be declared within the mission to make the AI_CARGO_DISPATCHER_HELICOPTER object recognize the cargo. +-- +-- --- +-- +-- # 1. AI\_CARGO\_DISPATCHER\_HELICOPTER constructor. +-- +-- * @{#AI_CARGO_DISPATCHER\_HELICOPTER.New}(): Creates a new AI\_CARGO\_DISPATCHER\_HELICOPTER object. +-- +-- --- +-- +-- # 2. AI\_CARGO\_DISPATCHER\_HELICOPTER is a Finite State Machine. +-- +-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed. +-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state. +-- +-- So, each of the rows have the following structure. +-- +-- * **From** => **Event** => **To** +-- +-- Important to know is that an event can only be executed if the **current state** is the **From** state. +-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed, +-- and the resulting state will be the **To** state. +-- +-- These are the different possible state transitions of this state machine implementation: +-- +-- * Idle => Start => Monitoring +-- * Monitoring => Monitor => Monitoring +-- * Monitoring => Stop => Idle +-- +-- * Monitoring => Pickup => Monitoring +-- * Monitoring => Load => Monitoring +-- * Monitoring => Loading => Monitoring +-- * Monitoring => Loaded => Monitoring +-- * Monitoring => PickedUp => Monitoring +-- * Monitoring => Deploy => Monitoring +-- * Monitoring => Unload => Monitoring +-- * Monitoring => Unloaded => Monitoring +-- * Monitoring => Deployed => Monitoring +-- * Monitoring => Home => Monitoring +-- +-- +-- ## 2.1) AI_CARGO_DISPATCHER States. +-- +-- * **Monitoring**: The process is dispatching. +-- * **Idle**: The process is idle. +-- +-- ## 2.2) AI_CARGO_DISPATCHER Events. +-- +-- * **Start**: Start the transport process. +-- * **Stop**: Stop the transport process. +-- * **Monitor**: Monitor and take action. +-- +-- * **Pickup**: Pickup cargo. +-- * **Load**: Load the cargo. +-- * **Loading**: The dispatcher is coordinating the loading of a cargo. +-- * **Loaded**: Flag that the cargo is loaded. +-- * **PickedUp**: The dispatcher has loaded all requested cargo into the CarrierGroup. +-- * **Deploy**: Deploy cargo to a location. +-- * **Unload**: Unload the cargo. +-- * **Unloaded**: Flag that the cargo is unloaded. +-- * **Deployed**: All cargo is unloaded from the carriers in the group. +-- * **Home**: A Carrier is going home. +-- +-- ## 2.3) Enhance your mission scripts with **Tailored** Event Handling! +-- +-- Within your mission, you can capture these events when triggered, and tailor the events with your own code! +-- Check out the @{AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER} class at chapter 3 for details on the different event handlers that are available and how to use them. +-- +-- **There are a lot of templates available that allows you to quickly setup an event handler for a specific event type!** +-- +-- --- +-- +-- ## 3. Set the pickup parameters. +-- +-- Several parameters can be set to pickup cargo: +-- +-- * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetPickupRadius}(): Sets or randomizes the pickup location for the helicopter around the cargo coordinate in a radius defined an outer and optional inner radius. +-- * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetPickupSpeed}(): Set the speed or randomizes the speed in km/h to pickup the cargo. +-- * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetPickupHeight}(): Set the height or randomizes the height in meters to pickup the cargo. +-- +-- --- +-- +-- ## 4. Set the deploy parameters. +-- +-- Several parameters can be set to deploy cargo: +-- +-- * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetDeployRadius}(): Sets or randomizes the deploy location for the helicopter around the cargo coordinate in a radius defined an outer and an optional inner radius. +-- * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetDeploySpeed}(): Set the speed or randomizes the speed in km/h to deploy the cargo. +-- * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetDeployHeight}(): Set the height or randomizes the height in meters to deploy the cargo. +-- +-- --- +-- +-- ## 5. Set the home zone when there isn't any more cargo to pickup. +-- +-- A home zone can be specified to where the Helicopters will move when there isn't any cargo left for pickup. +-- Use @{#AI_CARGO_DISPATCHER_HELICOPTER.SetHomeZone}() to specify the home zone. +-- +-- If no home zone is specified, the helicopters will wait near the deploy zone for a new pickup command. +-- +-- === +-- +-- @field #AI_CARGO_DISPATCHER_HELICOPTER +AI_CARGO_DISPATCHER_HELICOPTER = { + ClassName = "AI_CARGO_DISPATCHER_HELICOPTER", +} + +--- Creates a new AI_CARGO_DISPATCHER_HELICOPTER object. +-- @param #AI_CARGO_DISPATCHER_HELICOPTER self +-- @param Core.Set#SET_GROUP HelicopterSet The set of @{Wrapper.Group#GROUP} objects of helicopters that will transport the cargo. +-- @param Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects. +-- @param Core.Set#SET_ZONE PickupZoneSet (optional) The set of pickup zones, which are used to where the cargo can be picked up by the APCs. If nil, then cargo can be picked up everywhere. +-- @param Core.Set#SET_ZONE DeployZoneSet The set of deploy zones, which are used to where the cargo will be deployed by the Helicopters. +-- @return #AI_CARGO_DISPATCHER_HELICOPTER +-- @usage +-- +-- -- An AI dispatcher object for a helicopter squadron, moving infantry from pickup zones to deploy zones. +-- +-- local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart() +-- local SetHelicopter = SET_GROUP:New():FilterPrefixes( "Helicopter" ):FilterStart() +-- local SetPickupZones = SET_ZONE:New():FilterPrefixes( "Pickup" ):FilterStart() +-- local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart() +-- +-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) +-- AICargoDispatcherHelicopter:Start() +-- +function AI_CARGO_DISPATCHER_HELICOPTER:New( HelicopterSet, CargoSet, PickupZoneSet, DeployZoneSet ) + + local self = BASE:Inherit( self, AI_CARGO_DISPATCHER:New( HelicopterSet, CargoSet, PickupZoneSet, DeployZoneSet ) ) -- #AI_CARGO_DISPATCHER_HELICOPTER + + self:SetPickupSpeed( 350, 150 ) + self:SetDeploySpeed( 350, 150 ) + + self:SetPickupRadius( 0, 0 ) + self:SetDeployRadius( 0, 0 ) + + self:SetPickupHeight( 500, 200 ) + self:SetDeployHeight( 500, 200 ) + + return self +end + + +function AI_CARGO_DISPATCHER_HELICOPTER:AICargo( Helicopter, CargoSet ) + + return AI_CARGO_HELICOPTER:New( Helicopter, CargoSet ) +end + diff --git a/Moose Development/Moose/AI/AI_Cargo_Helicopter.lua b/Moose Development/Moose/AI/AI_Cargo_Helicopter.lua new file mode 100644 index 000000000..4b838fde5 --- /dev/null +++ b/Moose Development/Moose/AI/AI_Cargo_Helicopter.lua @@ -0,0 +1,625 @@ +--- **AI** -- (R2.4) - Models the intelligent transportation of infantry (cargo). +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- === +-- +-- @module AI.AI_Cargo_Helicopter +-- @image AI_Cargo_Dispatching_For_Helicopters.JPG + +--- @type AI_CARGO_HELICOPTER +-- @extends Core.Fsm#FSM_CONTROLLABLE + + +--- Brings a dynamic cargo handling capability for an AI helicopter group. +-- +-- Helicopter carriers can be mobilized to intelligently transport infantry and other cargo within the simulation. +-- +-- The AI_CARGO_HELICOPTER class uses the @{Cargo.Cargo} capabilities within the MOOSE framework. +-- @{Cargo.Cargo} must be declared within the mission to make the AI_CARGO_HELICOPTER object recognize the cargo. +-- Please consult the @{Cargo.Cargo} module for more information. +-- +-- ## Cargo pickup. +-- +-- Using the @{#AI_CARGO_HELICOPTER.Pickup}() method, you are able to direct the helicopters towards a point on the battlefield to board/load the cargo at the specific coordinate. +-- Ensure that the landing zone is horizontally flat, and that trees cannot be found in the landing vicinity, or the helicopters won't land or will even crash! +-- +-- ## Cargo deployment. +-- +-- Using the @{#AI_CARGO_HELICOPTER.Deploy}() method, you are able to direct the helicopters towards a point on the battlefield to unboard/unload the cargo at the specific coordinate. +-- Ensure that the landing zone is horizontally flat, and that trees cannot be found in the landing vicinity, or the helicopters won't land or will even crash! +-- +-- ## Infantry health. +-- +-- When infantry is unboarded from the APCs, the infantry is actually respawned into the battlefield. +-- As a result, the unboarding infantry is very _healthy_ every time it unboards. +-- This is due to the limitation of the DCS simulator, which is not able to specify the health of new spawned units as a parameter. +-- However, infantry that was destroyed when unboarded, won't be respawned again. Destroyed is destroyed. +-- As a result, there is some additional strength that is gained when an unboarding action happens, but in terms of simulation balance this has +-- marginal impact on the overall battlefield simulation. Fortunately, the firing strength of infantry is limited, and thus, respacing healthy infantry every +-- time is not so much of an issue ... +-- +-- +-- === +-- +-- @field #AI_CARGO_HELICOPTER +AI_CARGO_HELICOPTER = { + ClassName = "AI_CARGO_HELICOPTER", + Coordinate = nil, -- Core.Point#COORDINATE, +} + +AI_CARGO_QUEUE = {} + +--- Creates a new AI_CARGO_HELICOPTER object. +-- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @param Core.Set#SET_CARGO CargoSet +-- @return #AI_CARGO_HELICOPTER +function AI_CARGO_HELICOPTER:New( Helicopter, CargoSet ) + + local self = BASE:Inherit( self, AI_CARGO:New( Helicopter, CargoSet ) ) -- #AI_CARGO_HELICOPTER + + self.Zone = ZONE_GROUP:New( Helicopter:GetName(), Helicopter, 300 ) + + self:SetStartState( "Unloaded" ) + + self:AddTransition( "Unloaded", "Pickup", "*" ) + self:AddTransition( "Loaded", "Deploy", "*" ) + + self:AddTransition( { "Unloaded", "Loading" }, "Load", "Boarding" ) + self:AddTransition( "Boarding", "Board", "Boarding" ) + self:AddTransition( "Boarding", "Loaded", "Boarding" ) + self:AddTransition( "Boarding", "PickedUp", "Loaded" ) + self:AddTransition( "Loaded", "Unload", "Unboarding" ) + self:AddTransition( "Unboarding", "Unboard", "Unboarding" ) + self:AddTransition( "Unboarding", "Unloaded", "Unboarding" ) + self:AddTransition( "Unboarding", "Deployed", "Unloaded" ) + + self:AddTransition( "*", "Landed", "*" ) + self:AddTransition( "*", "Queue", "*" ) + self:AddTransition( "*", "Orbit" , "*" ) + self:AddTransition( "*", "Home" , "*" ) + + self:AddTransition( "*", "Destroyed", "Destroyed" ) + + --- Pickup Handler OnBefore for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] OnBeforePickup + -- @param #AI_CARGO_HELICOPTER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate + -- @return #boolean + + --- Pickup Handler OnAfter for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] OnAfterPickup + -- @param #AI_CARGO_HELICOPTER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go. + + --- Pickup Trigger for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] Pickup + -- @param #AI_CARGO_HELICOPTER self + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go. + + --- Pickup Asynchronous Trigger for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] __Pickup + -- @param #AI_CARGO_HELICOPTER self + -- @param #number Delay Delay in seconds. + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h to go to the pickup coordinate. Default is 50% of max possible speed the unit can go. + + --- Deploy Handler OnBefore for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] OnBeforeDeploy + -- @param #AI_CARGO_HELICOPTER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate Place at which cargo is deployed. + -- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go. + -- @return #boolean + + --- Deploy Handler OnAfter for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] OnAfterDeploy + -- @param #AI_CARGO_HELICOPTER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go. + + --- Deploy Trigger for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] Deploy + -- @param #AI_CARGO_HELICOPTER self + -- @param Core.Point#COORDINATE Coordinate Place at which the cargo is deployed. + -- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go. + + --- Deploy Asynchronous Trigger for AI_CARGO_HELICOPTER + -- @function [parent=#AI_CARGO_HELICOPTER] __Deploy + -- @param #number Delay Delay in seconds. + -- @param #AI_CARGO_HELICOPTER self + -- @param Core.Point#COORDINATE Coordinate Place at which the cargo is deployed. + -- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go. + + + -- We need to capture the Crash events for the helicopters. + -- The helicopter reference is used in the semaphore AI_CARGO_QUEUE. + -- So, we need to unlock this when the helo is not anymore ... + Helicopter:HandleEvent( EVENTS.Crash, + function( Helicopter, EventData ) + AI_CARGO_QUEUE[Helicopter] = nil + end + ) + + -- We need to capture the Land events for the helicopters. + -- The helicopter reference is used in the semaphore AI_CARGO_QUEUE. + -- So, we need to unlock this when the helo has landed, which can be anywhere ... + -- But only free the landing coordinate after 1 minute, to ensure that all helos have left. + Helicopter:HandleEvent( EVENTS.Land, + function( Helicopter, EventData ) + self:ScheduleOnce( 60, + function( Helicopter ) + AI_CARGO_QUEUE[Helicopter] = nil + end, Helicopter + ) + end + ) + + self:SetCarrier( Helicopter ) + + return self +end + + + + + +--- Set the Carrier. +-- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @return #AI_CARGO_HELICOPTER +function AI_CARGO_HELICOPTER:SetCarrier( Helicopter ) + + local AICargo = self + + self.Helicopter = Helicopter -- Wrapper.Group#GROUP + self.Helicopter:SetState( self.Helicopter, "AI_CARGO_HELICOPTER", self ) + + self.RoutePickup = false + self.RouteDeploy = false + + Helicopter:HandleEvent( EVENTS.Dead ) + Helicopter:HandleEvent( EVENTS.Hit ) + Helicopter:HandleEvent( EVENTS.Land ) + + function Helicopter:OnEventDead( EventData ) + local AICargoTroops = self:GetState( self, "AI_CARGO_HELICOPTER" ) + self:F({AICargoTroops=AICargoTroops}) + if AICargoTroops then + self:F({}) + if not AICargoTroops:Is( "Loaded" ) then + -- There are enemies within combat range. Unload the Helicopter. + AICargoTroops:Destroyed() + end + end + end + + function Helicopter:OnEventLand( EventData ) + AICargo:Landed() + end + + self.Coalition = self.Helicopter:GetCoalition() + + self:SetControllable( Helicopter ) + + return self +end + + +--- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @param From +-- @param Event +-- @param To +function AI_CARGO_HELICOPTER:onafterLanded( Helicopter, From, Event, To ) + + Helicopter:F( { Name = Helicopter:GetName() } ) + + if Helicopter and Helicopter:IsAlive() then + + -- S_EVENT_LAND is directly called in two situations: + -- 1 - When the helo lands normally on the ground. + -- 2 - when the helo is hit and goes RTB or even when it is destroyed. + -- For point 2, this is an issue, the infantry may not unload in this case! + -- So we check if the helo is on the ground, and velocity< 5. + -- Only then the infantry can unload (and load too, for consistency)! + + self:F( { Helicopter:GetName(), Height = Helicopter:GetHeight( true ), Velocity = Helicopter:GetVelocityKMH() } ) + + if self.RoutePickup == true then + if Helicopter:GetHeight( true ) <= 5 and Helicopter:GetVelocityKMH() < 10 then + --self:Load( Helicopter:GetPointVec2() ) + self:Load( self.PickupZone ) + self.RoutePickup = false + end + end + + if self.RouteDeploy == true then + if Helicopter:GetHeight( true ) <= 5 and Helicopter:GetVelocityKMH() < 10 then + self:Unload( self.DeployZone ) + self.RouteDeploy = false + end + end + + end + +end + +--- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate +-- @param #number Speed +function AI_CARGO_HELICOPTER:onafterQueue( Helicopter, From, Event, To, Coordinate, Speed, DeployZone ) + + local HelicopterInZone = false + + if Helicopter and Helicopter:IsAlive() == true then + + local Distance = Coordinate:DistanceFromPointVec2( Helicopter:GetCoordinate() ) + + if Distance > 2000 then + self:__Queue( -10, Coordinate, Speed, DeployZone ) + else + + local ZoneFree = true + + for Helicopter, ZoneQueue in pairs( AI_CARGO_QUEUE ) do + local ZoneQueue = ZoneQueue -- Core.Zone#ZONE_RADIUS + if ZoneQueue:IsCoordinateInZone( Coordinate ) then + ZoneFree = false + end + end + + self:F({ZoneFree=ZoneFree}) + + if ZoneFree == true then + + local ZoneQueue = ZONE_RADIUS:New( Helicopter:GetName(), Coordinate:GetVec2(), 100 ) + + AI_CARGO_QUEUE[Helicopter] = ZoneQueue + + local Route = {} + +-- local CoordinateFrom = Helicopter:GetCoordinate() +-- local WaypointFrom = CoordinateFrom:WaypointAir( +-- "RADIO", +-- POINT_VEC3.RoutePointType.TurningPoint, +-- POINT_VEC3.RoutePointAction.TurningPoint, +-- Speed, +-- true +-- ) +-- Route[#Route+1] = WaypointFrom + local CoordinateTo = Coordinate + local WaypointTo = CoordinateTo:WaypointAir( + "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + 50, + true + ) + Route[#Route+1] = WaypointTo + + local Tasks = {} + Tasks[#Tasks+1] = Helicopter:TaskLandAtVec2( CoordinateTo:GetVec2() ) + Route[#Route].task = Helicopter:TaskCombo( Tasks ) + + Route[#Route+1] = WaypointTo + + -- Now route the helicopter + Helicopter:Route( Route, 0 ) + + -- Keep the DeployZone, because when the helo has landed, we want to provide the DeployZone to the mission designer as part of the Unloaded event. + self.DeployZone = DeployZone + + else + self:__Queue( -10, Coordinate, Speed, DeployZone ) + end + end + else + AI_CARGO_QUEUE[Helicopter] = nil + end +end + + +--- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate +-- @param #number Speed +function AI_CARGO_HELICOPTER:onafterOrbit( Helicopter, From, Event, To, Coordinate ) + + if Helicopter and Helicopter:IsAlive() then + + local Route = {} + +-- local CoordinateFrom = Helicopter:GetCoordinate() +-- local WaypointFrom = CoordinateFrom:WaypointAir( +-- "RADIO", +-- POINT_VEC3.RoutePointType.TurningPoint, +-- POINT_VEC3.RoutePointAction.TurningPoint, +-- Speed, +-- true +-- ) +-- Route[#Route+1] = WaypointFrom + local CoordinateTo = Coordinate + local WaypointTo = CoordinateTo:WaypointAir( + "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + 50, + true + ) + Route[#Route+1] = WaypointTo + + local Tasks = {} + Tasks[#Tasks+1] = Helicopter:TaskOrbitCircle( math.random( 30, 80 ), 150, CoordinateTo:GetRandomCoordinateInRadius( 800, 500 ) ) + Route[#Route].task = Helicopter:TaskCombo( Tasks ) + + Route[#Route+1] = WaypointTo + + -- Now route the helicopter + Helicopter:Route( Route, 0 ) + end +end + + + +--- On after Deployed event. +-- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Cargo.Cargo#CARGO Cargo Cargo object. +-- @param #boolean Deployed Cargo is deployed. +-- @return #boolean True if all cargo has been unloaded. +function AI_CARGO_HELICOPTER:onafterDeployed( Helicopter, From, Event, To, DeployZone ) + self:F( { Helicopter, From, Event, To, DeployZone = DeployZone } ) + + self:Orbit( Helicopter:GetCoordinate(), 50 ) + + -- Free the coordinate zone after 30 seconds, so that the original helicopter can fly away first. + self:ScheduleOnce( 30, + function( Helicopter ) + AI_CARGO_QUEUE[Helicopter] = nil + end, Helicopter + ) + + self:GetParent( self, AI_CARGO_HELICOPTER ).onafterDeployed( self, Helicopter, From, Event, To, DeployZone ) + + +end + +--- On after Pickup event. +-- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate Pickup place. +-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the pickup coordinate. This parameter is ignored for APCs. +-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided. +function AI_CARGO_HELICOPTER:onafterPickup( Helicopter, From, Event, To, Coordinate, Speed, Height, PickupZone ) + + if Helicopter and Helicopter:IsAlive() ~= nil then + + Helicopter:Activate() + + self.RoutePickup = true + Coordinate.y = Height + + local _speed=Speed or Helicopter:GetSpeedMax()*0.5 + + local Route = {} + + --- Calculate the target route point. + local CoordinateFrom = Helicopter:GetCoordinate() + local CoordinateTo = Coordinate + + --- Create a route point of type air. + local WaypointFrom = CoordinateFrom:WaypointAir( + "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + _speed, + true + ) + + --- Create a route point of type air. + local WaypointTo = CoordinateTo:WaypointAir( + "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + _speed, + true + ) + + Route[#Route+1] = WaypointFrom + Route[#Route+1] = WaypointTo + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + Helicopter:WayPointInitialize( Route ) + + local Tasks = {} + + Tasks[#Tasks+1] = Helicopter:TaskLandAtVec2( CoordinateTo:GetVec2() ) + Route[#Route].task = Helicopter:TaskCombo( Tasks ) + + Route[#Route+1] = WaypointTo + + -- Now route the helicopter + Helicopter:Route( Route, 1 ) + + self.PickupZone = PickupZone + + self:GetParent( self, AI_CARGO_HELICOPTER ).onafterPickup( self, Helicopter, From, Event, To, Coordinate, Speed, Height, PickupZone ) + + end + +end + +--- Depoloy function and queue. +-- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP AICargoHelicopter +-- @param Core.Point#COORDINATE Coordinate Coordinate +function AI_CARGO_HELICOPTER:_Deploy( AICargoHelicopter, Coordinate, DeployZone ) + AICargoHelicopter:__Queue( -10, Coordinate, 100, DeployZone ) +end + +--- On after Deploy event. +-- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter Transport helicopter. +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate Place at which the cargo is deployed. +-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the deploy coordinate. +function AI_CARGO_HELICOPTER:onafterDeploy( Helicopter, From, Event, To, Coordinate, Speed, Height, DeployZone ) + + if Helicopter and Helicopter:IsAlive() ~= nil then + + self.RouteDeploy = true + + + local Route = {} + + --- Calculate the target route point. + + Coordinate.y = Height + + local _speed=Speed or Helicopter:GetSpeedMax()*0.5 + + --- Create a route point of type air. + local CoordinateFrom = Helicopter:GetCoordinate() + local WaypointFrom = CoordinateFrom:WaypointAir( + "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + _speed, + true + ) + Route[#Route+1] = WaypointFrom + Route[#Route+1] = WaypointFrom + + --- Create a route point of type air. + local CoordinateTo = Coordinate + local WaypointTo = CoordinateTo:WaypointAir( + "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + _speed, + true + ) + + Route[#Route+1] = WaypointTo + Route[#Route+1] = WaypointTo + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + Helicopter:WayPointInitialize( Route ) + + local Tasks = {} + + Tasks[#Tasks+1] = Helicopter:TaskFunction( "AI_CARGO_HELICOPTER._Deploy", self, Coordinate, DeployZone ) + Tasks[#Tasks+1] = Helicopter:TaskOrbitCircle( math.random( 30, 100 ), _speed, CoordinateTo:GetRandomCoordinateInRadius( 800, 500 ) ) + + --Tasks[#Tasks+1] = Helicopter:TaskLandAtVec2( CoordinateTo:GetVec2() ) + Route[#Route].task = Helicopter:TaskCombo( Tasks ) + + Route[#Route+1] = WaypointTo + + -- Now route the helicopter + Helicopter:Route( Route, 0 ) + + self:GetParent( self, AI_CARGO_HELICOPTER ).onafterDeploy( self, Helicopter, From, Event, To, Coordinate, Speed, Height, DeployZone ) + end + +end + + +--- On after Home event. +-- @param #AI_CARGO_HELICOPTER self +-- @param Wrapper.Group#GROUP Helicopter +-- @param From +-- @param Event +-- @param To +-- @param Core.Point#COORDINATE Coordinate Home place. +-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go. +-- @param #number Height Height in meters to move to the home coordinate. +-- @param Core.Zone#ZONE HomeZone The zone wherein the carrier will return when all cargo has been transported. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. +function AI_CARGO_HELICOPTER:onafterHome( Helicopter, From, Event, To, Coordinate, Speed, Height, HomeZone ) + + if Helicopter and Helicopter:IsAlive() ~= nil then + + self.RouteHome = true + + local Route = {} + + --- Calculate the target route point. + + Coordinate.y = Height + + Speed = Speed or Helicopter:GetSpeedMax()*0.5 + + --- Create a route point of type air. + local CoordinateFrom = Helicopter:GetCoordinate() + local WaypointFrom = CoordinateFrom:WaypointAir( + "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + Speed , + true + ) + Route[#Route+1] = WaypointFrom + + --- Create a route point of type air. + local CoordinateTo = Coordinate + local WaypointTo = CoordinateTo:WaypointAir( + "RADIO", + POINT_VEC3.RoutePointType.TurningPoint, + POINT_VEC3.RoutePointAction.TurningPoint, + Speed , + true + ) + + Route[#Route+1] = WaypointTo + + --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable... + Helicopter:WayPointInitialize( Route ) + + local Tasks = {} + + Tasks[#Tasks+1] = Helicopter:TaskLandAtVec2( CoordinateTo:GetVec2() ) + Route[#Route].task = Helicopter:TaskCombo( Tasks ) + + Route[#Route+1] = WaypointTo + + -- Now route the helicopter + Helicopter:Route( Route, 0 ) + + end + +end + diff --git a/Moose Development/Moose/AI/AI_Formation.lua b/Moose Development/Moose/AI/AI_Formation.lua index 7e5b2828e..c02096609 100644 --- a/Moose Development/Moose/AI/AI_Formation.lua +++ b/Moose Development/Moose/AI/AI_Formation.lua @@ -1,35 +1,11 @@ ---- **AI** -- (R2.2) - Build large airborne formations of aircraft. +--- **AI** -- Build large airborne formations of aircraft. -- --- === +-- **Features:** +-- +-- * Build in-air formations consisting of more than 40 aircraft as one group. +-- * Build different formation types. +-- * Assign a group leader that will guide the large formation path. -- --- ![Banner Image](..\Presentations\AI_FORMATION\Dia1.JPG) --- --- === --- --- AI_FORMATION makes AI @{GROUP}s fly in formation of various compositions. --- The AI_FORMATION class models formations in a different manner than the internal DCS formation logic!!! --- The purpose of the class is to: --- --- * Make formation building a process that can be managed while in flight, rather than a task. --- * Human players can guide formations, consisting of larget planes. --- * Build large formations (like a large bomber field). --- * Form formations that DCS does not support off the shelve. --- --- A few remarks: --- --- * Depending on the type of plane, the change in direction by the leader may result in the formation getting disentangled while in flight and needs to be rebuild. --- * Formations are vulnerable to collissions, but is depending on the type of plane, the distance between the planes and the speed and angle executed by the leader. --- * Formations may take a while to build up. --- --- As a result, the AI_FORMATION is not perfect, but is very useful to: --- --- * Model large formations when flying straight line. --- * Make humans guide a large formation, when the planes are wide from each other. --- --- There are the following types of classes defined: --- --- * @{#AI_FORMATION}: Create a formation from several @{GROUP}s. --- -- === -- -- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/FOR%20-%20Formation) @@ -45,13 +21,14 @@ -- -- === -- --- @module AI_Formation +-- @module AI.AI_Formation +-- @image AI_Large_Formations.JPG --- AI_FORMATION class -- @type AI_FORMATION --- @extends Fsm#FSM_SET --- @field Unit#UNIT FollowUnit --- @field Set#SET_GROUP FollowGroupSet +-- @extends Core.Fsm#FSM_SET +-- @field Wrapper.Unit#UNIT FollowUnit +-- @field Core.Set#SET_GROUP FollowGroupSet -- @field #string FollowName -- @field #AI_FORMATION.MODE FollowMode The mode the escort is in. -- @field Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. @@ -59,11 +36,10 @@ -- @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. ---- # AI_FORMATION class, extends @{Fsm#FSM_SET} --- --- The #AI_FORMATION class allows you to build large formations, make AI follow a @{Client#CLIENT} (player) leader or a @{Unit#UNIT} (AI) leader. +--- Build large formations, make AI follow a @{Wrapper.Client#CLIENT} (player) leader or a @{Wrapper.Unit#UNIT} (AI) leader. -- -- AI_FORMATION makes AI @{GROUP}s fly in formation of various compositions. -- The AI_FORMATION class models formations in a different manner than the internal DCS formation logic!!! @@ -89,25 +65,25 @@ -- -- Create a new SPAWN object with the @{#AI_FORMATION.New} method: -- --- * @{Follow#AI_FORMATION.New}(): Creates a new AI_FORMATION object from a @{Group#GROUP} for a @{Client#CLIENT} or a @{Unit#UNIT}, with an optional briefing text. +-- * @{#AI_FORMATION.New}(): Creates a new AI_FORMATION object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT} or a @{Wrapper.Unit#UNIT}, with an optional briefing text. -- -- ## Formation methods -- -- The following methods can be used to set or change the formation: -- --- * @{AI_Formation#AI_FORMATION.FormationLine}(): Form a line formation (core formation function). --- * @{AI_Formation#AI_FORMATION.FormationTrail}(): Form a trail formation. --- * @{AI_Formation#AI_FORMATION.FormationLeftLine}(): Form a left line formation. --- * @{AI_Formation#AI_FORMATION.FormationRightLine}(): Form a right line formation. --- * @{AI_Formation#AI_FORMATION.FormationRightWing}(): Form a right wing formation. --- * @{AI_Formation#AI_FORMATION.FormationLeftWing}(): Form a left wing formation. --- * @{AI_Formation#AI_FORMATION.FormationCenterWing}(): Form a center wing formation. --- * @{AI_Formation#AI_FORMATION.FormationCenterVic}(): Form a Vic formation (same as CenterWing. --- * @{AI_Formation#AI_FORMATION.FormationCenterBoxed}(): Form a center boxed formation. +-- * @{#AI_FORMATION.FormationLine}(): Form a line formation (core formation function). +-- * @{#AI_FORMATION.FormationTrail}(): Form a trail formation. +-- * @{#AI_FORMATION.FormationLeftLine}(): Form a left line formation. +-- * @{#AI_FORMATION.FormationRightLine}(): Form a right line formation. +-- * @{#AI_FORMATION.FormationRightWing}(): Form a right wing formation. +-- * @{#AI_FORMATION.FormationLeftWing}(): Form a left wing formation. +-- * @{#AI_FORMATION.FormationCenterWing}(): Form a center wing formation. +-- * @{#AI_FORMATION.FormationCenterVic}(): Form a Vic formation (same as CenterWing. +-- * @{#AI_FORMATION.FormationCenterBoxed}(): Form a center boxed formation. -- -- ## Randomization -- --- Use the method @{AI_Formation#AI_FORMATION.SetFlightRandomization}() to simulate the formation flying errors that pilots make while in formation. Is a range set in meters. +-- Use the method @{AI.AI_Formation#AI_FORMATION.SetFlightRandomization}() to simulate the formation flying errors that pilots make while in formation. Is a range set in meters. -- -- @usage -- local FollowGroupSet = SET_GROUP:New():FilterCategories("plane"):FilterCoalitions("blue"):FilterPrefixes("Follow"):FilterStart() @@ -131,6 +107,7 @@ 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 @@ -147,16 +124,17 @@ AI_FORMATION = { --- AI_FORMATION class constructor for an AI group -- @param #AI_FORMATION self --- @param Unit#UNIT FollowUnit The UNIT leading the FolllowGroupSet. +-- @param Wrapper.Unit#UNIT FollowUnit The UNIT leading the FolllowGroupSet. -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit. -- @param #string FollowName Name of the escort. +-- @param #string FollowBriefing Briefing. -- @return #AI_FORMATION self function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefing ) --R2.1 local self = BASE:Inherit( self, FSM_SET:New( FollowGroupSet ) ) self:F( { FollowUnit, FollowGroupSet, FollowName } ) - self.FollowUnit = FollowUnit -- Unit#UNIT - self.FollowGroupSet = FollowGroupSet -- Set#SET_GROUP + self.FollowUnit = FollowUnit -- Wrapper.Unit#UNIT + self.FollowGroupSet = FollowGroupSet -- Core.Set#SET_GROUP self:SetFlightRandomization( 2 ) @@ -164,7 +142,7 @@ function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefin self:AddTransition( "*", "Stop", "Stopped" ) - self:AddTransition( "None", "Start", "Following" ) + self:AddTransition( {"None", "Stopped"}, "Start", "Following" ) self:AddTransition( "*", "FormationLine", "*" ) --- FormationLine Handler OnBefore for AI_FORMATION @@ -645,6 +623,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 @@ -675,7 +663,7 @@ function AI_FORMATION:onafterFormationLine( FollowGroupSet, From , Event , To, X local FollowSet = FollowGroupSet:GetSet() - local i = 0 + local i = 1 --FF i=0 caused first unit to have no XSpace! Probably needs further adjustments. This is just a quick work around. for FollowID, FollowGroup in pairs( FollowSet ) do @@ -906,7 +894,7 @@ function AI_FORMATION:onafterFormationBox( FollowGroupSet, From , Event , To, XS end ---- Use the method @{AI_Formation#AI_FORMATION.SetFlightRandomization}() to make the air units in your formation randomize their flight a bit while in formation. +--- Use the method @{AI.AI_Formation#AI_FORMATION.SetFlightRandomization}() to make the air units in your formation randomize their flight a bit while in formation. -- @param #AI_FORMATION self -- @param #number FlightRandomization The formation flying errors that pilots can make while in formation. Is a range set in meters. -- @return #AI_FORMATION @@ -918,7 +906,30 @@ function AI_FORMATION:SetFlightRandomization( FlightRandomization ) --R2.1 end ---- @param Follow#AI_FORMATION self +--- 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:F( ) @@ -993,7 +1004,7 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 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 + 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 } @@ -1057,8 +1068,8 @@ function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1 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 4cc6e7d6e..c4c0330bf 100644 --- a/Moose Development/Moose/AI/AI_Patrol.lua +++ b/Moose Development/Moose/AI/AI_Patrol.lua @@ -1,8 +1,10 @@ ---- **AI** -- (R2.1) - Manages the independent process of Air Patrol for airplanes. +--- **AI** -- Perform Air Patrolling for airplanes. -- --- === +-- **Features:** -- --- ![Banner Image](..\Presentations\AI_PATROL\Dia1.JPG) +-- * Patrol AI airplanes within a given zone. +-- * Trigger detected events when enemy airplanes are detected. +-- * Manage a fuel treshold to RTB on time. -- -- === -- @@ -30,27 +32,25 @@ -- -- === -- --- @module AI_Patrol - +-- @module AI.AI_Patrol +-- @image AI_Air_Patrolling.JPG --- AI_PATROL_ZONE class -- @type AI_PATROL_ZONE --- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Controllable} patrolling. +-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling. -- @field Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @field Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @field Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @field Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @field Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. +-- @field DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @field DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @field DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @field DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. -- @field Core.Spawn#SPAWN CoordTest -- @extends Core.Fsm#FSM_CONTROLLABLE ---- # AI_PATROL_ZONE class, extends @{Fsm#FSM_CONTROLLABLE} --- --- The AI_PATROL_ZONE class implements the core functions to patrol a @{Zone} by an AI @{Controllable} or @{Group}. +--- Implements the core functions to patrol a @{Zone} by an AI @{Wrapper.Controllable} or @{Wrapper.Group}. -- -- ![Process](..\Presentations\AI_PATROL\Dia3.JPG) -- --- The AI_PATROL_ZONE is assigned a @{Group} and this must be done before the AI_PATROL_ZONE process can be started using the **Start** event. +-- The AI_PATROL_ZONE is assigned a @{Wrapper.Group} and this must be done before the AI_PATROL_ZONE process can be started using the **Start** event. -- -- ![Process](..\Presentations\AI_PATROL\Dia4.JPG) -- @@ -123,7 +123,7 @@ -- * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased. -- -- The detection frequency can be set with @{#AI_PATROL_ZONE.SetRefreshTimeInterval}( seconds ), where the amount of seconds specify how much seconds will be waited before the next detection. --- Use the method @{#AI_PATROL_ZONE.GetDetectedUnits}() to obtain a list of the @{Unit}s detected by the AI. +-- Use the method @{#AI_PATROL_ZONE.GetDetectedUnits}() to obtain a list of the @{Wrapper.Unit}s detected by the AI. -- -- The detection can be filtered to potential targets in a specific zone. -- Use the method @{#AI_PATROL_ZONE.SetDetectionZone}() to set the zone where targets need to be detected. @@ -155,11 +155,11 @@ AI_PATROL_ZONE = { --- Creates a new AI_PATROL_ZONE object -- @param #AI_PATROL_ZONE self -- @param Core.Zone#ZONE_BASE PatrolZone The @{Zone} where the patrol needs to be executed. --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO -- @return #AI_PATROL_ZONE self -- @usage -- -- Define a new AI_PATROL_ZONE Object. This PatrolArea will patrol an AIControllable within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h. @@ -454,8 +454,8 @@ end --- Sets (modifies) the minimum and maximum speed of the patrol. -- @param #AI_PATROL_ZONE self --- @param Dcs.DCSTypes#Speed PatrolMinSpeed The minimum speed of the @{Controllable} in km/h. --- @param Dcs.DCSTypes#Speed PatrolMaxSpeed The maximum speed of the @{Controllable} in km/h. +-- @param DCS#Speed PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h. +-- @param DCS#Speed PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h. -- @return #AI_PATROL_ZONE self function AI_PATROL_ZONE:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed ) self:F2( { PatrolMinSpeed, PatrolMaxSpeed } ) @@ -468,8 +468,8 @@ end --- Sets the floor and ceiling altitude of the patrol. -- @param #AI_PATROL_ZONE self --- @param Dcs.DCSTypes#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. --- @param Dcs.DCSTypes#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol. +-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol. -- @return #AI_PATROL_ZONE self function AI_PATROL_ZONE:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude ) self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } ) @@ -562,18 +562,18 @@ function AI_PATROL_ZONE:SetDetectionZone( DetectionZone ) end end ---- Gets a list of @{Unit#UNIT}s that were detected by the AI. +--- Gets a list of @{Wrapper.Unit#UNIT}s that were detected by the AI. -- No filtering is applied, so, ANY detected UNIT can be in this list. --- It is up to the mission designer to use the @{Unit} class and methods to filter the targets. +-- It is up to the mission designer to use the @{Wrapper.Unit} class and methods to filter the targets. -- @param #AI_PATROL_ZONE self --- @return #table The list of @{Unit#UNIT}s +-- @return #table The list of @{Wrapper.Unit#UNIT}s function AI_PATROL_ZONE:GetDetectedUnits() self:F2() return self.DetectedUnits end ---- Clears the list of @{Unit#UNIT}s that were detected by the AI. +--- Clears the list of @{Wrapper.Unit#UNIT}s that were detected by the AI. -- @param #AI_PATROL_ZONE self function AI_PATROL_ZONE:ClearDetectedUnits() self:F2() @@ -590,7 +590,6 @@ end -- @return #AI_PATROL_ZONE self function AI_PATROL_ZONE:ManageFuel( PatrolFuelThresholdPercentage, PatrolOutOfFuelOrbitTime ) - self.PatrolManageFuel = true self.PatrolFuelThresholdPercentage = PatrolFuelThresholdPercentage self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime @@ -824,7 +823,7 @@ function AI_PATROL_ZONE:onafterStatus() local RTB = false - local Fuel = self.Controllable:GetUnit(1):GetFuel() + local Fuel = self.Controllable:GetFuelMin() if Fuel < self.PatrolFuelThresholdPercentage then self:E( self.Controllable:GetName() .. " is out of fuel:" .. Fuel .. ", RTB!" ) local OldAIControllable = self.Controllable diff --git a/Moose Development/Moose/Actions/Act_Account.lua b/Moose Development/Moose/Actions/Act_Account.lua index 809a28690..b0d26c9ea 100644 --- a/Moose Development/Moose/Actions/Act_Account.lua +++ b/Moose Development/Moose/Actions/Act_Account.lua @@ -1,15 +1,15 @@ ---- **Actions** - ACT_ACCOUNT_ classes **account for** (detect, count & report) various DCS events occuring on @{Unit}s. +--- **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 Account - +-- @module Actions.Account +-- @image MOOSE.JPG do -- ACT_ACCOUNT - --- # @{#ACT_ACCOUNT} FSM class, extends @{Fsm#FSM_PROCESS} + --- # @{#ACT_ACCOUNT} FSM class, extends @{Core.Fsm#FSM_PROCESS} -- -- ## ACT_ACCOUNT state machine: -- @@ -55,7 +55,7 @@ do -- ACT_ACCOUNT -- These state transition methods need to provide a return value, which is specified at the function description. -- -- @type ACT_ACCOUNT - -- @field Set#SET_UNIT TargetSetUnit + -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends Core.Fsm#FSM_PROCESS ACT_ACCOUNT = { ClassName = "ACT_ACCOUNT", @@ -138,7 +138,7 @@ end -- ACT_ACCOUNT do -- ACT_ACCOUNT_DEADS - --- # @{#ACT_ACCOUNT_DEADS} FSM class, extends @{Fsm.Account#ACT_ACCOUNT} + --- # @{#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. @@ -151,7 +151,7 @@ do -- ACT_ACCOUNT_DEADS -- * @{#ACT_ACCOUNT_DEADS.New}(): Creates a new ACT_ACCOUNT_DEADS object. -- -- @type ACT_ACCOUNT_DEADS - -- @field Set#SET_UNIT TargetSetUnit + -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends #ACT_ACCOUNT ACT_ACCOUNT_DEADS = { ClassName = "ACT_ACCOUNT_DEADS", @@ -160,7 +160,7 @@ do -- ACT_ACCOUNT_DEADS --- Creates a new DESTROY process. -- @param #ACT_ACCOUNT_DEADS self - -- @param Set#SET_UNIT TargetSetUnit + -- @param Core.Set#SET_UNIT TargetSetUnit -- @param #string TaskName function ACT_ACCOUNT_DEADS:New() -- Inherits from BASE @@ -285,7 +285,7 @@ do -- ACT_ACCOUNT_DEADS end --- @param #ACT_ACCOUNT_DEADS self - -- @param Event#EVENTDATA EventData + -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:onfuncEventDead( EventData ) self:T( { "EventDead", EventData } ) @@ -297,7 +297,7 @@ do -- ACT_ACCOUNT_DEADS --- DCS Events --- @param #ACT_ACCOUNT_DEADS self - -- @param Event#EVENTDATA EventData + -- @param Core.Event#EVENTDATA EventData function ACT_ACCOUNT_DEADS:onfuncEventCrash( EventData ) self:T( { "EventDead", EventData } ) diff --git a/Moose Development/Moose/Actions/Act_Assign.lua b/Moose Development/Moose/Actions/Act_Assign.lua index 930810ee2..969009ab1 100644 --- a/Moose Development/Moose/Actions/Act_Assign.lua +++ b/Moose Development/Moose/Actions/Act_Assign.lua @@ -2,7 +2,7 @@ -- -- === -- --- # @{#ACT_ASSIGN} FSM template class, extends @{Fsm#FSM_PROCESS} +-- # @{#ACT_ASSIGN} FSM template class, extends @{Core.Fsm#FSM_PROCESS} -- -- ## ACT_ASSIGN state machine: -- @@ -54,7 +54,7 @@ -- -- === -- --- # 1) @{#ACT_ASSIGN_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} +-- # 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. -- @@ -64,7 +64,7 @@ -- -- === -- --- # 2) @{#ACT_ASSIGN_MENU_ACCEPT} class, extends @{Fsm.Assign#ACT_ASSIGN} +-- # 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. @@ -77,7 +77,8 @@ -- -- === -- --- @module Assign +-- @module Actions.Assign +-- @image MOOSE.JPG do -- ACT_ASSIGN @@ -155,8 +156,7 @@ do -- ACT_ASSIGN_ACCEPT -- @param #string Event -- @param #string From -- @param #string To - function ACT_ASSIGN_ACCEPT:onafterStart( ProcessUnit, From, Event, To ) - self:F( { ProcessUnit, From, Event, To } ) + function ACT_ASSIGN_ACCEPT:onafterStart( ProcessUnit, Task, From, Event, To ) self:__Assign( 1 ) end @@ -167,11 +167,8 @@ do -- ACT_ASSIGN_ACCEPT -- @param #string Event -- @param #string From -- @param #string To - function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, From, Event, To ) - self:F( { ProcessUnit, From, Event, To } ) + function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, Task, From, Event, To ) - local ProcessGroup = ProcessUnit:GetGroup() - self.Task:Assign( ProcessUnit, ProcessUnit:GetPlayerName() ) end @@ -192,36 +189,26 @@ do -- ACT_ASSIGN_MENU_ACCEPT --- Init. -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param #string TaskName -- @param #string TaskBriefing -- @return #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:New( TaskName, TaskBriefing ) + function ACT_ASSIGN_MENU_ACCEPT:New( TaskBriefing ) -- Inherits from BASE local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_MENU_ACCEPT - self.TaskName = TaskName self.TaskBriefing = TaskBriefing return self end - function ACT_ASSIGN_MENU_ACCEPT:Init( FsmAssign ) - - self.TaskName = FsmAssign.TaskName - self.TaskBriefing = FsmAssign.TaskBriefing - end - - + --- Creates a new task assignment state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator. -- @param #ACT_ASSIGN_MENU_ACCEPT self - -- @param #string TaskName -- @param #string TaskBriefing -- @return #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:Init( TaskName, TaskBriefing ) + function ACT_ASSIGN_MENU_ACCEPT:Init( TaskBriefing ) self.TaskBriefing = TaskBriefing - self.TaskName = TaskName return self end @@ -232,30 +219,31 @@ do -- ACT_ASSIGN_MENU_ACCEPT -- @param #string Event -- @param #string From -- @param #string To - function ACT_ASSIGN_MENU_ACCEPT:onafterStart( ProcessUnit, From, Event, To ) - self:F( { ProcessUnit, From, Event, To } ) + function ACT_ASSIGN_MENU_ACCEPT:onafterStart( ProcessUnit, Task, From, Event, To ) - self:GetCommandCenter():MessageTypeToGroup( "Access the radio menu to accept the task. You have 30 seconds or the assignment will be cancelled.", ProcessUnit:GetGroup(), MESSAGE.Type.Information ) + 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 ProcessGroup = 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.Menu = MENU_GROUP:New( ProcessGroup, "Task " .. self.TaskName .. " acceptance" ) - self.MenuAcceptTask = MENU_GROUP_COMMAND:New( ProcessGroup, "Accept task " .. self.TaskName, self.Menu, self.MenuAssign, self ) - self.MenuRejectTask = MENU_GROUP_COMMAND:New( ProcessGroup, "Reject task " .. self.TaskName, self.Menu, self.MenuReject, self ) + self:__Reject( 120, TaskGroup ) end --- Menu function. -- @param #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:MenuAssign() + function ACT_ASSIGN_MENU_ACCEPT:MenuAssign( TaskGroup ) - self:__Assign( 1 ) + self:__Assign( -1, TaskGroup ) end --- Menu function. -- @param #ACT_ASSIGN_MENU_ACCEPT self - function ACT_ASSIGN_MENU_ACCEPT:MenuReject() + function ACT_ASSIGN_MENU_ACCEPT:MenuReject( TaskGroup ) - self:__Reject( 1 ) + self:__Reject( -1, TaskGroup ) end --- StateMachine callback function @@ -264,8 +252,7 @@ do -- ACT_ASSIGN_MENU_ACCEPT -- @param #string Event -- @param #string From -- @param #string To - function ACT_ASSIGN_MENU_ACCEPT:onafterAssign( ProcessUnit, From, Event, To ) - self:F( { ProcessUnit.UnitNameFrom, Event, To } ) + function ACT_ASSIGN_MENU_ACCEPT:onafterAssign( ProcessUnit, Task, From, Event, To, TaskGroup ) self.Menu:Remove() end @@ -276,13 +263,25 @@ do -- ACT_ASSIGN_MENU_ACCEPT -- @param #string Event -- @param #string From -- @param #string To - function ACT_ASSIGN_MENU_ACCEPT:onafterReject( ProcessUnit, From, Event, To ) - self:F( { ProcessUnit.UnitName, From, Event, 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 - ProcessUnit:Destroy() + self.Task:RejectGroup( TaskGroup ) + end + + --- StateMachine callback function + -- @param #ACT_ASSIGN_ACCEPT self + -- @param Wrapper.Unit#UNIT ProcessUnit + -- @param #string Event + -- @param #string From + -- @param #string To + function ACT_ASSIGN_MENU_ACCEPT:onenterAssigned( ProcessUnit, Task, From, Event, To, TaskGroup ) + + --self.Task:AssignToGroup( TaskGroup ) + self.Task:Assign( ProcessUnit, ProcessUnit:GetPlayerName() ) end end -- ACT_ASSIGN_MENU_ACCEPT diff --git a/Moose Development/Moose/Actions/Act_Assist.lua b/Moose Development/Moose/Actions/Act_Assist.lua index 7d4e55d85..f9cd5fc3a 100644 --- a/Moose Development/Moose/Actions/Act_Assist.lua +++ b/Moose Development/Moose/Actions/Act_Assist.lua @@ -1,9 +1,5 @@ --- (SP) (MP) (FSM) Route AI or players through waypoints or to zones. -- --- === --- --- # @{#ACT_ASSIST} FSM class, extends @{Fsm#FSM_PROCESS} --- -- ## ACT_ASSIST state machine: -- -- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur. @@ -52,7 +48,7 @@ -- -- === -- --- # 1) @{#ACT_ASSIST_SMOKE_TARGETS_ZONE} class, extends @{Fsm.Route#ACT_ASSIST} +-- # 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. @@ -64,7 +60,9 @@ -- -- === -- --- @module Smoke +-- @module Actions.Assist +-- @image MOOSE.JPG + do -- ACT_ASSIST @@ -142,7 +140,7 @@ do -- ACT_ASSIST_SMOKE_TARGETS_ZONE --- ACT_ASSIST_SMOKE_TARGETS_ZONE class -- @type ACT_ASSIST_SMOKE_TARGETS_ZONE - -- @field Set#SET_UNIT TargetSetUnit + -- @field Core.Set#SET_UNIT TargetSetUnit -- @field Core.Zone#ZONE_BASE TargetZone -- @extends #ACT_ASSIST ACT_ASSIST_SMOKE_TARGETS_ZONE = { @@ -158,7 +156,7 @@ do -- ACT_ASSIST_SMOKE_TARGETS_ZONE --- 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 Set#SET_UNIT TargetSetUnit + -- @param Core.Set#SET_UNIT TargetSetUnit -- @param Core.Zone#ZONE_BASE TargetZone function ACT_ASSIST_SMOKE_TARGETS_ZONE:New( TargetSetUnit, TargetZone ) local self = BASE:Inherit( self, ACT_ASSIST:New() ) -- #ACT_ASSIST @@ -177,7 +175,7 @@ do -- ACT_ASSIST_SMOKE_TARGETS_ZONE --- 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 Set#SET_UNIT TargetSetUnit + -- @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 ) diff --git a/Moose Development/Moose/Actions/Act_JTAC.lua b/Moose Development/Moose/Actions/Act_JTAC.lua deleted file mode 100644 index 86f965eb0..000000000 --- a/Moose Development/Moose/Actions/Act_JTAC.lua +++ /dev/null @@ -1,198 +0,0 @@ ---- @module Process_JTAC - ---- PROCESS_JTAC class --- @type PROCESS_JTAC --- @field Wrapper.Unit#UNIT ProcessUnit --- @field Core.Set#SET_UNIT TargetSetUnit --- @extends Core.Fsm#FSM_PROCESS -PROCESS_JTAC = { - ClassName = "PROCESS_JTAC", - Fsm = {}, - TargetSetUnit = nil, -} - - ---- Creates a new DESTROY process. --- @param #PROCESS_JTAC self --- @param Tasking.Task#TASK Task --- @param Wrapper.Unit#UNIT ProcessUnit --- @param Core.Set#SET_UNIT TargetSetUnit --- @param Wrapper.Unit#UNIT FACUnit --- @return #PROCESS_JTAC self -function PROCESS_JTAC:New( Task, ProcessUnit, TargetSetUnit, FACUnit ) - - -- Inherits from BASE - local self = BASE:Inherit( self, PROCESS:New( "JTAC", Task, ProcessUnit ) ) -- #PROCESS_JTAC - - self.TargetSetUnit = TargetSetUnit - self.FACUnit = FACUnit - - self.DisplayInterval = 60 - self.DisplayCount = 30 - self.DisplayMessage = true - self.DisplayTime = 10 -- 10 seconds is the default - self.DisplayCategory = "HQ" -- Targets is the default display category - - - self.Fsm = FSM_PROCESS:New( self, { - initial = 'Assigned', - events = { - { name = 'Start', from = 'Assigned', to = 'CreatedMenu' }, - { name = 'JTACMenuUpdate', from = 'CreatedMenu', to = 'AwaitingMenu' }, - { name = 'JTACMenuAwait', from = 'AwaitingMenu', to = 'AwaitingMenu' }, - { name = 'JTACMenuSpot', from = 'AwaitingMenu', to = 'AwaitingMenu' }, - { name = 'JTACMenuCancel', from = 'AwaitingMenu', to = 'AwaitingMenu' }, - { name = 'JTACStatus', from = 'AwaitingMenu', to = 'AwaitingMenu' }, - { name = 'Fail', from = 'AwaitingMenu', to = 'Failed' }, - { name = 'Fail', from = 'CreatedMenu', to = 'Failed' }, - }, - callbacks = { - onStart = self.OnStart, - onJTACMenuUpdate = self.OnJTACMenuUpdate, - onJTACMenuAwait = self.OnJTACMenuAwait, - onJTACMenuSpot = self.OnJTACMenuSpot, - onJTACMenuCancel = self.OnJTACMenuCancel, - }, - endstates = { 'Failed' } - } ) - - self:HandleEvent( EVENTS.Dead, self.EventDead ) - - return self -end - ---- Process Events - ---- StateMachine callback function for a PROCESS --- @param #PROCESS_JTAC self --- @param Core.Fsm#FSM_PROCESS Fsm --- @param #string Event --- @param #string From --- @param #string To -function PROCESS_JTAC:OnStart( Fsm, From, Event, To ) - - self:NextEvent( Fsm.JTACMenuUpdate ) -end - ---- StateMachine callback function for a PROCESS --- @param #PROCESS_JTAC self --- @param Core.Fsm#FSM_PROCESS Fsm --- @param #string Event --- @param #string From --- @param #string To -function PROCESS_JTAC:OnJTACMenuUpdate( Fsm, From, Event, To ) - - local function JTACMenuSpot( MenuParam ) - self:F( MenuParam.TargetUnit.UnitName ) - local self = MenuParam.self - local TargetUnit = MenuParam.TargetUnit - - self:NextEvent( self.Fsm.JTACMenuSpot, TargetUnit ) - end - - local function JTACMenuCancel( MenuParam ) - self:F( MenuParam ) - local self = MenuParam.self - local TargetUnit = MenuParam.TargetUnit - - self:NextEvent( self.Fsm.JTACMenuCancel, TargetUnit ) - end - - - -- Loop each unit in the target set, and determine the threat levels map table. - local UnitThreatLevels = self.TargetSetUnit:GetUnitThreatLevels() - - self:F( {"UnitThreadLevels", UnitThreatLevels } ) - - local JTACMenu = self.ProcessGroup:GetState( self.ProcessGroup, "JTACMenu" ) - - if not JTACMenu then - JTACMenu = MENU_GROUP:New( self.ProcessGroup, "JTAC", self.MissionMenu ) - for ThreatLevel, ThreatLevelTable in pairs( UnitThreatLevels ) do - local JTACMenuThreatLevel = MENU_GROUP:New( self.ProcessGroup, ThreatLevelTable.UnitThreatLevelText, JTACMenu ) - for ThreatUnitName, ThreatUnit in pairs( ThreatLevelTable.Units ) do - local JTACMenuUnit = MENU_GROUP:New( self.ProcessGroup, ThreatUnit:GetTypeName(), JTACMenuThreatLevel ) - MENU_GROUP_COMMAND:New( self.ProcessGroup, "Lase Target", JTACMenuUnit, JTACMenuSpot, { self = self, TargetUnit = ThreatUnit } ) - MENU_GROUP_COMMAND:New( self.ProcessGroup, "Cancel Target", JTACMenuUnit, JTACMenuCancel, { self = self, TargetUnit = ThreatUnit } ) - end - end - end - -end - ---- StateMachine callback function for a PROCESS --- @param #PROCESS_JTAC self --- @param Core.Fsm#FSM_PROCESS Fsm --- @param #string Event --- @param #string From --- @param #string To -function PROCESS_JTAC:OnJTACMenuAwait( Fsm, From, Event, To ) - - if self.DisplayCount >= self.DisplayInterval then - - local TaskJTAC = self.Task -- Tasking.Task#TASK_JTAC - TaskJTAC.Spots = TaskJTAC.Spots or {} - for TargetUnitName, SpotData in pairs( TaskJTAC.Spots) do - local TargetUnit = UNIT:FindByName( TargetUnitName ) - self.FACUnit:MessageToGroup( "Lasing " .. TargetUnit:GetTypeName() .. " with laser code " .. SpotData:getCode(), 15, self.ProcessGroup ) - end - self.DisplayCount = 1 - else - self.DisplayCount = self.DisplayCount + 1 - end - - self:NextEvent( Fsm.JTACMenuAwait ) -end - ---- StateMachine callback function for a PROCESS --- @param #PROCESS_JTAC self --- @param Core.Fsm#FSM_PROCESS Fsm --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT TargetUnit -function PROCESS_JTAC:OnJTACMenuSpot( Fsm, From, Event, To, TargetUnit ) - - local TargetUnitName = TargetUnit:GetName() - - local TaskJTAC = self.Task -- Tasking.Task#TASK_JTAC - - TaskJTAC.Spots = TaskJTAC.Spots or {} - TaskJTAC.Spots[TargetUnitName] = TaskJTAC.Spots[TargetUnitName] or {} - - local DCSFACObject = self.FACUnit:GetDCSObject() - local TargetVec3 = TargetUnit:GetVec3() - - TaskJTAC.Spots[TargetUnitName] = Spot.createInfraRed( self.FACUnit:GetDCSObject(), { x = 0, y = 1, z = 0 }, TargetUnit:GetVec3(), math.random( 1000, 9999 ) ) - - local SpotData = TaskJTAC.Spots[TargetUnitName] - self.FACUnit:MessageToGroup( "Lasing " .. TargetUnit:GetTypeName() .. " with laser code " .. SpotData:getCode(), 15, self.ProcessGroup ) - - self:NextEvent( Fsm.JTACMenuAwait ) -end - ---- StateMachine callback function for a PROCESS --- @param #PROCESS_JTAC self --- @param Core.Fsm#FSM_PROCESS Fsm --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT TargetUnit -function PROCESS_JTAC:OnJTACMenuCancel( Fsm, From, Event, To, TargetUnit ) - - local TargetUnitName = TargetUnit:GetName() - - local TaskJTAC = self.Task -- Tasking.Task#TASK_JTAC - - TaskJTAC.Spots = TaskJTAC.Spots or {} - if TaskJTAC.Spots[TargetUnitName] then - TaskJTAC.Spots[TargetUnitName]:destroy() -- destroys the spot - TaskJTAC.Spots[TargetUnitName] = nil - end - - self.FACUnit:MessageToGroup( "Stopped lasing " .. TargetUnit:GetTypeName(), 15, self.ProcessGroup ) - - self:NextEvent( Fsm.JTACMenuAwait ) -end - - diff --git a/Moose Development/Moose/Actions/Act_Pickup.lua b/Moose Development/Moose/Actions/Act_Pickup.lua deleted file mode 100644 index 3ef76e0eb..000000000 --- a/Moose Development/Moose/Actions/Act_Pickup.lua +++ /dev/null @@ -1,173 +0,0 @@ ---- @module Process_Pickup - ---- PROCESS_PICKUP class --- @type PROCESS_PICKUP --- @field Wrapper.Unit#UNIT ProcessUnit --- @field Core.Set#SET_UNIT TargetSetUnit --- @extends Core.Fsm#FSM_PROCESS -PROCESS_PICKUP = { - ClassName = "PROCESS_PICKUP", - Fsm = {}, - TargetSetUnit = nil, -} - - ---- Creates a new DESTROY process. --- @param #PROCESS_PICKUP self --- @param Tasking.Task#TASK Task --- @param Wrapper.Unit#UNIT ProcessUnit --- @param Core.Set#SET_UNIT TargetSetUnit --- @return #PROCESS_PICKUP self -function PROCESS_PICKUP:New( Task, ProcessName, ProcessUnit ) - - -- Inherits from BASE - local self = BASE:Inherit( self, PROCESS:New( ProcessName, Task, ProcessUnit ) ) -- #PROCESS_PICKUP - - 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 - - self.Fsm = FSM_PROCESS:New( self, { - initial = 'Assigned', - events = { - { name = 'Start', from = 'Assigned', to = 'Navigating' }, - { name = 'Start', from = 'Navigating', to = 'Navigating' }, - { name = 'Nearby', from = 'Navigating', to = 'Preparing' }, - { name = 'Pickup', from = 'Preparing', to = 'Loading' }, - { name = 'Load', from = 'Loading', to = 'Success' }, - { name = 'Fail', from = 'Assigned', to = 'Failed' }, - { name = 'Fail', from = 'Navigating', to = 'Failed' }, - { name = 'Fail', from = 'Preparing', to = 'Failed' }, - }, - callbacks = { - onStart = self.OnStart, - onNearby = self.OnNearby, - onPickup = self.OnPickup, - onLoad = self.OnLoad, - }, - endstates = { 'Success', 'Failed' } - } ) - - return self -end - ---- Process Events - ---- StateMachine callback function for a PROCESS --- @param #PROCESS_PICKUP self --- @param Core.Fsm#FSM_PROCESS Fsm --- @param #string Event --- @param #string From --- @param #string To -function PROCESS_PICKUP:OnStart( Fsm, From, Event, To ) - - self:NextEvent( Fsm.Start ) -end - ---- StateMachine callback function for a PROCESS --- @param #PROCESS_PICKUP self --- @param Core.Fsm#FSM_PROCESS Fsm --- @param #string Event --- @param #string From --- @param #string To -function PROCESS_PICKUP:OnNavigating( Fsm, From, Event, To ) - - local TaskGroup = self.ProcessUnit:GetGroup() - if self.DisplayCount >= self.DisplayInterval then - MESSAGE:New( "Your group with assigned " .. self.Task:GetName() .. " task has " .. self.TargetSetUnit:GetUnitTypesText() .. " targets left to be destroyed.", 5, "HQ" ):ToGroup( TaskGroup ) - self.DisplayCount = 1 - else - self.DisplayCount = self.DisplayCount + 1 - end - - return true -- Process always the event. - -end - - ---- StateMachine callback function for a PROCESS --- @param #PROCESS_PICKUP self --- @param Core.Fsm#FSM_PROCESS Fsm --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Event#EVENTDATA Event -function PROCESS_PICKUP:OnHitTarget( Fsm, From, Event, To, Event ) - - - self.TargetSetUnit:Flush( self ) - - if self.TargetSetUnit:FindUnit( Event.IniUnitName ) then - self.TargetSetUnit:RemoveUnitsByName( Event.IniUnitName ) - local TaskGroup = self.ProcessUnit:GetGroup() - MESSAGE:New( "You hit a target. Your group with assigned " .. self.Task:GetName() .. " task has " .. self.TargetSetUnit:Count() .. " targets ( " .. self.TargetSetUnit:GetUnitTypesText() .. " ) left to be destroyed.", 15, "HQ" ):ToGroup( TaskGroup ) - end - - - if self.TargetSetUnit:Count() > 0 then - self:NextEvent( Fsm.MoreTargets ) - else - self:NextEvent( Fsm.Destroyed ) - end -end - ---- StateMachine callback function for a PROCESS --- @param #PROCESS_PICKUP self --- @param Core.Fsm#FSM_PROCESS Fsm --- @param #string Event --- @param #string From --- @param #string To -function PROCESS_PICKUP:OnMoreTargets( Fsm, From, Event, To ) - - -end - ---- StateMachine callback function for a PROCESS --- @param #PROCESS_PICKUP self --- @param Core.Fsm#FSM_PROCESS Fsm --- @param #string Event --- @param #string From --- @param #string To --- @param Core.Event#EVENTDATA DCSEvent -function PROCESS_PICKUP:OnKilled( Fsm, From, Event, To ) - - self:NextEvent( Fsm.Restart ) - -end - ---- StateMachine callback function for a PROCESS --- @param #PROCESS_PICKUP self --- @param Core.Fsm#FSM_PROCESS Fsm --- @param #string Event --- @param #string From --- @param #string To -function PROCESS_PICKUP:OnRestart( Fsm, From, Event, To ) - - self:NextEvent( Fsm.Menu ) - -end - ---- StateMachine callback function for a PROCESS --- @param #PROCESS_PICKUP self --- @param Core.Fsm#FSM_PROCESS Fsm --- @param #string Event --- @param #string From --- @param #string To -function PROCESS_PICKUP:OnDestroyed( Fsm, From, Event, To ) - -end - ---- DCS Events - ---- @param #PROCESS_PICKUP self --- @param Core.Event#EVENTDATA Event -function PROCESS_PICKUP:EventDead( Event ) - - if Event.IniDCSUnit then - self:NextEvent( self.Fsm.HitTarget, Event ) - end -end - - diff --git a/Moose Development/Moose/Actions/Act_Route.lua b/Moose Development/Moose/Actions/Act_Route.lua index e50c3ffcc..d719f1f7b 100644 --- a/Moose Development/Moose/Actions/Act_Route.lua +++ b/Moose Development/Moose/Actions/Act_Route.lua @@ -2,7 +2,7 @@ -- -- === -- --- # @{#ACT_ROUTE} FSM class, extends @{Fsm#FSM_PROCESS} +-- # @{#ACT_ROUTE} FSM class, extends @{Core.Fsm#FSM_PROCESS} -- -- ## ACT_ROUTE state machine: -- @@ -60,9 +60,9 @@ -- -- === -- --- # 1) @{#ACT_ROUTE_ZONE} class, extends @{Fsm.Route#ACT_ROUTE} +-- # 1) @{#ACT_ROUTE_ZONE} class, extends @{Core.Fsm.Route#ACT_ROUTE} -- --- The ACT_ROUTE_ZONE class implements the core functions to route an AIR @{Controllable} player @{Unit} to a @{Zone}. +-- The ACT_ROUTE_ZONE class implements the core functions to route an AIR @{Wrapper.Controllable} player @{Wrapper.Unit} to a @{Zone}. -- The player receives on perioding times messages with the coordinates of the route to follow. -- Upon arrival at the zone, a confirmation of arrival is sent, and the process will be ended. -- @@ -72,7 +72,8 @@ -- -- === -- --- @module Route +-- @module Actions.Route +-- @image MOOSE.JPG do -- ACT_ROUTE @@ -123,16 +124,20 @@ do -- ACT_ROUTE --- Set a Cancel Menu item. -- @param #ACT_ROUTE self -- @return #ACT_ROUTE - function ACT_ROUTE:SetMenuCancel( MenuGroup, MenuText, ParentMenu, MenuTime ) + function ACT_ROUTE:SetMenuCancel( MenuGroup, MenuText, ParentMenu, MenuTime, MenuTag ) - MENU_GROUP_COMMAND:New( + self.CancelMenuGroupCommand = MENU_GROUP_COMMAND:New( MenuGroup, MenuText, ParentMenu, self.MenuCancel, self - ):SetTime(MenuTime) + ):SetTime( MenuTime ):SetTag( MenuTag ) + + ParentMenu:SetTime( MenuTime ) + ParentMenu:Remove( MenuTime, MenuTag ) + return self end @@ -206,7 +211,9 @@ do -- ACT_ROUTE function ACT_ROUTE:MenuCancel() - self:Cancel() + self:F("Cancelled") + self.CancelMenuGroupCommand:Remove() + self:__Cancel( 1 ) end --- Task Events @@ -238,10 +245,8 @@ do -- ACT_ROUTE -- @param #string From -- @param #string To function ACT_ROUTE:onbeforeRoute( ProcessUnit, From, Event, To ) - self:F( { "BeforeRoute 1", self.DisplayCount, self.DisplayInterval } ) if ProcessUnit:IsAlive() then - self:F( "BeforeRoute 2" ) local HasArrived = self:onfuncHasArrived( ProcessUnit ) -- Polymorphic if self.DisplayCount >= self.DisplayInterval then self:T( { HasArrived = HasArrived } ) @@ -253,8 +258,6 @@ do -- ACT_ROUTE self.DisplayCount = self.DisplayCount + 1 end - self:T( { DisplayCount = self.DisplayCount } ) - if HasArrived then self:__Arrive( 1 ) else @@ -337,7 +340,7 @@ do -- ACT_ROUTE_POINT -- @param #ACT_ROUTE_POINT self -- @param #number Range The Range to consider the arrival. Default is 10000 meters. function ACT_ROUTE_POINT:SetRange( Range ) - self:F2( { self.Range } ) + self:F2( { Range } ) self.Range = Range or 10000 end @@ -345,6 +348,7 @@ do -- ACT_ROUTE_POINT -- @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 @@ -358,7 +362,7 @@ do -- ACT_ROUTE_POINT local Distance = self.Coordinate:Get2DDistance( ProcessUnit:GetCoordinate() ) if Distance <= self.Range then - local RouteText = "You have arrived." + local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", you have arrived." self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) return true end @@ -377,7 +381,7 @@ do -- ACT_ROUTE_POINT -- @param #string To function ACT_ROUTE_POINT:onafterReport( ProcessUnit, From, Event, To ) - local RouteText = self:GetRouteText( ProcessUnit ) + local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", " .. self:GetRouteText( ProcessUnit ) self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Update ) end @@ -449,7 +453,7 @@ do -- ACT_ROUTE_ZONE function ACT_ROUTE_ZONE:onfuncHasArrived( ProcessUnit ) if ProcessUnit:IsInZone( self.Zone ) then - local RouteText = "You have arrived within the zone." + local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", you have arrived within the zone." self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Information ) end @@ -467,7 +471,7 @@ do -- ACT_ROUTE_ZONE function ACT_ROUTE_ZONE:onafterReport( ProcessUnit, From, Event, To ) self:F( { ProcessUnit = ProcessUnit } ) - local RouteText = self:GetRouteText( 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/Cargo/Cargo.lua b/Moose Development/Moose/Cargo/Cargo.lua new file mode 100644 index 000000000..6a06e52f6 --- /dev/null +++ b/Moose Development/Moose/Cargo/Cargo.lua @@ -0,0 +1,1426 @@ +--- **Core** -- Management of CARGO logistics, that can be transported from and to transportation carriers. +-- +-- === +-- +-- # 1) MOOSE Cargo System. +-- +-- #### Those who have used the mission editor, know that the DCS mission editor provides cargo facilities. +-- However, these are merely static objects. Wouldn't it be nice if cargo could bring a new dynamism into your +-- simulations? Where various objects of various types could be treated also as cargo? +-- +-- This is what MOOSE brings to you, a complete new cargo object model that used the cargo capabilities of +-- DCS world, but enhances it. +-- +-- MOOSE Cargo introduces also a new concept, called a "carrier". These can be: +-- +-- - Helicopters +-- - Planes +-- - Ground Vehicles +-- - Ships +-- +-- With the MOOSE Cargo system, you can: +-- +-- - Take full control of the cargo as objects within your script (see below). +-- - Board/Unboard infantry into carriers. Also other objects can be boarded, like mortars. +-- - Load/Unload dcs world cargo objects into carriers. +-- - Load/Unload other static objects into carriers (like tires etc). +-- - Slingload cargo objects. +-- - Board units one by one... +-- +-- # 2) MOOSE Cargo Objects. +-- +-- In order to make use of the MOOSE cargo system, you need to **declare** the DCS objects as MOOSE cargo objects! +-- +-- This sounds complicated, but it is actually quite simple. +-- +-- See here an example: +-- +-- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) +-- +-- The above code declares a MOOSE cargo object called `EngineerCargoGroup`. +-- It actually just refers to an infantry group created within the sim called `"Engineers"`. +-- The infantry group now becomes controlled by the MOOSE cargo object `EngineerCargoGroup`. +-- A MOOSE cargo object also has properties, like the type of cargo, the logical name, and the reporting range. +-- +-- There are 4 types of MOOSE cargo objects possible, each represented by its own class: +-- +-- - @{Cargo.CargoGroup#CARGO_GROUP}: A MOOSE cargo that is represented by a DCS world GROUP object. +-- - @{Cargo.CargoCrate#CARGO_CRATE}: A MOOSE cargo that is represented by a DCS world cargo object (static object). +-- - @{Cargo.CargoUnit#CARGO_UNIT}: A MOOSE cargo that is represented by a DCS world unit object or static object. +-- - @{Cargo.CargoSlingload#CARGO_SLINGLOAD}: A MOOSE cargo that is represented by a DCS world cargo object (static object), that can be slingloaded. +-- +-- Note that a CARGO crate is not meant to be slingloaded (it can, but it is not **meant** to be handled like that. +-- Instead, a CARGO_CRATE is able to load itself into the bays of a carrier. +-- +-- Each of these MOOSE cargo objects behave in its own way, and have methods to be handled. +-- +-- local InfantryGroup = GROUP:FindByName( "Infantry" ) +-- local InfantryCargo = CARGO_GROUP:New( InfantryGroup, "Engineers", "Infantry Engineers", 2000 ) +-- local CargoCarrier = UNIT:FindByName( "Carrier" ) +-- -- This call will make the Cargo run to the CargoCarrier. +-- -- Upon arrival at the CargoCarrier, the Cargo will be Loaded into the Carrier. +-- -- This process is now fully automated. +-- InfantryCargo:Board( CargoCarrier, 25 ) +-- +-- The above would create a MOOSE cargo object called `InfantryCargo`, and using that object, +-- you can board the cargo into the carrier `CargoCarrier`. +-- Simple, isn't it? Told you, and this is only the beginning. +-- +-- The boarding, unboarding, loading, unloading of cargo is however something that is not meant to be coded manualy by mission designers. +-- It would be too low-level and not end-user friendly to deal with cargo handling complexity. +-- Things can become really complex if you want to make cargo being handled and behave in multiple scenarios. +-- +-- # 3) Cargo Handling Classes, the main engines for mission designers! +-- +-- For this reason, the MOOSE Cargo System is heavily used by 3 important **cargo handling class hierarchies** within MOOSE, +-- that make cargo come "alive" within your mission in a full automatic manner! +-- +-- ## 3.1) AI Cargo handlers. +-- +-- - @{AI.AI_Cargo_APC} will create for you the capatility to make an APC group handle cargo. +-- - @{AI.AI_Cargo_Helicopter} will create for you the capatility to make a Helicopter group handle cargo. +-- +-- +-- ## 3.2) AI Cargo transportation dispatchers. +-- +-- There are also dispatchers that make AI work together to transport cargo automatically!!! +-- +-- - @{AI.AI_Cargo_Dispatcher_APC} derived classes will create for your dynamic cargo handlers controlled by AI ground vehicle groups (APCs) to transport cargo between sites. +-- - @{AI.AI_Cargo_Dispatcher_Helicopters} derived classes will create for your dynamic cargo handlers controlled by AI helicpter groups to transport cargo between sites. +-- +-- ## 3.3) Cargo transportation tasking. +-- +-- And there is cargo transportation tasking for human players. +-- +-- - @{Tasking.Task_CARGO} derived classes will create for you cargo transportation tasks, that allow human players to interact with MOOSE cargo objects to complete tasks. +-- +-- Please refer to the documentation reflected within these modules to understand the detailed capabilties. +-- +-- # 4) Cargo SETs. +-- +-- To make life a bit more easy, MOOSE cargo objects can be grouped into a @{Core.Set#SET_CARGO}. +-- This is a collection of MOOSE cargo objects. +-- +-- This would work as follows: +-- +-- -- Define the cargo set. +-- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() +-- +-- -- Now add cargo the cargo set. +-- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) +-- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) +-- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) +-- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) +-- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) +-- +-- This is a very powerful concept! +-- Instead of having to deal with multiple MOOSE cargo objects yourself, the cargo set capability will group cargo objects into one set. +-- The key is the **cargo type** name given at each cargo declaration! +-- In the above example, the cargo type name is `"Workmaterials"`. Each cargo object declared is given that type name. (the 2nd parameter). +-- What happens now is that the cargo set `CargoSetWorkmaterials` will be added with each cargo object **dynamically** when the cargo object is created. +-- In other words, the cargo set `CargoSetWorkmaterials` will incorporate any `"Workmaterials"` dynamically into its set. +-- +-- The cargo sets are extremely important for the AI cargo transportation dispatchers and the cargo transporation tasking. +-- +-- # 5) Declare cargo directly in the mission editor! +-- +-- But I am not finished! There is something more, that is even more great! +-- Imagine the mission designers having to code all these lines every time it wants to embed cargo within a mission. +-- +-- -- Now add cargo the cargo set. +-- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) +-- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) +-- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) +-- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) +-- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) +-- +-- This would be extremely tiring and a huge overload. +-- However, the MOOSE framework allows to declare MOOSE cargo objects within the mission editor!!! +-- +-- So, at mission startup, MOOSE will search for objects following a special naming convention, and will **create** for you **dynamically +-- cargo objects** at **mission start**!!! -- These cargo objects can then be automatically incorporated within cargo set(s)!!! +-- In other words, your mission will be reduced to about a few lines of code, providing you with a full dynamic cargo handling mission! +-- +-- ## 5.1) Use \#CARGO tags in the mission editor: +-- +-- MOOSE can create automatically cargo objects, if the name of the cargo contains the **\#CARGO** tag. +-- When a mission starts, MOOSE will scan all group and static objects it found for the presence of the \#CARGO tag. +-- When found, MOOSE will declare the object as cargo (create in the background a CARGO_ object, like CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD. +-- The creation of these CARGO_ objects will allow to be filtered and automatically added in SET_CARGO objects. +-- In other words, with very minimal code as explained in the above code section, you are able to create vast amounts of cargo objects just from within the editor. +-- +-- What I talk about is this: +-- +-- -- BEFORE THIS SCRIPT STARTS, MOOSE WILL ALREADY HAVE SCANNED FOR OBJECTS WITH THE #CARGO TAG IN THE NAME. +-- -- FOR EACH OF THESE OBJECT, MOOSE WILL HAVE CREATED CARGO_ OBJECTS LIKE CARGO_GROUP, CARGO_CRATE AND CARGO_SLINGLOAD. +-- +-- HQ = GROUP:FindByName( "HQ", "Bravo" ) +-- +-- CommandCenter = COMMANDCENTER +-- :New( HQ, "Lima" ) +-- +-- Mission = MISSION +-- :New( CommandCenter, "Operation Cargo Fun", "Tactical", "Transport Cargo", coalition.side.RED ) +-- +-- TransportGroups = SET_GROUP:New():FilterCoalitions( "blue" ):FilterPrefixes( "Transport" ):FilterStart() +-- +-- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups ) +-- +-- -- This is the most important now. You setup a new SET_CARGO filtering the relevant type. +-- -- The actual cargo objects are now created by MOOSE in the background. +-- -- Each cargo is setup in the Mission Editor using the #CARGO tag in the group name. +-- -- This allows a truly dynamic setup. +-- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() +-- +-- local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." ) +-- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) +-- +-- The above code example has the `CargoSetWorkmaterials`, which is a SET_CARGO collection and will include the CARGO_ objects of the type "Workmaterials". +-- And there is NO cargo object actually declared within the script! However, if you would open the mission, there would be hundreds of cargo objects... +-- +-- The \#CARGO tag even allows for several options to be specified, which are important to learn. +-- +-- ## 5.2) The \#CARGO tag to create CARGO_GROUP objects: +-- +-- You can also use the \#CARGO tag on **group** objects of the mission editor. +-- +-- For example, the following #CARGO naming in the **group name** of the object, will create a CARGO_GROUP object when the mission starts. +-- +-- `Infantry #CARGO(T=Workmaterials,RR=500,NR=25)` +-- +-- This will create a CARGO_GROUP object: +-- +-- * with the group name `Infantry #CARGO` +-- * is of type `Workmaterials` +-- * will report when a carrier is within 500 meters +-- * will board to carriers when the carrier is within 500 meters from the cargo object +-- * will dissapear when the cargo is within 25 meters from the carrier during boarding +-- +-- So the overall syntax of the #CARGO naming tag and arguments are: +-- +-- `GroupName #CARGO(T=CargoTypeName,RR=Range,NR=Range)` +-- +-- * **T=** Provide a text that contains the type name of the cargo object. This type name can be used to filter cargo within a SET_CARGO object. +-- * **RR=** Provide the minimal range in meters when the report to the carrier, and board to the carrier. +-- Note that this option is optional, so can be omitted. The default value of the RR is 250 meters. +-- * **NR=** Provide the maximum range in meters when the cargo units will be boarded within the carrier during boarding. +-- Note that this option is optional, so can be omitted. The default value of the RR is 10 meters. +-- +-- ## 5.2) The \#CARGO tag to create CARGO_CRATE objects: +-- +-- You can also use the \#CARGO tag on **static** objects, including **static cargo** objects of the mission editor. +-- +-- For example, the following #CARGO naming in the **static name** of the object, will create a CARGO_CRATE object when the mission starts. +-- +-- `Static #CARGO(T=Workmaterials,RR=500,NR=25)` +-- +-- This will create a CARGO_CRATE object: +-- +-- * with the group name `Static #CARGO` +-- * is of type `Workmaterials` +-- * will report when a carrier is within 500 meters +-- * will board to carriers when the carrier is within 500 meters from the cargo object +-- * will dissapear when the cargo is within 25 meters from the carrier during boarding +-- +-- So the overall syntax of the #CARGO naming tag and arguments are: +-- +-- `StaticName #CARGO(T=CargoTypeName,RR=Range,NR=Range)` +-- +-- * **T=** Provide a text that contains the type name of the cargo object. This type name can be used to filter cargo within a SET_CARGO object. +-- * **RR=** Provide the minimal range in meters when the report to the carrier, and board to the carrier. +-- Note that this option is optional, so can be omitted. The default value of the RR is 250 meters. +-- * **NR=** Provide the maximum range in meters when the cargo units will be boarded within the carrier during boarding. +-- Note that this option is optional, so can be omitted. The default value of the RR is 10 meters. +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Cargo.Cargo +-- @image Cargo.JPG + +-- Events + +-- Board + +--- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#CARGO] Board +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. +-- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). + +--- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#CARGO] __Board +-- @param #CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. +-- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). + + +-- UnBoard + +--- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#CARGO] UnBoard +-- @param #CARGO self +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Core.Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. + +--- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#CARGO] __UnBoard +-- @param #CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Core.Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. + + +-- Load + +--- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#CARGO] Load +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. + +--- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **UnLoaded** state. +-- @function [parent=#CARGO] __Load +-- @param #CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. + + +-- UnLoad + +--- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#CARGO] UnLoad +-- @param #CARGO self +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Core.Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. + +--- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. +-- The cargo must be in the **Loaded** state. +-- @function [parent=#CARGO] __UnLoad +-- @param #CARGO self +-- @param #number DelaySeconds The amount of seconds to delay the action. +-- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Core.Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. + +-- State Transition Functions + +-- UnLoaded + +--- @function [parent=#CARGO] OnLeaveUnLoaded +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#CARGO] OnEnterUnLoaded +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable + +-- Loaded + +--- @function [parent=#CARGO] OnLeaveLoaded +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#CARGO] OnEnterLoaded +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable + +-- Boarding + +--- @function [parent=#CARGO] OnLeaveBoarding +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#CARGO] OnEnterBoarding +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). + +-- UnBoarding + +--- @function [parent=#CARGO] OnLeaveUnBoarding +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable +-- @return #boolean + +--- @function [parent=#CARGO] OnEnterUnBoarding +-- @param #CARGO self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable + + +-- TODO: Find all Carrier objects and make the type of the Carriers Wrapper.Unit#UNIT in the documentation. + +CARGOS = {} + +do -- CARGO + + --- @type CARGO + -- @extends Core.Fsm#FSM_PROCESS + -- @field #string Type A string defining the type of the cargo. eg. Engineers, Equipment, Screwdrivers. + -- @field #string Name A string defining the name of the cargo. The name is the unique identifier of the cargo. + -- @field #number Weight A number defining the weight of the cargo. The weight is expressed in kg. + -- @field #number NearRadius (optional) A number defining the radius in meters when the cargo is near to a Carrier, so that it can be loaded. + -- @field Wrapper.Unit#UNIT CargoObject The alive DCS object representing the cargo. This value can be nil, meaning, that the cargo is not represented anywhere... + -- @field Wrapper.Client#CLIENT CargoCarrier The alive DCS object carrying the cargo. This value can be nil, meaning, that the cargo is not contained anywhere... + -- @field #boolean Slingloadable This flag defines if the cargo can be slingloaded. + -- @field #boolean Moveable This flag defines if the cargo is moveable. + -- @field #boolean Representable This flag defines if the cargo can be represented by a DCS Unit. + -- @field #boolean Containable This flag defines if the cargo can be contained within a DCS Unit. + + --- Defines the core functions that defines a cargo object within MOOSE. + -- + -- A cargo is a **logical object** defined that is available for transport, and has a life status within a simulation. + -- + -- CARGO is not meant to be used directly by mission designers, but provides a base class for **concrete cargo implementation classes** to handle: + -- + -- * Cargo **group objects**, implemented by the @{Cargo.CargoGroup#CARGO_GROUP} class. + -- * Cargo **Unit objects**, implemented by the @{Cargo.CargoUnit#CARGO_UNIT} class. + -- * Cargo **Crate objects**, implemented by the @{Cargo.CargoCrate#CARGO_CRATE} class. + -- * Cargo **Sling Load objects**, implemented by the @{Cargo.CargoSlingload#CARGO_SLINGLOAD} class. + -- + -- The above cargo classes are used by the AI\_CARGO\_ classes to allow AI groups to transport cargo: + -- + -- * AI Armoured Personnel Carriers to transport cargo and engage in battles, using the @{AI.AI_Cargo_APC#AI_CARGO_APC} class. + -- * AI Helicopters to transport cargo, using the @{AI.AI_Cargo_Helicopter#AI_CARGO_HELICOPTER} class. + -- * AI Planes to transport cargo, using the @{AI.AI_Cargo_Plane#AI_CARGO_PLANE} class. + -- * AI Ships is planned. + -- + -- The above cargo classes are also used by the TASK\_CARGO\_ classes to allow human players to transport cargo as part of a tasking: + -- + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT} to transport cargo by human players. + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_CSAR} to transport downed pilots by human players. + -- + -- + -- The CARGO is a state machine: it manages the different events and states of the cargo. + -- All derived classes from CARGO follow the same state machine, expose the same cargo event functions, and provide the same cargo states. + -- + -- ## CARGO Events: + -- + -- * @{#CARGO.Board}( ToCarrier ): Boards the cargo to a carrier. + -- * @{#CARGO.Load}( ToCarrier ): Loads the cargo into a carrier, regardless of its position. + -- * @{#CARGO.UnBoard}( ToPointVec2 ): UnBoard the cargo from a carrier. This will trigger a movement of the cargo to the option ToPointVec2. + -- * @{#CARGO.UnLoad}( ToPointVec2 ): UnLoads the cargo from a carrier. + -- * @{#CARGO.Destroyed}( Controllable ): The cargo is dead. The cargo process will be ended. + -- + -- @field #CARGO + CARGO = { + ClassName = "CARGO", + Type = nil, + Name = nil, + Weight = nil, + CargoObject = nil, + CargoCarrier = nil, + Representable = false, + Slingloadable = false, + Moveable = false, + Containable = false, + Reported = {}, + } + + --- @type CARGO.CargoObjects + -- @map < #string, Wrapper.Positionable#POSITIONABLE > The alive POSITIONABLE objects representing the the cargo. + + + --- CARGO Constructor. This class is an abstract class and should not be instantiated. + -- @param #CARGO self + -- @param #string Type + -- @param #string Name + -- @param #number Weight + -- @param #number LoadRadius (optional) + -- @param #number NearRadius (optional) + -- @return #CARGO + function CARGO:New( Type, Name, Weight, LoadRadius, NearRadius ) --R2.1 + + local self = BASE:Inherit( self, FSM:New() ) -- #CARGO + self:F( { Type, Name, Weight, LoadRadius, NearRadius } ) + + self:SetStartState( "UnLoaded" ) + self:AddTransition( { "UnLoaded", "Boarding" }, "Board", "Boarding" ) + self:AddTransition( "Boarding" , "Boarding", "Boarding" ) + self:AddTransition( "Boarding", "CancelBoarding", "UnLoaded" ) + self:AddTransition( "Boarding", "Load", "Loaded" ) + self:AddTransition( "UnLoaded", "Load", "Loaded" ) + self:AddTransition( "Loaded", "UnBoard", "UnBoarding" ) + self:AddTransition( "UnBoarding", "UnBoarding", "UnBoarding" ) + self:AddTransition( "UnBoarding", "UnLoad", "UnLoaded" ) + self:AddTransition( "Loaded", "UnLoad", "UnLoaded" ) + self:AddTransition( "*", "Damaged", "Damaged" ) + self:AddTransition( "*", "Destroyed", "Destroyed" ) + self:AddTransition( "*", "Respawn", "UnLoaded" ) + self:AddTransition( "*", "Reset", "UnLoaded" ) + + self.Type = Type + self.Name = Name + self.Weight = Weight or 0 + self.CargoObject = nil + self.CargoCarrier = nil -- Wrapper.Client#CLIENT + self.Representable = false + self.Slingloadable = false + self.Moveable = false + self.Containable = false + + self.CargoLimit = 0 + + self.LoadRadius = LoadRadius or 500 + --self.NearRadius = NearRadius or 25 + + self:SetDeployed( false ) + + self.CargoScheduler = SCHEDULER:New() + + CARGOS[self.Name] = self + + + return self + end + + + --- Find a CARGO in the _DATABASE. + -- @param #CARGO self + -- @param #string CargoName The Cargo Name. + -- @return #CARGO self + function CARGO:FindByName( CargoName ) + + local CargoFound = _DATABASE:FindCargo( CargoName ) + return CargoFound + end + + --- Get the x position of the cargo. + -- @param #CARGO self + -- @return #number + function CARGO:GetX() + if self:IsLoaded() then + return self.CargoCarrier:GetCoordinate().x + else + return self.CargoObject:GetCoordinate().x + end + end + + --- Get the y position of the cargo. + -- @param #CARGO self + -- @return #number + function CARGO:GetY() + if self:IsLoaded() then + return self.CargoCarrier:GetCoordinate().z + else + return self.CargoObject:GetCoordinate().z + end + end + + --- Get the heading of the cargo. + -- @param #CARGO self + -- @return #number + function CARGO:GetHeading() + if self:IsLoaded() then + return self.CargoCarrier:GetHeading() + else + return self.CargoObject:GetHeading() + end + end + + + --- Check if the cargo can be Slingloaded. + -- @param #CARGO self + function CARGO:CanSlingload() + return false + end + + --- Check if the cargo can be Boarded. + -- @param #CARGO self + function CARGO:CanBoard() + return true + end + + --- Check if the cargo can be Unboarded. + -- @param #CARGO self + function CARGO:CanUnboard() + return true + end + + --- Check if the cargo can be Loaded. + -- @param #CARGO self + function CARGO:CanLoad() + return true + end + + --- Check if the cargo can be Unloaded. + -- @param #CARGO self + function CARGO:CanUnload() + return true + end + + + --- Destroy the cargo. + -- @param #CARGO self + function CARGO:Destroy() + if self.CargoObject then + self.CargoObject:Destroy() + end + self:Destroyed() + end + + --- Get the name of the Cargo. + -- @param #CARGO self + -- @return #string The name of the Cargo. + function CARGO:GetName() --R2.1 + return self.Name + end + + --- Get the current active object representing or being the Cargo. + -- @param #CARGO self + -- @return Wrapper.Positionable#POSITIONABLE The object representing or being the Cargo. + function CARGO:GetObject() + if self:IsLoaded() then + return self.CargoCarrier + else + return self.CargoObject + end + end + + --- Get the object name of the Cargo. + -- @param #CARGO self + -- @return #string The object name of the Cargo. + function CARGO:GetObjectName() --R2.1 + if self:IsLoaded() then + return self.CargoCarrier:GetName() + else + return self.CargoObject:GetName() + end + end + + --- Get the amount of Cargo. + -- @param #CARGO self + -- @return #number The amount of Cargo. + function CARGO:GetCount() + return 1 + end + + --- Get the type of the Cargo. + -- @param #CARGO self + -- @return #string The type of the Cargo. + function CARGO:GetType() + return self.Type + end + + + --- Get the transportation method of the Cargo. + -- @param #CARGO self + -- @return #string The transportation method of the Cargo. + function CARGO:GetTransportationMethod() + return self.TransportationMethod + end + + + --- Get the coalition of the Cargo. + -- @param #CARGO self + -- @return Coalition + function CARGO:GetCoalition() + if self:IsLoaded() then + return self.CargoCarrier:GetCoalition() + else + return self.CargoObject:GetCoalition() + end + end + + + --- Get the current coordinates of the Cargo. + -- @param #CARGO self + -- @return Core.Point#COORDINATE The coordinates of the Cargo. + function CARGO:GetCoordinate() + return self.CargoObject:GetCoordinate() + end + + --- Check if cargo is destroyed. + -- @param #CARGO self + -- @return #boolean true if destroyed + function CARGO:IsDestroyed() + return self:Is( "Destroyed" ) + end + + + --- Check if cargo is loaded. + -- @param #CARGO self + -- @return #boolean true if loaded + function CARGO:IsLoaded() + return self:Is( "Loaded" ) + end + + --- Check if cargo is loaded. + -- @param #CARGO self + -- @param Wrapper.Unit#UNIT Carrier + -- @return #boolean true if loaded + function CARGO:IsLoadedInCarrier( Carrier ) + return self.CargoCarrier and self.CargoCarrier:GetName() == Carrier:GetName() + end + + --- Check if cargo is unloaded. + -- @param #CARGO self + -- @return #boolean true if unloaded + function CARGO:IsUnLoaded() + return self:Is( "UnLoaded" ) + end + + --- Check if cargo is boarding. + -- @param #CARGO self + -- @return #boolean true if boarding + function CARGO:IsBoarding() + return self:Is( "Boarding" ) + end + + + --- Check if cargo is unboarding. + -- @param #CARGO self + -- @return #boolean true if unboarding + function CARGO:IsUnboarding() + return self:Is( "UnBoarding" ) + end + + + --- Check if cargo is alive. + -- @param #CARGO self + -- @return #boolean true if unloaded + function CARGO:IsAlive() + + if self:IsLoaded() then + return self.CargoCarrier:IsAlive() + else + return self.CargoObject:IsAlive() + end + end + + --- Set the cargo as deployed. + -- @param #CARGO self + -- @param #boolean Deployed true if the cargo is to be deployed. false or nil otherwise. + function CARGO:SetDeployed( Deployed ) + self.Deployed = Deployed + end + + --- Is the cargo deployed + -- @param #CARGO self + -- @return #boolean + function CARGO:IsDeployed() + return self.Deployed + end + + + + + --- Template method to spawn a new representation of the CARGO in the simulator. + -- @param #CARGO self + -- @return #CARGO + function CARGO:Spawn( PointVec2 ) + self:F() + + end + + --- Signal a flare at the position of the CARGO. + -- @param #CARGO self + -- @param Utilities.Utils#FLARECOLOR FlareColor + function CARGO:Flare( FlareColor ) + if self:IsUnLoaded() then + trigger.action.signalFlare( self.CargoObject:GetVec3(), FlareColor , 0 ) + end + end + + --- Signal a white flare at the position of the CARGO. + -- @param #CARGO self + function CARGO:FlareWhite() + self:Flare( trigger.flareColor.White ) + end + + --- Signal a yellow flare at the position of the CARGO. + -- @param #CARGO self + function CARGO:FlareYellow() + self:Flare( trigger.flareColor.Yellow ) + end + + --- Signal a green flare at the position of the CARGO. + -- @param #CARGO self + function CARGO:FlareGreen() + self:Flare( trigger.flareColor.Green ) + end + + --- Signal a red flare at the position of the CARGO. + -- @param #CARGO self + function CARGO:FlareRed() + self:Flare( trigger.flareColor.Red ) + end + + --- Smoke the CARGO. + -- @param #CARGO self + -- @param Utilities.Utils#SMOKECOLOR SmokeColor The color of the smoke. + -- @param #number Radius The radius of randomization around the center of the Cargo. + function CARGO:Smoke( SmokeColor, Radius ) + if self:IsUnLoaded() then + if Radius then + trigger.action.smoke( self.CargoObject:GetRandomVec3( Radius ), SmokeColor ) + else + trigger.action.smoke( self.CargoObject:GetVec3(), SmokeColor ) + end + end + end + + --- Smoke the CARGO Green. + -- @param #CARGO self + function CARGO:SmokeGreen() + self:Smoke( trigger.smokeColor.Green, Range ) + end + + --- Smoke the CARGO Red. + -- @param #CARGO self + function CARGO:SmokeRed() + self:Smoke( trigger.smokeColor.Red, Range ) + end + + --- Smoke the CARGO White. + -- @param #CARGO self + function CARGO:SmokeWhite() + self:Smoke( trigger.smokeColor.White, Range ) + end + + --- Smoke the CARGO Orange. + -- @param #CARGO self + function CARGO:SmokeOrange() + self:Smoke( trigger.smokeColor.Orange, Range ) + end + + --- Smoke the CARGO Blue. + -- @param #CARGO self + function CARGO:SmokeBlue() + self:Smoke( trigger.smokeColor.Blue, Range ) + end + + + --- Set the Load radius, which is the radius till when the Cargo can be loaded. + -- @param #CARGO self + -- @param #number LoadRadius The radius till Cargo can be loaded. + -- @return #CARGO + function CARGO:SetLoadRadius( LoadRadius ) + self.LoadRadius = LoadRadius or 150 + end + + --- Get the Load radius, which is the radius till when the Cargo can be loaded. + -- @param #CARGO self + -- @return #number The radius till Cargo can be loaded. + function CARGO:GetLoadRadius() + return self.LoadRadius + end + + + + --- Check if Cargo is in the LoadRadius for the Cargo to be Boarded or Loaded. + -- @param #CARGO self + -- @param Core.Point#COORDINATE Coordinate + -- @return #boolean true if the CargoGroup is within the loading radius. + function CARGO:IsInLoadRadius( Coordinate ) + self:F( { Coordinate, LoadRadius = self.LoadRadius } ) + + local Distance = 0 + if self:IsUnLoaded() then + local CargoCoordinate = self.CargoObject:GetCoordinate() + Distance = Coordinate:Get2DDistance( CargoCoordinate ) + self:T( Distance ) + if Distance <= self.LoadRadius then + return true + end + end + + return false + end + + + --- Check if the Cargo can report itself to be Boarded or Loaded. + -- @param #CARGO self + -- @param Core.Point#COORDINATE Coordinate + -- @return #boolean true if the Cargo can report itself. + function CARGO:IsInReportRadius( Coordinate ) + self:F( { Coordinate } ) + + local Distance = 0 + if self:IsUnLoaded() then + Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() ) + self:T( Distance ) + if Distance <= self.LoadRadius then + return true + end + end + + return false + end + + + --- Check if CargoCarrier is near the coordinate within NearRadius. + -- @param #CARGO self + -- @param Core.Point#COORDINATE Coordinate + -- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). + -- @return #boolean + function CARGO:IsNear( Coordinate, NearRadius ) + --self:F( { PointVec2 = PointVec2, NearRadius = NearRadius } ) + + if self.CargoObject:IsAlive() then + --local Distance = PointVec2:Get2DDistance( self.CargoObject:GetPointVec2() ) + --self:F( { CargoObjectName = self.CargoObject:GetName() } ) + --self:F( { CargoObjectVec2 = self.CargoObject:GetVec2() } ) + --self:F( { PointVec2 = PointVec2:GetVec2() } ) + local Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() ) + --self:F( { Distance = Distance, NearRadius = NearRadius or "nil" } ) + + if Distance <= NearRadius then + --self:F( { PointVec2 = PointVec2, NearRadius = NearRadius, IsNear = true } ) + return true + end + end + + --self:F( { PointVec2 = PointVec2, NearRadius = NearRadius, IsNear = false } ) + return false + end + + + + --- Check if Cargo is the given @{Zone}. + -- @param #CARGO self + -- @param Core.Zone#ZONE_BASE Zone + -- @return #boolean **true** if cargo is in the Zone, **false** if cargo is not in the Zone. + function CARGO:IsInZone( Zone ) + --self:F( { Zone } ) + + if self:IsLoaded() then + return Zone:IsPointVec2InZone( self.CargoCarrier:GetPointVec2() ) + else + --self:F( { Size = self.CargoObject:GetSize(), Units = self.CargoObject:GetUnits() } ) + if self.CargoObject:GetSize() ~= 0 then + return Zone:IsPointVec2InZone( self.CargoObject:GetPointVec2() ) + else + return false + end + end + + return nil + + end + + + --- Get the current PointVec2 of the cargo. + -- @param #CARGO self + -- @return Core.Point#POINT_VEC2 + function CARGO:GetPointVec2() + return self.CargoObject:GetPointVec2() + end + + --- Get the current Coordinate of the cargo. + -- @param #CARGO self + -- @return Core.Point#COORDINATE + function CARGO:GetCoordinate() + return self.CargoObject:GetCoordinate() + end + + --- Get the weight of the cargo. + -- @param #CARGO self + -- @return #number Weight The weight in kg. + function CARGO:GetWeight() + return self.Weight + end + + --- Set the weight of the cargo. + -- @param #CARGO self + -- @param #number Weight The weight in kg. + -- @return #CARGO + function CARGO:SetWeight( Weight ) + self.Weight = Weight + return self + end + + --- Get the volume of the cargo. + -- @param #CARGO self + -- @return #number Volume The volume in kg. + function CARGO:GetVolume() + return self.Volume + end + + --- Set the volume of the cargo. + -- @param #CARGO self + -- @param #number Volume The volume in kg. + -- @return #CARGO + function CARGO:SetVolume( Volume ) + self.Volume = Volume + return self + end + + --- Send a CC message to a @{Wrapper.Group}. + -- @param #CARGO self + -- @param #string Message + -- @param Wrapper.Group#GROUP CarrierGroup The Carrier Group. + -- @param #string Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown. + function CARGO:MessageToGroup( Message, CarrierGroup, Name ) + + MESSAGE:New( Message, 20, "Cargo " .. self:GetName() ):ToGroup( CarrierGroup ) + + end + + --- Report to a Carrier Group. + -- @param #CARGO self + -- @param #string Action The string describing the action for the cargo. + -- @param Wrapper.Group#GROUP CarrierGroup The Carrier Group to send the report to. + -- @return #CARGO + function CARGO:Report( ReportText, Action, CarrierGroup ) + + if not self.Reported[CarrierGroup] or not self.Reported[CarrierGroup][Action] then + self.Reported[CarrierGroup] = {} + self.Reported[CarrierGroup][Action] = true + self:MessageToGroup( ReportText, CarrierGroup ) + if self.ReportFlareColor then + if not self.Reported[CarrierGroup]["Flaring"] then + self:Flare( self.ReportFlareColor ) + self.Reported[CarrierGroup]["Flaring"] = true + end + end + if self.ReportSmokeColor then + if not self.Reported[CarrierGroup]["Smoking"] then + self:Smoke( self.ReportSmokeColor ) + self.Reported[CarrierGroup]["Smoking"] = true + end + end + end + end + + + --- Report to a Carrier Group with a Flaring signal. + -- @param #CARGO self + -- @param Utils#UTILS.FlareColor FlareColor the color of the flare. + -- @return #CARGO + function CARGO:ReportFlare( FlareColor ) + + self.ReportFlareColor = FlareColor + end + + + --- Report to a Carrier Group with a Smoking signal. + -- @param #CARGO self + -- @param Utils#UTILS.SmokeColor SmokeColor the color of the smoke. + -- @return #CARGO + function CARGO:ReportSmoke( SmokeColor ) + + self.ReportSmokeColor = SmokeColor + end + + + --- Reset the reporting for a Carrier Group. + -- @param #CARGO self + -- @param #string Action The string describing the action for the cargo. + -- @param Wrapper.Group#GROUP CarrierGroup The Carrier Group to send the report to. + -- @return #CARGO + function CARGO:ReportReset( Action, CarrierGroup ) + + self.Reported[CarrierGroup][Action] = nil + end + + --- Reset all the reporting for a Carrier Group. + -- @param #CARGO self + -- @param Wrapper.Group#GROUP CarrierGroup The Carrier Group to send the report to. + -- @return #CARGO + function CARGO:ReportResetAll( CarrierGroup ) + + self.Reported[CarrierGroup] = nil + end + + --- Respawn the cargo when destroyed + -- @param #CARGO self + -- @param #boolean RespawnDestroyed + function CARGO:RespawnOnDestroyed( RespawnDestroyed ) + + if RespawnDestroyed then + self.onenterDestroyed = function( self ) + self:Respawn() + end + else + self.onenterDestroyed = nil + end + + end + + + + +end -- CARGO + +do -- CARGO_REPRESENTABLE + + --- @type CARGO_REPRESENTABLE + -- @extends #CARGO + -- @field test + + --- Models CARGO that is representable by a Unit. + -- @field #CARGO_REPRESENTABLE CARGO_REPRESENTABLE + CARGO_REPRESENTABLE = { + ClassName = "CARGO_REPRESENTABLE" + } + + --- CARGO_REPRESENTABLE Constructor. + -- @param #CARGO_REPRESENTABLE self + -- @param #string Type + -- @param #string Name + -- @param #number LoadRadius (optional) + -- @param #number NearRadius (optional) + -- @return #CARGO_REPRESENTABLE + function CARGO_REPRESENTABLE:New( CargoObject, Type, Name, LoadRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO:New( Type, Name, 0, LoadRadius, NearRadius ) ) -- #CARGO_REPRESENTABLE + self:F( { Type, Name, LoadRadius, NearRadius } ) + + local Desc = CargoObject:GetDesc() + self:I( { Desc = Desc } ) + local Weight = math.random( 80, 120 ) + if Desc then + if Desc.typeName == "2B11 mortar" then + Weight = 210 + else + Weight = Desc.massEmpty + end + end + + self:SetWeight( Weight ) + +-- local Box = CargoUnit:GetBoundingBox() +-- local VolumeUnit = ( Box.max.x - Box.min.x ) * ( Box.max.y - Box.min.y ) * ( Box.max.z - Box.min.z ) +-- self:I( { VolumeUnit = VolumeUnit, WeightUnit = WeightUnit } ) + --self:SetVolume( VolumeUnit ) + + + return self + end + + --- CARGO_REPRESENTABLE Destructor. + -- @param #CARGO_REPRESENTABLE self + -- @return #CARGO_REPRESENTABLE + function CARGO_REPRESENTABLE:Destroy() + + -- Cargo objects are deleted from the _DATABASE and SET_CARGO objects. + self:F( { CargoName = self:GetName() } ) + --_EVENTDISPATCHER:CreateEventDeleteCargo( self ) + + return self + end + + --- Route a cargo unit to a PointVec2. + -- @param #CARGO_REPRESENTABLE self + -- @param Core.Point#POINT_VEC2 ToPointVec2 + -- @param #number Speed + -- @return #CARGO_REPRESENTABLE + function CARGO_REPRESENTABLE:RouteTo( ToPointVec2, Speed ) + self:F2( ToPointVec2 ) + + local Points = {} + + local PointStartVec2 = self.CargoObject:GetPointVec2() + + Points[#Points+1] = PointStartVec2:WaypointGround( Speed ) + Points[#Points+1] = ToPointVec2:WaypointGround( Speed ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 2 ) + return self + end + + --- Send a message to a @{Wrapper.Group} through a communication channel near the cargo. + -- @param #CARGO_REPRESENTABLE self + -- @param #string Message + -- @param Wrapper.Group#GROUP TaskGroup + -- @param #string Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown. + function CARGO_REPRESENTABLE:MessageToGroup( Message, TaskGroup, Name ) + + local CoordinateZone = ZONE_RADIUS:New( "Zone" , self:GetCoordinate():GetVec2(), 500 ) + CoordinateZone:Scan( { Object.Category.UNIT } ) + for _, DCSUnit in pairs( CoordinateZone:GetScannedUnits() ) do + local NearUnit = UNIT:Find( DCSUnit ) + self:F({NearUnit=NearUnit}) + local NearUnitCoalition = NearUnit:GetCoalition() + local CargoCoalition = self:GetCoalition() + if NearUnitCoalition == CargoCoalition then + local Attributes = NearUnit:GetDesc() + self:F({Desc=Attributes}) + if NearUnit:HasAttribute( "Trucks" ) then + MESSAGE:New( Message, 20, NearUnit:GetCallsign() .. " reporting - Cargo " .. self:GetName() ):ToGroup( TaskGroup ) + break + end + end + end + + end + + +end -- CARGO_REPRESENTABLE + +do -- CARGO_REPORTABLE + + --- @type CARGO_REPORTABLE + -- @extends #CARGO + CARGO_REPORTABLE = { + ClassName = "CARGO_REPORTABLE" + } + + --- CARGO_REPORTABLE Constructor. + -- @param #CARGO_REPORTABLE self + -- @param #string Type + -- @param #string Name + -- @param #number Weight + -- @param #number LoadRadius (optional) + -- @param #number NearRadius (optional) + -- @return #CARGO_REPORTABLE + function CARGO_REPORTABLE:New( Type, Name, Weight, LoadRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight, LoadRadius, NearRadius ) ) -- #CARGO_REPORTABLE + self:F( { Type, Name, Weight, LoadRadius, NearRadius } ) + + return self + end + + --- Send a CC message to a @{Wrapper.Group}. + -- @param #CARGO_REPORTABLE self + -- @param #string Message + -- @param Wrapper.Group#GROUP TaskGroup + -- @param #string Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown. + function CARGO_REPORTABLE:MessageToGroup( Message, TaskGroup, Name ) + + MESSAGE:New( Message, 20, "Cargo " .. self:GetName() .. " reporting" ):ToGroup( TaskGroup ) + + end + + + +end + + + + + + + +do -- CARGO_PACKAGE + + --- @type CARGO_PACKAGE + -- @extends #CARGO_REPRESENTABLE + CARGO_PACKAGE = { + ClassName = "CARGO_PACKAGE" + } + +--- CARGO_PACKAGE Constructor. +-- @param #CARGO_PACKAGE self +-- @param Wrapper.Unit#UNIT CargoCarrier The UNIT carrying the package. +-- @param #string Type +-- @param #string Name +-- @param #number Weight +-- @param #number LoadRadius (optional) +-- @param #number NearRadius (optional) +-- @return #CARGO_PACKAGE +function CARGO_PACKAGE:New( CargoCarrier, Type, Name, Weight, LoadRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoCarrier, Type, Name, Weight, LoadRadius, NearRadius ) ) -- #CARGO_PACKAGE + self:F( { Type, Name, Weight, LoadRadius, NearRadius } ) + + self:T( CargoCarrier ) + self.CargoCarrier = CargoCarrier + + return self +end + +--- Board Event. +-- @param #CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +-- @param #number BoardDistance +-- @param #number Angle +function CARGO_PACKAGE:onafterOnBoard( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + self:F() + + self.CargoInAir = self.CargoCarrier:InAir() + + self:T( self.CargoInAir ) + + -- Only move the CargoCarrier to the New CargoCarrier when the New CargoCarrier is not in the air. + if not self.CargoInAir then + + local Points = {} + + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + self:T( { CargoCarrierHeading, CargoDeployHeading } ) + local CargoDeployPointVec2 = CargoCarrier:GetPointVec2():Translate( BoardDistance, CargoDeployHeading ) + + Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) + + local TaskRoute = self.CargoCarrier:TaskRoute( Points ) + self.CargoCarrier:SetTask( TaskRoute, 1 ) + end + + self:Boarded( CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + +end + +--- Check if CargoCarrier is near the Cargo to be Loaded. +-- @param #CARGO_PACKAGE self +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @return #boolean +function CARGO_PACKAGE:IsNear( CargoCarrier ) + self:F() + + local CargoCarrierPoint = CargoCarrier:GetCoordinate() + + local Distance = CargoCarrierPoint:Get2DDistance( self.CargoCarrier:GetCoordinate() ) + self:T( Distance ) + + if Distance <= self.NearRadius then + return true + else + return false + end +end + +--- Boarded Event. +-- @param #CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +-- @param #number BoardDistance +-- @param #number LoadDistance +-- @param #number Angle +function CARGO_PACKAGE:onafterOnBoarded( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + self:F() + + if self:IsNear( CargoCarrier ) then + self:__Load( 1, CargoCarrier, Speed, LoadDistance, Angle ) + else + self:__Boarded( 1, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) + end +end + +--- UnBoard Event. +-- @param #CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +-- @param #number UnLoadDistance +-- @param #number UnBoardDistance +-- @param #number Radius +-- @param #number Angle +function CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle ) + self:F() + + self.CargoInAir = self.CargoCarrier:InAir() + + self:T( self.CargoInAir ) + + -- Only unboard the cargo when the carrier is not in the air. + -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). + if not self.CargoInAir then + + self:_Next( self.FsmP.UnLoad, UnLoadDistance, Angle ) + + local Points = {} + + local StartPointVec2 = CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + self:T( { CargoCarrierHeading, CargoDeployHeading } ) + local CargoDeployPointVec2 = StartPointVec2:Translate( UnBoardDistance, CargoDeployHeading ) + + Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) + + local TaskRoute = CargoCarrier:TaskRoute( Points ) + CargoCarrier:SetTask( TaskRoute, 1 ) + end + + self:__UnBoarded( 1 , CargoCarrier, Speed ) + +end + +--- UnBoarded Event. +-- @param #CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +function CARGO_PACKAGE:onafterUnBoarded( From, Event, To, CargoCarrier, Speed ) + self:F() + + if self:IsNear( CargoCarrier ) then + self:__UnLoad( 1, CargoCarrier, Speed ) + else + self:__UnBoarded( 1, CargoCarrier, Speed ) + end +end + +--- Load Event. +-- @param #CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +-- @param #number LoadDistance +-- @param #number Angle +function CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, LoadDistance, Angle ) + self:F() + + self.CargoCarrier = CargoCarrier + + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = StartPointVec2:Translate( LoadDistance, CargoDeployHeading ) + + local Points = {} + Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) + + local TaskRoute = self.CargoCarrier:TaskRoute( Points ) + self.CargoCarrier:SetTask( TaskRoute, 1 ) + +end + +--- UnLoad Event. +-- @param #CARGO_PACKAGE self +-- @param #string Event +-- @param #string From +-- @param #string To +-- @param Wrapper.Unit#UNIT CargoCarrier +-- @param #number Speed +-- @param #number Distance +-- @param #number Angle +function CARGO_PACKAGE:onafterUnLoad( From, Event, To, CargoCarrier, Speed, Distance, Angle ) + self:F() + + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) + + self.CargoCarrier = CargoCarrier + + local Points = {} + Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) + + local TaskRoute = self.CargoCarrier:TaskRoute( Points ) + self.CargoCarrier:SetTask( TaskRoute, 1 ) + +end + + +end diff --git a/Moose Development/Moose/Cargo/CargoCrate.lua b/Moose Development/Moose/Cargo/CargoCrate.lua new file mode 100644 index 000000000..50cef9a97 --- /dev/null +++ b/Moose Development/Moose/Cargo/CargoCrate.lua @@ -0,0 +1,332 @@ +--- **Cargo** -- Management of single cargo crates, which are based on a @{Static} object. +-- +-- === +-- +-- ### [Demo Missions]() +-- +-- ### [YouTube Playlist]() +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Cargo.CargoCrate +-- @image Cargo_Crates.JPG + +do -- CARGO_CRATE + + --- Models the behaviour of cargo crates, which can be slingloaded and boarded on helicopters. + -- @type CARGO_CRATE + -- @extends Cargo.Cargo#CARGO_REPRESENTABLE + + --- Defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. + -- Use the event functions as described above to Load, UnLoad, Board, UnBoard the CARGO\_CRATE objects to and from carriers. + -- + -- The above cargo classes are used by the following AI_CARGO_ classes to allow AI groups to transport cargo: + -- + -- * AI Armoured Personnel Carriers to transport cargo and engage in battles, using the @{AI.AI_Cargo_APC} module. + -- * AI Helicopters to transport cargo, using the @{AI.AI_Cargo_Helicopter} module. + -- * AI Planes to transport cargo, using the @{AI.AI_Cargo_Airplane} module. + -- * AI Ships is planned. + -- + -- The above cargo classes are also used by the TASK_CARGO_ classes to allow human players to transport cargo as part of a tasking: + -- + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT} to transport cargo by human players. + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_CSAR} to transport downed pilots by human players. + -- + -- === + -- + -- @field #CARGO_CRATE + CARGO_CRATE = { + ClassName = "CARGO_CRATE" + } + + --- CARGO_CRATE Constructor. + -- @param #CARGO_CRATE self + -- @param Wrapper.Static#STATIC CargoStatic + -- @param #string Type + -- @param #string Name + -- @param #number LoadRadius (optional) + -- @param #number NearRadius (optional) + -- @return #CARGO_CRATE + function CARGO_CRATE:New( CargoStatic, Type, Name, LoadRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoStatic, Type, Name, nil, LoadRadius, NearRadius ) ) -- #CARGO_CRATE + self:F( { Type, Name, NearRadius } ) + + self.CargoObject = CargoStatic -- Wrapper.Static#STATIC + + -- Cargo objects are added to the _DATABASE and SET_CARGO objects. + _EVENTDISPATCHER:CreateEventNewCargo( self ) + + self:HandleEvent( EVENTS.Dead, self.OnEventCargoDead ) + self:HandleEvent( EVENTS.Crash, self.OnEventCargoDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCargoDead ) + self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventCargoDead ) + + self:SetEventPriority( 4 ) + + self.NearRadius = NearRadius or 25 + + return self + end + + --- @param #CARGO_CRATE self + -- @param Core.Event#EVENTDATA EventData + function CARGO_CRATE:OnEventCargoDead( EventData ) + + local Destroyed = false + + if self:IsDestroyed() or self:IsUnLoaded() or self:IsBoarding() then + if self.CargoObject:GetName() == EventData.IniUnitName then + if not self.NoDestroy then + Destroyed = true + end + end + else + if self:IsLoaded() then + local CarrierName = self.CargoCarrier:GetName() + if CarrierName == EventData.IniDCSUnitName then + MESSAGE:New( "Cargo is lost from carrier " .. CarrierName, 15 ):ToAll() + Destroyed = true + self.CargoCarrier:ClearCargo() + end + end + end + + if Destroyed then + self:I( { "Cargo crate destroyed: " .. self.CargoObject:GetName() } ) + self:Destroyed() + end + + end + + + --- Enter UnLoaded State. + -- @param #CARGO_CRATE self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 + function CARGO_CRATE:onenterUnLoaded( From, Event, To, ToPointVec2 ) + --self:F( { ToPointVec2, From, Event, To } ) + + local Angle = 180 + local Speed = 10 + local Distance = 10 + + if From == "Loaded" then + local StartCoordinate = self.CargoCarrier:GetCoordinate() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployCoord = StartCoordinate:Translate( Distance, CargoDeployHeading ) + + ToPointVec2 = ToPointVec2 or COORDINATE:NewFromVec2( { x= CargoDeployCoord.x, y = CargoDeployCoord.z } ) + + -- Respawn the group... + if self.CargoObject then + self.CargoObject:ReSpawnAt( ToPointVec2, 0 ) + self.CargoCarrier = nil + end + + end + + if self.OnUnLoadedCallBack then + self.OnUnLoadedCallBack( self, unpack( self.OnUnLoadedParameters ) ) + self.OnUnLoadedCallBack = nil + end + + end + + + --- Loaded State. + -- @param #CARGO_CRATE self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Wrapper.Unit#UNIT CargoCarrier + function CARGO_CRATE:onenterLoaded( From, Event, To, CargoCarrier ) + --self:F( { From, Event, To, CargoCarrier } ) + + self.CargoCarrier = CargoCarrier + + -- Only destroy the CargoObject is if there is a CargoObject (packages don't have CargoObjects). + if self.CargoObject then + self:T("Destroying") + self.NoDestroy = true + self.CargoObject:Destroy( false ) -- Do not generate a remove unit event, because we want to keep the template for later respawn in the database. + --local Coordinate = self.CargoObject:GetCoordinate():GetRandomCoordinateInRadius( 50, 20 ) + --self.CargoObject:ReSpawnAt( Coordinate, 0 ) + end + end + + --- Check if the cargo can be Boarded. + -- @param #CARGO_CRATE self + function CARGO_CRATE:CanBoard() + return false + end + + --- Check if the cargo can be Unboarded. + -- @param #CARGO_CRATE self + function CARGO_CRATE:CanUnboard() + return false + end + + --- Check if the cargo can be sling loaded. + -- @param #CARGO_CRATE self + function CARGO_CRATE:CanSlingload() + return false + end + + --- Check if Cargo Crate is in the radius for the Cargo to be reported. + -- @param #CARGO_CRATE self + -- @param Core.Point#COORDINATE Coordinate + -- @return #boolean true if the Cargo Crate is within the report radius. + function CARGO_CRATE:IsInReportRadius( Coordinate ) + --self:F( { Coordinate, LoadRadius = self.LoadRadius } ) + + local Distance = 0 + if self:IsUnLoaded() then + Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() ) + --self:T( Distance ) + if Distance <= self.LoadRadius then + return true + end + end + + return false + end + + + --- Check if Cargo Crate is in the radius for the Cargo to be Boarded or Loaded. + -- @param #CARGO_CRATE self + -- @param Core.Point#Coordinate Coordinate + -- @return #boolean true if the Cargo Crate is within the loading radius. + function CARGO_CRATE:IsInLoadRadius( Coordinate ) + --self:F( { Coordinate, LoadRadius = self.NearRadius } ) + + local Distance = 0 + if self:IsUnLoaded() then + Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() ) + --self:T( Distance ) + if Distance <= self.NearRadius then + return true + end + end + + return false + end + + + + --- Get the current Coordinate of the CargoGroup. + -- @param #CARGO_CRATE self + -- @return Core.Point#COORDINATE The current Coordinate of the first Cargo of the CargoGroup. + -- @return #nil There is no valid Cargo in the CargoGroup. + function CARGO_CRATE:GetCoordinate() + --self:F() + + return self.CargoObject:GetCoordinate() + end + + --- Check if the CargoGroup is alive. + -- @param #CARGO_CRATE self + -- @return #boolean true if the CargoGroup is alive. + -- @return #boolean false if the CargoGroup is dead. + function CARGO_CRATE:IsAlive() + + local Alive = true + + -- When the Cargo is Loaded, the Cargo is in the CargoCarrier, so we check if the CargoCarrier is alive. + -- When the Cargo is not Loaded, the Cargo is the CargoObject, so we check if the CargoObject is alive. + if self:IsLoaded() then + Alive = Alive == true and self.CargoCarrier:IsAlive() + else + Alive = Alive == true and self.CargoObject:IsAlive() + end + + return Alive + + end + + + --- Route Cargo to Coordinate and randomize locations. + -- @param #CARGO_CRATE self + -- @param Core.Point#COORDINATE Coordinate + function CARGO_CRATE:RouteTo( Coordinate ) + self:F( {Coordinate = Coordinate } ) + + end + + + --- Check if Cargo is near to the Carrier. + -- The Cargo is near to the Carrier within NearRadius. + -- @param #CARGO_CRATE self + -- @param Wrapper.Group#GROUP CargoCarrier + -- @param #number NearRadius + -- @return #boolean The Cargo is near to the Carrier. + -- @return #nil The Cargo is not near to the Carrier. + function CARGO_CRATE:IsNear( CargoCarrier, NearRadius ) + self:F( {NearRadius = NearRadius } ) + + return self:IsNear( CargoCarrier:GetCoordinate(), NearRadius ) + end + + --- Respawn the CargoGroup. + -- @param #CARGO_CRATE self + function CARGO_CRATE:Respawn() + + self:F( { "Respawning crate " .. self:GetName() } ) + + + -- Respawn the group... + if self.CargoObject then + self.CargoObject:ReSpawn() -- A cargo destroy crates a DEAD event. + self:__Reset( -0.1 ) + end + + + end + + + --- Respawn the CargoGroup. + -- @param #CARGO_CRATE self + function CARGO_CRATE:onafterReset() + + self:F( { "Reset crate " .. self:GetName() } ) + + + -- Respawn the group... + if self.CargoObject then + self:SetDeployed( false ) + self:SetStartState( "UnLoaded" ) + self.CargoCarrier = nil + -- Cargo objects are added to the _DATABASE and SET_CARGO objects. + _EVENTDISPATCHER:CreateEventNewCargo( self ) + end + + + end + + --- Get the transportation method of the Cargo. + -- @param #CARGO_CRATE self + -- @return #string The transportation method of the Cargo. + function CARGO_CRATE:GetTransportationMethod() + if self:IsLoaded() then + return "for unloading" + else + if self:IsUnLoaded() then + return "for loading" + else + if self:IsDeployed() then + return "delivered" + end + end + end + return "" + end + +end + diff --git a/Moose Development/Moose/Cargo/CargoGroup.lua b/Moose Development/Moose/Cargo/CargoGroup.lua new file mode 100644 index 000000000..b64d205c2 --- /dev/null +++ b/Moose Development/Moose/Cargo/CargoGroup.lua @@ -0,0 +1,760 @@ +--- **Cargo** -- Management of grouped cargo logistics, which are based on a @{Wrapper.Group} object. +-- +-- === +-- +-- ### [Demo Missions]() +-- +-- ### [YouTube Playlist]() +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Cargo.CargoGroup +-- @image Cargo_Groups.JPG + + +do -- CARGO_GROUP + + --- @type CARGO_GROUP + -- @field Core.Set#SET_CARGO CargoSet The collection of derived CARGO objects. + -- @field #string GroupName The name of the CargoGroup. + -- @extends Cargo.Cargo#CARGO_REPORTABLE + + --- Defines a cargo that is represented by a @{Wrapper.Group} object within the simulator. + -- The cargo can be Loaded, UnLoaded, Boarded, UnBoarded to and from Carriers. + -- + -- The above cargo classes are used by the following AI_CARGO_ classes to allow AI groups to transport cargo: + -- + -- * AI Armoured Personnel Carriers to transport cargo and engage in battles, using the @{AI.AI_Cargo_APC} module. + -- * AI Helicopters to transport cargo, using the @{AI.AI_Cargo_Helicopter} module. + -- * AI Planes to transport cargo, using the @{AI.AI_Cargo_Airplane} module. + -- * AI Ships is planned. + -- + -- The above cargo classes are also used by the TASK_CARGO_ classes to allow human players to transport cargo as part of a tasking: + -- + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT} to transport cargo by human players. + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_CSAR} to transport downed pilots by human players. + -- + -- @field #CARGO_GROUP CARGO_GROUP + -- + CARGO_GROUP = { + ClassName = "CARGO_GROUP", + } + + --- CARGO_GROUP constructor. + -- This make a new CARGO_GROUP from a @{Wrapper.Group} object. + -- It will "ungroup" the group object within the sim, and will create a @{Set} of individual Unit objects. + -- @param #CARGO_GROUP self + -- @param Wrapper.Group#GROUP CargoGroup Group to be transported as cargo. + -- @param #string Type Cargo type, e.g. "Infantry". This is the type used in SET_CARGO:New():FilterTypes("Infantry") to define the valid cargo groups of the set. + -- @param #string Name A user defined name of the cargo group. This name CAN be the same as the group object but can also have a different name. This name MUST be unique! + -- @param #number LoadRadius (optional) Distance in meters until which a cargo is loaded into the carrier. Cargo outside this radius has to be routed by other means to within the radius to be loaded. + -- @param #number NearRadius (optional) Once the units are within this radius of the carrier, they are actually loaded, i.e. disappear from the scene. + -- @return #CARGO_GROUP Cargo group object. + function CARGO_GROUP:New( CargoGroup, Type, Name, LoadRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO_REPORTABLE:New( Type, Name, 0, LoadRadius, NearRadius ) ) -- #CARGO_GROUP + self:F( { Type, Name, LoadRadius } ) + + self.CargoSet = SET_CARGO:New() + self.CargoGroup = CargoGroup + self.Grouped = true + self.CargoUnitTemplate = {} + + self.NearRadius = NearRadius + + self:SetDeployed( false ) + + local WeightGroup = 0 + local VolumeGroup = 0 + + self.CargoGroup:Destroy() -- destroy and generate a unit removal event, so that the database gets cleaned, and the linked sets get properly cleaned. + + local GroupName = CargoGroup:GetName() + self.CargoName = Name + self.CargoTemplate = UTILS.DeepCopy( _DATABASE:GetGroupTemplate( GroupName ) ) + + self.GroupTemplate = UTILS.DeepCopy( self.CargoTemplate ) + self.GroupTemplate.name = self.CargoName .. "#CARGO" + self.GroupTemplate.groupId = nil + + self.GroupTemplate.units = {} + + for UnitID, UnitTemplate in pairs( self.CargoTemplate.units ) do + UnitTemplate.name = UnitTemplate.name .. "#CARGO" + local CargoUnitName = UnitTemplate.name + self.CargoUnitTemplate[CargoUnitName] = UnitTemplate + + self.GroupTemplate.units[#self.GroupTemplate.units+1] = self.CargoUnitTemplate[CargoUnitName] + self.GroupTemplate.units[#self.GroupTemplate.units].unitId = nil + + -- And we register the spawned unit as part of the CargoSet. + local Unit = UNIT:Register( CargoUnitName ) + + end + + -- Then we register the new group in the database + self.CargoGroup = GROUP:NewTemplate( self.GroupTemplate, self.GroupTemplate.CoalitionID, self.GroupTemplate.CategoryID, self.GroupTemplate.CountryID ) + + -- Now we spawn the new group based on the template created. + self.CargoObject = _DATABASE:Spawn( self.GroupTemplate ) + + for CargoUnitID, CargoUnit in pairs( self.CargoObject:GetUnits() ) do + + + local CargoUnitName = CargoUnit:GetName() + + local Cargo = CARGO_UNIT:New( CargoUnit, Type, CargoUnitName, LoadRadius, NearRadius ) + self.CargoSet:Add( CargoUnitName, Cargo ) + + WeightGroup = WeightGroup + Cargo:GetWeight() + + end + + self:SetWeight( WeightGroup ) + + self:T( { "Weight Cargo", WeightGroup } ) + + -- Cargo objects are added to the _DATABASE and SET_CARGO objects. + _EVENTDISPATCHER:CreateEventNewCargo( self ) + + self:HandleEvent( EVENTS.Dead, self.OnEventCargoDead ) + self:HandleEvent( EVENTS.Crash, self.OnEventCargoDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCargoDead ) + self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventCargoDead ) + + self:SetEventPriority( 4 ) + + return self + end + + + --- Respawn the CargoGroup. + -- @param #CARGO_GROUP self + function CARGO_GROUP:Respawn() + + self:F( { "Respawning" } ) + + for CargoID, CargoData in pairs( self.CargoSet:GetSet() ) do + local Cargo = CargoData -- Cargo.Cargo#CARGO + Cargo:Destroy() -- Destroy the cargo and generate a remove unit event to update the sets. + Cargo:SetStartState( "UnLoaded" ) + end + + -- Now we spawn the new group based on the template created. + _DATABASE:Spawn( self.GroupTemplate ) + + for CargoUnitID, CargoUnit in pairs( self.CargoObject:GetUnits() ) do + + local CargoUnitName = CargoUnit:GetName() + + local Cargo = CARGO_UNIT:New( CargoUnit, self.Type, CargoUnitName, self.LoadRadius ) + self.CargoSet:Add( CargoUnitName, Cargo ) + + end + + self:SetDeployed( false ) + self:SetStartState( "UnLoaded" ) + + end + + --- Ungroup the cargo group into individual groups with one unit. + -- This is required because by default a group will move in formation and this is really an issue for group control. + -- Therefore this method is made to be able to ungroup a group. + -- This works for ground only groups. + -- @param #CARGO_GROUP self + function CARGO_GROUP:Ungroup() + + if self.Grouped == true then + + self.Grouped = false + + self.CargoGroup:Destroy() + + for CargoUnitName, CargoUnit in pairs( self.CargoSet:GetSet() ) do + local CargoUnit = CargoUnit -- Cargo.CargoUnit#CARGO_UNIT + + if CargoUnit:IsUnLoaded() then + local GroupTemplate = UTILS.DeepCopy( self.CargoTemplate ) + --local GroupName = env.getValueDictByKey( GroupTemplate.name ) + + -- We create a new group object with one unit... + -- First we prepare the template... + GroupTemplate.name = self.CargoName .. "#CARGO#" .. CargoUnitName + GroupTemplate.groupId = nil + + if CargoUnit:IsUnLoaded() then + GroupTemplate.units = {} + GroupTemplate.units[1] = self.CargoUnitTemplate[CargoUnitName] + GroupTemplate.units[#GroupTemplate.units].unitId = nil + GroupTemplate.units[#GroupTemplate.units].x = CargoUnit:GetX() + GroupTemplate.units[#GroupTemplate.units].y = CargoUnit:GetY() + GroupTemplate.units[#GroupTemplate.units].heading = CargoUnit:GetHeading() + end + + + -- Then we register the new group in the database + local CargoGroup = GROUP:NewTemplate( GroupTemplate, GroupTemplate.CoalitionID, GroupTemplate.CategoryID, GroupTemplate.CountryID) + + -- Now we spawn the new group based on the template created. + _DATABASE:Spawn( GroupTemplate ) + end + end + + self.CargoObject = nil + end + + + end + + --- Regroup the cargo group into one group with multiple unit. + -- This is required because by default a group will move in formation and this is really an issue for group control. + -- Therefore this method is made to be able to regroup a group. + -- This works for ground only groups. + -- @param #CARGO_GROUP self + function CARGO_GROUP:Regroup() + + self:F("Regroup") + + if self.Grouped == false then + + self.Grouped = true + + local GroupTemplate = UTILS.DeepCopy( self.CargoTemplate ) + GroupTemplate.name = self.CargoName .. "#CARGO" + GroupTemplate.groupId = nil + GroupTemplate.units = {} + + for CargoUnitName, CargoUnit in pairs( self.CargoSet:GetSet() ) do + local CargoUnit = CargoUnit -- Cargo.CargoUnit#CARGO_UNIT + + self:F( { CargoUnit:GetName(), UnLoaded = CargoUnit:IsUnLoaded() } ) + + if CargoUnit:IsUnLoaded() then + + CargoUnit.CargoObject:Destroy() + + GroupTemplate.units[#GroupTemplate.units+1] = self.CargoUnitTemplate[CargoUnitName] + GroupTemplate.units[#GroupTemplate.units].unitId = nil + GroupTemplate.units[#GroupTemplate.units].x = CargoUnit:GetX() + GroupTemplate.units[#GroupTemplate.units].y = CargoUnit:GetY() + GroupTemplate.units[#GroupTemplate.units].heading = CargoUnit:GetHeading() + end + end + + -- Then we register the new group in the database + self.CargoGroup = GROUP:NewTemplate( GroupTemplate, GroupTemplate.CoalitionID, GroupTemplate.CategoryID, GroupTemplate.CountryID ) + + self:F( { "Regroup", GroupTemplate } ) + + -- Now we spawn the new group based on the template created. + self.CargoObject = _DATABASE:Spawn( GroupTemplate ) + end + + end + + + --- @param #CARGO_GROUP self + -- @param Core.Event#EVENTDATA EventData + function CARGO_GROUP:OnEventCargoDead( EventData ) + + self:E(EventData) + + local Destroyed = false + + if self:IsDestroyed() or self:IsUnLoaded() or self:IsBoarding() or self:IsUnboarding() then + Destroyed = true + for CargoID, CargoData in pairs( self.CargoSet:GetSet() ) do + local Cargo = CargoData -- Cargo.Cargo#CARGO + if Cargo:IsAlive() then + Destroyed = false + else + Cargo:Destroyed() + end + end + else + local CarrierName = self.CargoCarrier:GetName() + if CarrierName == EventData.IniDCSUnitName then + MESSAGE:New( "Cargo is lost from carrier " .. CarrierName, 15 ):ToAll() + Destroyed = true + self.CargoCarrier:ClearCargo() + end + end + + if Destroyed then + self:Destroyed() + self:E( { "Cargo group destroyed" } ) + end + + end + + --- After Board Event. + -- @param #CARGO_GROUP self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Wrapper.Unit#UNIT CargoCarrier + -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier. + function CARGO_GROUP:onafterBoard( From, Event, To, CargoCarrier, NearRadius, ... ) + self:F( { CargoCarrier.UnitName, From, Event, To, NearRadius = NearRadius } ) + + NearRadius = NearRadius or self.NearRadius + + -- For each Cargo object within the CARGO_GROUPED, route each object to the CargoLoadPointVec2 + self.CargoSet:ForEach( + function( Cargo, ... ) + self:F( { "Board Unit", Cargo:GetName( ), Cargo:IsDestroyed(), Cargo.CargoObject:IsAlive() } ) + local CargoGroup = Cargo.CargoObject --Wrapper.Group#GROUP + CargoGroup:OptionAlarmStateGreen() + Cargo:__Board( 1, CargoCarrier, NearRadius, ... ) + end, ... + ) + + self:__Boarding( -1, CargoCarrier, NearRadius, ... ) + + end + + --- Enter Loaded State. + -- @param #CARGO_GROUP self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Wrapper.Unit#UNIT CargoCarrier + function CARGO_GROUP:onafterLoad( From, Event, To, CargoCarrier, ... ) + --self:F( { From, Event, To, CargoCarrier, ...} ) + + if From == "UnLoaded" then + -- For each Cargo object within the CARGO_GROUP, load each cargo to the CargoCarrier. + for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do + if not Cargo:IsDestroyed() then + Cargo:Load( CargoCarrier ) + end + end + end + + --self.CargoObject:Destroy() + self.CargoCarrier = CargoCarrier + self.CargoCarrier:AddCargo( self ) + + end + + --- Leave Boarding State. + -- @param #CARGO_GROUP self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Wrapper.Unit#UNIT CargoCarrier + -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier. + function CARGO_GROUP:onafterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) + --self:F( { CargoCarrier.UnitName, From, Event, To } ) + + local Boarded = true + local Cancelled = false + local Dead = true + + self.CargoSet:Flush() + + -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 + for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do + --self:T( { Cargo:GetName(), Cargo.current } ) + + + if not Cargo:is( "Loaded" ) + and (not Cargo:is( "Destroyed" )) then -- If one or more units of a group defined as CARGO_GROUP died, the CARGO_GROUP:Board() command does not trigger the CARGO_GRUOP:OnEnterLoaded() function. + Boarded = false + end + + if Cargo:is( "UnLoaded" ) then + Cancelled = true + end + + if not Cargo:is( "Destroyed" ) then + Dead = false + end + + end + + if not Dead then + + if not Cancelled then + if not Boarded then + self:__Boarding( -5, CargoCarrier, NearRadius, ... ) + else + self:F("Group Cargo is loaded") + self:__Load( 1, CargoCarrier, ... ) + end + else + self:__CancelBoarding( 1, CargoCarrier, NearRadius, ... ) + end + else + self:__Destroyed( 1, CargoCarrier, NearRadius, ... ) + end + + end + + --- Enter UnBoarding State. + -- @param #CARGO_GROUP self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 ToPointVec2 + -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier. + function CARGO_GROUP:onafterUnBoard( From, Event, To, ToPointVec2, NearRadius, ... ) + self:F( {From, Event, To, ToPointVec2, NearRadius } ) + + NearRadius = NearRadius or 25 + + local Timer = 1 + + if From == "Loaded" then + + if self.CargoObject then + self.CargoObject:Destroy() + end + + -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 + self.CargoSet:ForEach( + --- @param Cargo.Cargo#CARGO Cargo + function( Cargo, NearRadius ) + if not Cargo:IsDestroyed() then + local ToVec=nil + if ToPointVec2==nil then + ToVec=self.CargoCarrier:GetPointVec2():GetRandomPointVec2InRadius(2*NearRadius, NearRadius) + else + ToVec=ToPointVec2 + end + Cargo:__UnBoard( Timer, ToVec, NearRadius ) + Timer = Timer + 1 + end + end, { NearRadius } + ) + + + self:__UnBoarding( 1, ToPointVec2, NearRadius, ... ) + end + + end + + --- Leave UnBoarding State. + -- @param #CARGO_GROUP self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 ToPointVec2 + -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier. + function CARGO_GROUP:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius, ... ) + --self:F( { From, Event, To, ToPointVec2, NearRadius } ) + + --local NearRadius = NearRadius or 25 + + local Angle = 180 + local Speed = 10 + local Distance = 5 + + if From == "UnBoarding" then + local UnBoarded = true + + -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 + for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do + self:T( { Cargo:GetName(), Cargo.current } ) + if not Cargo:is( "UnLoaded" ) and not Cargo:IsDestroyed() then + UnBoarded = false + end + end + + if UnBoarded then + self:__UnLoad( 1, ToPointVec2, ... ) + else + self:__UnBoarding( 1, ToPointVec2, NearRadius, ... ) + end + + return false + end + + end + + --- Enter UnLoaded State. + -- @param #CARGO_GROUP self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 + function CARGO_GROUP:onafterUnLoad( From, Event, To, ToPointVec2, ... ) + --self:F( { From, Event, To, ToPointVec2 } ) + + if From == "Loaded" then + + -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 + self.CargoSet:ForEach( + function( Cargo ) + --Cargo:UnLoad( ToPointVec2 ) + local RandomVec2=ToPointVec2:GetRandomPointVec2InRadius(20, 10) + Cargo:UnBoard( RandomVec2 ) + end + ) + + end + + self.CargoCarrier:RemoveCargo( self ) + self.CargoCarrier = nil + + end + + + --- Get the current Coordinate of the CargoGroup. + -- @param #CARGO_GROUP self + -- @return Core.Point#COORDINATE The current Coordinate of the first Cargo of the CargoGroup. + -- @return #nil There is no valid Cargo in the CargoGroup. + function CARGO_GROUP:GetCoordinate() + local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO + + if Cargo then + return Cargo.CargoObject:GetCoordinate() + end + + return nil + end + + --- Get the x position of the cargo. + -- @param #CARGO_GROUP self + -- @return #number + function CARGO:GetX() + + local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO + + if Cargo then + return Cargo:GetCoordinate().x + end + + return nil + end + + --- Get the y position of the cargo. + -- @param #CARGO_GROUP self + -- @return #number + function CARGO:GetY() + + local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO + + if Cargo then + return Cargo:GetCoordinate().z + end + + return nil + end + + + + --- Check if the CargoGroup is alive. + -- @param #CARGO_GROUP self + -- @return #boolean true if the CargoGroup is alive. + -- @return #boolean false if the CargoGroup is dead. + function CARGO_GROUP:IsAlive() + + local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO + return Cargo ~= nil + + end + + + --- Get the first alive Cargo Unit of the Cargo Group. + -- @param #CARGO_GROUP self + -- @return #CARGO_GROUP + function CARGO_GROUP:GetFirstAlive() + + local CargoFirstAlive = nil + + for _, Cargo in pairs( self.CargoSet:GetSet() ) do + if not Cargo:IsDestroyed() then + CargoFirstAlive = Cargo + break + end + end + return CargoFirstAlive + end + + + --- Get the amount of cargo units in the group. + -- @param #CARGO_GROUP self + -- @return #CARGO_GROUP + function CARGO_GROUP:GetCount() + return self.CargoSet:Count() + end + + + --- Get the amount of cargo units in the group. + -- @param #CARGO_GROUP self + -- @return #CARGO_GROUP + function CARGO_GROUP:GetGroup( Cargo ) + local Cargo = Cargo or self:GetFirstAlive() -- Cargo.Cargo#CARGO + return Cargo.CargoObject:GetGroup() + end + + + --- Route Cargo to Coordinate and randomize locations. + -- @param #CARGO_GROUP self + -- @param Core.Point#COORDINATE Coordinate + function CARGO_GROUP:RouteTo( Coordinate ) + --self:F( {Coordinate = Coordinate } ) + + -- For each Cargo within the CargoSet, route each object to the Coordinate + self.CargoSet:ForEach( + function( Cargo ) + Cargo.CargoObject:RouteGroundTo( Coordinate, 10, "vee", 0 ) + end + ) + + end + + --- Check if Cargo is near to the Carrier. + -- The Cargo is near to the Carrier if the first unit of the Cargo Group is within NearRadius. + -- @param #CARGO_GROUP self + -- @param Wrapper.Group#GROUP CargoCarrier + -- @param #number NearRadius + -- @return #boolean The Cargo is near to the Carrier or #nil if the Cargo is not near to the Carrier. + function CARGO_GROUP:IsNear( CargoCarrier, NearRadius ) + self:F( {NearRadius = NearRadius } ) + + for _, Cargo in pairs( self.CargoSet:GetSet() ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + if Cargo:IsAlive() then + if Cargo:IsNear( CargoCarrier:GetCoordinate(), NearRadius ) then + self:F( "Near" ) + return true + end + end + end + + return nil + end + + --- Check if Cargo Group is in the radius for the Cargo to be Boarded. + -- @param #CARGO_GROUP self + -- @param Core.Point#COORDINATE Coordinate + -- @return #boolean true if the Cargo Group is within the load radius. + function CARGO_GROUP:IsInLoadRadius( Coordinate ) + --self:F( { Coordinate } ) + + local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO + + if Cargo then + local Distance = 0 + local CargoCoordinate + if Cargo:IsLoaded() then + CargoCoordinate = Cargo.CargoCarrier:GetCoordinate() + else + CargoCoordinate = Cargo.CargoObject:GetCoordinate() + end + + -- FF check if coordinate could be obtained. This was commented out for some (unknown) reason. But the check seems valid! + if CargoCoordinate then + Distance = Coordinate:Get2DDistance( CargoCoordinate ) + else + return false + end + + self:F( { Distance = Distance, LoadRadius = self.LoadRadius } ) + if Distance <= self.LoadRadius then + return true + else + return false + end + end + + return nil + + end + + + --- Check if Cargo Group is in the report radius. + -- @param #CARGO_GROUP self + -- @param Core.Point#Coordinate Coordinate + -- @return #boolean true if the Cargo Group is within the report radius. + function CARGO_GROUP:IsInReportRadius( Coordinate ) + --self:F( { Coordinate } ) + + local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO + + if Cargo then + self:F( { Cargo } ) + local Distance = 0 + if Cargo:IsUnLoaded() then + Distance = Coordinate:Get2DDistance( Cargo.CargoObject:GetCoordinate() ) + --self:T( Distance ) + if Distance <= self.LoadRadius then + return true + end + end + end + + return nil + + end + + + --- Signal a flare at the position of the CargoGroup. + -- @param #CARGO_GROUP self + -- @param Utilities.Utils#FLARECOLOR FlareColor + function CARGO_GROUP:Flare( FlareColor ) + + local Cargo = self.CargoSet:GetFirst() -- Cargo.Cargo#CARGO + if Cargo then + Cargo:Flare( FlareColor ) + end + end + + --- Smoke the CargoGroup. + -- @param #CARGO_GROUP self + -- @param Utilities.Utils#SMOKECOLOR SmokeColor The color of the smoke. + -- @param #number Radius The radius of randomization around the center of the first element of the CargoGroup. + function CARGO_GROUP:Smoke( SmokeColor, Radius ) + + local Cargo = self.CargoSet:GetFirst() -- Cargo.Cargo#CARGO + + if Cargo then + Cargo:Smoke( SmokeColor, Radius ) + end + end + + --- Check if the first element of the CargoGroup is the given @{Zone}. + -- @param #CARGO_GROUP self + -- @param Core.Zone#ZONE_BASE Zone + -- @return #boolean **true** if the first element of the CargoGroup is in the Zone + -- @return #boolean **false** if there is no element of the CargoGroup in the Zone. + function CARGO_GROUP:IsInZone( Zone ) + --self:F( { Zone } ) + + local Cargo = self.CargoSet:GetFirst() -- Cargo.Cargo#CARGO + + if Cargo then + return Cargo:IsInZone( Zone ) + end + + return nil + + end + + --- Get the transportation method of the Cargo. + -- @param #CARGO_GROUP self + -- @return #string The transportation method of the Cargo. + function CARGO_GROUP:GetTransportationMethod() + if self:IsLoaded() then + return "for unboarding" + else + if self:IsUnLoaded() then + return "for boarding" + else + if self:IsDeployed() then + return "delivered" + end + end + end + return "" + end + + + +end -- CARGO_GROUP diff --git a/Moose Development/Moose/Cargo/CargoSlingload.lua b/Moose Development/Moose/Cargo/CargoSlingload.lua new file mode 100644 index 000000000..8af9d5304 --- /dev/null +++ b/Moose Development/Moose/Cargo/CargoSlingload.lua @@ -0,0 +1,270 @@ +--- **Cargo** -- Management of single cargo crates, which are based on a @{Static} object. The cargo can only be slingloaded. +-- +-- === +-- +-- ### [Demo Missions]() +-- +-- ### [YouTube Playlist]() +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Cargo.CargoSlingload +-- @image Cargo_Slingload.JPG + + +do -- CARGO_SLINGLOAD + + --- Models the behaviour of cargo crates, which can only be slingloaded. + -- @type CARGO_SLINGLOAD + -- @extends Cargo.Cargo#CARGO_REPRESENTABLE + + --- Defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. + -- + -- The above cargo classes are also used by the TASK_CARGO_ classes to allow human players to transport cargo as part of a tasking: + -- + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT} to transport cargo by human players. + -- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_CSAR} to transport downed pilots by human players. + -- + -- === + -- + -- @field #CARGO_SLINGLOAD + CARGO_SLINGLOAD = { + ClassName = "CARGO_SLINGLOAD" + } + + --- CARGO_SLINGLOAD Constructor. + -- @param #CARGO_SLINGLOAD self + -- @param Wrapper.Static#STATIC CargoStatic + -- @param #string Type + -- @param #string Name + -- @param #number LoadRadius (optional) + -- @param #number NearRadius (optional) + -- @return #CARGO_SLINGLOAD + function CARGO_SLINGLOAD:New( CargoStatic, Type, Name, LoadRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoStatic, Type, Name, nil, LoadRadius, NearRadius ) ) -- #CARGO_SLINGLOAD + self:F( { Type, Name, NearRadius } ) + + self.CargoObject = CargoStatic + + -- Cargo objects are added to the _DATABASE and SET_CARGO objects. + _EVENTDISPATCHER:CreateEventNewCargo( self ) + + self:HandleEvent( EVENTS.Dead, self.OnEventCargoDead ) + self:HandleEvent( EVENTS.Crash, self.OnEventCargoDead ) + --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCargoDead ) + self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventCargoDead ) + + self:SetEventPriority( 4 ) + + self.NearRadius = NearRadius or 25 + + return self + end + + + --- @param #CARGO_SLINGLOAD self + -- @param Core.Event#EVENTDATA EventData + function CARGO_SLINGLOAD:OnEventCargoDead( EventData ) + + local Destroyed = false + + if self:IsDestroyed() or self:IsUnLoaded() then + if self.CargoObject:GetName() == EventData.IniUnitName then + if not self.NoDestroy then + Destroyed = true + end + end + end + + if Destroyed then + self:I( { "Cargo crate destroyed: " .. self.CargoObject:GetName() } ) + self:Destroyed() + end + + end + + + --- Check if the cargo can be Slingloaded. + -- @param #CARGO_SLINGLOAD self + function CARGO_SLINGLOAD:CanSlingload() + return true + end + + --- Check if the cargo can be Boarded. + -- @param #CARGO_SLINGLOAD self + function CARGO_SLINGLOAD:CanBoard() + return false + end + + --- Check if the cargo can be Unboarded. + -- @param #CARGO_SLINGLOAD self + function CARGO_SLINGLOAD:CanUnboard() + return false + end + + --- Check if the cargo can be Loaded. + -- @param #CARGO_SLINGLOAD self + function CARGO_SLINGLOAD:CanLoad() + return false + end + + --- Check if the cargo can be Unloaded. + -- @param #CARGO_SLINGLOAD self + function CARGO_SLINGLOAD:CanUnload() + return false + end + + + --- Check if Cargo Crate is in the radius for the Cargo to be reported. + -- @param #CARGO_SLINGLOAD self + -- @param Core.Point#COORDINATE Coordinate + -- @return #boolean true if the Cargo Crate is within the report radius. + function CARGO_SLINGLOAD:IsInReportRadius( Coordinate ) + --self:F( { Coordinate, LoadRadius = self.LoadRadius } ) + + local Distance = 0 + if self:IsUnLoaded() then + Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() ) + if Distance <= self.LoadRadius then + return true + end + end + + return false + end + + + --- Check if Cargo Slingload is in the radius for the Cargo to be Boarded or Loaded. + -- @param #CARGO_SLINGLOAD self + -- @param Core.Point#COORDINATE Coordinate + -- @return #boolean true if the Cargo Slingload is within the loading radius. + function CARGO_SLINGLOAD:IsInLoadRadius( Coordinate ) + --self:F( { Coordinate } ) + + local Distance = 0 + if self:IsUnLoaded() then + Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() ) + if Distance <= self.NearRadius then + return true + end + end + + return false + end + + + + --- Get the current Coordinate of the CargoGroup. + -- @param #CARGO_SLINGLOAD self + -- @return Core.Point#COORDINATE The current Coordinate of the first Cargo of the CargoGroup. + -- @return #nil There is no valid Cargo in the CargoGroup. + function CARGO_SLINGLOAD:GetCoordinate() + --self:F() + + return self.CargoObject:GetCoordinate() + end + + --- Check if the CargoGroup is alive. + -- @param #CARGO_SLINGLOAD self + -- @return #boolean true if the CargoGroup is alive. + -- @return #boolean false if the CargoGroup is dead. + function CARGO_SLINGLOAD:IsAlive() + + local Alive = true + + -- When the Cargo is Loaded, the Cargo is in the CargoCarrier, so we check if the CargoCarrier is alive. + -- When the Cargo is not Loaded, the Cargo is the CargoObject, so we check if the CargoObject is alive. + if self:IsLoaded() then + Alive = Alive == true and self.CargoCarrier:IsAlive() + else + Alive = Alive == true and self.CargoObject:IsAlive() + end + + return Alive + + end + + + --- Route Cargo to Coordinate and randomize locations. + -- @param #CARGO_SLINGLOAD self + -- @param Core.Point#COORDINATE Coordinate + function CARGO_SLINGLOAD:RouteTo( Coordinate ) + --self:F( {Coordinate = Coordinate } ) + + end + + + --- Check if Cargo is near to the Carrier. + -- The Cargo is near to the Carrier within NearRadius. + -- @param #CARGO_SLINGLOAD self + -- @param Wrapper.Group#GROUP CargoCarrier + -- @param #number NearRadius + -- @return #boolean The Cargo is near to the Carrier. + -- @return #nil The Cargo is not near to the Carrier. + function CARGO_SLINGLOAD:IsNear( CargoCarrier, NearRadius ) + --self:F( {NearRadius = NearRadius } ) + + return self:IsNear( CargoCarrier:GetCoordinate(), NearRadius ) + end + + + --- Respawn the CargoGroup. + -- @param #CARGO_SLINGLOAD self + function CARGO_SLINGLOAD:Respawn() + + --self:F( { "Respawning slingload " .. self:GetName() } ) + + + -- Respawn the group... + if self.CargoObject then + self.CargoObject:ReSpawn() -- A cargo destroy crates a DEAD event. + self:__Reset( -0.1 ) + end + + + end + + + --- Respawn the CargoGroup. + -- @param #CARGO_SLINGLOAD self + function CARGO_SLINGLOAD:onafterReset() + + --self:F( { "Reset slingload " .. self:GetName() } ) + + + -- Respawn the group... + if self.CargoObject then + self:SetDeployed( false ) + self:SetStartState( "UnLoaded" ) + self.CargoCarrier = nil + -- Cargo objects are added to the _DATABASE and SET_CARGO objects. + _EVENTDISPATCHER:CreateEventNewCargo( self ) + end + + + end + + --- Get the transportation method of the Cargo. + -- @param #CARGO_SLINGLOAD self + -- @return #string The transportation method of the Cargo. + function CARGO_SLINGLOAD:GetTransportationMethod() + if self:IsLoaded() then + return "for sling loading" + else + if self:IsUnLoaded() then + return "for sling loading" + else + if self:IsDeployed() then + return "delivered" + end + end + end + return "" + end + +end diff --git a/Moose Development/Moose/Cargo/CargoUnit.lua b/Moose Development/Moose/Cargo/CargoUnit.lua new file mode 100644 index 000000000..61319476d --- /dev/null +++ b/Moose Development/Moose/Cargo/CargoUnit.lua @@ -0,0 +1,382 @@ +--- **Cargo** -- Management of single cargo logistics, which are based on a @{Wrapper.Unit} object. +-- +-- === +-- +-- ### [Demo Missions]() +-- +-- ### [YouTube Playlist]() +-- +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Cargo.CargoUnit +-- @image Cargo_Units.JPG + +do -- CARGO_UNIT + + --- Models CARGO in the form of units, which can be boarded, unboarded, loaded, unloaded. + -- @type CARGO_UNIT + -- @extends Cargo.Cargo#CARGO_REPRESENTABLE + + --- Defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. + -- Use the event functions as described above to Load, UnLoad, Board, UnBoard the CARGO_UNIT objects to and from carriers. + -- Note that ground forces behave in a group, and thus, act in formation, regardless if one unit is commanded to move. + -- + -- This class is used in CARGO_GROUP, and is not meant to be used by mission designers individually. + -- + -- === + -- + -- @field #CARGO_UNIT CARGO_UNIT + -- + CARGO_UNIT = { + ClassName = "CARGO_UNIT" + } + + --- CARGO_UNIT Constructor. + -- @param #CARGO_UNIT self + -- @param Wrapper.Unit#UNIT CargoUnit + -- @param #string Type + -- @param #string Name + -- @param #number Weight + -- @param #number LoadRadius (optional) + -- @param #number NearRadius (optional) + -- @return #CARGO_UNIT + function CARGO_UNIT:New( CargoUnit, Type, Name, LoadRadius, NearRadius ) + local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoUnit, Type, Name, LoadRadius, NearRadius ) ) -- #CARGO_UNIT + self:I( { Type, Name, LoadRadius, NearRadius } ) + + self:T( CargoUnit ) + self.CargoObject = CargoUnit + + self:T( self.ClassName ) + + self:SetEventPriority( 5 ) + + return self + end + + --- Enter UnBoarding State. + -- @param #CARGO_UNIT self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 ToPointVec2 + -- @param #number NearRadius (optional) Defaut 25 m. + function CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) + self:F( { From, Event, To, ToPointVec2, NearRadius } ) + + local Angle = 180 + local Speed = 60 + local DeployDistance = 9 + local RouteDistance = 60 + + if From == "Loaded" then + + if not self:IsDestroyed() then + + local CargoCarrier = self.CargoCarrier -- Wrapper.Controllable#CONTROLLABLE + + if CargoCarrier:IsAlive() then + + local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + + + local CargoRoutePointVec2 = CargoCarrierPointVec2:Translate( RouteDistance, CargoDeployHeading ) + + + -- if there is no ToPointVec2 given, then use the CargoRoutePointVec2 + local FromDirectionVec3 = CargoCarrierPointVec2:GetDirectionVec3( ToPointVec2 or CargoRoutePointVec2 ) + local FromAngle = CargoCarrierPointVec2:GetAngleDegrees(FromDirectionVec3) + local FromPointVec2 = CargoCarrierPointVec2:Translate( DeployDistance, FromAngle ) + --local CargoDeployPointVec2 = CargoCarrierPointVec2:GetRandomCoordinateInRadius( 10, 5 ) + + ToPointVec2 = ToPointVec2 or CargoCarrierPointVec2:GetRandomCoordinateInRadius( NearRadius, DeployDistance ) + + -- Respawn the group... + if self.CargoObject then + self.CargoObject:ReSpawnAt( FromPointVec2, CargoDeployHeading ) + self:F( { "CargoUnits:", self.CargoObject:GetGroup():GetName() } ) + self.CargoCarrier = nil + + local Points = {} + + -- From + Points[#Points+1] = FromPointVec2:WaypointGround( Speed, "Vee" ) + + -- To + Points[#Points+1] = ToPointVec2:WaypointGround( Speed, "Vee" ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 1 ) + + + self:__UnBoarding( 1, ToPointVec2, NearRadius ) + end + else + -- the Carrier is dead. This cargo is dead too! + self:Destroyed() + end + end + end + + end + + --- Leave UnBoarding State. + -- @param #CARGO_UNIT self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 ToPointVec2 + -- @param #number NearRadius (optional) Defaut 100 m. + function CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2, NearRadius ) + self:F( { From, Event, To, ToPointVec2, NearRadius } ) + + local Angle = 180 + local Speed = 10 + local Distance = 5 + + if From == "UnBoarding" then + --if self:IsNear( ToPointVec2, NearRadius ) then + return true + --else + + --self:__UnBoarding( 1, ToPointVec2, NearRadius ) + --end + --return false + end + + end + + --- UnBoard Event. + -- @param #CARGO_UNIT self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 ToPointVec2 + -- @param #number NearRadius (optional) Defaut 100 m. + function CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) + self:F( { From, Event, To, ToPointVec2, NearRadius } ) + + self.CargoInAir = self.CargoObject:InAir() + + self:T( self.CargoInAir ) + + -- Only unboard the cargo when the carrier is not in the air. + -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). + if not self.CargoInAir then + + end + + self:__UnLoad( 1, ToPointVec2, NearRadius ) + + end + + + + --- Enter UnLoaded State. + -- @param #CARGO_UNIT self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Core.Point#POINT_VEC2 + function CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 ) + self:F( { ToPointVec2, From, Event, To } ) + + local Angle = 180 + local Speed = 10 + local Distance = 5 + + if From == "Loaded" then + local StartPointVec2 = self.CargoCarrier:GetPointVec2() + local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployCoord = StartPointVec2:Translate( Distance, CargoDeployHeading ) + + ToPointVec2 = ToPointVec2 or COORDINATE:New( CargoDeployCoord.x, CargoDeployCoord.z ) + + -- Respawn the group... + if self.CargoObject then + self.CargoObject:ReSpawnAt( ToPointVec2, 0 ) + self.CargoCarrier = nil + end + + end + + if self.OnUnLoadedCallBack then + self.OnUnLoadedCallBack( self, unpack( self.OnUnLoadedParameters ) ) + self.OnUnLoadedCallBack = nil + end + + end + + --- Board Event. + -- @param #CARGO_UNIT self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Wrapper.Group#GROUP CargoCarrier + -- @param #number NearRadius + function CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier, NearRadius, ... ) + self:F( { From, Event, To, CargoCarrier, NearRadius = NearRadius } ) + + self.CargoInAir = self.CargoObject:InAir() + + local Desc = self.CargoObject:GetDesc() + local MaxSpeed = Desc.speedMaxOffRoad + local TypeName = Desc.typeName + + --self:F({Unit=self.CargoObject:GetName()}) + + -- A cargo unit can only be boarded if it is not dead + + -- Only move the group to the carrier when the cargo is not in the air + -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). + if not self.CargoInAir then + -- If NearRadius is given, then use the given NearRadius, otherwise calculate the NearRadius + -- based upon the Carrier bounding radius, which is calculated from the bounding rectangle on the Y axis. + local NearRadius = NearRadius or CargoCarrier:GetBoundingRadius() + 5 + if self:IsNear( CargoCarrier:GetPointVec2(), NearRadius ) then + self:Load( CargoCarrier, NearRadius, ... ) + else + if MaxSpeed and MaxSpeed == 0 or TypeName and TypeName == "Stinger comm" then + self:Load( CargoCarrier, NearRadius, ... ) + else + + local Speed = 90 + local Angle = 180 + local Distance = 0 + + local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() + local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading ) + + -- Set the CargoObject to state Green to ensure it is boarding! + self.CargoObject:OptionAlarmStateGreen() + + local Points = {} + + local PointStartVec2 = self.CargoObject:GetPointVec2() + + Points[#Points+1] = PointStartVec2:WaypointGround( Speed ) + Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 2 ) + self:__Boarding( -5, CargoCarrier, NearRadius, ... ) + self.RunCount = 0 + end + end + end + end + + + --- Boarding Event. + -- @param #CARGO_UNIT self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Wrapper.Client#CLIENT CargoCarrier + -- @param #number NearRadius Default 25 m. + function CARGO_UNIT:onafterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) + self:F( { From, Event, To, CargoCarrier:GetName(), NearRadius = NearRadius } ) + + self:F( { IsAlive=self.CargoObject:IsAlive() } ) + + if CargoCarrier and CargoCarrier:IsAlive() then -- and self.CargoObject and self.CargoObject:IsAlive() then + if (CargoCarrier:IsAir() and not CargoCarrier:InAir()) or true then + local NearRadius = NearRadius or CargoCarrier:GetBoundingRadius( NearRadius ) + 5 + if self:IsNear( CargoCarrier:GetPointVec2(), NearRadius ) then + self:__Load( -1, CargoCarrier, ... ) + else + if self:IsNear( CargoCarrier:GetPointVec2(), 20 ) then + self:__Boarding( -1, CargoCarrier, NearRadius, ... ) + self.RunCount = self.RunCount + 1 + else + self:__Boarding( -2, CargoCarrier, NearRadius, ... ) + self.RunCount = self.RunCount + 2 + end + if self.RunCount >= 40 then + self.RunCount = 0 + local Speed = 90 + local Angle = 180 + local Distance = 0 + + --self:F({Unit=self.CargoObject:GetName()}) + + local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() + local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. + local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) + local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading ) + + -- Set the CargoObject to state Green to ensure it is boarding! + self.CargoObject:OptionAlarmStateGreen() + + local Points = {} + + local PointStartVec2 = self.CargoObject:GetPointVec2() + + Points[#Points+1] = PointStartVec2:WaypointGround( Speed, "Off road" ) + Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed, "Off road" ) + + local TaskRoute = self.CargoObject:TaskRoute( Points ) + self.CargoObject:SetTask( TaskRoute, 0.2 ) + end + end + else + self.CargoObject:MessageToGroup( "Cancelling Boarding... Get back on the ground!", 5, CargoCarrier:GetGroup(), self:GetName() ) + self:CancelBoarding( CargoCarrier, NearRadius, ... ) + self.CargoObject:SetCommand( self.CargoObject:CommandStopRoute( true ) ) + end + else + self:E("Something is wrong") + end + + end + + + --- Loaded State. + -- @param #CARGO_UNIT self + -- @param #string Event + -- @param #string From + -- @param #string To + -- @param Wrapper.Unit#UNIT CargoCarrier + function CARGO_UNIT:onenterLoaded( From, Event, To, CargoCarrier ) + self:F( { From, Event, To, CargoCarrier } ) + + self.CargoCarrier = CargoCarrier + + --self:F({Unit=self.CargoObject:GetName()}) + + -- Only destroy the CargoObject if there is a CargoObject (packages don't have CargoObjects). + if self.CargoObject then + self.CargoObject:Destroy( false ) + --self.CargoObject:ReSpawnAt( COORDINATE:NewFromVec2( {x=0,y=0} ), 0 ) + end + end + + --- Get the transportation method of the Cargo. + -- @param #CARGO_UNIT self + -- @return #string The transportation method of the Cargo. + function CARGO_UNIT:GetTransportationMethod() + if self:IsLoaded() then + return "for unboarding" + else + if self:IsUnLoaded() then + return "for boarding" + else + if self:IsDeployed() then + return "delivered" + end + end + end + return "" + end + +end -- CARGO_UNIT diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index f642d79b1..cca8fedeb 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -1,6 +1,20 @@ ---- **Core** -- BASE forms **the basis of the MOOSE framework**. Each class within the MOOSE framework derives from BASE. +--- **Core** - The base class within the framework. -- --- ![Banner Image](..\Presentations\BASE\Dia1.JPG) +-- === +-- +-- ## Features: +-- +-- * The construction and inheritance of MOOSE classes. +-- * The class naming and numbering system. +-- * The class hierarchy search system. +-- * The tracing of information or objects during mission execution for debuggin purposes. +-- * The subscription to DCS events for event handling in MOOSE objects. +-- * Object inspection. +-- +-- === +-- +-- All classes within the MOOSE framework are derived from the BASE class. +-- Note: The BASE class is an abstract class and is not meant to be used directly. -- -- === -- @@ -9,7 +23,8 @@ -- -- === -- --- @module Base +-- @module Core.Base +-- @image Core_Base.JPG @@ -26,26 +41,14 @@ local _ClassID = 0 -- @field ClassID The ID number of the class. -- @field ClassNameAndID The name of the class concatenated with the ID number of the class. ---- # 1) #BASE class +--- BASE class +-- +-- # 1. BASE constructor. -- --- All classes within the MOOSE framework are derived from the BASE class. --- --- BASE provides facilities for : +-- Any class derived from BASE, will use the @{Core.Base#BASE.New} constructor embedded in the @{Core.Base#BASE.Inherit} method. +-- See an example at the @{Core.Base#BASE.New} method how this is done. -- --- * The construction and inheritance of MOOSE classes. --- * The class naming and numbering system. --- * The class hierarchy search system. --- * The tracing of information or objects during mission execution for debuggin purposes. --- * The subscription to DCS events for event handling in MOOSE objects. --- --- Note: The BASE class is an abstract class and is not meant to be used directly. --- --- ## 1.1) BASE constructor --- --- Any class derived from BASE, will use the @{Base#BASE.New} constructor embedded in the @{Base#BASE.Inherit} method. --- See an example at the @{Base#BASE.New} method how this is done. --- --- ## 1.2) Trace information for debugging +-- # 2. Trace information for debugging. -- -- The BASE class contains trace methods to trace progress within a mission execution of a certain object. -- These trace methods are inherited by each MOOSE class interiting BASE, soeach object created from derived class from BASE can use the tracing methods to trace its execution. @@ -76,7 +79,7 @@ local _ClassID = 0 -- -- Below a more detailed explanation of the different method types for tracing. -- --- ### 1.2.1) Tracing methods categories +-- ## 2.1. Tracing methods categories. -- -- There are basically 3 types of tracing methods available: -- @@ -84,9 +87,9 @@ local _ClassID = 0 -- * @{#BASE.T}: Used to trace further logic within a function giving optional variables or parameters. A T is indicated at column 44 in the DCS.log file. -- * @{#BASE.E}: Used to always trace information giving optional variables or parameters. An E is indicated at column 44 in the DCS.log file. -- --- ### 1.2.2) Tracing levels +-- ## 2.2 Tracing levels. -- --- There are 3 tracing levels within MOOSE. +-- There are 3 tracing levels within MOOSE. -- These tracing levels were defined to avoid bulks of tracing to be generated by lots of objects. -- -- As such, the F and T methods have additional variants to trace level 2 and 3 respectively: @@ -96,7 +99,7 @@ local _ClassID = 0 -- * @{#BASE.T2}: Trace further logic within a function giving optional variables or parameters with tracing level 2. -- * @{#BASE.T3}: Trace further logic within a function giving optional variables or parameters with tracing level 3. -- --- ### 1.2.3) Trace activation. +-- ## 2.3. Trace activation. -- -- Tracing can be activated in several ways: -- @@ -106,16 +109,17 @@ local _ClassID = 0 -- * Activate only the tracing of a certain method of a certain class through the @{#BASE.TraceClassMethod}() method. -- * Activate only the tracing of a certain level through the @{#BASE.TraceLevel}() method. -- --- ### 1.2.4) Check if tracing is on. +-- ## 2.4. Check if tracing is on. -- -- The method @{#BASE.IsTrace}() will validate if tracing is activated or not. -- --- ## 1.3 DCS simulator Event Handling +-- +-- # 3. DCS simulator Event Handling. -- -- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, -- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. -- --- ### 1.3.1 Subscribe / Unsubscribe to DCS Events +-- ## 3.1. Subscribe / Unsubscribe to DCS Events. -- -- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. -- So, when the DCS event occurs, the class will be notified of that event. @@ -124,10 +128,10 @@ local _ClassID = 0 -- * @{#BASE.HandleEvent}(): Subscribe to a DCS Event. -- * @{#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. -- --- ### 1.3.2 Event Handling of DCS Events +-- ## 3.2. Event Handling of DCS Events. -- -- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called --- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information +-- when the DCS event occurs. The Event Handling method receives an @{Core.Event#EVENTDATA} structure, which contains a lot of information -- about the event that occurred. -- -- Find below an example of the prototype how to write an event handling function for two units: @@ -159,7 +163,7 @@ local _ClassID = 0 -- -- See the @{Event} module for more information about event handling. -- --- ## 1.4) Class identification methods +-- # 4. Class identification methods. -- -- BASE provides methods to get more information of each object: -- @@ -167,7 +171,7 @@ local _ClassID = 0 -- * @{#BASE.GetClassName}(): Gets the name of the object, which is the name of the class the object was instantiated from. -- * @{#BASE.GetClassNameAndID}(): Gets the name and ID of the object. -- --- ## 1.5) All objects derived from BASE can have "States" +-- # 5. All objects derived from BASE can have "States". -- -- A mechanism is in place in MOOSE, that allows to let the objects administer **states**. -- States are essentially properties of objects, which are identified by a **Key** and a **Value**. @@ -182,7 +186,7 @@ local _ClassID = 0 -- Thus, if the state is to be set for the same object as the object for which the method is used, then provide the same -- object name to the method. -- --- ## 1.10) Inheritance +-- # 6. Inheritance. -- -- The following methods are available to implement inheritance -- @@ -191,8 +195,7 @@ local _ClassID = 0 -- -- === -- --- @field #BASE BASE --- +-- @field #BASE BASE = { ClassName = "BASE", ClassID = 0, @@ -609,8 +612,8 @@ end --- Creation of a Birth Event. -- @param #BASE self --- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. --- @param Dcs.DCSWrapper.Object#Object Initiator The initiating object of the event. +-- @param DCS#Time EventTime The time stamp of the event. +-- @param DCS#Object Initiator The initiating object of the event. -- @param #string IniUnitName The initiating unit name. -- @param place -- @param subplace @@ -631,8 +634,8 @@ end --- Creation of a Crash Event. -- @param #BASE self --- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. --- @param Dcs.DCSWrapper.Object#Object Initiator The initiating object of the event. +-- @param DCS#Time EventTime The time stamp of the event. +-- @param DCS#Object Initiator The initiating object of the event. function BASE:CreateEventCrash( EventTime, Initiator ) self:F( { EventTime, Initiator } ) @@ -645,10 +648,42 @@ function BASE:CreateEventCrash( EventTime, Initiator ) world.onEvent( Event ) end +--- Creation of a Dead Event. +-- @param #BASE self +-- @param DCS#Time EventTime The time stamp of the event. +-- @param DCS#Object Initiator The initiating object of the event. +function BASE:CreateEventDead( EventTime, Initiator ) + self:F( { EventTime, Initiator } ) + + local Event = { + id = world.event.S_EVENT_DEAD, + time = EventTime, + initiator = Initiator, + } + + world.onEvent( Event ) +end + +--- Creation of a Remove Unit Event. +-- @param #BASE self +-- @param DCS#Time EventTime The time stamp of the event. +-- @param DCS#Object Initiator The initiating object of the event. +function BASE:CreateEventRemoveUnit( EventTime, Initiator ) + self:F( { EventTime, Initiator } ) + + local Event = { + id = EVENTS.RemoveUnit, + time = EventTime, + initiator = Initiator, + } + + world.onEvent( Event ) +end + --- Creation of a Takeoff Event. -- @param #BASE self --- @param Dcs.DCSTypes#Time EventTime The time stamp of the event. --- @param Dcs.DCSWrapper.Object#Object Initiator The initiating object of the event. +-- @param DCS#Time EventTime The time stamp of the event. +-- @param DCS#Object Initiator The initiating object of the event. function BASE:CreateEventTakeoff( EventTime, Initiator ) self:F( { EventTime, Initiator } ) @@ -661,10 +696,10 @@ function BASE:CreateEventTakeoff( EventTime, Initiator ) world.onEvent( Event ) end --- TODO: Complete Dcs.DCSTypes#Event structure. +-- TODO: Complete DCS#Event structure. --- The main event handling function... This function captures all events generated for the class. -- @param #BASE self --- @param Dcs.DCSTypes#Event event +-- @param DCS#Event event function BASE:onEvent(event) --self:F( { BaseEventCodes[event.id], event } ) @@ -787,8 +822,7 @@ end -- @param Object The object that will hold the Value set by the Key. -- @param Key The key that is used as a reference of the value. Note that the key can be a #string, but it can also be any other type! -- @param Value The value to is stored in the object. --- @return The Value set. --- @return #nil The Key was not found and thus the Value could not be retrieved. +-- @return The Value set. function BASE:SetState( Object, Key, Value ) local ClassNameAndID = Object:GetClassNameAndID() @@ -805,7 +839,7 @@ end -- @param #BASE self -- @param Object The object that holds the Value set by the Key. -- @param Key The key that is used to retrieve the value. Note that the key can be a #string, but it can also be any other type! --- @return The Value retrieved. +-- @return The Value retrieved or nil if the Key was not found and thus the Value could not be retrieved. function BASE:GetState( Object, Key ) local ClassNameAndID = Object:GetClassNameAndID() @@ -818,6 +852,10 @@ function BASE:GetState( Object, Key ) return nil end +--- Clear the state of an object. +-- @param #BASE self +-- @param Object The object that holds the Value set by the Key. +-- @param StateName The key that is should be cleared. function BASE:ClearState( Object, StateName ) local ClassNameAndID = Object:GetClassNameAndID() diff --git a/Moose Development/Moose/Core/Cargo.lua b/Moose Development/Moose/Core/Cargo.lua deleted file mode 100644 index 991669931..000000000 --- a/Moose Development/Moose/Core/Cargo.lua +++ /dev/null @@ -1,1677 +0,0 @@ ---- **Core** -- Management of CARGO logistics, that can be transported from and to transportation carriers. --- --- ![Banner Image](..\Presentations\CARGO\Dia1.JPG) --- --- === --- --- Cargo can be of various forms, always are composed out of ONE object ( one unit or one static or one slingload crate ): --- --- * CARGO_UNIT, represented by a @{Unit} in a singleton @{Group}: Cargo can be represented by a Unit in a Group. a CARGO_UNIT is representable... --- * CARGO_GROUP, represented by a @{Group}. A CARGO_GROUP is reportable... --- --- This module is still under construction, but is described above works already, and will keep working ... --- --- === --- --- # Demo Missions --- --- ### [CARGO Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/CGO%20-%20Cargo) --- --- ### [CARGO Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/CGO%20-%20Cargo) --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- === --- --- # YouTube Channel --- --- ### [CARGO YouTube Channel](https://www.youtube.com/watch?v=tM00lTlkpYs&list=PL7ZUrU4zZUl2zUTuKrLW5RsO9zLMqUtbf) --- --- === --- --- ### Author: **FlightControl** --- ### Contributions: --- --- === --- --- @module Cargo - --- Events - --- Board - ---- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#CARGO] Board --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. --- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). - ---- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#CARGO] __Board --- @param #CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. --- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). - - --- UnBoard - ---- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. --- The cargo must be in the **Loaded** state. --- @function [parent=#CARGO] UnBoard --- @param #CARGO self --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - ---- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier. --- The cargo must be in the **Loaded** state. --- @function [parent=#CARGO] __UnBoard --- @param #CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location. - - --- Load - ---- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#CARGO] Load --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - ---- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading. --- The cargo must be in the **UnLoaded** state. --- @function [parent=#CARGO] __Load --- @param #CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo. - - --- UnLoad - ---- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. --- The cargo must be in the **Loaded** state. --- @function [parent=#CARGO] UnLoad --- @param #CARGO self --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - ---- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading. --- The cargo must be in the **Loaded** state. --- @function [parent=#CARGO] __UnLoad --- @param #CARGO self --- @param #number DelaySeconds The amount of seconds to delay the action. --- @param Core.Point#POINT_VEC2 ToPointVec2 (optional) @{Point#POINT_VEC2) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location. - --- State Transition Functions - --- UnLoaded - ---- @function [parent=#CARGO] OnLeaveUnLoaded --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#CARGO] OnEnterUnLoaded --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- Loaded - ---- @function [parent=#CARGO] OnLeaveLoaded --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#CARGO] OnEnterLoaded --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - --- Boarding - ---- @function [parent=#CARGO] OnLeaveBoarding --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#CARGO] OnEnterBoarding --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). - --- UnBoarding - ---- @function [parent=#CARGO] OnLeaveUnBoarding --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable --- @return #boolean - ---- @function [parent=#CARGO] OnEnterUnBoarding --- @param #CARGO self --- @param Wrapper.Controllable#CONTROLLABLE Controllable - - --- TODO: Find all Carrier objects and make the type of the Carriers Wrapper.Unit#UNIT in the documentation. - -CARGOS = {} - -do -- CARGO - - --- @type CARGO - -- @extends Core.Fsm#FSM_PROCESS - -- @field #string Type A string defining the type of the cargo. eg. Engineers, Equipment, Screwdrivers. - -- @field #string Name A string defining the name of the cargo. The name is the unique identifier of the cargo. - -- @field #number Weight A number defining the weight of the cargo. The weight is expressed in kg. - -- @field #number NearRadius (optional) A number defining the radius in meters when the cargo is near to a Carrier, so that it can be loaded. - -- @field Wrapper.Controllable#CONTROLLABLE CargoObject The alive DCS object representing the cargo. This value can be nil, meaning, that the cargo is not represented anywhere... - -- @field Wrapper.Client#CLIENT CargoCarrier The alive DCS object carrying the cargo. This value can be nil, meaning, that the cargo is not contained anywhere... - -- @field #boolean Slingloadable This flag defines if the cargo can be slingloaded. - -- @field #boolean Moveable This flag defines if the cargo is moveable. - -- @field #boolean Representable This flag defines if the cargo can be represented by a DCS Unit. - -- @field #boolean Containable This flag defines if the cargo can be contained within a DCS Unit. - - --- # (R2.1) CARGO class, extends @{Fsm#FSM_PROCESS} - -- - -- The CARGO class defines the core functions that defines a cargo object within MOOSE. - -- A cargo is a logical object defined that is available for transport, and has a life status within a simulation. - -- - -- The CARGO is a state machine: it manages the different events and states of the cargo. - -- All derived classes from CARGO follow the same state machine, expose the same cargo event functions, and provide the same cargo states. - -- - -- ## CARGO Events: - -- - -- * @{#CARGO.Board}( ToCarrier ): Boards the cargo to a carrier. - -- * @{#CARGO.Load}( ToCarrier ): Loads the cargo into a carrier, regardless of its position. - -- * @{#CARGO.UnBoard}( ToPointVec2 ): UnBoard the cargo from a carrier. This will trigger a movement of the cargo to the option ToPointVec2. - -- * @{#CARGO.UnLoad}( ToPointVec2 ): UnLoads the cargo from a carrier. - -- * @{#CARGO.Dead}( Controllable ): The cargo is dead. The cargo process will be ended. - -- - -- ## CARGO States: - -- - -- * **UnLoaded**: The cargo is unloaded from a carrier. - -- * **Boarding**: The cargo is currently boarding (= running) into a carrier. - -- * **Loaded**: The cargo is loaded into a carrier. - -- * **UnBoarding**: The cargo is currently unboarding (=running) from a carrier. - -- * **Dead**: The cargo is dead ... - -- * **End**: The process has come to an end. - -- - -- ## CARGO 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: - -- - -- * **Leaving** the state. - -- The state transition method needs to start with the name **OnLeave + the name of the state**. - -- If the state transition method returns false, then the processing of the state transition will not be done! - -- If you want to change the behaviour of the AIControllable at this event, return false, - -- but then you'll need to specify your own logic using the AIControllable! - -- - -- * **Entering** the state. - -- The state transition method needs to start with the name **OnEnter + the name of the state**. - -- These state transition methods need to provide a return value, which is specified at the function description. - -- - -- @field #CARGO - CARGO = { - ClassName = "CARGO", - Type = nil, - Name = nil, - Weight = nil, - CargoObject = nil, - CargoCarrier = nil, - Representable = false, - Slingloadable = false, - Moveable = false, - Containable = false, - } - ---- @type CARGO.CargoObjects --- @map < #string, Wrapper.Positionable#POSITIONABLE > The alive POSITIONABLE objects representing the the cargo. - - ---- CARGO Constructor. This class is an abstract class and should not be instantiated. --- @param #CARGO self --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number NearRadius (optional) --- @return #CARGO -function CARGO:New( Type, Name, Weight ) --R2.1 - - local self = BASE:Inherit( self, FSM:New() ) -- #CARGO - self:F( { Type, Name, Weight } ) - - self:SetStartState( "UnLoaded" ) - self:AddTransition( { "UnLoaded", "Boarding" }, "Board", "Boarding" ) - self:AddTransition( "Boarding" , "Boarding", "Boarding" ) - self:AddTransition( "Boarding", "CancelBoarding", "UnLoaded" ) - self:AddTransition( "Boarding", "Load", "Loaded" ) - self:AddTransition( "UnLoaded", "Load", "Loaded" ) - self:AddTransition( "Loaded", "UnBoard", "UnBoarding" ) - self:AddTransition( "UnBoarding", "UnBoarding", "UnBoarding" ) - self:AddTransition( "UnBoarding", "UnLoad", "UnLoaded" ) - self:AddTransition( "Loaded", "UnLoad", "UnLoaded" ) - self:AddTransition( "*", "Damaged", "Damaged" ) - self:AddTransition( "*", "Destroyed", "Destroyed" ) - self:AddTransition( "*", "Respawn", "UnLoaded" ) - - - self.Type = Type - self.Name = Name - self.Weight = Weight - self.CargoObject = nil - self.CargoCarrier = nil - self.Representable = false - self.Slingloadable = false - self.Moveable = false - self.Containable = false - - self:SetDeployed( false ) - - self.CargoScheduler = SCHEDULER:New() - - CARGOS[self.Name] = self - - - return self -end - ---- Destroy the cargo. --- @param #CARGO self -function CARGO:Destroy() - if self.CargoObject then - self.CargoObject:Destroy() - end - self:Destroyed() -end - ---- Get the name of the Cargo. --- @param #CARGO self --- @return #string The name of the Cargo. -function CARGO:GetName() --R2.1 - return self.Name -end - ---- Get the object name of the Cargo. --- @param #CARGO self --- @return #string The object name of the Cargo. -function CARGO:GetObjectName() --R2.1 - if self:IsLoaded() then - return self.CargoCarrier:GetName() - else - return self.CargoObject:GetName() - end -end - ---- Get the type of the Cargo. --- @param #CARGO self --- @return #string The type of the Cargo. -function CARGO:GetType() - return self.Type -end - ---- Get the current coordinates of the Cargo. --- @param #CARGO self --- @return Core.Point#COORDINATE The coordinates of the Cargo. -function CARGO:GetCoordinate() - return self.CargoObject:GetCoordinate() -end - ---- Check if cargo is destroyed. --- @param #CARGO self --- @return #boolean true if destroyed -function CARGO:IsDestroyed() - return self:Is( "Destroyed" ) -end - - ---- Check if cargo is loaded. --- @param #CARGO self --- @return #boolean true if loaded -function CARGO:IsLoaded() - return self:Is( "Loaded" ) -end - ---- Check if cargo is unloaded. --- @param #CARGO self --- @return #boolean true if unloaded -function CARGO:IsUnLoaded() - return self:Is( "UnLoaded" ) -end - ---- Check if cargo is boarding. --- @param #CARGO self --- @return #boolean true if boarding -function CARGO:IsBoarding() - return self:Is( "Boarding" ) -end - ---- Check if cargo is alive. --- @param #CARGO self --- @return #boolean true if unloaded -function CARGO:IsAlive() - - if self:IsLoaded() then - return self.CargoCarrier:IsAlive() - else - return self.CargoObject:IsAlive() - end -end - ---- Set the cargo as deployed --- @param #CARGO self -function CARGO:SetDeployed( Deployed ) - self.Deployed = Deployed -end - ---- Is the cargo deployed --- @param #CARGO self --- @return #boolean -function CARGO:IsDeployed() - return self.Deployed -end - - - - ---- Template method to spawn a new representation of the CARGO in the simulator. --- @param #CARGO self --- @return #CARGO -function CARGO:Spawn( PointVec2 ) - self:F() - -end - ---- Signal a flare at the position of the CARGO. --- @param #CARGO self --- @param Utilities.Utils#FLARECOLOR FlareColor -function CARGO:Flare( FlareColor ) - if self:IsUnLoaded() then - trigger.action.signalFlare( self.CargoObject:GetVec3(), FlareColor , 0 ) - end -end - ---- Signal a white flare at the position of the CARGO. --- @param #CARGO self -function CARGO:FlareWhite() - self:Flare( trigger.flareColor.White ) -end - ---- Signal a yellow flare at the position of the CARGO. --- @param #CARGO self -function CARGO:FlareYellow() - self:Flare( trigger.flareColor.Yellow ) -end - ---- Signal a green flare at the position of the CARGO. --- @param #CARGO self -function CARGO:FlareGreen() - self:Flare( trigger.flareColor.Green ) -end - ---- Signal a red flare at the position of the CARGO. --- @param #CARGO self -function CARGO:FlareRed() - self:Flare( trigger.flareColor.Red ) -end - ---- Smoke the CARGO. --- @param #CARGO self -function CARGO:Smoke( SmokeColor, Range ) - self:F2() - if self:IsUnLoaded() then - if Range then - trigger.action.smoke( self.CargoObject:GetRandomVec3( Range ), SmokeColor ) - else - trigger.action.smoke( self.CargoObject:GetVec3(), SmokeColor ) - end - end -end - ---- Smoke the CARGO Green. --- @param #CARGO self -function CARGO:SmokeGreen() - self:Smoke( trigger.smokeColor.Green, Range ) -end - ---- Smoke the CARGO Red. --- @param #CARGO self -function CARGO:SmokeRed() - self:Smoke( trigger.smokeColor.Red, Range ) -end - ---- Smoke the CARGO White. --- @param #CARGO self -function CARGO:SmokeWhite() - self:Smoke( trigger.smokeColor.White, Range ) -end - ---- Smoke the CARGO Orange. --- @param #CARGO self -function CARGO:SmokeOrange() - self:Smoke( trigger.smokeColor.Orange, Range ) -end - ---- Smoke the CARGO Blue. --- @param #CARGO self -function CARGO:SmokeBlue() - self:Smoke( trigger.smokeColor.Blue, Range ) -end - - - - - - ---- Check if Cargo is the given @{Zone}. --- @param #CARGO self --- @param Core.Zone#ZONE_BASE Zone --- @return #boolean **true** if cargo is in the Zone, **false** if cargo is not in the Zone. -function CARGO:IsInZone( Zone ) - self:F( { Zone } ) - - if self:IsLoaded() then - return Zone:IsPointVec2InZone( self.CargoCarrier:GetPointVec2() ) - else - self:F( { Size = self.CargoObject:GetSize(), Units = self.CargoObject:GetUnits() } ) - if self.CargoObject:GetSize() ~= 0 then - return Zone:IsPointVec2InZone( self.CargoObject:GetPointVec2() ) - else - return false - end - end - - return nil - -end - - ---- Check if CargoCarrier is near the Cargo to be Loaded. --- @param #CARGO self --- @param Core.Point#POINT_VEC2 PointVec2 --- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision). --- @return #boolean -function CARGO:IsNear( PointVec2, NearRadius ) - self:F( { PointVec2, NearRadius } ) - - --local Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) - local Distance = PointVec2:Get2DDistance( self.CargoObject:GetPointVec2() ) - self:T( Distance ) - - if Distance <= NearRadius then - return true - else - return false - end -end - ---- Get the current PointVec2 of the cargo. --- @param #CARGO self --- @return Core.Point#POINT_VEC2 -function CARGO:GetPointVec2() - return self.CargoObject:GetPointVec2() -end - ---- Get the current Coordinate of the cargo. --- @param #CARGO self --- @return Core.Point#COORDINATE -function CARGO:GetCoordinate() - return self.CargoObject:GetCoordinate() -end - ---- Set the weight of the cargo. --- @param #CARGO self --- @param #number Weight The weight in kg. --- @return #CARGO -function CARGO:SetWeight( Weight ) - self.Weight = Weight - return self -end - -end - - -do -- CARGO_REPRESENTABLE - - --- @type CARGO_REPRESENTABLE - -- @extends #CARGO - -- @field test - - --- Models CARGO that is representable by a Unit. - -- @field #CARGO_REPRESENTABLE CARGO_REPRESENTABLE - CARGO_REPRESENTABLE = { - ClassName = "CARGO_REPRESENTABLE" - } - - --- CARGO_REPRESENTABLE Constructor. - -- @param #CARGO_REPRESENTABLE self - -- @param #string Type - -- @param #string Name - -- @param #number Weight - -- @param #number ReportRadius (optional) - -- @param #number NearRadius (optional) - -- @return #CARGO_REPRESENTABLE - function CARGO_REPRESENTABLE:New( CargoObject, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight, ReportRadius, NearRadius ) ) -- #CARGO_REPRESENTABLE - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - return self - end - - --- CARGO_REPRESENTABLE Destructor. - -- @param #CARGO_REPRESENTABLE self - -- @return #CARGO_REPRESENTABLE - function CARGO_REPRESENTABLE:Destroy() - - -- Cargo objects are deleted from the _DATABASE and SET_CARGO objects. - self:F( { CargoName = self:GetName() } ) - _EVENTDISPATCHER:CreateEventDeleteCargo( self ) - - return self - end - - --- Route a cargo unit to a PointVec2. - -- @param #CARGO_REPRESENTABLE self - -- @param Core.Point#POINT_VEC2 ToPointVec2 - -- @param #number Speed - -- @return #CARGO_REPRESENTABLE - function CARGO_REPRESENTABLE:RouteTo( ToPointVec2, Speed ) - self:F2( ToPointVec2 ) - - local Points = {} - - local PointStartVec2 = self.CargoObject:GetPointVec2() - - Points[#Points+1] = PointStartVec2:WaypointGround( Speed ) - Points[#Points+1] = ToPointVec2:WaypointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 2 ) - return self - end - - -end -- CARGO_REPRESENTABLE - - do -- CARGO_REPORTABLE - - --- @type CARGO_REPORTABLE - -- @extends #CARGO - CARGO_REPORTABLE = { - ClassName = "CARGO_REPORTABLE" - } - - --- CARGO_REPORTABLE Constructor. - -- @param #CARGO_REPORTABLE self - -- @param Wrapper.Controllable#Controllable CargoObject - -- @param #string Type - -- @param #string Name - -- @param #number Weight - -- @param #number ReportRadius (optional) - -- @param #number NearRadius (optional) - -- @return #CARGO_REPORTABLE - function CARGO_REPORTABLE:New( CargoObject, Type, Name, Weight, ReportRadius ) - local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight ) ) -- #CARGO_REPORTABLE - self:F( { Type, Name, Weight, ReportRadius } ) - - self.CargoSet = SET_CARGO:New() -- Core.Set#SET_CARGO - - self.ReportRadius = ReportRadius or 1000 - self.CargoObject = CargoObject - - - - return self - end - - --- Check if CargoCarrier is in the ReportRadius for the Cargo to be Loaded. - -- @param #CARGO_REPORTABLE self - -- @param Core.Point#POINT_VEC2 PointVec2 - -- @return #boolean - function CARGO_REPORTABLE:IsInRadius( PointVec2 ) - self:F( { PointVec2 } ) - - local Distance = 0 - if self:IsLoaded() then - Distance = PointVec2:DistanceFromPointVec2( self.CargoCarrier:GetPointVec2() ) - else - Distance = PointVec2:DistanceFromPointVec2( self.CargoObject:GetPointVec2() ) - end - self:T( Distance ) - - if Distance <= self.ReportRadius then - return true - else - return false - end - - - end - - --- Send a CC message to a GROUP. - -- @param #CARGO_REPORTABLE self - -- @param #string Message - -- @param Wrapper.Group#GROUP TaskGroup - -- @param #sring Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown. - function CARGO_REPORTABLE:MessageToGroup( Message, TaskGroup, Name ) - - local Prefix = Name and "@ " .. Name .. ": " or "@ " .. TaskGroup:GetCallsign() .. ": " - Message = Prefix .. Message - MESSAGE:New( Message, 20, "Cargo: " .. self:GetName() ):ToGroup( TaskGroup ) - - end - - --- Get the range till cargo will board. - -- @param #CARGO_REPORTABLE self - -- @return #number The range till cargo will board. - function CARGO_REPORTABLE:GetBoardingRange() - return self.ReportRadius - end - - --- Respawn the cargo. - -- @param #CARGO_REPORTABLE self - function CARGO_REPORTABLE:Respawn() - - self:F({"Respawning"}) - - for CargoID, CargoData in pairs( self.CargoSet:GetSet() ) do - local Cargo = CargoData -- #CARGO - Cargo:Destroy() - Cargo:SetStartState( "UnLoaded" ) - end - - local CargoObject = self.CargoObject -- Wrapper.Group#GROUP - CargoObject:Destroy() - local Template = CargoObject:GetTemplate() - CargoObject:Respawn( Template ) - - self:SetDeployed( false ) - - local WeightGroup = 0 - - self:SetStartState( "UnLoaded" ) - - end - - -end - -do -- CARGO_UNIT - - --- Models CARGO in the form of units, which can be boarded, unboarded, loaded, unloaded. - -- @type CARGO_UNIT - -- @extends #CARGO_REPRESENTABLE - - --- # CARGO\_UNIT class, extends @{#CARGO_REPRESENTABLE} - -- - -- The CARGO\_UNIT class defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. - -- Use the event functions as described above to Load, UnLoad, Board, UnBoard the CARGO\_UNIT objects to and from carriers. - -- - -- === - -- - -- @field #CARGO_UNIT CARGO_UNIT - -- - CARGO_UNIT = { - ClassName = "CARGO_UNIT" - } - - --- CARGO_UNIT Constructor. - -- @param #CARGO_UNIT self - -- @param Wrapper.Unit#UNIT CargoUnit - -- @param #string Type - -- @param #string Name - -- @param #number Weight - -- @param #number ReportRadius (optional) - -- @param #number NearRadius (optional) - -- @return #CARGO_UNIT - function CARGO_UNIT:New( CargoUnit, Type, Name, Weight, NearRadius ) - local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoUnit, Type, Name, Weight, NearRadius ) ) -- #CARGO_UNIT - self:F( { Type, Name, Weight, NearRadius } ) - - self:T( CargoUnit ) - self.CargoObject = CargoUnit - - self:T( self.ClassName ) - - self:SetEventPriority( 5 ) - - return self - end - - --- Enter UnBoarding State. - -- @param #CARGO_UNIT self - -- @param #string Event - -- @param #string From - -- @param #string To - -- @param Core.Point#POINT_VEC2 ToPointVec2 - function CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) - self:F( { From, Event, To, ToPointVec2, NearRadius } ) - - NearRadius = NearRadius or 25 - - local Angle = 180 - local Speed = 60 - local DeployDistance = 9 - local RouteDistance = 60 - - if From == "Loaded" then - - local CargoCarrier = self.CargoCarrier -- Wrapper.Controllable#CONTROLLABLE - - local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - - - local CargoRoutePointVec2 = CargoCarrierPointVec2:Translate( RouteDistance, CargoDeployHeading ) - - - -- if there is no ToPointVec2 given, then use the CargoRoutePointVec2 - ToPointVec2 = ToPointVec2 or CargoRoutePointVec2 - local DirectionVec3 = CargoCarrierPointVec2:GetDirectionVec3(ToPointVec2) - local Angle = CargoCarrierPointVec2:GetAngleDegrees(DirectionVec3) - - local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( DeployDistance, Angle ) - - local FromPointVec2 = CargoCarrierPointVec2 - - -- Respawn the group... - if self.CargoObject then - self.CargoObject:ReSpawn( CargoDeployPointVec2:GetVec3(), CargoDeployHeading ) - self:F( { "CargoUnits:", self.CargoObject:GetGroup():GetName() } ) - self.CargoCarrier = nil - - local Points = {} - Points[#Points+1] = CargoCarrierPointVec2:WaypointGround( Speed ) - - Points[#Points+1] = ToPointVec2:WaypointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 1 ) - - - self:__UnBoarding( 1, ToPointVec2, NearRadius ) - end - end - - end - - --- Leave UnBoarding State. - -- @param #CARGO_UNIT self - -- @param #string Event - -- @param #string From - -- @param #string To - -- @param Core.Point#POINT_VEC2 ToPointVec2 - function CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2, NearRadius ) - self:F( { From, Event, To, ToPointVec2, NearRadius } ) - - NearRadius = NearRadius or 25 - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "UnBoarding" then - if self:IsNear( ToPointVec2, NearRadius ) then - return true - else - - self:__UnBoarding( 1, ToPointVec2, NearRadius ) - end - return false - end - - end - - --- UnBoard Event. - -- @param #CARGO_UNIT self - -- @param #string Event - -- @param #string From - -- @param #string To - -- @param Core.Point#POINT_VEC2 ToPointVec2 - function CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius ) - self:F( { From, Event, To, ToPointVec2, NearRadius } ) - - NearRadius = NearRadius or 25 - - self.CargoInAir = self.CargoObject:InAir() - - self:T( self.CargoInAir ) - - -- Only unboard the cargo when the carrier is not in the air. - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - - end - - self:__UnLoad( 1, ToPointVec2, NearRadius ) - - end - - - - --- Enter UnLoaded State. - -- @param #CARGO_UNIT self - -- @param #string Event - -- @param #string From - -- @param #string To - -- @param Core.Point#POINT_VEC2 - function CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "Loaded" then - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployCoord = StartPointVec2:Translate( Distance, CargoDeployHeading ) - - ToPointVec2 = ToPointVec2 or COORDINATE:New( CargoDeployCoord.x, CargoDeployCoord.z ) - - -- Respawn the group... - if self.CargoObject then - self.CargoObject:ReSpawn( ToPointVec2:GetVec3(), 0 ) - self.CargoCarrier = nil - end - - end - - if self.OnUnLoadedCallBack then - self.OnUnLoadedCallBack( self, unpack( self.OnUnLoadedParameters ) ) - self.OnUnLoadedCallBack = nil - end - - end - - --- Board Event. - -- @param #CARGO_UNIT self - -- @param #string Event - -- @param #string From - -- @param #string To - function CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier, NearRadius, ... ) - self:F( { From, Event, To, CargoCarrier, NearRadius } ) - - local NearRadius = NearRadius or 25 - - self.CargoInAir = self.CargoObject:InAir() - - self:T( self.CargoInAir ) - - -- Only move the group to the carrier when the cargo is not in the air - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - if self:IsNear( CargoCarrier:GetPointVec2(), NearRadius ) then - self:Load( CargoCarrier, NearRadius, ... ) - else - local Speed = 90 - local Angle = 180 - local Distance = 5 - - NearRadius = NearRadius or 25 - - local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading ) - - local Points = {} - - local PointStartVec2 = self.CargoObject:GetPointVec2() - - Points[#Points+1] = PointStartVec2:WaypointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 2 ) - self:__Boarding( -1, CargoCarrier, NearRadius ) - self.RunCount = 0 - end - end - - end - - - --- Boarding Event. - -- @param #CARGO_UNIT self - -- @param #string Event - -- @param #string From - -- @param #string To - -- @param Wrapper.Unit#UNIT CargoCarrier - -- @param #number NearRadius - function CARGO_UNIT:onafterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) - self:F( { From, Event, To, CargoCarrier.UnitName, NearRadius } ) - - - if CargoCarrier and CargoCarrier:IsAlive() then - if CargoCarrier:InAir() == false then - if self:IsNear( CargoCarrier:GetPointVec2(), NearRadius ) then - self:__Load( 1, CargoCarrier, ... ) - else - self:__Boarding( -1, CargoCarrier, NearRadius, ... ) - self.RunCount = self.RunCount + 1 - if self.RunCount >= 60 then - self.RunCount = 0 - local Speed = 90 - local Angle = 180 - local Distance = 5 - - NearRadius = NearRadius or 25 - - local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading ) - - local Points = {} - - local PointStartVec2 = self.CargoObject:GetPointVec2() - - Points[#Points+1] = PointStartVec2:WaypointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) - - local TaskRoute = self.CargoObject:TaskRoute( Points ) - self.CargoObject:SetTask( TaskRoute, 0.2 ) - end - end - else - self.CargoObject:MessageToGroup( "Cancelling Boarding... Get back on the ground!", 5, CargoCarrier:GetGroup(), self:GetName() ) - self:CancelBoarding( CargoCarrier, NearRadius, ... ) - self.CargoObject:SetCommand( self.CargoObject:CommandStopRoute( true ) ) - end - else - self:E("Something is wrong") - end - - end - - - --- Enter Boarding State. - -- @param #CARGO_UNIT self - -- @param #string Event - -- @param #string From - -- @param #string To - -- @param Wrapper.Unit#UNIT CargoCarrier - function CARGO_UNIT:onenterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) - self:F( { From, Event, To, CargoCarrier.UnitName, NearRadius } ) - - local Speed = 90 - local Angle = 180 - local Distance = 5 - - local NearRadius = NearRadius or 25 - - if From == "UnLoaded" or From == "Boarding" then - - end - - end - - --- Loaded State. - -- @param #CARGO_UNIT self - -- @param #string Event - -- @param #string From - -- @param #string To - -- @param Wrapper.Unit#UNIT CargoCarrier - function CARGO_UNIT:onenterLoaded( From, Event, To, CargoCarrier ) - self:F( { From, Event, To, CargoCarrier } ) - - self.CargoCarrier = CargoCarrier - - -- Only destroy the CargoObject is if there is a CargoObject (packages don't have CargoObjects). - if self.CargoObject then - self:T("Destroying") - self.CargoObject:Destroy() - end - end - -end -- CARGO_UNIT - - -do -- CARGO_CRATE - - --- Models the behaviour of cargo crates, which can be slingloaded and boarded on helicopters using the DCS menus. - -- @type CARGO_CRATE - -- @extends #CARGO_REPRESENTABLE - - --- # CARGO\_CRATE class, extends @{#CARGO_REPRESENTABLE} - -- - -- The CARGO\_CRATE class defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier. - -- Use the event functions as described above to Load, UnLoad, Board, UnBoard the CARGO\_CRATE objects to and from carriers. - -- - -- === - -- - -- @field #CARGO_CRATE - CARGO_CRATE = { - ClassName = "CARGO_CRATE" - } - - --- CARGO_CRATE Constructor. - -- @param #CARGO_CRATE self - -- @param #string CrateName - -- @param #string Type - -- @param #string Name - -- @param #number Weight - -- @param #number ReportRadius (optional) - -- @param #number NearRadius (optional) - -- @return #CARGO_CRATE - function CARGO_CRATE:New( CargoCrateName, Type, Name, NearRadius ) - local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoCrateName, Type, Name, nil, NearRadius ) ) -- #CARGO_CRATE - self:F( { Type, Name, NearRadius } ) - - self:T( CargoCrateName ) - _DATABASE:AddStatic( CargoCrateName ) - - self.CargoObject = STATIC:FindByName( CargoCrateName ) - - self:T( self.ClassName ) - - self:SetEventPriority( 5 ) - - return self - end - - - - --- Enter UnLoaded State. - -- @param #CARGO_CRATE self - -- @param #string Event - -- @param #string From - -- @param #string To - -- @param Core.Point#POINT_VEC2 - function CARGO_CRATE:onenterUnLoaded( From, Event, To, ToPointVec2 ) - self:F( { ToPointVec2, From, Event, To } ) - - local Angle = 180 - local Speed = 10 - local Distance = 10 - - if From == "Loaded" then - local StartCoordinate = self.CargoCarrier:GetCoordinate() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployCoord = StartCoordinate:Translate( Distance, CargoDeployHeading ) - - ToPointVec2 = ToPointVec2 or COORDINATE:NewFromVec2( { x= CargoDeployCoord.x, y = CargoDeployCoord.z } ) - - -- Respawn the group... - if self.CargoObject then - self.CargoObject:ReSpawn( ToPointVec2, 0 ) - self.CargoCarrier = nil - end - - end - - if self.OnUnLoadedCallBack then - self.OnUnLoadedCallBack( self, unpack( self.OnUnLoadedParameters ) ) - self.OnUnLoadedCallBack = nil - end - - end - - - --- Loaded State. - -- @param #CARGO_CRATE self - -- @param #string Event - -- @param #string From - -- @param #string To - -- @param Wrapper.Unit#UNIT CargoCarrier - function CARGO_CRATE:onenterLoaded( From, Event, To, CargoCarrier ) - self:F( { From, Event, To, CargoCarrier } ) - - self.CargoCarrier = CargoCarrier - - -- Only destroy the CargoObject is if there is a CargoObject (packages don't have CargoObjects). - if self.CargoObject then - self:T("Destroying") - self.CargoObject:Destroy() - end - end - - - -end - -do -- CARGO_GROUP - - --- @type CARGO_GROUP - -- @extends #CARGO_REPORTABLE - - --- # CARGO\_GROUP class - -- - -- The CARGO\_GROUP class defines a cargo that is represented by a @{Group} object within the simulator, and can be transported by a carrier. - -- Use the event functions as described above to Load, UnLoad, Board, UnBoard the CARGO\_GROUP to and from carrier. - -- - -- @field #CARGO_GROUP CARGO_GROUP - -- - CARGO_GROUP = { - ClassName = "CARGO_GROUP", - } - ---- CARGO_GROUP constructor. --- @param #CARGO_GROUP self --- @param Wrapper.Group#GROUP CargoGroup --- @param #string Type --- @param #string Name --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #CARGO_GROUP -function CARGO_GROUP:New( CargoGroup, Type, Name, ReportRadius ) - local self = BASE:Inherit( self, CARGO_REPORTABLE:New( CargoGroup, Type, Name, 0, ReportRadius ) ) -- #CARGO_GROUP - self:F( { Type, Name, ReportRadius } ) - - self.CargoObject = CargoGroup - self:SetDeployed( false ) - self.CargoGroup = CargoGroup - - local WeightGroup = 0 - - for UnitID, UnitData in pairs( CargoGroup:GetUnits() ) do - local Unit = UnitData -- Wrapper.Unit#UNIT - local WeightUnit = Unit:GetDesc().massEmpty - WeightGroup = WeightGroup + WeightUnit - local CargoUnit = CARGO_UNIT:New( Unit, Type, Unit:GetName(), WeightUnit ) - self.CargoSet:Add( CargoUnit:GetName(), CargoUnit ) - end - - self:SetWeight( WeightGroup ) - - self:T( { "Weight Cargo", WeightGroup } ) - - -- Cargo objects are added to the _DATABASE and SET_CARGO objects. - _EVENTDISPATCHER:CreateEventNewCargo( self ) - - self:HandleEvent( EVENTS.Dead, self.OnEventCargoDead ) - self:HandleEvent( EVENTS.Crash, self.OnEventCargoDead ) - self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventCargoDead ) - - self:SetEventPriority( 4 ) - - return self -end - ---- @param #CARGO_GROUP self --- @param Core.Event#EVENTDATA EventData -function CARGO_GROUP:OnEventCargoDead( EventData ) - - local Destroyed = false - - if self:IsDestroyed() or self:IsUnLoaded() then - Destroyed = true - for CargoID, CargoData in pairs( self.CargoSet:GetSet() ) do - local Cargo = CargoData -- #CARGO - if Cargo:IsAlive() then - Destroyed = false - else - Cargo:Destroyed() - end - end - else - local CarrierName = self.CargoCarrier:GetName() - if CarrierName == EventData.IniDCSUnitName then - MESSAGE:New( "Cargo is lost from carrier " .. CarrierName, 15 ):ToAll() - Destroyed = true - self.CargoCarrier:ClearCargo() - end - end - - if Destroyed then - self:Destroyed() - self:E( { "Cargo group destroyed" } ) - end - -end - ---- Enter Boarding State. --- @param #CARGO_GROUP self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function CARGO_GROUP:onenterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - local NearRadius = NearRadius or 25 - - if From == "UnLoaded" then - - -- For each Cargo object within the CARGO_GROUPED, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo, ... ) - Cargo:__Board( 1, CargoCarrier, NearRadius, ... ) - end, ... - ) - - self:__Boarding( 1, CargoCarrier, NearRadius, ... ) - end - -end - ---- Enter Loaded State. --- @param #CARGO_GROUP self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function CARGO_GROUP:onenterLoaded( From, Event, To, CargoCarrier, ... ) - self:F( { From, Event, To, CargoCarrier, ...} ) - - if From == "UnLoaded" then - -- For each Cargo object within the CARGO_GROUP, load each cargo to the CargoCarrier. - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - Cargo:Load( CargoCarrier ) - end - end - - --self.CargoObject:Destroy() - self.CargoCarrier = CargoCarrier - -end - ---- Leave Boarding State. --- @param #CARGO_GROUP self --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #string Event --- @param #string From --- @param #string To -function CARGO_GROUP:onafterBoarding( From, Event, To, CargoCarrier, NearRadius, ... ) - self:F( { CargoCarrier.UnitName, From, Event, To } ) - - local NearRadius = NearRadius or 25 - - local Boarded = true - local Cancelled = false - local Dead = true - - self.CargoSet:Flush() - - -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - self:T( { Cargo:GetName(), Cargo.current } ) - - - if not Cargo:is( "Loaded" ) - and (not Cargo:is( "Destroyed" )) then -- If one or more units of a group defined as CARGO_GROUP died, the CARGO_GROUP:Board() command does not trigger the CARGO_GRUOP:OnEnterLoaded() function. - Boarded = false - end - - if Cargo:is( "UnLoaded" ) then - Cancelled = true - end - - if not Cargo:is( "Destroyed" ) then - Dead = false - end - - end - - if not Dead then - - if not Cancelled then - if not Boarded then - self:__Boarding( 1, CargoCarrier, NearRadius, ... ) - else - self:__Load( 1, CargoCarrier, ... ) - end - else - self:__CancelBoarding( 1, CargoCarrier, NearRadius, ... ) - end - else - self:__Destroyed( 1, CargoCarrier, NearRadius, ... ) - end - -end - ---- Get the amount of cargo units in the group. --- @param #CARGO_GROUP self --- @return #CARGO_GROUP -function CARGO_GROUP:GetCount() - return self.CargoSet:Count() -end - - ---- Enter UnBoarding State. --- @param #CARGO_GROUP self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function CARGO_GROUP:onenterUnBoarding( From, Event, To, ToPointVec2, NearRadius, ... ) - self:F( {From, Event, To, ToPointVec2, NearRadius } ) - - NearRadius = NearRadius or 25 - - local Timer = 1 - - if From == "Loaded" then - - if self.CargoObject then - self.CargoObject:Destroy() - end - - -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo, NearRadius ) - - Cargo:__UnBoard( Timer, ToPointVec2, NearRadius ) - Timer = Timer + 10 - end, { NearRadius } - ) - - - self:__UnBoarding( 1, ToPointVec2, NearRadius, ... ) - end - -end - ---- Leave UnBoarding State. --- @param #CARGO_GROUP self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function CARGO_GROUP:onleaveUnBoarding( From, Event, To, ToPointVec2, NearRadius, ... ) - self:F( { From, Event, To, ToPointVec2, NearRadius } ) - - --local NearRadius = NearRadius or 25 - - local Angle = 180 - local Speed = 10 - local Distance = 5 - - if From == "UnBoarding" then - local UnBoarded = true - - -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 - for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do - self:T( Cargo.current ) - if not Cargo:is( "UnLoaded" ) then - UnBoarded = false - end - end - - if UnBoarded then - return true - else - self:__UnBoarding( 1, ToPointVec2, NearRadius, ... ) - end - - return false - end - -end - ---- UnBoard Event. --- @param #CARGO_GROUP self --- @param Core.Point#POINT_VEC2 ToPointVec2 --- @param #string Event --- @param #string From --- @param #string To -function CARGO_GROUP:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius, ... ) - self:F( { From, Event, To, ToPointVec2, NearRadius } ) - - --local NearRadius = NearRadius or 25 - - self:__UnLoad( 1, ToPointVec2, ... ) -end - - - ---- Enter UnLoaded State. --- @param #CARGO_GROUP self --- @param Core.Point#POINT_VEC2 --- @param #string Event --- @param #string From --- @param #string To -function CARGO_GROUP:onenterUnLoaded( From, Event, To, ToPointVec2, ... ) - self:F( { From, Event, To, ToPointVec2 } ) - - if From == "Loaded" then - - -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2 - self.CargoSet:ForEach( - function( Cargo ) - --Cargo:UnLoad( ToPointVec2 ) - local RandomVec2=ToPointVec2:GetRandomPointVec2InRadius(10) - Cargo:UnLoad( RandomVec2 ) - end - ) - - end - -end - - - --- Respawn the cargo when destroyed - -- @param #CARGO_GROUP self - -- @param #boolean RespawnDestroyed - function CARGO_GROUP:RespawnOnDestroyed( RespawnDestroyed ) - self:F({"In function RespawnOnDestroyed"}) - if RespawnDestroyed then - self.onenterDestroyed = function( self ) - self:F("IN FUNCTION") - self:Respawn() - end - else - self.onenterDestroyed = nil - end - - end - -end -- CARGO_GROUP - -do -- CARGO_PACKAGE - - --- @type CARGO_PACKAGE - -- @extends #CARGO_REPRESENTABLE - CARGO_PACKAGE = { - ClassName = "CARGO_PACKAGE" - } - ---- CARGO_PACKAGE Constructor. --- @param #CARGO_PACKAGE self --- @param Wrapper.Unit#UNIT CargoCarrier The UNIT carrying the package. --- @param #string Type --- @param #string Name --- @param #number Weight --- @param #number ReportRadius (optional) --- @param #number NearRadius (optional) --- @return #CARGO_PACKAGE -function CARGO_PACKAGE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) - local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoCarrier, Type, Name, Weight, ReportRadius, NearRadius ) ) -- #CARGO_PACKAGE - self:F( { Type, Name, Weight, ReportRadius, NearRadius } ) - - self:T( CargoCarrier ) - self.CargoCarrier = CargoCarrier - - return self -end - ---- Board Event. --- @param #CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #number Speed --- @param #number BoardDistance --- @param #number Angle -function CARGO_PACKAGE:onafterOnBoard( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - self:F() - - self.CargoInAir = self.CargoCarrier:InAir() - - self:T( self.CargoInAir ) - - -- Only move the CargoCarrier to the New CargoCarrier when the New CargoCarrier is not in the air. - if not self.CargoInAir then - - local Points = {} - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - self:T( { CargoCarrierHeading, CargoDeployHeading } ) - local CargoDeployPointVec2 = CargoCarrier:GetPointVec2():Translate( BoardDistance, CargoDeployHeading ) - - Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - end - - self:Boarded( CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - -end - ---- Check if CargoCarrier is near the Cargo to be Loaded. --- @param #CARGO_PACKAGE self --- @param Wrapper.Unit#UNIT CargoCarrier --- @return #boolean -function CARGO_PACKAGE:IsNear( CargoCarrier ) - self:F() - - local CargoCarrierPoint = CargoCarrier:GetPointVec2() - - local Distance = CargoCarrierPoint:DistanceFromPointVec2( self.CargoCarrier:GetPointVec2() ) - self:T( Distance ) - - if Distance <= self.NearRadius then - return true - else - return false - end -end - ---- Boarded Event. --- @param #CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function CARGO_PACKAGE:onafterOnBoarded( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - self:F() - - if self:IsNear( CargoCarrier ) then - self:__Load( 1, CargoCarrier, Speed, LoadDistance, Angle ) - else - self:__Boarded( 1, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle ) - end -end - ---- UnBoard Event. --- @param #CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param #number Speed --- @param #number UnLoadDistance --- @param #number UnBoardDistance --- @param #number Radius --- @param #number Angle -function CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle ) - self:F() - - self.CargoInAir = self.CargoCarrier:InAir() - - self:T( self.CargoInAir ) - - -- Only unboard the cargo when the carrier is not in the air. - -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea). - if not self.CargoInAir then - - self:_Next( self.FsmP.UnLoad, UnLoadDistance, Angle ) - - local Points = {} - - local StartPointVec2 = CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - self:T( { CargoCarrierHeading, CargoDeployHeading } ) - local CargoDeployPointVec2 = StartPointVec2:Translate( UnBoardDistance, CargoDeployHeading ) - - Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) - - local TaskRoute = CargoCarrier:TaskRoute( Points ) - CargoCarrier:SetTask( TaskRoute, 1 ) - end - - self:__UnBoarded( 1 , CargoCarrier, Speed ) - -end - ---- UnBoarded Event. --- @param #CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier -function CARGO_PACKAGE:onafterUnBoarded( From, Event, To, CargoCarrier, Speed ) - self:F() - - if self:IsNear( CargoCarrier ) then - self:__UnLoad( 1, CargoCarrier, Speed ) - else - self:__UnBoarded( 1, CargoCarrier, Speed ) - end -end - ---- Load Event. --- @param #CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param Wrapper.Unit#UNIT CargoCarrier --- @param #number Speed --- @param #number LoadDistance --- @param #number Angle -function CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, LoadDistance, Angle ) - self:F() - - self.CargoCarrier = CargoCarrier - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( LoadDistance, CargoDeployHeading ) - - local Points = {} - Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - -end - ---- UnLoad Event. --- @param #CARGO_PACKAGE self --- @param #string Event --- @param #string From --- @param #string To --- @param #number Distance --- @param #number Angle -function CARGO_PACKAGE:onafterUnLoad( From, Event, To, CargoCarrier, Speed, Distance, Angle ) - self:F() - - local StartPointVec2 = self.CargoCarrier:GetPointVec2() - local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees. - local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle ) - local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading ) - - self.CargoCarrier = CargoCarrier - - local Points = {} - Points[#Points+1] = StartPointVec2:WaypointGround( Speed ) - Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed ) - - local TaskRoute = self.CargoCarrier:TaskRoute( Points ) - self.CargoCarrier:SetTask( TaskRoute, 1 ) - -end - - -end diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua index d90e01284..1a3d7c7e5 100644 --- a/Moose Development/Moose/Core/Database.lua +++ b/Moose Development/Moose/Core/Database.lua @@ -1,4 +1,25 @@ ---- **Core** -- DATABASE manages the database of mission objects. +--- **Core** - Manages several databases containing templates, mission objects, and mission information. +-- +-- === +-- +-- ## Features: +-- +-- * During mission startup, scan the mission environment, and create / instantiate intelligently the different objects as defined within the mission. +-- * Manage database of DCS Group templates (as modelled using the mission editor). +-- - Group templates. +-- - Unit templates. +-- - Statics templates. +-- * Manage database of @{Wrapper.Group#GROUP} objects alive in the mission. +-- * Manage database of @{Wrapper.Unit#UNIT} objects alive in the mission. +-- * Manage database of @{Wrapper.Static#STATIC} objects alive in the mission. +-- * Manage database of players. +-- * Manage database of client slots defined using the mission editor. +-- * Manage database of airbases on the map, and from FARPs and ships as defined using the mission editor. +-- * Manage database of countries. +-- * Manage database of zone names. +-- * Manage database of hits to units and statics. +-- * Manage database of destroys of units and statics. +-- * Manage database of @{Core.Zone#ZONE_BASE} objects. -- -- === -- @@ -7,13 +28,14 @@ -- -- === -- --- @module Database +-- @module Core.Database +-- @image Core_Database.JPG --- @type DATABASE -- @extends Core.Base#BASE ---- # DATABASE class, extends @{Base#BASE} +--- Contains collections of wrapper objects defined within MOOSE that reflect objects within the simulator. -- -- Mission designers can use the DATABASE class to refer to: -- @@ -58,12 +80,14 @@ DATABASE = { ZONENAMES = {}, HITS = {}, DESTROYS = {}, + ZONES = {}, } local _DATABASECoalition = { [1] = "Red", [2] = "Blue", + [3] = "Neutral", } local _DATABASECategory = @@ -92,9 +116,12 @@ function DATABASE:New() self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash ) self:HandleEvent( EVENTS.Hit, self.AccountHits ) self:HandleEvent( EVENTS.NewCargo ) self:HandleEvent( EVENTS.DeleteCargo ) + self:HandleEvent( EVENTS.NewZone ) + self:HandleEvent( EVENTS.DeleteZone ) -- Follow alive players and clients --self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) -- This is not working anymore!, handling this through the birth event. @@ -112,7 +139,7 @@ function DATABASE:New() --- @param #DATABASE self local function CheckPlayers( self ) - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } + local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ), AlivePlayersNeutral = coalition.getPlayers( coalition.side.NEUTRAL )} for CoalitionId, CoalitionData in pairs( CoalitionsData ) do --self:E( { "CoalitionData:", CoalitionData } ) for UnitId, UnitData in pairs( CoalitionData ) do @@ -182,7 +209,10 @@ function DATABASE:AddStatic( DCSStaticName ) if not self.STATICS[DCSStaticName] then self.STATICS[DCSStaticName] = STATIC:Register( DCSStaticName ) + return self.STATICS[DCSStaticName] end + + return nil end @@ -242,35 +272,179 @@ function DATABASE:FindAirbase( AirbaseName ) return AirbaseFound end ---- Adds a Cargo based on the Cargo Name in the DATABASE. --- @param #DATABASE self --- @param #string CargoName The name of the airbase -function DATABASE:AddCargo( Cargo ) - if not self.CARGOS[Cargo.Name] then - self.CARGOS[Cargo.Name] = Cargo +do -- Zones + + --- Finds a @{Zone} based on the zone name. + -- @param #DATABASE self + -- @param #string ZoneName The name of the zone. + -- @return Core.Zone#ZONE_BASE The found ZONE. + function DATABASE:FindZone( ZoneName ) + + local ZoneFound = self.ZONES[ZoneName] + return ZoneFound + end + + --- Adds a @{Zone} based on the zone name in the DATABASE. + -- @param #DATABASE self + -- @param #string ZoneName The name of the zone. + -- @param Core.Zone#ZONE_BASE Zone The zone. + function DATABASE:AddZone( ZoneName, Zone ) + + if not self.ZONES[ZoneName] then + self.ZONES[ZoneName] = Zone + end + end + + + --- Deletes a @{Zone} from the DATABASE based on the zone name. + -- @param #DATABASE self + -- @param #string ZoneName The name of the zone. + function DATABASE:DeleteZone( ZoneName ) + + self.ZONES[ZoneName] = nil + end + + --- 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 -end ---- Deletes a Cargo from the DATABASE based on the Cargo Name. --- @param #DATABASE self --- @param #string CargoName The name of the airbase -function DATABASE:DeleteCargo( CargoName ) + --- Private method that registers new ZONE_BASE derived objects within the DATABASE Object. + -- @param #DATABASE self + -- @return #DATABASE self + function DATABASE:_RegisterZones() - self.CARGOS[CargoName] = nil -end + for ZoneID, ZoneData in pairs( env.mission.triggers.zones ) do + local ZoneName = ZoneData.name ---- Finds an CARGO based on the CargoName. --- @param #DATABASE self --- @param #string CargoName --- @return Wrapper.Cargo#CARGO The found CARGO. -function DATABASE:FindCargo( CargoName ) + self:I( { "Register ZONE:", Name = ZoneName } ) + local Zone = ZONE:New( ZoneName ) + self.ZONENAMES[ZoneName] = ZoneName + self:AddZone( ZoneName, Zone ) + end + + for ZoneGroupName, ZoneGroup in pairs( self.GROUPS ) do + if ZoneGroupName:match("#ZONE_POLYGON") then + local ZoneName1 = ZoneGroupName:match("(.*)#ZONE_POLYGON") + local ZoneName2 = ZoneGroupName:match(".*#ZONE_POLYGON(.*)") + local ZoneName = ZoneName1 .. ( ZoneName2 or "" ) + + self:I( { "Register ZONE_POLYGON:", Name = ZoneName } ) + local Zone_Polygon = ZONE_POLYGON:New( ZoneName, ZoneGroup ) + self.ZONENAMES[ZoneName] = ZoneName + self:AddZone( ZoneName, Zone_Polygon ) + end + end + + end - local CargoFound = self.CARGOS[CargoName] - return CargoFound -end +end -- zone + + +do -- cargo + + --- Adds a Cargo based on the Cargo Name in the DATABASE. + -- @param #DATABASE self + -- @param #string CargoName The name of the airbase + function DATABASE:AddCargo( Cargo ) + + if not self.CARGOS[Cargo.Name] then + self.CARGOS[Cargo.Name] = Cargo + end + end + + + --- Deletes a Cargo from the DATABASE based on the Cargo Name. + -- @param #DATABASE self + -- @param #string CargoName The name of the airbase + function DATABASE:DeleteCargo( CargoName ) + + self.CARGOS[CargoName] = nil + end + + --- Finds an CARGO based on the CargoName. + -- @param #DATABASE self + -- @param #string CargoName + -- @return Wrapper.Cargo#CARGO The found CARGO. + function DATABASE:FindCargo( CargoName ) + + local CargoFound = self.CARGOS[CargoName] + return CargoFound + end + + --- Checks if the Template name has a #CARGO tag. + -- If yes, the group is a cargo. + -- @param #DATABASE self + -- @param #string TemplateName + -- @return #boolean + function DATABASE:IsCargo( TemplateName ) + + TemplateName = env.getValueDictByKey( TemplateName ) + + local Cargo = TemplateName:match( "#(CARGO)" ) + + return Cargo and Cargo == "CARGO" + end + + --- Private method that registers new Static Templates within the DATABASE Object. + -- @param #DATABASE self + -- @return #DATABASE self + function DATABASE:_RegisterCargos() + + local Groups = UTILS.DeepCopy( self.GROUPS ) -- This is a very important statement. CARGO_GROUP:New creates a new _DATABASE.GROUP entry, which will confuse the loop. I searched 4 hours on this to find the bug! + + for CargoGroupName, CargoGroup in pairs( Groups ) do + self:I( { Cargo = CargoGroupName } ) + if self:IsCargo( CargoGroupName ) then + local CargoInfo = CargoGroupName:match("#CARGO(.*)") + local CargoParam = CargoInfo and CargoInfo:match( "%((.*)%)") + local CargoName1 = CargoGroupName:match("(.*)#CARGO%(.*%)") + local CargoName2 = CargoGroupName:match(".*#CARGO%(.*%)(.*)") + local CargoName = CargoName1 .. ( CargoName2 or "" ) + local Type = CargoParam and CargoParam:match( "T=([%a%d ]+),?") + local Name = CargoParam and CargoParam:match( "N=([%a%d]+),?") or CargoName + local LoadRadius = CargoParam and tonumber( CargoParam:match( "RR=([%a%d]+),?") ) + local NearRadius = CargoParam and tonumber( CargoParam:match( "NR=([%a%d]+),?") ) + + self:I({"Register CargoGroup:",Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius}) + CARGO_GROUP:New( CargoGroup, Type, Name, LoadRadius, NearRadius ) + end + end + + for CargoStaticName, CargoStatic in pairs( self.STATICS ) do + if self:IsCargo( CargoStaticName ) then + local CargoInfo = CargoStaticName:match("#CARGO(.*)") + local CargoParam = CargoInfo and CargoInfo:match( "%((.*)%)") + local CargoName = CargoStaticName:match("(.*)#CARGO") + local Type = CargoParam and CargoParam:match( "T=([%a%d ]+),?") + local Category = CargoParam and CargoParam:match( "C=([%a%d ]+),?") + local Name = CargoParam and CargoParam:match( "N=([%a%d]+),?") or CargoName + local LoadRadius = CargoParam and tonumber( CargoParam:match( "RR=([%a%d]+),?") ) + local NearRadius = CargoParam and tonumber( CargoParam:match( "NR=([%a%d]+),?") ) + + if Category == "SLING" then + self:I({"Register CargoSlingload:",Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius}) + CARGO_SLINGLOAD:New( CargoStatic, Type, Name, LoadRadius, NearRadius ) + else + if Category == "CRATE" then + self:I({"Register CargoCrate:",Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius}) + CARGO_CRATE:New( CargoStatic, Type, Name, LoadRadius, NearRadius ) + end + end + end + end + + end + +end -- cargo --- Finds a CLIENT based on the ClientName. -- @param #DATABASE self @@ -311,7 +485,7 @@ end function DATABASE:AddGroup( GroupName ) if not self.GROUPS[GroupName] then - self:E( { "Add GROUP:", GroupName } ) + self:I( { "Add GROUP:", GroupName } ) self.GROUPS[GroupName] = GROUP:Register( GroupName ) end @@ -323,7 +497,7 @@ end function DATABASE:AddPlayer( UnitName, PlayerName ) if PlayerName then - self:E( { "Add player for unit:", UnitName, PlayerName } ) + self:I( { "Add player for unit:", UnitName, PlayerName } ) self.PLAYERS[PlayerName] = UnitName self.PLAYERUNITS[PlayerName] = self:FindUnit( UnitName ) self.PLAYERSJOINED[PlayerName] = PlayerName @@ -335,7 +509,7 @@ end function DATABASE:DeletePlayer( UnitName, PlayerName ) if PlayerName then - self:E( { "Clean player:", PlayerName } ) + self:I( { "Clean player:", PlayerName } ) self.PLAYERS[PlayerName] = nil self.PLAYERUNITS[PlayerName] = nil end @@ -385,8 +559,8 @@ end -- SpawnCountryID, SpawnCategoryID -- This method is used by the SPAWN class. -- @param #DATABASE self --- @param #table SpawnTemplate --- @return #DATABASE self +-- @param #table SpawnTemplate Template of the group to spawn. +-- @return Wrapper.Group#GROUP Spawned group. function DATABASE:Spawn( SpawnTemplate ) self:F( SpawnTemplate.name ) @@ -442,16 +616,14 @@ end --- Private method that registers new Group Templates within the DATABASE Object. -- @param #DATABASE self -- @param #table GroupTemplate --- @param Dcs.DCScoalition#coalition.side CoalitionSide The coalition.side of the object. --- @param Dcs.DCSObject#Object.Category CategoryID The Object.category of the object. --- @param Dcs.DCScountry#country.id CountryID the country.id of the object +-- @param DCS#coalition.side CoalitionSide The coalition.side of the object. +-- @param DCS#Object.Category CategoryID The Object.category of the object. +-- @param DCS#country.id CountryID the country.id of the object -- @return #DATABASE self function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, CategoryID, CountryID, GroupName ) local GroupTemplateName = GroupName or env.getValueDictByKey( GroupTemplate.name ) - local TraceTable = {} - if not self.Templates.Groups[GroupTemplateName] then self.Templates.Groups[GroupTemplateName] = {} self.Templates.Groups[GroupTemplateName].Status = nil @@ -475,18 +647,7 @@ function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, Category self.Templates.Groups[GroupTemplateName].CoalitionID = CoalitionSide self.Templates.Groups[GroupTemplateName].CountryID = CountryID - - TraceTable[#TraceTable+1] = "Group" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].GroupName - - TraceTable[#TraceTable+1] = "Coalition" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].CoalitionID - TraceTable[#TraceTable+1] = "Category" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].CategoryID - TraceTable[#TraceTable+1] = "Country" - TraceTable[#TraceTable+1] = self.Templates.Groups[GroupTemplateName].CountryID - - TraceTable[#TraceTable+1] = "Units" + local UnitNames = {} for unit_num, UnitTemplate in pairs( GroupTemplate.units ) do @@ -510,10 +671,16 @@ function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, Category self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate end - TraceTable[#TraceTable+1] = self.Templates.Units[UnitTemplate.name].UnitName + UnitNames[#UnitNames+1] = self.Templates.Units[UnitTemplate.name].UnitName end - self:E( TraceTable ) + self:I( { Group = self.Templates.Groups[GroupTemplateName].GroupName, + Coalition = self.Templates.Groups[GroupTemplateName].CoalitionID, + Category = self.Templates.Groups[GroupTemplateName].CategoryID, + Country = self.Templates.Groups[GroupTemplateName].CountryID, + Units = UnitNames + } + ) end function DATABASE:GetGroupTemplate( GroupName ) @@ -526,11 +693,11 @@ end --- Private method that registers new Static Templates within the DATABASE Object. -- @param #DATABASE self --- @param #table GroupTemplate +-- @param #table StaticTemplate -- @return #DATABASE self function DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, CategoryID, CountryID ) - local TraceTable = {} + local StaticTemplate = UTILS.DeepCopy( StaticTemplate ) local StaticTemplateName = env.getValueDictByKey(StaticTemplate.name) @@ -547,28 +714,28 @@ function DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, Category self.Templates.Statics[StaticTemplateName].CoalitionID = CoalitionID self.Templates.Statics[StaticTemplateName].CountryID = CountryID + self:I( { Static = self.Templates.Statics[StaticTemplateName].StaticName, + Coalition = self.Templates.Statics[StaticTemplateName].CoalitionID, + Category = self.Templates.Statics[StaticTemplateName].CategoryID, + Country = self.Templates.Statics[StaticTemplateName].CountryID + } + ) + + self:AddStatic( StaticTemplateName ) - TraceTable[#TraceTable+1] = "Static" - TraceTable[#TraceTable+1] = self.Templates.Statics[StaticTemplateName].StaticName - - TraceTable[#TraceTable+1] = "Coalition" - TraceTable[#TraceTable+1] = self.Templates.Statics[StaticTemplateName].CoalitionID - TraceTable[#TraceTable+1] = "Category" - TraceTable[#TraceTable+1] = self.Templates.Statics[StaticTemplateName].CategoryID - TraceTable[#TraceTable+1] = "Country" - TraceTable[#TraceTable+1] = self.Templates.Statics[StaticTemplateName].CountryID - - self:E( TraceTable ) end +--- @param #DATABASE self +function DATABASE:GetStaticGroupTemplate( StaticName ) + local StaticTemplate = self.Templates.Statics[StaticName].GroupTemplate + return StaticTemplate, self.Templates.Statics[StaticName].CoalitionID, self.Templates.Statics[StaticName].CategoryID, self.Templates.Statics[StaticName].CountryID +end + --- @param #DATABASE self function DATABASE:GetStaticUnitTemplate( StaticName ) - local StaticTemplate = self.Templates.Statics[StaticName].UnitTemplate - StaticTemplate.SpawnCoalitionID = self.Templates.Statics[StaticName].CoalitionID - StaticTemplate.SpawnCategoryID = self.Templates.Statics[StaticName].CategoryID - StaticTemplate.SpawnCountryID = self.Templates.Statics[StaticName].CountryID - return StaticTemplate + local UnitTemplate = self.Templates.Statics[StaticName].UnitTemplate + return UnitTemplate, self.Templates.Statics[StaticName].CoalitionID, self.Templates.Statics[StaticName].CategoryID, self.Templates.Statics[StaticName].CountryID end @@ -609,7 +776,7 @@ end -- @return #DATABASE self function DATABASE:_RegisterPlayers() - local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } + local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ), AlivePlayersNeutral = coalition.getPlayers( coalition.side.NEUTRAL ) } for CoalitionId, CoalitionData in pairs( CoalitionsData ) do for UnitId, UnitData in pairs( CoalitionData ) do self:T3( { "UnitData:", UnitData } ) @@ -617,7 +784,7 @@ function DATABASE:_RegisterPlayers() local UnitName = UnitData:getName() local PlayerName = UnitData:getPlayerName() if not self.PLAYERS[PlayerName] then - self:E( { "Add player for unit:", UnitName, PlayerName } ) + self:I( { "Add player for unit:", UnitName, PlayerName } ) self:AddPlayer( UnitName, PlayerName ) end end @@ -633,20 +800,20 @@ end -- @return #DATABASE self function DATABASE:_RegisterGroupsAndUnits() - local CoalitionsData = { GroupsRed = coalition.getGroups( coalition.side.RED ), GroupsBlue = coalition.getGroups( coalition.side.BLUE ) } + local CoalitionsData = { GroupsRed = coalition.getGroups( coalition.side.RED ), GroupsBlue = coalition.getGroups( coalition.side.BLUE ), GroupsNeutral = coalition.getGroups( coalition.side.NEUTRAL ) } for CoalitionId, CoalitionData in pairs( CoalitionsData ) do for DCSGroupId, DCSGroup in pairs( CoalitionData ) do if DCSGroup:isExist() then local DCSGroupName = DCSGroup:getName() - self:E( { "Register Group:", DCSGroupName } ) + self:I( { "Register Group:", DCSGroupName } ) self:AddGroup( DCSGroupName ) for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do local DCSUnitName = DCSUnit:getName() - self:E( { "Register Unit:", DCSUnitName } ) + self:I( { "Register Unit:", DCSUnitName } ) self:AddUnit( DCSUnitName ) end else @@ -655,6 +822,11 @@ function DATABASE:_RegisterGroupsAndUnits() end end + + self:I("Groups:") + for GroupName, Group in pairs( self.GROUPS ) do + self:I( { "Group:", GroupName } ) + end return self end @@ -665,7 +837,7 @@ end function DATABASE:_RegisterClients() for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do - self:E( { "Register Client:", ClientName } ) + self:I( { "Register Client:", ClientName } ) self:AddClient( ClientName ) end @@ -676,14 +848,14 @@ end function DATABASE:_RegisterStatics() local CoalitionsData = { GroupsRed = coalition.getStaticObjects( coalition.side.RED ), GroupsBlue = coalition.getStaticObjects( coalition.side.BLUE ) } - self:E( { Statics = CoalitionsData } ) + self:I( { Statics = CoalitionsData } ) for CoalitionId, CoalitionData in pairs( CoalitionsData ) do for DCSStaticId, DCSStatic in pairs( CoalitionData ) do if DCSStatic:isExist() then local DCSStaticName = DCSStatic:getName() - self:E( { "Register Static:", DCSStaticName } ) + self:I( { "Register Static:", DCSStaticName } ) self:AddStatic( DCSStaticName ) else self:E( { "Static does not exist: ", DCSStatic } ) @@ -703,7 +875,7 @@ function DATABASE:_RegisterAirbases() local DCSAirbaseName = DCSAirbase:getName() - self:E( { "Register Airbase:", DCSAirbaseName } ) + self:I( { "Register Airbase:", DCSAirbaseName, DCSAirbase:getID() } ) self:AddAirbase( DCSAirbaseName ) end end @@ -733,9 +905,8 @@ function DATABASE:_EventOnBirth( Event ) Event.IniUnit = self:FindUnit( Event.IniDCSUnitName ) Event.IniGroup = self:FindGroup( Event.IniDCSGroupName ) local PlayerName = Event.IniUnit:GetPlayerName() - self:E( { "PlayerName:", PlayerName } ) - if PlayerName ~= "" then - self:E( { "Player Joined:", PlayerName } ) + if PlayerName then + self:I( { "Player Joined:", PlayerName } ) if not self.PLAYERS[PlayerName] then self:AddPlayer( Event.IniUnitName, PlayerName ) end @@ -803,8 +974,8 @@ function DATABASE:_EventOnPlayerLeaveUnit( Event ) if Event.IniUnit then if Event.IniObjectCategory == 1 then local PlayerName = Event.IniUnit:GetPlayerName() - if self.PLAYERS[PlayerName] then - self:E( { "Player Left:", PlayerName } ) + if PlayerName and self.PLAYERS[PlayerName] then + self:I( { "Player Left:", PlayerName } ) local Settings = SETTINGS:Set( PlayerName ) Settings:RemovePlayerMenu( Event.IniUnit ) self:DeletePlayer( Event.IniUnit, PlayerName ) @@ -988,6 +1159,31 @@ function DATABASE:OnEventDeleteCargo( EventData ) end +--- Handles the OnEventNewZone event. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA EventData +function DATABASE:OnEventNewZone( EventData ) + self:F2( { EventData } ) + + if EventData.Zone then + self:AddZone( EventData.Zone ) + end +end + + +--- Handles the OnEventDeleteZone. +-- @param #DATABASE self +-- @param Core.Event#EVENTDATA EventData +function DATABASE:OnEventDeleteZone( EventData ) + self:F2( { EventData } ) + + if EventData.Zone then + self:DeleteZone( EventData.Zone.ZoneName ) + end +end + + + --- Gets the player settings -- @param #DATABASE self -- @param #string PlayerName @@ -1019,13 +1215,20 @@ function DATABASE:_RegisterTemplates() self.UNITS = {} --Build routines.db.units and self.Navpoints for CoalitionName, coa_data in pairs(env.mission.coalition) do + self:T({CoalitionName=CoalitionName}) - if (CoalitionName == 'red' or CoalitionName == 'blue') and type(coa_data) == 'table' then + if (CoalitionName == 'red' or CoalitionName == 'blue' or CoalitionName == 'neutrals') and type(coa_data) == 'table' then --self.Units[coa_name] = {} local CoalitionSide = coalition.side[string.upper(CoalitionName)] + if CoalitionName=="red" then + CoalitionSide=coalition.side.NEUTRAL + elseif CoalitionName=="blue" then + CoalitionSide=coalition.side.BLUE + else + CoalitionSide=coalition.side.NEUTRAL + end - ---------------------------------------------- -- build nav points DB self.Navpoints[CoalitionName] = {} if coa_data.nav_points then --navpoints @@ -1040,8 +1243,9 @@ function DATABASE:_RegisterTemplates() self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0 self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y end + end end - end + ------------------------------------------------- if coa_data.country then --there is a country table for cntry_id, cntry_data in pairs(coa_data.country) do @@ -1094,11 +1298,6 @@ function DATABASE:_RegisterTemplates() end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then end --for coa_name, coa_data in pairs(mission.coalition) do - for ZoneID, ZoneData in pairs( env.mission.triggers.zones ) do - local ZoneName = ZoneData.name - self.ZONENAMES[ZoneName] = ZoneName - end - return self end @@ -1181,18 +1380,12 @@ end self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } ) end - self:T( "Something got destroyed" ) - local Destroyed = false -- What is the player destroying? if self.HITS[Event.IniUnitName] then -- Was there a hit for this unit for this player before registered??? - - self.DESTROYS[Event.IniUnitName] = self.DESTROYS[Event.IniUnitName] or {} - self.DESTROYS[Event.IniUnitName] = true - end end diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 1907c3a4c..386169cb0 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -1,10 +1,15 @@ ---- **Core** -- EVENT models DCS **event dispatching** using a **publish-subscribe** model. --- --- ![Banner Image](..\Presentations\EVENT\Dia1.JPG) +--- **Core** - Models DCS event dispatching using a publish-subscribe model. -- -- === -- --- # 1) Event Handling Overview +-- ## Features: +-- +-- * Capture DCS events and dispatch them to the subscribed objects. +-- * Generate DCS events to the subscribed objects from within the code. +-- +-- === +-- +-- # Event Handling Overview -- -- ![Objects](..\Presentations\EVENT\Dia2.JPG) -- @@ -16,7 +21,7 @@ -- Objects can subscribe to different events. The Event dispatcher will publish the received DCS events to the subscribed MOOSE objects, in a specified order. -- In this way, the subscribed MOOSE objects are kept in sync with your evolving running mission. -- --- ## 1.1) Event Dispatching +-- ## 1. Event Dispatching -- -- ![Objects](..\Presentations\EVENT\Dia4.JPG) -- @@ -43,7 +48,7 @@ -- -- But for some DCS events, the publishing order is reversed. This is due to the fact that objects need to be **erased** instead of added. -- --- ## 1.2) Event Handling +-- # 2. Event Handling -- -- ![Objects](..\Presentations\EVENT\Dia8.JPG) -- @@ -55,14 +60,14 @@ -- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator, -- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently. -- --- ### 1.2.1 Subscribe / Unsubscribe to DCS Events +-- ## 2.1. Subscribe to / Unsubscribe from DCS Events. -- -- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class. -- So, when the DCS event occurs, the class will be notified of that event. -- There are two functions which you use to subscribe to or unsubscribe from an event. -- --- * @{Base#BASE.HandleEvent}(): Subscribe to a DCS Event. --- * @{Base#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. +-- * @{Core.Base#BASE.HandleEvent}(): Subscribe to a DCS Event. +-- * @{Core.Base#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event. -- -- Note that for a UNIT, the event will be handled **for that UNIT only**! -- Note that for a GROUP, the event will be handled **for all the UNITs in that GROUP only**! @@ -71,10 +76,10 @@ -- So if a UNIT within the mission has the subscribed event for that object, -- then the object event handler will receive the event for that UNIT! -- --- ### 1.3.2 Event Handling of DCS Events +-- ## 2.2 Event Handling of DCS Events -- -- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called --- when the DCS event occurs. The Event Handling method receives an @{Event#EVENTDATA} structure, which contains a lot of information +-- when the DCS event occurs. The Event Handling method receives an @{Core.Event#EVENTDATA} structure, which contains a lot of information -- about the event that occurred. -- -- Find below an example of the prototype how to write an event handling function for two units: @@ -102,21 +107,21 @@ -- self:SmokeBlue() -- end -- --- ### 1.3.3 Event Handling methods that are automatically called upon subscribed DCS events +-- ## 2.3 Event Handling methods that are automatically called upon subscribed DCS events. -- -- ![Objects](..\Presentations\EVENT\Dia10.JPG) -- -- The following list outlines which EVENTS item in the structure corresponds to which Event Handling method. -- Always ensure that your event handling methods align with the events being subscribed to, or nothing will be executed. -- --- # 2) EVENTS type +-- # 3. EVENTS type -- -- The EVENTS structure contains names for all the different DCS events that objects can subscribe to using the --- @{Base#BASE.HandleEvent}() method. +-- @{Core.Base#BASE.HandleEvent}() method. -- --- # 3) EVENTDATA type +-- # 4. EVENTDATA type -- --- The @{Event#EVENTDATA} structure contains all the fields that are populated with event information before +-- The @{Core.Event#EVENTDATA} structure contains all the fields that are populated with event information before -- an Event Handler method is being called by the event dispatcher. -- The Event Handler received the EVENTDATA object as a parameter, and can be used to investigate further the different events. -- There are basically 4 main categories of information stored in the EVENTDATA structure: @@ -164,23 +169,31 @@ -- -- === -- --- @module Event +-- @module Core.Event +-- @image Core_Event.JPG ---- The EVENT structure --- @type EVENT +--- @type EVENT -- @field #EVENT.Events Events -- @extends Core.Base#BASE + +--- The EVENT class +-- @field #EVENT EVENT = { ClassName = "EVENT", ClassID = 0, + MissionEnd = false, } world.event.S_EVENT_NEW_CARGO = world.event.S_EVENT_MAX + 1000 world.event.S_EVENT_DELETE_CARGO = world.event.S_EVENT_MAX + 1001 +world.event.S_EVENT_NEW_ZONE = world.event.S_EVENT_MAX + 1002 +world.event.S_EVENT_DELETE_ZONE = world.event.S_EVENT_MAX + 1003 +world.event.S_EVENT_REMOVE_UNIT = world.event.S_EVENT_MAX + 1004 + --- The different types of events supported by MOOSE. --- Use this structure to subscribe to events using the @{Base#BASE.HandleEvent}() method. +-- Use this structure to subscribe to events using the @{Core.Base#BASE.HandleEvent}() method. -- @type EVENTS EVENTS = { Shot = world.event.S_EVENT_SHOT, @@ -206,8 +219,14 @@ EVENTS = { PlayerComment = world.event.S_EVENT_PLAYER_COMMENT, ShootingStart = world.event.S_EVENT_SHOOTING_START, ShootingEnd = world.event.S_EVENT_SHOOTING_END, + MarkAdded = world.event.S_EVENT_MARK_ADDED, + MarkChange = world.event.S_EVENT_MARK_CHANGE, + MarkRemoved = world.event.S_EVENT_MARK_REMOVED, 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, + RemoveUnit = world.event.S_EVENT_REMOVE_UNIT, } --- The Event structure @@ -219,36 +238,40 @@ EVENTS = { -- @type EVENTDATA -- @field #number id The identifier of the event. -- --- @field Dcs.DCSUnit#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{Dcs.DCSUnit#Unit} or @{Dcs.DCSStaticObject#StaticObject}. --- @field Dcs.DCSObject#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ). --- @field Dcs.DCSUnit#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. +-- @field DCS#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{DCS#Unit} or @{DCS#StaticObject}. +-- @field DCS#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ). +-- @field DCS#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCS#Unit} or @{DCS#StaticObject}. -- @field #string IniDCSUnitName (UNIT/STATIC) The initiating Unit name. --- @field Wrapper.Unit#UNIT IniUnit (UNIT/STATIC) The initiating MOOSE wrapper @{Unit#UNIT} of the initiator Unit object. +-- @field Wrapper.Unit#UNIT IniUnit (UNIT/STATIC) The initiating MOOSE wrapper @{Wrapper.Unit#UNIT} of the initiator Unit object. -- @field #string IniUnitName (UNIT/STATIC) The initiating UNIT name (same as IniDCSUnitName). --- @field Dcs.DCSGroup#Group IniDCSGroup (UNIT) The initiating {DCSGroup#Group}. +-- @field DCS#Group IniDCSGroup (UNIT) The initiating {DCSGroup#Group}. -- @field #string IniDCSGroupName (UNIT) The initiating Group name. --- @field Wrapper.Group#GROUP IniGroup (UNIT) The initiating MOOSE wrapper @{Group#GROUP} of the initiator Group object. +-- @field Wrapper.Group#GROUP IniGroup (UNIT) The initiating MOOSE wrapper @{Wrapper.Group#GROUP} of the initiator Group object. -- @field #string IniGroupName UNIT) The initiating GROUP name (same as IniDCSGroupName). -- @field #string IniPlayerName (UNIT) The name of the initiating player in case the Unit is a client or player slot. --- @field Dcs.DCScoalition#coalition.side IniCoalition (UNIT) The coalition of the initiator. --- @field Dcs.DCSUnit#Unit.Category IniCategory (UNIT) The category of the initiator. +-- @field DCS#coalition.side IniCoalition (UNIT) The coalition of the initiator. +-- @field DCS#Unit.Category IniCategory (UNIT) The category of the initiator. -- @field #string IniTypeName (UNIT) The type name of the initiator. -- --- @field Dcs.DCSUnit#Unit target (UNIT/STATIC) The target @{Dcs.DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. --- @field Dcs.DCSObject#Object.Category TgtObjectCategory (UNIT/STATIC) The target object category ( Object.Category.UNIT or Object.Category.STATIC ). --- @field Dcs.DCSUnit#Unit TgtDCSUnit (UNIT/STATIC) The target @{DCSUnit#Unit} or @{DCSStaticObject#StaticObject}. +-- @field DCS#Unit target (UNIT/STATIC) The target @{DCS#Unit} or @{DCS#StaticObject}. +-- @field DCS#Object.Category TgtObjectCategory (UNIT/STATIC) The target object category ( Object.Category.UNIT or Object.Category.STATIC ). +-- @field DCS#Unit TgtDCSUnit (UNIT/STATIC) The target @{DCS#Unit} or @{DCS#StaticObject}. -- @field #string TgtDCSUnitName (UNIT/STATIC) The target Unit name. --- @field Wrapper.Unit#UNIT TgtUnit (UNIT/STATIC) The target MOOSE wrapper @{Unit#UNIT} of the target Unit object. +-- @field Wrapper.Unit#UNIT TgtUnit (UNIT/STATIC) The target MOOSE wrapper @{Wrapper.Unit#UNIT} of the target Unit object. -- @field #string TgtUnitName (UNIT/STATIC) The target UNIT name (same as TgtDCSUnitName). --- @field Dcs.DCSGroup#Group TgtDCSGroup (UNIT) The target {DCSGroup#Group}. +-- @field DCS#Group TgtDCSGroup (UNIT) The target {DCSGroup#Group}. -- @field #string TgtDCSGroupName (UNIT) The target Group name. --- @field Wrapper.Group#GROUP TgtGroup (UNIT) The target MOOSE wrapper @{Group#GROUP} of the target Group object. +-- @field Wrapper.Group#GROUP TgtGroup (UNIT) The target MOOSE wrapper @{Wrapper.Group#GROUP} of the target Group object. -- @field #string TgtGroupName (UNIT) The target GROUP name (same as TgtDCSGroupName). -- @field #string TgtPlayerName (UNIT) The name of the target player in case the Unit is a client or player slot. --- @field Dcs.DCScoalition#coalition.side TgtCoalition (UNIT) The coalition of the target. --- @field Dcs.DCSUnit#Unit.Category TgtCategory (UNIT) The category of the target. +-- @field DCS#coalition.side TgtCoalition (UNIT) The coalition of the target. +-- @field DCS#Unit.Category TgtCategory (UNIT) The category of the target. -- @field #string TgtTypeName (UNIT) The type name of the target. -- +-- @field DCS#Airbase place The @{DCS#Airbase} +-- @field Wrapper.Airbase#AIRBASE Place The MOOSE airbase object. +-- @field #string PlaceName The name of the airbase. +-- -- @field weapon The weapon used during the event. -- @field Weapon -- @field WeaponName @@ -395,6 +418,24 @@ local _EVENTMETA = { Event = "OnEventShootingEnd", Text = "S_EVENT_SHOOTING_END" }, + [world.event.S_EVENT_MARK_ADDED] = { + Order = 1, + Side = "I", + Event = "OnEventMarkAdded", + Text = "S_EVENT_MARK_ADDED" + }, + [world.event.S_EVENT_MARK_CHANGE] = { + Order = 1, + Side = "I", + Event = "OnEventMarkChange", + Text = "S_EVENT_MARK_CHANGE" + }, + [world.event.S_EVENT_MARK_REMOVED] = { + Order = 1, + Side = "I", + Event = "OnEventMarkRemoved", + Text = "S_EVENT_MARK_REMOVED" + }, [EVENTS.NewCargo] = { Order = 1, Event = "OnEventNewCargo", @@ -405,6 +446,21 @@ local _EVENTMETA = { Event = "OnEventDeleteCargo", Text = "S_EVENT_DELETE_CARGO" }, + [EVENTS.NewZone] = { + Order = 1, + Event = "OnEventNewZone", + Text = "S_EVENT_NEW_ZONE" + }, + [EVENTS.DeleteZone] = { + Order = 1, + Event = "OnEventDeleteZone", + Text = "S_EVENT_DELETE_ZONE" + }, + [EVENTS.RemoveUnit] = { + Order = -1, + Event = "OnEventRemoveUnit", + Text = "S_EVENT_REMOVE_UNIT" + }, } @@ -422,7 +478,7 @@ end --- Initializes the Events structure for the event -- @param #EVENT self --- @param Dcs.DCSWorld#world.event EventID +-- @param DCS#world.event EventID -- @param Core.Base#BASE EventClass -- @return #EVENT.Events function EVENT:Init( EventID, EventClass ) @@ -448,7 +504,7 @@ end --- Removes a subscription -- @param #EVENT self -- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param Dcs.DCSWorld#world.event EventID +-- @param DCS#world.event EventID -- @return #EVENT.Events function EVENT:RemoveEvent( EventClass, EventID ) @@ -459,7 +515,6 @@ function EVENT:RemoveEvent( EventClass, EventID ) self.Events = self.Events or {} self.Events[EventID] = self.Events[EventID] or {} self.Events[EventID][EventPriority] = self.Events[EventID][EventPriority] or {} - self.Events[EventID][EventPriority][EventClass] = self.Events[EventID][EventPriority][EventClass] self.Events[EventID][EventPriority][EventClass] = nil @@ -468,7 +523,7 @@ end --- Resets subscriptions -- @param #EVENT self -- @param Core.Base#BASE EventClass The self instance of the class for which the event is. --- @param Dcs.DCSWorld#world.event EventID +-- @param DCS#world.event EventID -- @return #EVENT.Events function EVENT:Reset( EventObject ) --R2.1 @@ -491,7 +546,7 @@ end ---- Clears all event subscriptions for a @{Base#BASE} derived object. +--- Clears all event subscriptions for a @{Core.Base#BASE} derived object. -- @param #EVENT self -- @param Core.Base#BASE EventObject function EVENT:RemoveAll( EventObject ) @@ -683,7 +738,7 @@ do -- Event Creation -- @param #EVENT self -- @param AI.AI_Cargo#AI_CARGO Cargo The Cargo created. function EVENT:CreateEventNewCargo( Cargo ) - self:F( { Cargo } ) + self:I( { Cargo } ) local Event = { id = EVENTS.NewCargo, @@ -709,6 +764,36 @@ do -- Event Creation world.onEvent( Event ) end + --- Creation of a New Zone Event. + -- @param #EVENT self + -- @param Core.Zone#ZONE_BASE Zone The Zone created. + function EVENT:CreateEventNewZone( Zone ) + self:F( { Zone } ) + + local Event = { + id = EVENTS.NewZone, + time = timer.getTime(), + zone = Zone, + } + + world.onEvent( Event ) + end + + --- Creation of a Zone Deletion Event. + -- @param #EVENT self + -- @param Core.Zone#ZONE_BASE Zone The Zone created. + function EVENT:CreateEventDeleteZone( Zone ) + self:F( { Zone } ) + + local Event = { + id = EVENTS.DeleteZone, + time = timer.getTime(), + zone = Zone, + } + + world.onEvent( Event ) + end + --- Creation of a S_EVENT_PLAYER_ENTER_UNIT Event. -- @param #EVENT self -- @param Wrapper.Unit#UNIT PlayerUnit. @@ -748,8 +833,13 @@ function EVENT:onEvent( Event ) 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() @@ -788,6 +878,16 @@ function EVENT:onEvent( Event ) 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() @@ -826,7 +926,7 @@ function EVENT:onEvent( Event ) Event.TgtDCSUnit = Event.target Event.TgtDCSUnitName = Event.TgtDCSUnit:getName() Event.TgtUnitName = Event.TgtDCSUnitName - Event.TgtUnit = STATIC:FindByName( 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() @@ -853,17 +953,40 @@ function EVENT:onEvent( Event ) --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:E( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) + self:T( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } ) end for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do @@ -872,7 +995,7 @@ function EVENT:onEvent( Event ) -- 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 @@ -887,7 +1010,8 @@ function EVENT:onEvent( Event ) if EventClass:IsAlive() or Event.id == EVENTS.PlayerEnterUnit or Event.id == EVENTS.Crash or - Event.id == EVENTS.Dead then + Event.id == EVENTS.Dead or + Event.id == EVENTS.RemoveUnit then local UnitName = EventClass:GetName() @@ -937,7 +1061,8 @@ function EVENT:onEvent( Event ) if EventClass:IsAlive() or Event.id == EVENTS.PlayerEnterUnit or Event.id == EVENTS.Crash or - Event.id == EVENTS.Dead then + 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() @@ -1021,6 +1146,16 @@ function EVENT:onEvent( Event ) end end end + + -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. + -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. + -- And this is a problem because it will remove all entries from the SET_CARGOs. + -- To prevent this from happening, the Cargo object has a flag NoDestroy. + -- When true, the SET_CARGO won't Remove the Cargo object from the set. + -- But we need to switch that flag off after the event handlers have been called. + if Event.id == EVENTS.DeleteCargo then + Event.Cargo.NoDestroy = nil + end else self:T( { EventMeta.Text, Event } ) end diff --git a/Moose Development/Moose/Core/Fsm.lua b/Moose Development/Moose/Core/Fsm.lua index 0c49c79e3..ba8e3c4de 100644 --- a/Moose Development/Moose/Core/Fsm.lua +++ b/Moose Development/Moose/Core/Fsm.lua @@ -1,7 +1,18 @@ ---- **Core** -- The **FSM** (**F**inite **S**tate **M**achine) class and derived **FSM\_** classes --- are design patterns allowing efficient (long-lasting) processes and workflows. +--- **Core** - FSM (Finite State Machine) are objects that model and control long lasting business processes and workflow. -- --- ![Banner Image](..\Presentations\FSM\Dia1.JPG) +-- === +-- +-- ## Features: +-- +-- * Provide a base class to model your own state machines. +-- * Trigger events synchronously. +-- * Trigger events asynchronously. +-- * Handle events before or after the event was triggered. +-- * Handle state transitions as a result of event before and after the state change. +-- * For internal moose purposes, further state machines have been designed: +-- - to handle controllables (groups and units). +-- - to handle tasks. +-- - to handle processes. -- -- === -- @@ -52,7 +63,7 @@ -- -- * @{#FSM_TASK}: Models Finite State Machines for @{Task}s. -- * @{#FSM_PROCESS}: Models Finite State Machines for @{Task} actions, which control @{Client}s. --- * @{#FSM_CONTROLLABLE}: Models Finite State Machines for @{Controllable}s, which are @{Group}s, @{Unit}s, @{Client}s. +-- * @{#FSM_CONTROLLABLE}: Models Finite State Machines for @{Wrapper.Controllable}s, which are @{Wrapper.Group}s, @{Wrapper.Unit}s, @{Client}s. -- * @{#FSM_SET}: Models Finite State Machines for @{Set}s. Note that these FSMs control multiple objects!!! So State concerns here -- for multiple objects or the position of the state machine in the process. -- @@ -64,7 +75,8 @@ -- -- === -- --- @module Fsm +-- @module Core.Fsm +-- @image Core_Finite_State_Machine.JPG do -- FSM @@ -72,9 +84,7 @@ do -- FSM -- @extends Core.Base#BASE - --- # FSM class, extends @{Base#BASE} - -- - -- A Finite State Machine (FSM) models a process flow that transitions between various **States** through triggered **Events**. + --- A Finite State Machine (FSM) models a process flow that transitions between various **States** through triggered **Events**. -- -- A FSM can only be in one of a finite number of states. -- The machine is in only one state at a time; the state it is in at any given time is called the **current state**. @@ -328,7 +338,7 @@ do -- FSM -- -- === -- - -- @field #FSM FSM + -- @field #FSM -- FSM = { ClassName = "FSM", @@ -337,7 +347,7 @@ do -- FSM --- Creates a new FSM object. -- @param #FSM self -- @return #FSM - function FSM:New( FsmT ) + function FSM:New() -- Inherits from BASE self = BASE:Inherit( self, BASE:New() ) @@ -410,7 +420,7 @@ do -- FSM return self._Transitions or {} end - --- Set the default @{Process} template with key ProcessName providing the ProcessClass and the process object when it is assigned to a @{Controllable} by the task. + --- Set the default @{Process} template with key ProcessName providing the ProcessClass and the process object when it is assigned to a @{Wrapper.Controllable} by the task. -- @param #FSM self -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states. -- @param #string Event The Event name. @@ -441,6 +451,8 @@ do -- FSM -- @return #table function FSM:GetProcesses() + self:F( { Processes = self._Processes } ) + return self._Processes or {} end @@ -455,6 +467,18 @@ do -- FSM error( "Sub-Process from state " .. From .. " with event " .. Event .. " not found!" ) end + function FSM:SetProcess( From, Event, Fsm ) + + for ProcessID, Process in pairs( self:GetProcesses() ) do + if Process.From == From and Process.Event == Event then + Process.fsm = Fsm + return true + end + end + + error( "Sub-Process from state " .. From .. " with event " .. Event .. " not found!" ) + end + --- Adds an End state. function FSM:AddEndState( State ) @@ -557,8 +581,9 @@ do -- FSM end - function FSM:_call_handler( handler, params, EventName ) + function FSM:_call_handler( step, trigger, params, EventName ) + local handler = step .. trigger local ErrorHandler = function( errmsg ) env.info( "Error in SCHEDULER function:" .. errmsg ) @@ -569,83 +594,117 @@ do -- FSM return errmsg end if self[handler] then - self:T2( "Calling " .. handler ) + self:T( "*** FSM *** " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] ) self._EventSchedules[EventName] = nil local Result, Value = xpcall( function() return self[handler]( self, unpack( params ) ) end, ErrorHandler ) return Value end end + --- @param #FSM self function FSM._handler( self, EventName, ... ) - local Can, to = self:can( EventName ) + local Can, To = self:can( EventName ) - if to == "*" then - to = self.current + if To == "*" then + To = self.current end if Can then - local from = self.current - local params = { from, EventName, to, ... } + local From = self.current + local Params = { From, EventName, To, ... } - if self.Controllable then - self:T( "FSM Transition for " .. self.Controllable.ControllableName .. " :" .. self.current .. " --> " .. EventName .. " --> " .. to ) + + if self["onleave".. From] or + self["OnLeave".. From] or + self["onbefore".. EventName] or + self["OnBefore".. EventName] or + self["onafter".. EventName] or + self["OnAfter".. EventName] or + self["onenter".. To] or + self["OnEnter".. To] + then + if self:_call_handler( "onbefore", EventName, Params, EventName ) == false then + self:T( "*** FSM *** Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** onbefore" .. EventName ) + return false + else + if self:_call_handler( "OnBefore", EventName, Params, EventName ) == false then + self:T( "*** FSM *** Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** OnBefore" .. EventName ) + return false + else + if self:_call_handler( "onleave", From, Params, EventName ) == false then + self:T( "*** FSM *** Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** onleave" .. From ) + return false + else + if self:_call_handler( "OnLeave", From, Params, EventName ) == false then + self:T( "*** FSM *** Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** OnLeave" .. From ) + return false + end + end + end + end else - self:T( "FSM Transition:" .. self.current .. " --> " .. EventName .. " --> " .. to ) - end + local ClassName = self:GetClassName() + if ClassName == "FSM" then + self:T( "*** FSM *** Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To ) + end - if ( self:_call_handler("onbefore" .. EventName, params, EventName ) == false ) - or ( self:_call_handler("OnBefore" .. EventName, params, EventName ) == false ) - or ( self:_call_handler("onleave" .. from, params, EventName ) == false ) - or ( self:_call_handler("OnLeave" .. from, params, EventName ) == false ) then - self:T( "Cancel Transition" ) - return false + if ClassName == "FSM_TASK" then + self:T( "*** FSM *** Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** Task: " .. self.TaskName ) + end + + if ClassName == "FSM_CONTROLLABLE" then + self:T( "*** FSM *** Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** TaskUnit: " .. self.Controllable.ControllableName .. " *** " ) + end + + if ClassName == "FSM_PROCESS" then + self:T( "*** FSM *** Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** Task: " .. self.Task:GetName() .. ", TaskUnit: " .. self.Controllable.ControllableName .. " *** " ) + end end - self.current = to + self.current = To local execute = true - local subtable = self:_gosub( from, EventName ) + local subtable = self:_gosub( From, EventName ) for _, sub in pairs( subtable ) do --if sub.nextevent then -- self:F2( "nextevent = " .. sub.nextevent ) -- self[sub.nextevent]( self ) --end - self:T( "calling sub start event: " .. sub.StartEvent ) + self:T( "*** FSM *** Sub *** " .. sub.StartEvent ) sub.fsm.fsmparent = self sub.fsm.ReturnEvents = sub.ReturnEvents sub.fsm[sub.StartEvent]( sub.fsm ) execute = false end - local fsmparent, Event = self:_isendstate( to ) + local fsmparent, Event = self:_isendstate( To ) if fsmparent and Event then - self:F2( { "end state: ", fsmparent, Event } ) - self:_call_handler("onenter" .. to, params, EventName ) - self:_call_handler("OnEnter" .. to, params, EventName ) - self:_call_handler("onafter" .. EventName, params, EventName ) - self:_call_handler("OnAfter" .. EventName, params, EventName ) - self:_call_handler("onstatechange", params, EventName ) + self:T( "*** FSM *** End *** " .. Event ) + self:_call_handler("onenter", To, Params, EventName ) + self:_call_handler("OnEnter", To, Params, EventName ) + self:_call_handler("onafter", EventName, Params, EventName ) + self:_call_handler("OnAfter", EventName, Params, EventName ) + self:_call_handler("onstate", "change", Params, EventName ) fsmparent[Event]( fsmparent ) execute = false end if execute then + self:_call_handler("onafter", EventName, Params, EventName ) + self:_call_handler("OnAfter", EventName, Params, EventName ) + -- only execute the call if the From state is not equal to the To state! Otherwise this function should never execute! --if from ~= to then - self:_call_handler("onenter" .. to, params, EventName ) - self:_call_handler("OnEnter" .. to, params, EventName ) + self:_call_handler("onenter", To, Params, EventName ) + self:_call_handler("OnEnter", To, Params, EventName ) --end - - self:_call_handler("onafter" .. EventName, params, EventName ) - self:_call_handler("OnAfter" .. EventName, params, EventName ) - - self:_call_handler("onstatechange", params, EventName ) + + self:_call_handler("onstate", "change", Params, EventName ) end else - self:T( "Cannot execute transition." ) - self:T( { From = self.current, Event = EventName, To = to, Can = Can } ) + self:T( "*** FSM *** NO Transition *** " .. self.current .. " --> " .. EventName .. " --> ? " ) end return nil @@ -691,17 +750,16 @@ do -- FSM function FSM:_isendstate( Current ) local FSMParent = self.fsmparent if FSMParent and self.endstates[Current] then - self:T( { state = Current, endstates = self.endstates, endstate = self.endstates[Current] } ) + --self:T( { state = Current, endstates = self.endstates, endstate = self.endstates[Current] } ) FSMParent.current = Current local ParentFrom = FSMParent.current - self:T( ParentFrom ) - self:T( self.ReturnEvents ) + --self:T( { ParentFrom, self.ReturnEvents } ) local Event = self.ReturnEvents[Current] - self:T( { ParentFrom, Event, self.ReturnEvents } ) + --self:T( { Event } ) if Event then return FSMParent, Event else - self:T( { "Could not find parent event name for state ", ParentFrom } ) + --self:T( { "Could not find parent event name for state ", ParentFrom } ) end end @@ -724,6 +782,10 @@ do -- FSM return self.current end + function FSM:GetCurrentState() + return self.current + end + function FSM:Is( State ) return self.current == State @@ -752,14 +814,11 @@ do -- FSM_CONTROLLABLE -- @field Wrapper.Controllable#CONTROLLABLE Controllable -- @extends Core.Fsm#FSM - --- # FSM_CONTROLLABLE, extends @{#FSM} - -- - -- FSM_CONTROLLABLE class models Finite State Machines for @{Controllable}s, which are @{Group}s, @{Unit}s, @{Client}s. + --- Models Finite State Machines for @{Wrapper.Controllable}s, which are @{Wrapper.Group}s, @{Wrapper.Unit}s, @{Client}s. -- -- === -- - -- @field #FSM_CONTROLLABLE FSM_CONTROLLABLE - -- + -- @field #FSM_CONTROLLABLE FSM_CONTROLLABLE = { ClassName = "FSM_CONTROLLABLE", } @@ -769,10 +828,10 @@ do -- FSM_CONTROLLABLE -- @param #table FSMT Finite State Machine Table -- @param Wrapper.Controllable#CONTROLLABLE Controllable (optional) The CONTROLLABLE object that the FSM_CONTROLLABLE governs. -- @return #FSM_CONTROLLABLE - function FSM_CONTROLLABLE:New( FSMT, Controllable ) + function FSM_CONTROLLABLE:New( Controllable ) -- Inherits from BASE - local self = BASE:Inherit( self, FSM:New( FSMT ) ) -- Core.Fsm#FSM_CONTROLLABLE + local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_CONTROLLABLE if Controllable then self:SetControllable( Controllable ) @@ -855,7 +914,9 @@ do -- FSM_CONTROLLABLE return self.Controllable end - function FSM_CONTROLLABLE:_call_handler( handler, params, EventName ) + function FSM_CONTROLLABLE:_call_handler( step, trigger, params, EventName ) + + local handler = step .. trigger local ErrorHandler = function( errmsg ) @@ -868,7 +929,7 @@ do -- FSM_CONTROLLABLE end if self[handler] then - self:F3( "Calling " .. handler ) + self:T( "*** FSM *** " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] .. " *** TaskUnit: " .. self.Controllable:GetName() ) self._EventSchedules[EventName] = nil local Result, Value = xpcall( function() return self[handler]( self, self.Controllable, unpack( params ) ) end, ErrorHandler ) return Value @@ -885,9 +946,7 @@ do -- FSM_PROCESS -- @extends Core.Fsm#FSM_CONTROLLABLE - --- # FSM_PROCESS, extends @{#FSM} - -- - -- FSM_PROCESS class models Finite State Machines for @{Task} actions, which control @{Client}s. + --- FSM_PROCESS class models Finite State Machines for @{Task} actions, which control @{Client}s. -- -- === -- @@ -905,9 +964,9 @@ do -- FSM_PROCESS local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- Core.Fsm#FSM_PROCESS --self:F( Controllable ) - + self:Assign( Controllable, Task ) - + return self end @@ -915,7 +974,9 @@ do -- FSM_PROCESS self:T( "No Initialisation" ) end - function FSM_PROCESS:_call_handler( handler, params, EventName ) + function FSM_PROCESS:_call_handler( step, trigger, params, EventName ) + + local handler = step .. trigger local ErrorHandler = function( errmsg ) @@ -928,9 +989,14 @@ do -- FSM_PROCESS end if self[handler] then - self:F3( "Calling " .. handler ) + if handler ~= "onstatechange" then + self:T( "*** FSM *** " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] .. " *** Task: " .. self.Task:GetName() .. ", TaskUnit: " .. self.Controllable:GetName() ) + end self._EventSchedules[EventName] = nil - local Result, Value = xpcall( function() return self[handler]( self, self.Controllable, self.Task, unpack( params ) ) end, ErrorHandler ) + local Result, Value + if self.Controllable and self.Controllable:IsAlive() == true then + Result, Value = xpcall( function() return self[handler]( self, self.Controllable, self.Task, unpack( params ) ) end, ErrorHandler ) + end return Value --return self[handler]( self, self.Controllable, unpack( params ) ) end @@ -946,7 +1012,7 @@ do -- FSM_PROCESS local NewFsm = self:New( Controllable, Task ) -- Core.Fsm#FSM_PROCESS NewFsm:Assign( Controllable, Task ) - + -- Polymorphic call to initialize the new FSM_PROCESS based on self FSM_PROCESS NewFsm:Init( self ) @@ -1037,26 +1103,26 @@ do -- FSM_PROCESS -- TODO: Need to check and fix that an FSM_PROCESS is only for a UNIT. Not for a GROUP. --- Send a message of the @{Task} to the Group of the Unit. --- @param #FSM_PROCESS self -function FSM_PROCESS:Message( Message ) - self:F( { Message = Message } ) - - local CC = self:GetCommandCenter() - local TaskGroup = self.Controllable:GetGroup() + -- @param #FSM_PROCESS self + function FSM_PROCESS:Message( Message ) + self:F( { Message = Message } ) - local PlayerName = self.Controllable:GetPlayerName() -- Only for a unit - PlayerName = PlayerName and " (" .. PlayerName .. ")" or "" -- If PlayerName is nil, then keep it nil, otherwise add brackets. - local Callsign = self.Controllable:GetCallsign() - local Prefix = Callsign and " @ " .. Callsign .. PlayerName or "" - - Message = Prefix .. ": " .. Message - CC:MessageToGroup( Message, TaskGroup ) -end + local CC = self:GetCommandCenter() + local TaskGroup = self.Controllable:GetGroup() + + local PlayerName = self.Controllable:GetPlayerName() -- Only for a unit + PlayerName = PlayerName and " (" .. PlayerName .. ")" or "" -- If PlayerName is nil, then keep it nil, otherwise add brackets. + local Callsign = self.Controllable:GetCallsign() + local Prefix = Callsign and " @ " .. Callsign .. PlayerName or "" + + Message = Prefix .. ": " .. Message + CC:MessageToGroup( Message, TaskGroup ) + end - --- Assign the process to a @{Unit} and activate the process. + --- Assign the process to a @{Wrapper.Unit} and activate the process. -- @param #FSM_PROCESS self -- @param Task.Tasking#TASK Task -- @param Wrapper.Unit#UNIT ProcessUnit @@ -1072,14 +1138,16 @@ end return self end - function FSM_PROCESS:onenterAssigned( ProcessUnit ) - self:T( "Assign" ) +-- function FSM_PROCESS:onenterAssigned( ProcessUnit, Task, From, Event, To ) +-- +-- if From( "Planned" ) then +-- self:T( "*** FSM *** Assign *** " .. Task:GetName() .. "/" .. ProcessUnit:GetName() .. " *** " .. From .. " --> " .. Event .. " --> " .. To ) +-- self.Task:Assign() +-- end +-- end - self.Task:Assign() - end - - function FSM_PROCESS:onenterFailed( ProcessUnit ) - self:T( "Failed" ) + function FSM_PROCESS:onenterFailed( ProcessUnit, Task, From, Event, To ) + self:T( "*** FSM *** Failed *** " .. Task:GetName() .. "/" .. ProcessUnit:GetName() .. " *** " .. From .. " --> " .. Event .. " --> " .. To ) self.Task:Fail() end @@ -1091,14 +1159,17 @@ end -- @param #string Event -- @param #string From -- @param #string To - function FSM_PROCESS:onstatechange( ProcessUnit, Task, From, Event, To, Dummy ) - self:T( { ProcessUnit:GetName(), From, Event, To, Dummy, self:IsTrace() } ) + function FSM_PROCESS:onstatechange( ProcessUnit, Task, From, Event, To ) - if self:IsTrace() then - --MESSAGE:New( "@ Process " .. self:GetClassNameAndID() .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() + if From ~= To then + self:T( "*** FSM *** Change *** " .. Task:GetName() .. "/" .. ProcessUnit:GetName() .. " *** " .. From .. " --> " .. Event .. " --> " .. To ) end - self:T( { Scores = self._Scores, To = To } ) +-- if self:IsTrace() then +-- MESSAGE:New( "@ Process " .. self:GetClassNameAndID() .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll() +-- self:F2( { Scores = self._Scores, To = To } ) +-- end + -- TODO: This needs to be reworked with a callback functions allocated within Task, and set within the mission script from the Task Objects... if self._Scores[To] then @@ -1119,9 +1190,7 @@ do -- FSM_TASK -- @field Tasking.Task#TASK Task -- @extends #FSM - --- # FSM_TASK, extends @{#FSM} - -- - -- FSM_TASK class models Finite State Machines for @{Task}s. + --- Models Finite State Machines for @{Tasking.Task}s. -- -- === -- @@ -1133,24 +1202,37 @@ do -- FSM_TASK --- Creates a new FSM_TASK object. -- @param #FSM_TASK self - -- @param #table FSMT - -- @param Tasking.Task#TASK Task - -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #string TaskName The name of the task. -- @return #FSM_TASK - function FSM_TASK:New( FSMT ) + function FSM_TASK:New( TaskName ) - local self = BASE:Inherit( self, FSM_CONTROLLABLE:New( FSMT ) ) -- Core.Fsm#FSM_TASK + local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- Core.Fsm#FSM_TASK self["onstatechange"] = self.OnStateChange + self.TaskName = TaskName return self end - function FSM_TASK:_call_handler( handler, params, EventName ) + function FSM_TASK:_call_handler( step, trigger, params, EventName ) + local handler = step .. trigger + + local ErrorHandler = function( errmsg ) + + env.info( "Error in SCHEDULER function:" .. errmsg ) + if BASE.Debug ~= nil then + env.info( BASE.Debug.traceback() ) + end + + return errmsg + end + if self[handler] then - self:T( "Calling " .. handler ) + self:T( "*** FSM *** " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] .. " *** Task: " .. self.TaskName ) self._EventSchedules[EventName] = nil - return self[handler]( self, unpack( params ) ) + --return self[handler]( self, unpack( params ) ) + local Result, Value = xpcall( function() return self[handler]( self, unpack( params ) ) end, ErrorHandler ) + return Value end end @@ -1164,9 +1246,7 @@ do -- FSM_SET -- @extends Core.Fsm#FSM - --- # FSM_SET, extends @{#FSM} - -- - -- FSM_SET class models Finite State Machines for @{Set}s. Note that these FSMs control multiple objects!!! So State concerns here + --- FSM_SET class models Finite State Machines for @{Set}s. Note that these FSMs control multiple objects!!! So State concerns here -- for multiple objects or the position of the state machine in the process. -- -- === @@ -1210,9 +1290,10 @@ do -- FSM_SET return self.Controllable end - function FSM_SET:_call_handler( handler, params, EventName ) + function FSM_SET:_call_handler( step, trigger, params, EventName ) + local handler = step .. trigger if self[handler] then - self:T( "Calling " .. handler ) + self:T( "*** FSM *** " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] ) self._EventSchedules[EventName] = nil return self[handler]( self, self.Set, unpack( params ) ) end diff --git a/Moose Development/Moose/Core/Goal.lua b/Moose Development/Moose/Core/Goal.lua index ff6976877..4165e7f3f 100644 --- a/Moose Development/Moose/Core/Goal.lua +++ b/Moose Development/Moose/Core/Goal.lua @@ -1,8 +1,16 @@ ---- **Core (WIP)** -- Base class to allow the modeling of processes to achieve Goals. +--- **Core** - Models the process to achieve goal(s). -- -- === -- --- GOAL models processes that have an objective with a defined achievement. Derived classes implement the ways how the achievements can be realized. +-- ## Features: +-- +-- * Define the goal. +-- * Monitor the goal achievement. +-- * Manage goal contribution by players. +-- +-- === +-- +-- Classes that implement a goal achievement, will derive from GOAL to implement the ways how the achievements can be realized. -- -- === -- @@ -10,7 +18,9 @@ -- -- === -- --- @module Goal +-- @module Core.Goal +-- @image Core_Goal.JPG + do -- Goal @@ -18,25 +28,45 @@ do -- Goal -- @extends Core.Fsm#FSM - --- # GOAL class, extends @{Fsm#FSM} + --- Models processes that have an objective with a defined achievement. Derived classes implement the ways how the achievements can be realized. -- - -- GOAL models processes that have an objective with a defined achievement. Derived classes implement the ways how the achievements can be realized. - -- - -- ## 1. GOAL constructor + -- # 1. GOAL constructor -- -- * @{#GOAL.New}(): Creates a new GOAL object. -- - -- ## 2. GOAL is a finite state machine (FSM). + -- # 2. GOAL is a finite state machine (FSM). -- - -- ### 2.1 GOAL States + -- ## 2.1. GOAL States -- -- * **Pending**: The goal object is in progress. -- * **Achieved**: The goal objective is Achieved. -- - -- ### 2.2 GOAL Events + -- ## 2.2. GOAL Events -- -- * **Achieved**: Set the goal objective to Achieved. -- + -- # 3. Player contributions. + -- + -- Goals are most of the time achieved by players. These player achievements can be registered as part of the goal achievement. + -- Use @{#GOAL.AddPlayerContribution}() to add a player contribution to the goal. + -- The player contributions are based on a points system, an internal counter per player. + -- So once the goal has been achieved, the player contributions can be queried using @{#GOAL.GetPlayerContributions}(), + -- that retrieves all contributions done by the players. For one player, the contribution can be queried using @{#GOAL.GetPlayerContribution}(). + -- The total amount of player contributions can be queried using @{#GOAL.GetTotalContributions}(). + -- + -- # 4. Goal achievement. + -- + -- Once the goal is achieved, the mission designer will need to trigger the goal achievement using the **Achieved** event. + -- The underlying 2 examples will achieve the goals for the `Goal` object: + -- + -- Goal:Achieved() -- Achieve the goal immediately. + -- Goal:__Achieved( 30 ) -- Achieve the goal within 30 seconds. + -- + -- # 5. Check goal achievement. + -- + -- The method @{#GOAL.IsAchieved}() will return true if the goal is achieved (the trigger **Achieved** was executed). + -- You can use this method to check asynchronously if a goal has been achieved, for example using a scheduler. + -- -- @field #GOAL GOAL = { ClassName = "GOAL", @@ -108,8 +138,9 @@ do -- Goal end - --- @param #GOAL self - -- @param #string PlayerName + --- Add a new contribution by a player. + -- @param #GOAL self + -- @param #string PlayerName The name of the player. function GOAL:AddPlayerContribution( PlayerName ) self.Players[PlayerName] = self.Players[PlayerName] or 0 self.Players[PlayerName] = self.Players[PlayerName] + 1 @@ -124,21 +155,28 @@ do -- Goal end - --- @param #GOAL self + --- Get the players who contributed to achieve the goal. + -- The result is a list of players, sorted by the name of the players. + -- @param #GOAL self + -- @return #list The list of players, indexed by the player name. function GOAL:GetPlayerContributions() return self.Players or {} end - --- @param #GOAL self + --- Gets the total contributions that happened to achieve the goal. + -- The result is a number. + -- @param #GOAL self + -- @return #number The total number of contributions. 0 is returned if there were no contributions (yet). function GOAL:GetTotalContributions() return self.TotalContributions or 0 end - --- @param #GOAL self - -- @return #boolean true if the goal is Achieved + --- Validates if the goal is achieved. + -- @param #GOAL self + -- @return #boolean true if the goal is achieved. function GOAL:IsAchieved() return self:Is( "Achieved" ) end diff --git a/Moose Development/Moose/Core/Menu.lua b/Moose Development/Moose/Core/Menu.lua index 9bb30be21..fa7ab6a72 100644 --- a/Moose Development/Moose/Core/Menu.lua +++ b/Moose Development/Moose/Core/Menu.lua @@ -1,7 +1,27 @@ ---- **Core** -- MENU_ classes model the definition of **hierarchical menu structures** and **commands for players** within a mission. +--- **Core** - Manage hierarchical menu structures and commands for players within a mission. -- -- === -- +-- ### Features: +-- +-- * Setup mission sub menus. +-- * Setup mission command menus. +-- * Setup coalition sub menus. +-- * Setup coalition command menus. +-- * Setup group sub menus. +-- * Setup group command menus. +-- * Manage menu creation intelligently, avoid double menu creation. +-- * Only create or delete menus when required, and keep existing menus persistent. +-- * Update menu structures. +-- * Refresh menu structures intelligently, based on a time stamp of updates. +-- - Delete obscolete menus. +-- - Create new one where required. +-- - Don't touch the existing ones. +-- * Provide a variable amount of parameters to menus. +-- * Update the parameters and the receiving methods, without updating the menu within DCS! +-- * Provide a great performance boost in menu management. +-- * Provide a great tool to manage menus in your code. +-- -- DCS Menus can be managed using the MENU classes. -- The advantage of using MENU classes is that it hides the complexity of dealing with menu management in more advanced scanerios where you need to -- set menus and later remove them, and later set them again. You'll find while using use normal DCS scripting functions, that setting and removing @@ -13,15 +33,15 @@ -- -- ### To manage **main menus**, the classes begin with **MENU_**: -- --- * @{Menu#MENU_MISSION}: Manages main menus for whole mission file. --- * @{Menu#MENU_COALITION}: Manages main menus for whole coalition. --- * @{Menu#MENU_GROUP}: Manages main menus for GROUPs. +-- * @{Core.Menu#MENU_MISSION}: Manages main menus for whole mission file. +-- * @{Core.Menu#MENU_COALITION}: Manages main menus for whole coalition. +-- * @{Core.Menu#MENU_GROUP}: Manages main menus for GROUPs. -- -- ### To manage **command menus**, which are menus that allow the player to issue **functions**, the classes begin with **MENU_COMMAND_**: -- --- * @{Menu#MENU_MISSION_COMMAND}: Manages command menus for whole mission file. --- * @{Menu#MENU_COALITION_COMMAND}: Manages command menus for whole coalition. --- * @{Menu#MENU_GROUP_COMMAND}: Manages command menus for GROUPs. +-- * @{Core.Menu#MENU_MISSION_COMMAND}: Manages command menus for whole mission file. +-- * @{Core.Menu#MENU_COALITION_COMMAND}: Manages command menus for whole coalition. +-- * @{Core.Menu#MENU_GROUP_COMMAND}: Manages command menus for GROUPs. -- -- === --- @@ -30,7 +50,8 @@ -- -- === -- --- @module Menu +-- @module Core.Menu +-- @image Core_Menu.JPG MENU_INDEX = {} @@ -88,11 +109,14 @@ function MENU_INDEX:PrepareCoalition( CoalitionSide ) self.Coalition[CoalitionSide].Menus = self.Coalition[CoalitionSide].Menus or {} end - +--- +-- @param Wrapper.Group#GROUP Group function MENU_INDEX:PrepareGroup( Group ) + if Group and Group:IsAlive() ~= nil then -- something was changed here! local GroupName = Group:GetName() self.Group[GroupName] = self.Group[GroupName] or {} self.Group[GroupName].Menus = self.Group[GroupName].Menus or {} + end end @@ -132,14 +156,17 @@ end function MENU_INDEX:HasGroupMenu( Group, Path ) - - local MenuGroupName = Group:GetName() - return self.Group[MenuGroupName].Menus[Path] + if Group and Group:IsAlive() then + local MenuGroupName = Group:GetName() + return self.Group[MenuGroupName].Menus[Path] + end + return nil end function MENU_INDEX:SetGroupMenu( Group, Path, Menu ) local MenuGroupName = Group:GetName() + Group:F({MenuGroupName=MenuGroupName,Path=Path}) self.Group[MenuGroupName].Menus[Path] = Menu end @@ -182,8 +209,7 @@ do -- MENU_BASE --- @type MENU_BASE -- @extends Base#BASE - --- # MENU_BASE class, extends @{Base#BASE} - -- The MENU_BASE class defines the main MENU class where other MENU classes are derived from. + --- Defines the main MENU class where other MENU classes are derived from. -- This is an abstract class, so don't use it. -- @field #MENU_BASE MENU_BASE = { @@ -213,6 +239,7 @@ do -- MENU_BASE self.Menus = {} self.MenuCount = 0 self.MenuTime = timer.getTime() + self.MenuRemoveParent = false if self.ParentMenu then self.ParentMenu.Menus = self.ParentMenu.Menus or {} @@ -226,14 +253,30 @@ do -- MENU_BASE if self.ParentMenu then self.ParentMenu.Menus = self.ParentMenu.Menus or {} self.ParentMenu.Menus[MenuText] = Menu + self.ParentMenu.MenuCount = self.ParentMenu.MenuCount + 1 end end function MENU_BASE:ClearParentMenu( MenuText ) if self.ParentMenu and self.ParentMenu.Menus[MenuText] then self.ParentMenu.Menus[MenuText] = nil + self.ParentMenu.MenuCount = self.ParentMenu.MenuCount - 1 + if self.ParentMenu.MenuCount == 0 then + --self.ParentMenu:Remove() + end end end + + --- Sets a @{Menu} to remove automatically the parent menu when the menu removed is the last child menu of that parent @{Menu}. + -- @param #MENU_BASE self + -- @param #boolean RemoveParent If true, the parent menu is automatically removed when this menu is the last child menu of that parent @{Menu}. + -- @return #MENU_BASE + function MENU_BASE:SetRemoveParent( RemoveParent ) + --self:F( { RemoveParent } ) + self.MenuRemoveParent = RemoveParent + return self + end + --- Gets a @{Menu} from a parent @{Menu} -- @param #MENU_BASE self @@ -269,9 +312,7 @@ do -- MENU_COMMAND_BASE -- @field #function MenuCallHandler -- @extends Core.Menu#MENU_BASE - --- # MENU_COMMAND_BASE class, extends @{Base#BASE} - -- ---------------------------------------------------------- - -- The MENU_COMMAND_BASE class defines the main MENU class where other MENU COMMAND_ + --- Defines the main MENU class where other MENU COMMAND_ -- classes are derived from, in order to set commands. -- -- @field #MENU_COMMAND_BASE @@ -341,9 +382,8 @@ do -- MENU_MISSION --- @type MENU_MISSION -- @extends Core.Menu#MENU_BASE - --- # MENU_MISSION class, extends @{Menu#MENU_BASE} + --- Manages the main menus for a complete mission. -- - -- The MENU_MISSION class manages the main menus for a complete mission. -- You can add menus with the @{#MENU_MISSION.New} method, which constructs a MENU_MISSION object and returns you the object reference. -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION.Remove}. -- @field #MENU_MISSION @@ -438,9 +478,8 @@ do -- MENU_MISSION_COMMAND --- @type MENU_MISSION_COMMAND -- @extends Core.Menu#MENU_COMMAND_BASE - --- # MENU_MISSION_COMMAND class, extends @{Menu#MENU_COMMAND_BASE} - -- - -- The MENU_MISSION_COMMAND class manages the command menus for a complete mission, which allow players to execute functions during mission execution. + --- Manages the command menus for a complete mission, which allow players to execute functions during mission execution. + -- -- You can add menus with the @{#MENU_MISSION_COMMAND.New} method, which constructs a MENU_MISSION_COMMAND object and returns you the object reference. -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION_COMMAND.Remove}. -- @@ -452,7 +491,7 @@ do -- MENU_MISSION_COMMAND --- MENU_MISSION constructor. Creates a new radio command item for a complete mission file, which can invoke a function with parameters. -- @param #MENU_MISSION_COMMAND self -- @param #string MenuText The text for the menu. - -- @param Menu#MENU_MISSION ParentMenu The parent menu. + -- @param Core.Menu#MENU_MISSION ParentMenu The parent menu. -- @param CommandMenuFunction A function that is called when the menu key is pressed. -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this. -- @return #MENU_MISSION_COMMAND self @@ -525,9 +564,8 @@ do -- MENU_COALITION --- @type MENU_COALITION -- @extends Core.Menu#MENU_BASE - --- # MENU_COALITION class, extends @{Menu#MENU_BASE} + --- Manages the main menus for @{DCS.coalition}s. -- - -- The @{Menu#MENU_COALITION} class manages the main menus for coalitions. -- You can add menus with the @{#MENU_COALITION.New} method, which constructs a MENU_COALITION object and returns you the object reference. -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION.Remove}. -- @@ -576,7 +614,7 @@ do -- MENU_COALITION --- MENU_COALITION constructor. Creates a new MENU_COALITION object and creates the menu for a complete coalition. -- @param #MENU_COALITION self - -- @param Dcs.DCSCoalition#coalition.side Coalition The coalition owning the menu. + -- @param DCS#coalition.side Coalition The coalition owning the menu. -- @param #string MenuText The text for the menu. -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the perent menu of DCS world (under F10 other). -- @return #MENU_COALITION self @@ -663,9 +701,8 @@ do -- MENU_COALITION_COMMAND --- @type MENU_COALITION_COMMAND -- @extends Core.Menu#MENU_COMMAND_BASE - --- # MENU_COALITION_COMMAND class, extends @{Menu#MENU_COMMAND_BASE} + --- Manages the command menus for coalitions, which allow players to execute functions during mission execution. -- - -- The MENU_COALITION_COMMAND class manages the command menus for coalitions, which allow players to execute functions during mission execution. -- You can add menus with the @{#MENU_COALITION_COMMAND.New} method, which constructs a MENU_COALITION_COMMAND object and returns you the object reference. -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION_COMMAND.Remove}. -- @@ -676,9 +713,9 @@ do -- MENU_COALITION_COMMAND --- MENU_COALITION constructor. Creates a new radio command item for a coalition, which can invoke a function with parameters. -- @param #MENU_COALITION_COMMAND self - -- @param Dcs.DCSCoalition#coalition.side Coalition The coalition owning the menu. + -- @param DCS#coalition.side Coalition The coalition owning the menu. -- @param #string MenuText The text for the menu. - -- @param Menu#MENU_COALITION ParentMenu The parent menu. + -- @param Core.Menu#MENU_COALITION ParentMenu The parent menu. -- @param CommandMenuFunction A function that is called when the menu key is pressed. -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this. -- @return #MENU_COALITION_COMMAND @@ -763,9 +800,8 @@ do -- @extends Core.Menu#MENU_BASE - --- #MENU_GROUP class, extends @{Menu#MENU_BASE} + --- Manages the main menus for @{Wrapper.Group}s. -- - -- The MENU_GROUP class manages the main menus for coalitions. -- You can add menus with the @{#MENU_GROUP.New} method, which constructs a MENU_GROUP object and returns you the object reference. -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP.Remove}. -- @@ -900,8 +936,8 @@ do self:RemoveSubMenus( MenuTime, MenuTag ) if not MenuTime or self.MenuTime ~= MenuTime then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then - self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) if self.MenuPath ~= nil then + self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) end MENU_INDEX:ClearGroupMenu( self.Group, Path ) @@ -921,9 +957,7 @@ do --- @type MENU_GROUP_COMMAND -- @extends Core.Menu#MENU_COMMAND_BASE - --- # MENU_GROUP_COMMAND class, extends @{Menu#MENU_COMMAND_BASE} - -- - -- The @{Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. + --- The @{Core.Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution. -- You can add menus with the @{#MENU_GROUP_COMMAND.New} method, which constructs a MENU_GROUP_COMMAND object and returns you the object reference. -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP_COMMAND.Remove}. -- @@ -992,8 +1026,8 @@ do if GroupMenu == self then if not MenuTime or self.MenuTime ~= MenuTime then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then - self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) if self.MenuPath ~= nil then + self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) end MENU_INDEX:ClearGroupMenu( self.Group, Path ) @@ -1018,9 +1052,7 @@ do -- @extends Core.Menu#MENU_BASE - --- #MENU_GROUP_DELAYED class, extends @{Menu#MENU_BASE} - -- - -- The MENU_GROUP_DELAYED class manages the main menus for groups. + --- The MENU_GROUP_DELAYED class manages the main menus for groups. -- You can add menus with the @{#MENU_GROUP.New} method, which constructs a MENU_GROUP object and returns you the object reference. -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP.Remove}. -- The creation of the menu item is delayed however, and must be created using the @{#MENU_GROUP.Set} method. @@ -1133,8 +1165,8 @@ do self:RemoveSubMenus( MenuTime, MenuTag ) if not MenuTime or self.MenuTime ~= MenuTime then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then - self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) if self.MenuPath ~= nil then + self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) end MENU_INDEX:ClearGroupMenu( self.Group, Path ) @@ -1154,9 +1186,8 @@ do --- @type MENU_GROUP_COMMAND_DELAYED -- @extends Core.Menu#MENU_COMMAND_BASE - --- # MENU_GROUP_COMMAND_DELAYED class, extends @{Menu#MENU_COMMAND_BASE} + --- Manages the command menus for coalitions, which allow players to execute functions during mission execution. -- - -- The @{Menu#MENU_GROUP_COMMAND_DELAYED} class manages the command menus for coalitions, which allow players to execute functions during mission execution. -- You can add menus with the @{#MENU_GROUP_COMMAND_DELAYED.New} method, which constructs a MENU_GROUP_COMMAND_DELAYED object and returns you the object reference. -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP_COMMAND_DELAYED.Remove}. -- @@ -1244,8 +1275,8 @@ do if GroupMenu == self then if not MenuTime or self.MenuTime ~= MenuTime then if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then - self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) if self.MenuPath ~= nil then + self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } ) missionCommands.removeItemForGroup( self.GroupID, self.MenuPath ) end MENU_INDEX:ClearGroupMenu( self.Group, Path ) diff --git a/Moose Development/Moose/Core/Message.lua b/Moose Development/Moose/Core/Message.lua index 11a1e19dc..8e1f8324f 100644 --- a/Moose Development/Moose/Core/Message.lua +++ b/Moose Development/Moose/Core/Message.lua @@ -1,43 +1,51 @@ ---- **Core** -- MESSAGE class takes are of the **real-time notifications** and **messages to players** during a simulation. --- --- ![Banner Image](..\Presentations\MESSAGE\Dia1.JPG) +--- **Core** - Informs the players using messages during a simulation. -- -- === -- --- @module Message +-- ## Features: +-- +-- * A more advanced messaging system using the DCS message system. +-- * Time messages. +-- * Send messages based on a message type, which has a pre-defined duration that can be tweaked in SETTINGS. +-- * Send message to all players. +-- * Send messages to a coalition. +-- * Send messages to a specific group. +-- +-- === +-- +-- @module Core.Message +-- @image Core_Message.JPG --- The MESSAGE class -- @type MESSAGE -- @extends Core.Base#BASE ---- # MESSAGE class, extends @{Base#BASE} --- --- Message System to display Messages to Clients, Coalitions or All. +--- Message System to display Messages to Clients, Coalitions or All. -- Messages are shown on the display panel for an amount of seconds, and will then disappear. -- Messages can contain a category which is indicating the category of the message. -- -- ## MESSAGE construction -- --- Messages are created with @{Message#MESSAGE.New}. Note that when the MESSAGE object is created, no message is sent yet. +-- Messages are created with @{#MESSAGE.New}. Note that when the MESSAGE object is created, no message is sent yet. -- To send messages, you need to use the To functions. -- -- ## Send messages to an audience -- -- Messages are sent: -- --- * To a @{Client} using @{Message#MESSAGE.ToClient}(). --- * To a @{Group} using @{Message#MESSAGE.ToGroup}() --- * To a coalition using @{Message#MESSAGE.ToCoalition}(). --- * To the red coalition using @{Message#MESSAGE.ToRed}(). --- * To the blue coalition using @{Message#MESSAGE.ToBlue}(). --- * To all Players using @{Message#MESSAGE.ToAll}(). +-- * To a @{Client} using @{#MESSAGE.ToClient}(). +-- * To a @{Wrapper.Group} using @{#MESSAGE.ToGroup}() +-- * To a coalition using @{#MESSAGE.ToCoalition}(). +-- * To the red coalition using @{#MESSAGE.ToRed}(). +-- * To the blue coalition using @{#MESSAGE.ToBlue}(). +-- * To all Players using @{#MESSAGE.ToAll}(). -- -- ## Send conditionally to an audience -- -- Messages can be sent conditionally to an audience (when a condition is true): -- --- * To all players using @{Message#MESSAGE.ToAllIf}(). --- * To a coalition using @{Message#MESSAGE.ToCoalitionIf}(). +-- * To all players using @{#MESSAGE.ToAllIf}(). +-- * To a coalition using @{#MESSAGE.ToCoalitionIf}(). -- -- === -- @@ -69,6 +77,7 @@ MESSAGE.Type = { -- @param #string MessageText is the text of the Message. -- @param #number MessageDuration is a number in seconds of how long the MESSAGE should be shown on the display panel. -- @param #string MessageCategory (optional) is a string expressing the "category" of the Message. The category will be shown as the first text in the message followed by a ": ". +-- @param #boolean ClearScreen (optional) Clear all previous messages if true. -- @return #MESSAGE -- @usage -- -- Create a series of new Messages. @@ -80,7 +89,7 @@ MESSAGE.Type = { -- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty" ) -- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", 25, "Score" ) -- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", 25, "Score") -function MESSAGE:New( MessageText, MessageDuration, MessageCategory ) +function MESSAGE:New( MessageText, MessageDuration, MessageCategory, ClearScreen ) local self = BASE:Inherit( self, BASE:New() ) self:F( { MessageText, MessageDuration, MessageCategory } ) @@ -97,6 +106,11 @@ function MESSAGE:New( MessageText, MessageDuration, MessageCategory ) else self.MessageCategory = "" end + + self.ClearScreen=false + if ClearScreen~=nil then + self.ClearScreen=ClearScreen + end self.MessageDuration = MessageDuration or 5 self.MessageTime = timer.getTime() @@ -117,18 +131,24 @@ end -- @param self -- @param #string MessageText is the text of the Message. -- @param #MESSAGE.Type MessageType The type of the message. +-- @param #boolean ClearScreen (optional) Clear all previous messages. -- @return #MESSAGE -- @usage -- MessageAll = MESSAGE:NewType( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", MESSAGE.Type.Information ) -- MessageRED = MESSAGE:NewType( "To the RED Players: You receive a penalty because you've killed one of your own units", MESSAGE.Type.Information ) -- MessageClient1 = MESSAGE:NewType( "Congratulations, you've just hit a target", MESSAGE.Type.Update ) -- MessageClient2 = MESSAGE:NewType( "Congratulations, you've just killed a target", MESSAGE.Type.Update ) -function MESSAGE:NewType( MessageText, MessageType ) +function MESSAGE:NewType( MessageText, MessageType, ClearScreen ) local self = BASE:Inherit( self, BASE:New() ) self:F( { MessageText } ) self.MessageType = MessageType + + self.ClearScreen=false + if ClearScreen~=nil then + self.ClearScreen=ClearScreen + end self.MessageTime = timer.getTime() self.MessageText = MessageText:gsub("^\n","",1):gsub("\n$","",1) @@ -138,11 +158,21 @@ end +--- Clears all previous messages from the screen before the new message is displayed. Not that this must come before all functions starting with ToX(), e.g. ToAll(), ToGroup() etc. +-- @param #MESSAGE self +-- @return #MESSAGE +function MESSAGE:Clear() + self:F() + self.ClearScreen=true + return self +end + --- Sends a MESSAGE to a Client Group. Note that the Group needs to be defined within the ME with the skillset "Client" or "Player". -- @param #MESSAGE self -- @param Wrapper.Client#CLIENT Client is the Group of the Client. +-- @param Core.Settings#SETTINGS Settings Settings used to display the message. -- @return #MESSAGE -- @usage -- -- Send the 2 messages created with the @{New} method to the Client Group. @@ -173,7 +203,7 @@ function MESSAGE:ToClient( Client, Settings ) if self.MessageDuration ~= 0 then local ClientGroupID = Client:GetClientGroupID() self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForGroup( ClientGroupID, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) + trigger.action.outTextForGroup( ClientGroupID, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration , self.ClearScreen) end end @@ -182,8 +212,8 @@ end --- Sends a MESSAGE to a Group. -- @param #MESSAGE self --- @param Wrapper.Group#GROUP Group is the Group. --- @return #MESSAGE +-- @param Wrapper.Group#GROUP Group to which the message is displayed. +-- @return #MESSAGE Message object. function MESSAGE:ToGroup( Group, Settings ) self:F( Group.GroupName ) @@ -197,7 +227,7 @@ function MESSAGE:ToGroup( Group, Settings ) if self.MessageDuration ~= 0 then self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForGroup( Group:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) + trigger.action.outTextForGroup( Group:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration, self.ClearScreen ) end end @@ -243,8 +273,9 @@ end --- Sends a MESSAGE to a Coalition. -- @param #MESSAGE self --- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. --- @return #MESSAGE +-- @param #DCS.coalition.side CoalitionSide @{#DCS.coalition.side} to which the message is displayed. +-- @param Core.Settings#SETTINGS Settings (Optional) Settings for message display. +-- @return #MESSAGE Message object. -- @usage -- -- Send a message created with the @{New} method to the RED coalition. -- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED ) @@ -265,7 +296,7 @@ function MESSAGE:ToCoalition( CoalitionSide, Settings ) if CoalitionSide then if self.MessageDuration ~= 0 then self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outTextForCoalition( CoalitionSide, self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) + trigger.action.outTextForCoalition( CoalitionSide, self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration, self.ClearScreen ) end end @@ -274,8 +305,9 @@ end --- Sends a MESSAGE to a Coalition if the given Condition is true. -- @param #MESSAGE self --- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. --- @return #MESSAGE +-- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{coalition.side}. +-- @param #boolean Condition Sends the message only if the condition is true. +-- @return #MESSAGE self function MESSAGE:ToCoalitionIf( CoalitionSide, Condition ) self:F( CoalitionSide ) @@ -288,6 +320,7 @@ end --- Sends a MESSAGE to all players. -- @param #MESSAGE self +-- @param Core.Settings#Settings Settings (Optional) Settings for message display. -- @return #MESSAGE -- @usage -- -- Send a message created to all players. @@ -297,7 +330,7 @@ end -- or -- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ) -- MessageAll:ToAll() -function MESSAGE:ToAll() +function MESSAGE:ToAll(Settings) self:F() if self.MessageType then @@ -308,7 +341,7 @@ function MESSAGE:ToAll() if self.MessageDuration ~= 0 then self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration ) - trigger.action.outText( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration ) + trigger.action.outText( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration, self.ClearScreen ) end return self diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index b5556abea..b5cd1ce26 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -1,7 +1,11 @@ ---- **Core** -- **POINT\_VEC** classes define an **extensive API** to **manage 3D points** in the simulation space. --- --- ![Banner Image](..\Presentations\POINT\Dia1.JPG) +--- **Core** - Defines an extensive API to manage 3D points in the DCS World 3D simulation space. -- +-- ## Features: +-- +-- * Provides a COORDINATE class, which allows to manage points in 3D space and perform various operations on it. +-- * Provides a POINT\_VEC2 class, which is derived from COORDINATE, and allows to manage points in 3D space, but from a Lat/Lon and Altitude perspective. +-- * Provides a POINT\_VEC3 class, which is derived from COORDINATE, and allows to manage points in 3D space, but from a X, Z and Y vector perspective. +-- -- === -- -- # Demo Missions @@ -26,8 +30,8 @@ -- -- ### Contributions: -- --- @module Point - +-- @module Core.Point +-- @image Core_Coordinate.JPG @@ -38,33 +42,22 @@ do -- COORDINATE -- @extends Core.Base#BASE - --- # COORDINATE class, extends @{Base#BASE} + --- Defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space. -- - -- COORDINATE defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space. + -- # 1) Create a COORDINATE object. -- - -- ## COORDINATE constructor + -- A new COORDINATE object can be created with 3 various methods: -- - -- A new COORDINATE object can be created with: - -- - -- * @{#COORDINATE.New}(): a 3D point. - -- * @{#COORDINATE.NewFromVec2}(): a 2D point created from a @{DCSTypes#Vec2}. - -- * @{#COORDINATE.NewFromVec3}(): a 3D point created from a @{DCSTypes#Vec3}. - -- - -- ## Create waypoints for routes - -- - -- A COORDINATE can prepare waypoints for Ground and Air groups to be embedded into a Route. - -- - -- * @{#COORDINATE.WaypointAir}(): Build an air route point. - -- * @{#COORDINATE.WaypointGround}(): Build a ground route point. - -- - -- Route points can be used in the Route methods of the @{Group#GROUP} class. + -- * @{#COORDINATE.New}(): from a 3D point. + -- * @{#COORDINATE.NewFromVec2}(): from a @{DCS#Vec2} and possible altitude. + -- * @{#COORDINATE.NewFromVec3}(): from a @{DCS#Vec3}. -- -- - -- ## Smoke, flare, explode, illuminate + -- # 2) Smoke, flare, explode, illuminate at the coordinate. -- -- At the point a smoke, flare, explosion and illumination bomb can be triggered. Use the following methods: -- - -- ### Smoke + -- ## 2.1) Smoke -- -- * @{#COORDINATE.Smoke}(): To smoke the point in a certain color. -- * @{#COORDINATE.SmokeBlue}(): To smoke the point in blue. @@ -73,7 +66,7 @@ do -- COORDINATE -- * @{#COORDINATE.SmokeWhite}(): To smoke the point in white. -- * @{#COORDINATE.SmokeGreen}(): To smoke the point in green. -- - -- ### Flare + -- ## 2.2) Flare -- -- * @{#COORDINATE.Flare}(): To flare the point in a certain color. -- * @{#COORDINATE.FlareRed}(): To flare the point in red. @@ -81,18 +74,19 @@ do -- COORDINATE -- * @{#COORDINATE.FlareWhite}(): To flare the point in white. -- * @{#COORDINATE.FlareGreen}(): To flare the point in green. -- - -- ### Explode + -- ## 2.3) Explode -- -- * @{#COORDINATE.Explosion}(): To explode the point with a certain intensity. -- - -- ### Illuminate + -- ## 2.4) Illuminate -- -- * @{#COORDINATE.IlluminationBomb}(): To illuminate the point. -- -- - -- ## Markings + -- # 3) Create markings on the map. -- - -- Place markers (text boxes with clarifications for briefings, target locations or any other reference point) on the map for all players, coalitions or specific groups: + -- Place markers (text boxes with clarifications for briefings, target locations or any other reference point) + -- on the map for all players, coalitions or specific groups: -- -- * @{#COORDINATE.MarkToAll}(): Place a mark to all players. -- * @{#COORDINATE.MarkToCoalition}(): Place a mark to a coalition. @@ -100,47 +94,99 @@ do -- COORDINATE -- * @{#COORDINATE.MarkToCoalitionBlue}(): Place a mark to the blue coalition. -- * @{#COORDINATE.MarkToGroup}(): Place a mark to a group (needs to have a client in it or a CA group (CA group is bugged)). -- * @{#COORDINATE.RemoveMark}(): Removes a mark from the map. - -- - -- - -- ## 3D calculation methods + -- + -- # 4) Coordinate calculation methods. -- -- Various calculation methods exist to use or manipulate 3D space. Find below a short description of each method: -- - -- ### Distance + -- ## 4.1) Get the distance between 2 points. -- -- * @{#COORDINATE.Get3DDistance}(): Obtain the distance from the current 3D point to the provided 3D point in 3D space. -- * @{#COORDINATE.Get2DDistance}(): Obtain the distance from the current 3D point to the provided 3D point in 2D space. -- - -- ### Angle + -- ## 4.2) Get the angle. -- -- * @{#COORDINATE.GetAngleDegrees}(): Obtain the angle in degrees from the current 3D point with the provided 3D direction vector. -- * @{#COORDINATE.GetAngleRadians}(): Obtain the angle in radians from the current 3D point with the provided 3D direction vector. -- * @{#COORDINATE.GetDirectionVec3}(): Obtain the 3D direction vector from the current 3D point to the provided 3D point. -- - -- ### Translation + -- ## 4.3) Coordinate translation. -- -- * @{#COORDINATE.Translate}(): Translate the current 3D point towards an other 3D point using the given Distance and Angle. -- - -- ### Get the North correction of the current location + -- ## 4.4) Get the North correction of the current location. -- -- * @{#COORDINATE.GetNorthCorrection}(): Obtains the north correction at the current 3D point. -- - -- - -- ## Point Randomization + -- ## 4.5) Point Randomization -- -- Various methods exist to calculate random locations around a given 3D point. -- -- * @{#COORDINATE.GetRandomVec2InRadius}(): Provides a random 2D vector around the current 3D point, in the given inner to outer band. -- * @{#COORDINATE.GetRandomVec3InRadius}(): Provides a random 3D vector around the current 3D point, in the given inner to outer band. + -- + -- ## 4.6) LOS between coordinates. + -- + -- Calculate if the coordinate has Line of Sight (LOS) with the other given coordinate. + -- Mountains, trees and other objects can be positioned between the two 3D points, preventing visibilty in a straight continuous line. + -- The method @{#COORDINATE.IsLOS}() returns if the two coodinates have LOS. + -- + -- ## 4.7) Check the coordinate position. + -- + -- Various methods are available that allow to check if a coordinate is: + -- + -- * @{#COORDINATE.IsInRadius}(): in a give radius. + -- * @{#COORDINATE.IsInSphere}(): is in a given sphere. + -- * @{#COORDINATE.IsAtCoordinate2D}(): is in a given coordinate within a specific precision. + -- + -- + -- + -- # 5) Measure the simulation environment at the coordinate. + -- + -- ## 5.1) Weather specific. + -- + -- Within the DCS simulator, a coordinate has specific environmental properties, like wind, temperature, humidity etc. + -- + -- * @{#COORDINATE.GetWind}(): Retrieve the wind at the specific coordinate within the DCS simulator. + -- * @{#COORDINATE.GetTemperature}(): Retrieve the temperature at the specific height within the DCS simulator. + -- * @{#COORDINATE.GetPressure}(): Retrieve the pressure at the specific height within the DCS simulator. + -- + -- ## 5.2) Surface specific. + -- + -- Within the DCS simulator, the surface can have various objects placed at the coordinate, and the surface height will vary. + -- + -- * @{#COORDINATE.GetLandHeight}(): Retrieve the height of the surface (on the ground) within the DCS simulator. + -- * @{#COORDINATE.GetSurfaceType}(): Retrieve the surface type (on the ground) within the DCS simulator. + -- + -- # 6) Create waypoints for routes. + -- + -- A COORDINATE can prepare waypoints for Ground and Air groups to be embedded into a Route. + -- + -- * @{#COORDINATE.WaypointAir}(): Build an air route point. + -- * @{#COORDINATE.WaypointGround}(): Build a ground route point. + -- + -- Route points can be used in the Route methods of the @{Wrapper.Group#GROUP} class. + -- + -- ## 7) Manage the roads. + -- + -- Important for ground vehicle transportation and movement, the method @{#COORDINATE.GetClosestPointToRoad}() will calculate + -- the closest point on the nearest road. + -- + -- In order to use the most optimal road system to transport vehicles, the method @{#COORDINATE.GetPathOnRoad}() will calculate + -- the most optimal path following the road between two coordinates. + -- -- -- - -- ## Metric system + -- + -- + -- ## 8) Metric or imperial system -- -- * @{#COORDINATE.IsMetric}(): Returns if the 3D point is Metric or Nautical Miles. -- * @{#COORDINATE.SetMetric}(): Sets the 3D point to Metric or Nautical Miles. -- -- - -- ## Coorinate text generation + -- ## 9) Coordinate text generation + -- -- -- * @{#COORDINATE.ToStringBR}(): Generates a Bearing & Range text in the format of DDD for DI where DDD is degrees and DI is distance. -- * @{#COORDINATE.ToStringLL}(): Generates a Latutude & Longutude text. @@ -178,9 +224,9 @@ do -- COORDINATE --- COORDINATE constructor. -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. - -- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing to the Right. - -- @param Dcs.DCSTypes#Distance z The z coordinate of the Vec3 point, pointing to the Right. + -- @param DCS#Distance x The x coordinate of the Vec3 point, pointing to the North. + -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing to the Right. + -- @param DCS#Distance z The z coordinate of the Vec3 point, pointing to the Right. -- @return #COORDINATE function COORDINATE:New( x, y, z ) @@ -192,10 +238,24 @@ do -- COORDINATE return self end + --- COORDINATE constructor. + -- @param #COORDINATE self + -- @param #COORDINATE Coordinate. + -- @return #COORDINATE + function COORDINATE:NewFromCoordinate( Coordinate ) + + local self = BASE:Inherit( self, BASE:New() ) -- #COORDINATE + self.x = Coordinate.x + self.y = Coordinate.y + self.z = Coordinate.z + + return self + end + --- Create a new COORDINATE object from Vec2 coordinates. -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. - -- @param Dcs.DCSTypes#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. + -- @param DCS#Vec2 Vec2 The Vec2 point. + -- @param DCS#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. -- @return #COORDINATE function COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) @@ -214,7 +274,7 @@ do -- COORDINATE --- Create a new COORDINATE object from Vec3 coordinates. -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. + -- @param DCS#Vec3 Vec3 The Vec3 point. -- @return #COORDINATE function COORDINATE:NewFromVec3( Vec3 ) @@ -228,7 +288,7 @@ do -- COORDINATE --- Return the coordinates of the COORDINATE in Vec3 format. -- @param #COORDINATE self - -- @return Dcs.DCSTypes#Vec3 The Vec3 format coordinate. + -- @return DCS#Vec3 The Vec3 format coordinate. function COORDINATE:GetVec3() return { x = self.x, y = self.y, z = self.z } end @@ -236,46 +296,215 @@ do -- COORDINATE --- Return the coordinates of the COORDINATE in Vec2 format. -- @param #COORDINATE self - -- @return Dcs.DCSTypes#Vec2 The Vec2 format coordinate. + -- @return DCS#Vec2 The Vec2 format coordinate. function COORDINATE:GetVec2() return { x = self.x, y = self.z } end - --TODO: check this to replace - --- Calculate the distance from a reference @{DCSTypes#Vec2}. + --- Returns the coordinate from the latitude and longitude given in decimal degrees. -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Vec2 Vec2Reference The reference @{DCSTypes#Vec2}. - -- @return Dcs.DCSTypes#Distance The distance from the reference @{DCSTypes#Vec2} in meters. - function COORDINATE:DistanceFromVec2( Vec2Reference ) - self:F2( Vec2Reference ) + -- @param #number latitude Latitude in decimal degrees. + -- @param #number longitude Longitude in decimal degrees. + -- @param #number altitude (Optional) Altitude in meters. Default is the land height at the coordinate. + -- @return #COORDINATE + function COORDINATE:NewFromLLDD( latitude, longitude, altitude) + + -- Returns a point from latitude and longitude in the vec3 format. + local vec3=coord.LLtoLO(latitude, longitude) + + -- Convert vec3 to coordinate object. + local _coord=self:NewFromVec3(vec3) + + -- Adjust height + if altitude==nil then + _coord.y=altitude + else + _coord.y=self:GetLandHeight() + end - local Distance = ( ( Vec2Reference.x - self.x ) ^ 2 + ( Vec2Reference.y - self.z ) ^2 ) ^0.5 + return _coord + end + + + --- Returns if the 2 coordinates are at the same 2D position. + -- @param #COORDINATE self + -- @param #COORDINATE Coordinate + -- @param #number Precision + -- @return #boolean true if at the same position. + function COORDINATE:IsAtCoordinate2D( Coordinate, Precision ) + + self:F( { Coordinate = Coordinate:GetVec2() } ) + self:F( { self = self:GetVec2() } ) + + local x = Coordinate.x + local z = Coordinate.z + + return x - Precision <= self.x and x + Precision >= self.x and z - Precision <= self.z and z + Precision >= self.z + end + + --- Scan/find objects (units, statics, scenery) within a certain radius around the coordinate using the world.searchObjects() DCS API function. + -- @param #COORDINATE self + -- @param #number radius (Optional) Scan radius in meters. Default 100 m. + -- @param #boolean scanunits (Optional) If true scan for units. Default true. + -- @param #boolean scanstatics (Optional) If true scan for static objects. Default true. + -- @param #boolean scanscenery (Optional) If true scan for scenery objects. Default false. + -- @return #boolean True if units were found. + -- @return #boolean True if statics were found. + -- @return #boolean True if scenery objects were found. + -- @return #table Table of MOOSE @[#Wrapper.Unit#UNIT} objects found. + -- @return #table Table of DCS static objects found. + -- @return #table Table of DCS scenery objects found. + function COORDINATE:ScanObjects(radius, scanunits, scanstatics, scanscenery) + self:F(string.format("Scanning in radius %.1f m.", radius)) + + local SphereSearch = { + id = world.VolumeType.SPHERE, + params = { + point = self:GetVec3(), + radius = radius, + } + } + + -- Defaults + radius=radius or 100 + if scanunits==nil then + scanunits=true + end + if scanstatics==nil then + scanstatics=true + end + if scanscenery==nil then + scanscenery=false + end + + --{Object.Category.UNIT, Object.Category.STATIC, Object.Category.SCENERY} + local scanobjects={} + if scanunits then + table.insert(scanobjects, Object.Category.UNIT) + end + if scanstatics then + table.insert(scanobjects, Object.Category.STATIC) + end + if scanscenery then + table.insert(scanobjects, Object.Category.SCENERY) + end + + -- Found stuff. + local Units = {} + local Statics = {} + local Scenery = {} + local gotstatics=false + local gotunits=false + local gotscenery=false + + local function EvaluateZone(ZoneObject) + + if ZoneObject then + + -- Get category of scanned object. + local ObjectCategory = ZoneObject:getCategory() + + -- Check for unit or static objects + if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() then + + table.insert(Units, UNIT:Find(ZoneObject)) + gotunits=true + + elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then + + table.insert(Statics, ZoneObject) + gotstatics=true + + elseif ObjectCategory==Object.Category.SCENERY then + + table.insert(Scenery, ZoneObject) + gotscenery=true + + end + + end + + return true + end + + -- Search the world. + world.searchObjects(scanobjects, SphereSearch, EvaluateZone) + + for _,unit in pairs(Units) do + self:T(string.format("Scan found unit %s", unit:GetName())) + end + for _,static in pairs(Statics) do + self:T(string.format("Scan found static %s", static:getName())) + end + for _,scenery in pairs(Scenery) do + self:T(string.format("Scan found scenery %s", scenery:getTypeName())) + end + + return gotunits, gotstatics, gotscenery, Units, Statics, Scenery + end + + --- Calculate the distance from a reference @{#COORDINATE}. + -- @param #COORDINATE self + -- @param #COORDINATE PointVec2Reference The reference @{#COORDINATE}. + -- @return DCS#Distance The distance from the reference @{#COORDINATE} in meters. + function COORDINATE:DistanceFromPointVec2( PointVec2Reference ) + self:F2( PointVec2Reference ) + + local Distance = ( ( PointVec2Reference.x - self.x ) ^ 2 + ( PointVec2Reference.z - self.z ) ^2 ) ^ 0.5 self:T2( Distance ) return Distance end - --- Add a Distance in meters from the COORDINATE orthonormal plane, with the given angle, and calculate the new COORDINATE. -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Distance Distance The Distance to be added in meters. - -- @param Dcs.DCSTypes#Angle Angle The Angle in degrees. - -- @return #COORDINATE The new calculated COORDINATE. - function COORDINATE:Translate( Distance, Angle ) + -- @param DCS#Distance Distance The Distance to be added in meters. + -- @param DCS#Angle Angle The Angle in degrees. Defaults to 0 if not specified (nil). + -- @param #boolean Keepalt If true, keep altitude of original coordinate. Default is that the new coordinate is created at the translated land height. + -- @return Core.Point#COORDINATE The new calculated COORDINATE. + function COORDINATE:Translate( Distance, Angle, Keepalt ) local SX = self.x local SY = self.z - local Radians = Angle / 180 * math.pi + local Radians = (Angle or 0) / 180 * math.pi local TX = Distance * math.cos( Radians ) + SX local TY = Distance * math.sin( Radians ) + SY - - return COORDINATE:NewFromVec2( { x = TX, y = TY } ) + + if Keepalt then + return COORDINATE:NewFromVec3( { x = TX, y=self.y, z = TY } ) + else + return COORDINATE:NewFromVec2( { x = TX, y = TY } ) + end end + --- Rotate coordinate in 2D (x,z) space. + -- @param #COORDINATE self + -- @param DCS#Angle Angle Angle of rotation in degrees. + -- @return Core.Point#COORDINATE The rotated coordinate. + function COORDINATE:Rotate2D(Angle) + + if not Angle then + return self + end + + local phi=math.rad(Angle) + + local X=self.z + local Y=self.x + + --slocal R=math.sqrt(X*X+Y*Y) + + local x=X*math.cos(phi)-Y*math.sin(phi) + local y=X*math.sin(phi)+Y*math.cos(phi) + + -- Coordinate assignment looks bit strange but is correct. + return COORDINATE:NewFromVec3({x=y, y=self.y, z=x}) + end + --- Return a random Vec2 within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE. -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Distance OuterRadius - -- @param Dcs.DCSTypes#Distance InnerRadius - -- @return Dcs.DCSTypes#Vec2 Vec2 + -- @param DCS#Distance OuterRadius + -- @param DCS#Distance InnerRadius + -- @return DCS#Vec2 Vec2 function COORDINATE:GetRandomVec2InRadius( OuterRadius, InnerRadius ) self:F2( { OuterRadius, InnerRadius } ) @@ -303,11 +532,23 @@ do -- COORDINATE end + --- Return a random Coordinate within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE. + -- @param #COORDINATE self + -- @param DCS#Distance OuterRadius + -- @param DCS#Distance InnerRadius + -- @return #COORDINATE + function COORDINATE:GetRandomCoordinateInRadius( OuterRadius, InnerRadius ) + self:F2( { OuterRadius, InnerRadius } ) + + return COORDINATE:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) ) + end + + --- Return a random Vec3 within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE. -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Distance OuterRadius - -- @param Dcs.DCSTypes#Distance InnerRadius - -- @return Dcs.DCSTypes#Vec3 Vec3 + -- @param DCS#Distance OuterRadius + -- @param DCS#Distance InnerRadius + -- @return DCS#Vec3 Vec3 function COORDINATE:GetRandomVec3InRadius( OuterRadius, InnerRadius ) local RandomVec2 = self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) @@ -370,7 +611,7 @@ do -- COORDINATE --- Return a direction vector Vec3 from COORDINATE to the COORDINATE. -- @param #COORDINATE self -- @param #COORDINATE TargetCoordinate The target COORDINATE. - -- @return Dcs.DCSTypes#Vec3 DirectionVec3 The direction vector in Vec3 format. + -- @return DCS#Vec3 DirectionVec3 The direction vector in Vec3 format. function COORDINATE:GetDirectionVec3( TargetCoordinate ) return { x = TargetCoordinate.x - self.x, y = TargetCoordinate.y - self.y, z = TargetCoordinate.z - self.z } end @@ -389,7 +630,7 @@ do -- COORDINATE --- Return an angle in radians from the COORDINATE using a direction vector in Vec3 format. -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Vec3 DirectionVec3 The direction vector in Vec3 format. + -- @param DCS#Vec3 DirectionVec3 The direction vector in Vec3 format. -- @return #number DirectionRadians The angle in radians. function COORDINATE:GetAngleRadians( DirectionVec3 ) local DirectionRadians = math.atan2( DirectionVec3.z, DirectionVec3.x ) @@ -402,7 +643,7 @@ do -- COORDINATE --- Return an angle in degrees from the COORDINATE using a direction vector in Vec3 format. -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Vec3 DirectionVec3 The direction vector in Vec3 format. + -- @param DCS#Vec3 DirectionVec3 The direction vector in Vec3 format. -- @return #number DirectionRadians The angle in degrees. function COORDINATE:GetAngleDegrees( DirectionVec3 ) local AngleRadians = self:GetAngleRadians( DirectionVec3 ) @@ -414,7 +655,7 @@ do -- COORDINATE --- Return the 2D distance in meters between the target COORDINATE and the COORDINATE. -- @param #COORDINATE self -- @param #COORDINATE TargetCoordinate The target COORDINATE. - -- @return Dcs.DCSTypes#Distance Distance The distance in meters. + -- @return DCS#Distance Distance The distance in meters. function COORDINATE:Get2DDistance( TargetCoordinate ) local TargetVec3 = TargetCoordinate:GetVec3() local SourceVec3 = self:GetVec3() @@ -426,8 +667,8 @@ do -- COORDINATE -- @param height (Optional) parameter specifying the height ASL. -- @return Temperature in Degrees Celsius. function COORDINATE:GetTemperature(height) + self:F2(height) local y=height or self.y - env.info("FF height = "..y) local point={x=self.x, y=height or self.y, z=self.z} -- get temperature [K] and pressure [Pa] at point local T,P=atmosphere.getTemperatureAndPressure(point) @@ -516,6 +757,20 @@ do -- COORDINATE return nil end + --- Returns the heading from this to another coordinate. + -- @param #COORDINATE self + -- @param #COORDINATE ToCoordinate + -- @return #number Heading in degrees. + function COORDINATE:HeadingTo(ToCoordinate) + local dz=ToCoordinate.z-self.z + local dx=ToCoordinate.x-self.x + local heading=math.deg(math.atan2(dz, dx)) + if heading < 0 then + heading = 360 + heading + end + return heading + end + --- Returns the wind direction (from) and strength. -- @param #COORDINATE self -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground. @@ -540,6 +795,24 @@ do -- COORDINATE -- Return wind direction and strength km/h. return direction, strength end + + --- Returns the wind direction (from) and strength. + -- @param #COORDINATE self + -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground. + -- @return Direction the wind is blowing from in degrees. + function COORDINATE:GetWindWithTurbulenceVec3(height) + + -- AGL height if + local landheight=self:GetLandHeight()+0.1 -- we at 0.1 meters to be sure to be above ground since wind is zero below ground level. + + -- Point at which the wind is evaluated. + local point={x=self.x, y=math.max(height or self.y, landheight), z=self.z} + + -- Get wind velocity vector including turbulences. + local vec3 = atmosphere.getWindWithTurbulence(point) + + return vec3 + end --- Returns a text documenting the wind direction (from) and strength according the measurement system @{Settings}. @@ -578,7 +851,7 @@ do -- COORDINATE --- Return the 3D distance in meters between the target COORDINATE and the COORDINATE. -- @param #COORDINATE self -- @param #COORDINATE TargetCoordinate The target COORDINATE. - -- @return Dcs.DCSTypes#Distance Distance The distance in meters. + -- @return DCS#Distance Distance The distance in meters. function COORDINATE:Get3DDistance( TargetCoordinate ) local TargetVec3 = TargetCoordinate:GetVec3() local SourceVec3 = self:GetVec3() @@ -711,11 +984,26 @@ do -- COORDINATE end + --- Set altitude. + -- @param #COORDINATE self + -- @param #number altitude New altitude in meters. + -- @param #boolean asl Altitude above sea level. Default is above ground level. + -- @return #COORDINATE The COORDINATE with adjusted altitude. + function COORDINATE:SetAltitude(altitude, asl) + local alt=altitude + if asl then + alt=altitude + else + alt=self:GetLandHeight()+altitude + end + self.y=alt + return self + end --- Add a Distance in meters from the COORDINATE horizontal plane, with the given angle, and calculate the new COORDINATE. -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Distance Distance The Distance to be added in meters. - -- @param Dcs.DCSTypes#Angle Angle The Angle in degrees. + -- @param DCS#Distance Distance The Distance to be added in meters. + -- @param DCS#Angle Angle The Angle in degrees. -- @return #COORDINATE The new calculated COORDINATE. function COORDINATE:Translate( Distance, Angle ) local SX = self.x @@ -734,42 +1022,78 @@ do -- COORDINATE -- @param #COORDINATE.WaypointAltType AltType The altitude type. -- @param #COORDINATE.WaypointType Type The route point type. -- @param #COORDINATE.WaypointAction Action The route point action. - -- @param Dcs.DCSTypes#Speed Speed Airspeed in km/h. + -- @param DCS#Speed Speed Airspeed in km/h. Default is 500 km/h. -- @param #boolean SpeedLocked true means the speed is locked. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase for takeoff and landing points. + -- @param #table DCSTasks A table of @{DCS#Task} items which are executed at the waypoint. + -- @param #string description A text description of the waypoint, which will be shown on the F10 map. -- @return #table The route point. - function COORDINATE:WaypointAir( AltType, Type, Action, Speed, SpeedLocked ) + function COORDINATE:WaypointAir( AltType, Type, Action, Speed, SpeedLocked, airbase, DCSTasks, description ) self:F2( { AltType, Type, Action, Speed, SpeedLocked } ) - + + -- Set alttype or "RADIO" which is AGL. + AltType=AltType or "RADIO" + + -- Speedlocked by default + if SpeedLocked==nil then + SpeedLocked=true + end + + -- Speed or default 500 km/h. + Speed=Speed or 500 + + -- Waypoint array. local RoutePoint = {} + + -- Coordinates. RoutePoint.x = self.x RoutePoint.y = self.z + + -- Altitude. RoutePoint.alt = self.y - RoutePoint.alt_type = AltType or "RADIO" - + RoutePoint.alt_type = AltType + + -- Waypoint type. RoutePoint.type = Type or nil RoutePoint.action = Action or nil - - RoutePoint.speed = ( Speed and Speed / 3.6 ) or ( 500 / 3.6 ) - RoutePoint.speed_locked = true - - -- ["task"] = - -- { - -- ["id"] = "ComboTask", - -- ["params"] = - -- { - -- ["tasks"] = - -- { - -- }, -- end of ["tasks"] - -- }, -- end of ["params"] - -- }, -- end of ["task"] - - + + -- Speed. + RoutePoint.speed = Speed/3.6 + RoutePoint.speed_locked = SpeedLocked + + -- ETA. + RoutePoint.ETA=nil + RoutePoint.ETA_locked = false + + -- Waypoint description. + RoutePoint.name=description + + -- Airbase parameters for takeoff and landing points. + if airbase then + local AirbaseID = airbase:GetID() + local AirbaseCategory = airbase:GetDesc().category + if AirbaseCategory == Airbase.Category.SHIP or AirbaseCategory == Airbase.Category.HELIPAD then + RoutePoint.linkUnit = AirbaseID + RoutePoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.AIRDROME then + RoutePoint.airdromeId = AirbaseID + else + self:T("ERROR: Unknown airbase category in COORDINATE:WaypointAir()!") + end + + --self:MarkToAll(string.format("Landing waypoint at airbase %s", airbase:GetName())) + end + + -- Waypoint tasks. RoutePoint.task = {} RoutePoint.task.id = "ComboTask" RoutePoint.task.params = {} - RoutePoint.task.params.tasks = {} - + RoutePoint.task.params.tasks = DCSTasks or {} + -- Debug. + self:T({RoutePoint=RoutePoint}) + + -- Return waypoint. return RoutePoint end @@ -777,17 +1101,19 @@ do -- COORDINATE --- Build a Waypoint Air "Turning Point". -- @param #COORDINATE self -- @param #COORDINATE.WaypointAltType AltType The altitude type. - -- @param Dcs.DCSTypes#Speed Speed Airspeed in km/h. + -- @param DCS#Speed Speed Airspeed in km/h. + -- @param #table DCSTasks (Optional) A table of @{DCS#Task} items which are executed at the waypoint. + -- @param #string description (Optional) A text description of the waypoint, which will be shown on the F10 map. -- @return #table The route point. - function COORDINATE:WaypointAirTurningPoint( AltType, Speed ) - return self:WaypointAir( AltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed ) + function COORDINATE:WaypointAirTurningPoint( AltType, Speed, DCSTasks, description ) + return self:WaypointAir( AltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true, nil, DCSTasks, description ) end --- Build a Waypoint Air "Fly Over Point". -- @param #COORDINATE self -- @param #COORDINATE.WaypointAltType AltType The altitude type. - -- @param Dcs.DCSTypes#Speed Speed Airspeed in km/h. + -- @param DCS#Speed Speed Airspeed in km/h. -- @return #table The route point. function COORDINATE:WaypointAirFlyOverPoint( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.FlyoverPoint, Speed ) @@ -797,7 +1123,7 @@ do -- COORDINATE --- Build a Waypoint Air "Take Off Parking Hot". -- @param #COORDINATE self -- @param #COORDINATE.WaypointAltType AltType The altitude type. - -- @param Dcs.DCSTypes#Speed Speed Airspeed in km/h. + -- @param DCS#Speed Speed Airspeed in km/h. -- @return #table The route point. function COORDINATE:WaypointAirTakeOffParkingHot( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOffParkingHot, COORDINATE.WaypointAction.FromParkingAreaHot, Speed ) @@ -807,7 +1133,7 @@ do -- COORDINATE --- Build a Waypoint Air "Take Off Parking". -- @param #COORDINATE self -- @param #COORDINATE.WaypointAltType AltType The altitude type. - -- @param Dcs.DCSTypes#Speed Speed Airspeed in km/h. + -- @param DCS#Speed Speed Airspeed in km/h. -- @return #table The route point. function COORDINATE:WaypointAirTakeOffParking( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, Speed ) @@ -817,7 +1143,7 @@ do -- COORDINATE --- Build a Waypoint Air "Take Off Runway". -- @param #COORDINATE self -- @param #COORDINATE.WaypointAltType AltType The altitude type. - -- @param Dcs.DCSTypes#Speed Speed Airspeed in km/h. + -- @param DCS#Speed Speed Airspeed in km/h. -- @return #table The route point. function COORDINATE:WaypointAirTakeOffRunway( AltType, Speed ) return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOff, COORDINATE.WaypointAction.FromRunway, Speed ) @@ -826,7 +1152,10 @@ do -- COORDINATE --- Build a Waypoint Air "Landing". -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Speed Speed Airspeed in km/h. + -- @param DCS#Speed Speed Airspeed in km/h. + -- @param Wrapper.Airbase#AIRBASE airbase The airbase for takeoff and landing points. + -- @param #table DCSTasks A table of @{DCS#Task} items which are executed at the waypoint. + -- @param #string description A text description of the waypoint, which will be shown on the F10 map. -- @return #table The route point. -- @usage -- @@ -835,8 +1164,8 @@ do -- COORDINATE -- LandingWaypoint = LandingCoord:WaypointAirLanding( 60 ) -- HeliGroup:Route( { LandWaypoint }, 1 ) -- Start landing the helicopter in one second. -- - function COORDINATE:WaypointAirLanding( Speed ) - return self:WaypointAir( nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed ) + function COORDINATE:WaypointAirLanding( Speed, airbase, DCSTasks, description ) + return self:WaypointAir(nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, nil, airbase, DCSTasks, description) end @@ -844,17 +1173,19 @@ do -- COORDINATE --- Build an ground type route point. -- @param #COORDINATE self - -- @param #number Speed (optional) Speed in km/h. The default speed is 999 km/h. + -- @param #number Speed (optional) Speed in km/h. The default speed is 20 km/h. -- @param #string Formation (optional) The route point Formation, which is a text string that specifies exactly the Text in the Type of the route point, like "Vee", "Echelon Right". -- @return #table The route point. function COORDINATE:WaypointGround( Speed, Formation ) self:F2( { Formation, Speed } ) + local RoutePoint = {} RoutePoint.x = self.x RoutePoint.y = self.z RoutePoint.action = Formation or "" + --RoutePoint.formation_template = Formation and "" or nil RoutePoint.speed = ( Speed or 20 ) / 3.6 @@ -880,34 +1211,278 @@ do -- COORDINATE return RoutePoint end - - --- Gets the nearest coordinate to a road. + + --- Gets the nearest airbase with respect to the current coordinates. -- @param #COORDINATE self - -- @return #COORDINATE Coordinate of the nearest road. - function COORDINATE:GetClosestPointToRoad() - local x,y = land.getClosestPointOnRoads("roads", self.x, self.z) - local vec2={ x = x, y = y } - return COORDINATE:NewFromVec2(vec2) + -- @param #number Category (Optional) Category of the airbase. Enumerator of @{Wrapper.Airbase#AIRBASE.Category}. + -- @param #number Coalition (Optional) Coalition of the airbase. + -- @return Wrapper.Airbase#AIRBASE Closest Airbase to the given coordinate. + -- @return #number Distance to the closest airbase in meters. + function COORDINATE:GetClosestAirbase(Category, Coalition) + + -- Get all airbases of the map. + local airbases=AIRBASE.GetAllAirbases(Coalition) + + local closest=nil + local distmin=nil + -- Loop over all airbases. + for _,_airbase in pairs(airbases) do + local airbase=_airbase --Wrapper.Airbase#AIRBASE + local category=airbase:GetDesc().category + if Category and Category==category or Category==nil then + local dist=self:Get2DDistance(airbase:GetCoordinate()) + if closest==nil then + distmin=dist + closest=airbase + else + if dist=2 then + for i=1,#Path-1 do + Way=Way+Path[i+1]:Get2DDistance(Path[i]) + end + else + -- There are cases where no path on road can be found. + return nil,nil + end + + return Path, Way, GotPath end + --- Gets the surface type at the coordinate. + -- @param #COORDINATE self + -- @return DCS#SurfaceType Surface type. + function COORDINATE:GetSurfaceType() + local vec2=self:GetVec2() + local surface=land.getSurfaceType(vec2) + return surface + end + + --- Checks if the surface type is on land. + -- @param #COORDINATE self + -- @return #boolean If true, the surface type at the coordinate is land. + function COORDINATE:IsSurfaceTypeLand() + return self:GetSurfaceType()==land.SurfaceType.LAND + end + + --- Checks if the surface type is road. + -- @param #COORDINATE self + -- @return #boolean If true, the surface type at the coordinate is land. + function COORDINATE:IsSurfaceTypeLand() + return self:GetSurfaceType()==land.SurfaceType.LAND + end + + + --- Checks if the surface type is road. + -- @param #COORDINATE self + -- @return #boolean If true, the surface type at the coordinate is a road. + function COORDINATE:IsSurfaceTypeRoad() + return self:GetSurfaceType()==land.SurfaceType.ROAD + end + + --- Checks if the surface type is runway. + -- @param #COORDINATE self + -- @return #boolean If true, the surface type at the coordinate is a runway or taxi way. + function COORDINATE:IsSurfaceTypeRunway() + return self:GetSurfaceType()==land.SurfaceType.RUNWAY + end + + --- Checks if the surface type is shallow water. + -- @param #COORDINATE self + -- @return #boolean If true, the surface type at the coordinate is a shallow water. + function COORDINATE:IsSurfaceTypeShallowWater() + return self:GetSurfaceType()==land.SurfaceType.SHALLOW_WATER + end + + --- Checks if the surface type is water. + -- @param #COORDINATE self + -- @return #boolean If true, the surface type at the coordinate is a deep water. + function COORDINATE:IsSurfaceTypeWater() + return self:GetSurfaceType()==land.SurfaceType.WATER + end + + --- Creates an explosion at the point of a certain intensity. -- @param #COORDINATE self - -- @param #number ExplosionIntensity + -- @param #number ExplosionIntensity Intensity of the explosion in kg TNT. function COORDINATE:Explosion( ExplosionIntensity ) self:F2( { ExplosionIntensity } ) trigger.action.explosion( self:GetVec3(), ExplosionIntensity ) @@ -915,9 +1490,10 @@ do -- COORDINATE --- Creates an illumination bomb at the point. -- @param #COORDINATE self - function COORDINATE:IlluminationBomb() + -- @param #number power + function COORDINATE:IlluminationBomb(power) self:F2() - trigger.action.illuminationBomb( self:GetVec3() ) + trigger.action.illuminationBomb( self:GetVec3(), power ) end @@ -964,10 +1540,92 @@ do -- COORDINATE self:Smoke( SMOKECOLOR.Blue ) end + --- Big smoke and fire at the coordinate. + -- @param #COORDINATE self + -- @param Utilities.Utils#BIGSMOKEPRESET preset Smoke preset (0=small smoke and fire, 1=medium smoke and fire, 2=large smoke and fire, 3=huge smoke and fire, 4=small smoke, 5=medium smoke, 6=large smoke, 7=huge smoke). + -- @param #number density (Optional) Smoke density. Number in [0,...,1]. Default 0.5. + function COORDINATE:BigSmokeAndFire( preset, density ) + self:F2( { preset=preset, density=density } ) + density=density or 0.5 + trigger.action.effectSmokeBig( self:GetVec3(), preset, density ) + end + + --- Small smoke and fire at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeAndFireSmall( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmokeAndFire, density) + end + + --- Medium smoke and fire at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeAndFireMedium( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmokeAndFire, density) + end + + --- Large smoke and fire at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeAndFireLarge( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmokeAndFire, density) + end + + --- Huge smoke and fire at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeAndFireHuge( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmokeAndFire, density) + end + + --- Small smoke at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeSmall( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmoke, density) + end + + --- Medium smoke at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeMedium( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmoke, density) + end + + --- Large smoke at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeLarge( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmoke, density) + end + + --- Huge smoke at the coordinate. + -- @param #COORDINATE self + -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5. + function COORDINATE:BigSmokeHuge( density ) + self:F2( { density=density } ) + density=density or 0.5 + self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmoke, density) + end + --- Flares the point in a color. -- @param #COORDINATE self -- @param Utilities.Utils#FLARECOLOR FlareColor - -- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. + -- @param DCS#Azimuth Azimuth (optional) The azimuth of the flare direction. The default azimuth is 0. function COORDINATE:Flare( FlareColor, Azimuth ) self:F2( { FlareColor } ) trigger.action.signalFlare( self:GetVec3(), FlareColor, Azimuth and Azimuth or 0 ) @@ -975,7 +1633,7 @@ do -- COORDINATE --- Flare the COORDINATE White. -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. + -- @param DCS#Azimuth Azimuth (optional) The azimuth of the flare direction. The default azimuth is 0. function COORDINATE:FlareWhite( Azimuth ) self:F2( Azimuth ) self:Flare( FLARECOLOR.White, Azimuth ) @@ -983,7 +1641,7 @@ do -- COORDINATE --- Flare the COORDINATE Yellow. -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. + -- @param DCS#Azimuth Azimuth (optional) The azimuth of the flare direction. The default azimuth is 0. function COORDINATE:FlareYellow( Azimuth ) self:F2( Azimuth ) self:Flare( FLARECOLOR.Yellow, Azimuth ) @@ -991,7 +1649,7 @@ do -- COORDINATE --- Flare the COORDINATE Green. -- @param #COORDINATE self - -- @param Dcs.DCSTypes#Azimuth (optional) Azimuth The azimuth of the flare direction. The default azimuth is 0. + -- @param DCS#Azimuth Azimuth (optional) The azimuth of the flare direction. The default azimuth is 0. function COORDINATE:FlareGreen( Azimuth ) self:F2( Azimuth ) self:Flare( FLARECOLOR.Green, Azimuth ) @@ -1009,13 +1667,19 @@ do -- COORDINATE --- Mark to All -- @param #COORDINATE self -- @param #string MarkText Free format text that shows the marking clarification. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. -- @return #number The resulting Mark ID which is a number. -- @usage -- local TargetCoord = TargetGroup:GetCoordinate() -- local MarkID = TargetCoord:MarkToAll( "This is a target for all players" ) - function COORDINATE:MarkToAll( MarkText ) + function COORDINATE:MarkToAll( MarkText, ReadOnly, Text ) local MarkID = UTILS.GetMarkID() - trigger.action.markToAll( MarkID, MarkText, self:GetVec3(), false, "" ) + if ReadOnly==nil then + ReadOnly=false + end + local text=Text or "" + trigger.action.markToAll( MarkID, MarkText, self:GetVec3(), ReadOnly, text) return MarkID end @@ -1023,50 +1687,66 @@ do -- COORDINATE -- @param #COORDINATE self -- @param #string MarkText Free format text that shows the marking clarification. -- @param Coalition + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. -- @return #number The resulting Mark ID which is a number. -- @usage -- local TargetCoord = TargetGroup:GetCoordinate() -- local MarkID = TargetCoord:MarkToCoalition( "This is a target for the red coalition", coalition.side.RED ) - function COORDINATE:MarkToCoalition( MarkText, Coalition ) + function COORDINATE:MarkToCoalition( MarkText, Coalition, ReadOnly, Text ) local MarkID = UTILS.GetMarkID() - trigger.action.markToCoalition( MarkID, MarkText, self:GetVec3(), Coalition, false, "" ) + if ReadOnly==nil then + ReadOnly=false + end + local text=Text or "" + trigger.action.markToCoalition( MarkID, MarkText, self:GetVec3(), Coalition, ReadOnly, text ) return MarkID end --- Mark to Red Coalition -- @param #COORDINATE self -- @param #string MarkText Free format text that shows the marking clarification. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. -- @return #number The resulting Mark ID which is a number. -- @usage -- local TargetCoord = TargetGroup:GetCoordinate() -- local MarkID = TargetCoord:MarkToCoalitionRed( "This is a target for the red coalition" ) - function COORDINATE:MarkToCoalitionRed( MarkText ) - return self:MarkToCoalition( MarkText, coalition.side.RED ) + function COORDINATE:MarkToCoalitionRed( MarkText, ReadOnly, Text ) + return self:MarkToCoalition( MarkText, coalition.side.RED, ReadOnly, Text ) end --- Mark to Blue Coalition -- @param #COORDINATE self -- @param #string MarkText Free format text that shows the marking clarification. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. -- @return #number The resulting Mark ID which is a number. -- @usage -- local TargetCoord = TargetGroup:GetCoordinate() -- local MarkID = TargetCoord:MarkToCoalitionBlue( "This is a target for the blue coalition" ) - function COORDINATE:MarkToCoalitionBlue( MarkText ) - return self:MarkToCoalition( MarkText, coalition.side.BLUE ) + function COORDINATE:MarkToCoalitionBlue( MarkText, ReadOnly, Text ) + return self:MarkToCoalition( MarkText, coalition.side.BLUE, ReadOnly, Text ) end --- Mark to Group -- @param #COORDINATE self -- @param #string MarkText Free format text that shows the marking clarification. - -- @param Wrapper.Group#GROUP MarkGroup The @{Group} that receives the mark. + -- @param Wrapper.Group#GROUP MarkGroup The @{Wrapper.Group} that receives the mark. + -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false. + -- @param #string Text (Optional) Text displayed when mark is added. Default none. -- @return #number The resulting Mark ID which is a number. -- @usage -- local TargetCoord = TargetGroup:GetCoordinate() -- local MarkGroup = GROUP:FindByName( "AttackGroup" ) -- local MarkID = TargetCoord:MarkToGroup( "This is a target for the attack group", AttackGroup ) - function COORDINATE:MarkToGroup( MarkText, MarkGroup ) + function COORDINATE:MarkToGroup( MarkText, MarkGroup, ReadOnly, Text ) local MarkID = UTILS.GetMarkID() - trigger.action.markToGroup( MarkID, MarkText, self:GetVec3(), MarkGroup:GetID(), false, "" ) + if ReadOnly==nil then + ReadOnly=false + end + local text=Text or "" + trigger.action.markToGroup( MarkID, MarkText, self:GetVec3(), MarkGroup:GetID(), ReadOnly, text ) return MarkID end @@ -1140,7 +1820,8 @@ do -- COORDINATE --- Return a BR string from a COORDINATE to the COORDINATE. -- @param #COORDINATE self - -- @param #COORDINATE TargetCoordinate The target 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:ToStringBR( FromCoordinate, Settings ) local DirectionVec3 = FromCoordinate:GetDirectionVec3( self ) @@ -1151,7 +1832,8 @@ do -- COORDINATE --- Return a BRAA string from a COORDINATE to the COORDINATE. -- @param #COORDINATE self - -- @param #COORDINATE TargetCoordinate The target 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 ) local DirectionVec3 = FromCoordinate:GetDirectionVec3( self ) @@ -1163,7 +1845,8 @@ do -- COORDINATE --- Return a BULLS string out of the BULLS of the coalition to the COORDINATE. -- @param #COORDINATE self - -- @param Dcs.DCSCoalition#coalition.side Coalition The coalition. + -- @param DCS#coalition.side Coalition The coalition. + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The BR text. function COORDINATE:ToStringBULLS( Coalition, Settings ) local BullsCoordinate = COORDINATE:NewFromVec3( coalition.getMainRefPoint( Coalition ) ) @@ -1203,7 +1886,7 @@ do -- COORDINATE --- Provides a Lat Lon string in Degree Minute Second format. -- @param #COORDINATE self - -- @param Core.Settings#SETTINGS Settings (optional) Settings + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The LL DMS Text function COORDINATE:ToStringLLDMS( Settings ) @@ -1214,7 +1897,7 @@ do -- COORDINATE --- Provides a Lat Lon string in Degree Decimal Minute format. -- @param #COORDINATE self - -- @param Core.Settings#SETTINGS Settings (optional) Settings + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The LL DDM Text function COORDINATE:ToStringLLDDM( Settings ) @@ -1225,7 +1908,7 @@ do -- COORDINATE --- Provides a MGRS string -- @param #COORDINATE self - -- @param Core.Settings#SETTINGS Settings (optional) Settings + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The MGRS Text function COORDINATE:ToStringMGRS( Settings ) --R2.1 Fixes issue #424. @@ -1239,12 +1922,14 @@ 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 #COORDINATE ReferenceCoord The refrence coordinate. + -- @param #string ReferenceName The refrence name. -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param Core.Settings#SETTINGS Settings + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The coordinate Text in the configured coordinate system. - function COORDINATE:ToStringFromRP( ReferenceCoord, ReferenceName, Controllable, Settings ) -- R2.2 + function COORDINATE:ToStringFromRP( ReferenceCoord, ReferenceName, Controllable, Settings ) - self:F( { ReferenceCoord = ReferenceCoord, ReferenceName = ReferenceName } ) + self:F2( { ReferenceCoord = ReferenceCoord, ReferenceName = ReferenceName } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -1269,11 +1954,11 @@ do -- COORDINATE --- Provides a coordinate string of the point, based on the A2G coordinate format system. -- @param #COORDINATE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param Core.Settings#SETTINGS Settings + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The coordinate Text in the configured coordinate system. - function COORDINATE:ToStringA2G( Controllable, Settings ) -- R2.2 + function COORDINATE:ToStringA2G( Controllable, Settings ) - self:F( { Controllable = Controllable and Controllable:GetName() } ) + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -1304,11 +1989,11 @@ do -- COORDINATE --- Provides a coordinate string of the point, based on the A2A coordinate format system. -- @param #COORDINATE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param Core.Settings#SETTINGS Settings + -- @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 - self:F( { Controllable = Controllable and Controllable:GetName() } ) + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -1343,20 +2028,23 @@ do -- COORDINATE -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. -- @param #COORDINATE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param Core.Settings#SETTINGS Settings + -- @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 ) -- R2.2 + function COORDINATE:ToString( Controllable, Settings, Task ) - self:F( { Controllable = Controllable and Controllable:GetName() } ) + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS local ModeA2A = false + self:E('A2A false') 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 @@ -1393,11 +2081,11 @@ do -- COORDINATE -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. -- @param #COORDINATE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param Core.Settings#SETTINGS Settings + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The pressure text in the configured measurement system. function COORDINATE:ToStringPressure( Controllable, Settings ) -- R2.3 - self:F( { Controllable = Controllable and Controllable:GetName() } ) + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -1409,11 +2097,11 @@ do -- COORDINATE -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. -- @param #COORDINATE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param Core.Settings#SETTINGS Settings + -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object. -- @return #string The wind text in the configured measurement system. - function COORDINATE:ToStringWind( Controllable, Settings ) -- R2.3 + function COORDINATE:ToStringWind( Controllable, Settings ) - self:F( { Controllable = Controllable and Controllable:GetName() } ) + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -1425,11 +2113,11 @@ do -- COORDINATE -- * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default. -- @param #COORDINATE self -- @param Wrapper.Controllable#CONTROLLABLE Controllable - -- @param Core.Settings#SETTINGS Settings + -- @param Core.Settings#SETTINGS -- @return #string The temperature text in the configured measurement system. - function COORDINATE:ToStringTemperature( Controllable, Settings ) -- R2.3 + function COORDINATE:ToStringTemperature( Controllable, Settings ) - self:F( { Controllable = Controllable and Controllable:GetName() } ) + self:F2( { Controllable = Controllable and Controllable:GetName() } ) local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS @@ -1450,12 +2138,10 @@ do -- POINT_VEC3 -- @field #POINT_VEC3.RoutePointAltType RoutePointAltType -- @field #POINT_VEC3.RoutePointType RoutePointType -- @field #POINT_VEC3.RoutePointAction RoutePointAction - -- @extends Core.Point#COORDINATE + -- @extends #COORDINATE - --- # POINT_VEC3 class, extends @{Point#COORDINATE} - -- - -- POINT_VEC3 defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space. + --- Defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space. -- -- **Important Note:** Most of the functions in this section were taken from MIST, and reworked to OO concepts. -- In order to keep the credibility of the the author, @@ -1468,7 +2154,7 @@ do -- POINT_VEC3 -- A new POINT_VEC3 object can be created with: -- -- * @{#POINT_VEC3.New}(): a 3D point. - -- * @{#POINT_VEC3.NewFromVec3}(): a 3D point created from a @{DCSTypes#Vec3}. + -- * @{#POINT_VEC3.NewFromVec3}(): a 3D point created from a @{DCS#Vec3}. -- -- -- ## Manupulate the X, Y, Z coordinates of the POINT_VEC3 @@ -1532,9 +2218,9 @@ do -- POINT_VEC3 --- Create a new POINT_VEC3 object. -- @param #POINT_VEC3 self - -- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. - -- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing Upwards. - -- @param Dcs.DCSTypes#Distance z The z coordinate of the Vec3 point, pointing to the Right. + -- @param DCS#Distance x The x coordinate of the Vec3 point, pointing to the North. + -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing Upwards. + -- @param DCS#Distance z The z coordinate of the Vec3 point, pointing to the Right. -- @return Core.Point#POINT_VEC3 function POINT_VEC3:New( x, y, z ) @@ -1546,8 +2232,8 @@ do -- POINT_VEC3 --- Create a new POINT_VEC3 object from Vec2 coordinates. -- @param #POINT_VEC3 self - -- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. - -- @param Dcs.DCSTypes#Distance LandHeightAdd (optional) Add a landheight. + -- @param DCS#Vec2 Vec2 The Vec2 point. + -- @param DCS#Distance LandHeightAdd (optional) Add a landheight. -- @return Core.Point#POINT_VEC3 self function POINT_VEC3:NewFromVec2( Vec2, LandHeightAdd ) @@ -1560,7 +2246,7 @@ do -- POINT_VEC3 --- Create a new POINT_VEC3 object from Vec3 coordinates. -- @param #POINT_VEC3 self - -- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. + -- @param DCS#Vec3 Vec3 The Vec3 point. -- @return Core.Point#POINT_VEC3 self function POINT_VEC3:NewFromVec3( Vec3 ) @@ -1649,8 +2335,8 @@ do -- POINT_VEC3 --- Return a random POINT_VEC3 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3. -- @param #POINT_VEC3 self - -- @param Dcs.DCSTypes#Distance OuterRadius - -- @param Dcs.DCSTypes#Distance InnerRadius + -- @param DCS#Distance OuterRadius + -- @param DCS#Distance InnerRadius -- @return #POINT_VEC3 function POINT_VEC3:GetRandomPointVec3InRadius( OuterRadius, InnerRadius ) @@ -1662,20 +2348,18 @@ end do -- POINT_VEC2 --- @type POINT_VEC2 - -- @field Dcs.DCSTypes#Distance x The x coordinate in meters. - -- @field Dcs.DCSTypes#Distance y the y coordinate in meters. + -- @field DCS#Distance x The x coordinate in meters. + -- @field DCS#Distance y the y coordinate in meters. -- @extends Core.Point#COORDINATE - --- # POINT_VEC2 class, extends @{Point#COORDINATE} - -- - -- The @{Point#POINT_VEC2} class defines a 2D point in the simulator. The height coordinate (if needed) will be the land height + an optional added height specified. + --- Defines a 2D point in the simulator. The height coordinate (if needed) will be the land height + an optional added height specified. -- -- ## POINT_VEC2 constructor -- -- A new POINT_VEC2 instance can be created with: -- - -- * @{Point#POINT_VEC2.New}(): a 2D point, taking an additional height parameter. - -- * @{Point#POINT_VEC2.NewFromVec2}(): a 2D point created from a @{DCSTypes#Vec2}. + -- * @{Core.Point#POINT_VEC2.New}(): a 2D point, taking an additional height parameter. + -- * @{Core.Point#POINT_VEC2.NewFromVec2}(): a 2D point created from a @{DCS#Vec2}. -- -- ## Manupulate the X, Altitude, Y coordinates of the 2D point -- @@ -1700,9 +2384,9 @@ do -- POINT_VEC2 --- POINT_VEC2 constructor. -- @param #POINT_VEC2 self - -- @param Dcs.DCSTypes#Distance x The x coordinate of the Vec3 point, pointing to the North. - -- @param Dcs.DCSTypes#Distance y The y coordinate of the Vec3 point, pointing to the Right. - -- @param Dcs.DCSTypes#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. + -- @param DCS#Distance x The x coordinate of the Vec3 point, pointing to the North. + -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing to the Right. + -- @param DCS#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height. -- @return Core.Point#POINT_VEC2 function POINT_VEC2:New( x, y, LandHeightAdd ) @@ -1719,7 +2403,7 @@ do -- POINT_VEC2 --- Create a new POINT_VEC2 object from Vec2 coordinates. -- @param #POINT_VEC2 self - -- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 point. + -- @param DCS#Vec2 Vec2 The Vec2 point. -- @return Core.Point#POINT_VEC2 self function POINT_VEC2:NewFromVec2( Vec2, LandHeightAdd ) @@ -1736,7 +2420,7 @@ do -- POINT_VEC2 --- Create a new POINT_VEC2 object from Vec3 coordinates. -- @param #POINT_VEC2 self - -- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 point. + -- @param DCS#Vec3 Vec3 The Vec3 point. -- @return Core.Point#POINT_VEC2 self function POINT_VEC2:NewFromVec3( Vec3 ) @@ -1856,8 +2540,8 @@ do -- POINT_VEC2 --- Return a random POINT_VEC2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC2. -- @param #POINT_VEC2 self - -- @param Dcs.DCSTypes#Distance OuterRadius - -- @param Dcs.DCSTypes#Distance InnerRadius + -- @param DCS#Distance OuterRadius + -- @param DCS#Distance InnerRadius -- @return #POINT_VEC2 function POINT_VEC2:GetRandomPointVec2InRadius( OuterRadius, InnerRadius ) self:F2( { OuterRadius, InnerRadius } ) @@ -1869,7 +2553,7 @@ do -- POINT_VEC2 --- Calculate the distance from a reference @{#POINT_VEC2}. -- @param #POINT_VEC2 self -- @param #POINT_VEC2 PointVec2Reference The reference @{#POINT_VEC2}. - -- @return Dcs.DCSTypes#Distance The distance from the reference @{#POINT_VEC2} in meters. + -- @return DCS#Distance The distance from the reference @{#POINT_VEC2} in meters. function POINT_VEC2:DistanceFromPointVec2( PointVec2Reference ) self:F2( PointVec2Reference ) diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Core/Radio.lua index 1d41d25b7..662b7b94f 100644 --- a/Moose Development/Moose/Core/Radio.lua +++ b/Moose Development/Moose/Core/Radio.lua @@ -1,27 +1,30 @@ ---- **Core** -- The RADIO Module is responsible for everything that is related to radio transmission and you can hear in DCS, be it TACAN beacons, Radio transmissions... --- --- ![Banner Image](..\Presentations\RADIO\Dia1.JPG) +--- **Core** - Is responsible for everything that is related to radio transmission and you can hear in DCS, be it TACAN beacons, Radio transmissions. -- -- === +-- +-- ## Features: +-- +-- * Provide radio functionality to broadcast radio transmissions. +-- * Provide beacon functionality to assist pilots. -- -- 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. -- --- Due to weird DCS quirks, **radio communications behave differently** if sent by a @{Unit#UNIT} or a @{Group#GROUP} or by any other @{Positionable#POSITIONABLE} +-- 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 @{Unit#UNIT} or a @{Group#GROUP}, DCS will set the power of the transmission automatically, --- * If the transmitter is any other @{Positionable#POSITIONABLE}, the transmisison can't be subtitled or looped. +-- * 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). @@ -30,56 +33,58 @@ -- -- === -- --- ### Author: Hugues "Grey_Echo" Bousquet +-- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky -- --- @module Radio +-- @module Core.Radio +-- @image Core_Radio.JPG ---- # RADIO class, extends @{Base#BASE} +--- Models the radio capabilty. -- -- ## RADIO usage -- -- There are 3 steps to a successful radio transmission. -- --- * First, you need to **"add a @{#RADIO} object** to your @{Positionable#POSITIONABLE}. This is done using the @{Positionable#POSITIONABLE.GetRadio}() function, +-- * First, you need to **"add a @{#RADIO} object** to your @{Wrapper.Positionable#POSITIONABLE}. This is done using the @{Wrapper.Positionable#POSITIONABLE.GetRadio}() function, -- * Then, you will **set the relevant parameters** to the transmission (see below), -- * When done, you can actually **broadcast the transmission** (i.e. play the sound) with the @{RADIO.Broadcast}() function. -- --- Methods to set relevant parameters for both a @{Unit#UNIT} or a @{Group#GROUP} or any other @{Positionable#POSITIONABLE} +-- Methods to set relevant parameters for both a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or any other @{Wrapper.Positionable#POSITIONABLE} -- -- * @{#RADIO.SetFileName}() : Sets the file name of your sound file (e.g. "Noise.ogg"), -- * @{#RADIO.SetFrequency}() : Sets the frequency of your transmission. -- * @{#RADIO.SetModulation}() : Sets the modulation of your transmission. -- * @{#RADIO.SetLoop}() : Choose if you want the transmission to be looped. If you need your transmission to be looped, you might need a @{#BEACON} instead... -- --- Additional Methods to set relevant parameters if the transmiter is a @{Unit#UNIT} or a @{Group#GROUP} +-- Additional Methods to set relevant parameters if the transmiter 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 @{Positionable#POSITIONABLE} +-- Additional Methods to set relevant parameters if the transmiter 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 @{Positionable#POSITIONABLE} other than a @{Unit#UNIT} or a @{Group#GROUP}, you can set the power of the antenna, +-- * 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", @@ -89,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 @{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 @@ -109,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 @@ -121,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-88 / 108-152 / 225-400MHz. -- @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 + + -- 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) @@ -179,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 @@ -228,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 @@ -242,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}) @@ -262,63 +297,86 @@ end --- Create a new transmission, that is to say, populate the RADIO with relevant data -- In this function the data is especially relevant if the broadcaster is a UNIT or a GROUP, --- but it will work for any @{Positionable#POSITIONABLE}. +-- 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) - if Subtitle then self:SetSubtitle(Subtitle) end - 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#RADIO.NewGenericTransmission} or @{Radio#RADIO.NewUnitTransmission} +-- * Please use RADIO setters or either @{#RADIO.NewGenericTransmission} or @{#RADIO.NewUnitTransmission} -- * This class is in fact pretty smart, it determines the right DCS function to use depending on the type of POSITIONABLE -- * If the POSITIONABLE is not a UNIT or a GROUP, we use the generic (but limited) trigger.action.radioTransmission() -- * If the POSITIONABLE is a UNIT or a GROUP, we use the "TransmitMessage" Command -- * If your POSITIONABLE is a UNIT or a GROUP, the Power is ignored. -- * If your POSITIONABLE is not a UNIT or a GROUP, the Subtitle, SubtitleDuration are ignored -- @param #RADIO self +-- @param #boolean viatrigger Use trigger.action.radioTransmission() in any case, i.e. also for UNITS and GROUPS. -- @return #RADIO self -function RADIO:Broadcast() - 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 @@ -327,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)) @@ -339,41 +397,103 @@ function RADIO:StopBroadcast() end ---- # BEACON class, extends @{Base#BASE} --- --- After attaching a @{#BEACON} to your @{Positionable#POSITIONABLE}, you need to select the right function to activate the kind of beacon you want. +--- After attaching a @{#BEACON} to your @{Wrapper.Positionable#POSITIONABLE}, you need to select the right function to activate the kind of beacon you want. -- There are two types of BEACONs available : the AA TACAN Beacon and the general purpose Radio Beacon. -- Note that in both case, you can set an optional parameter : the `BeaconDuration`. This can be very usefull to simulate the battery time if your BEACON is -- attach to a cargo crate, for exemple. -- -- ## AA TACAN Beacon usage -- --- This beacon only works with airborne @{Unit#UNIT} or a @{Group#GROUP}. Use @{#BEACON:AATACAN}() to set the beacon parameters and start the beacon. +-- This beacon only works with airborne @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}. Use @{#BEACON:AATACAN}() to set the beacon parameters and start the beacon. -- Use @#BEACON:StopAATACAN}() to stop it. -- -- ## General Purpose Radio Beacon usage -- --- This beacon will work with any @{Positionable#POSITIONABLE}, but **it won't follow the @{Positionable#POSITIONABLE}** ! This means that you should only use it with --- @{Positionable#POSITIONABLE} that don't move, or move very slowly. Use @{#BEACON:RadioBeacon}() to set the beacon parameters and start the beacon. +-- This beacon will work with any @{Wrapper.Positionable#POSITIONABLE}, but **it won't follow the @{Wrapper.Positionable#POSITIONABLE}** ! This means that you should only use it with +-- @{Wrapper.Positionable#POSITIONABLE} that don't move, or move very slowly. Use @{#BEACON:RadioBeacon}() to set the beacon parameters and start the beacon. -- Use @{#BEACON:StopRadioBeacon}() to stop it. -- -- @type BEACON +-- @field #string ClassName Name of the class "BEACON". +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will receive radio capabilities. -- @extends Core.Base#BASE BEACON = { ClassName = "BEACON", + Positionable = nil, } ---- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.AATACAN} or @{#BEACON.Generic} --- If you want to create a BEACON, you probably should use @{Positionable#POSITIONABLE.GetBeacon}() instead. +--- 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. +-- @type BEACON.System +-- @field #number PAR_10 +-- @field #number RSBN_5 +-- @field #number TACAN +-- @field #number TACAN_TANKER +-- @field #number ILS_LOCALIZER +-- @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 return self @@ -384,44 +504,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:TACAN(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=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:T({"TACAN BEACON started!"}) + + -- 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 @@ -474,7 +645,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) @@ -585,4 +756,44 @@ 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/Report.lua b/Moose Development/Moose/Core/Report.lua index 4b32cdaf1..35800c60e 100644 --- a/Moose Development/Moose/Core/Report.lua +++ b/Moose Development/Moose/Core/Report.lua @@ -1,6 +1,26 @@ ---- The REPORT class --- @type REPORT +--- **Core** - Provides a handy means to create messages and reports. +-- +-- === +-- +-- ## Features: +-- +-- * Create text blocks that are formatted. +-- * Create automatic indents. +-- * Variate the delimiters between reporting lines. +-- +-- === +-- +-- ### Authors: FlightControl : Design & Programming +-- +-- @module Core.Report +-- @image Core_Report.JPG + + +--- @type REPORT -- @extends Core.Base#BASE + +--- Provides a handy means to create messages and reports. +-- @field #REPORT REPORT = { ClassName = "REPORT", Title = "", diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index 57073a516..8173c85c8 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -22,7 +22,7 @@ -- -- The SCHEDULEDISPATCHER allows multiple scheduled functions to be planned and executed for one SCHEDULER object. -- The SCHEDULER object therefore keeps a table of "CallID's", which are returned after each planning of a new scheduled function by the SCHEDULEDISPATCHER. --- The SCHEDULER object plans new scheduled functions through the @{Scheduler#SCHEDULER.Schedule}() method. +-- The SCHEDULER object plans new scheduled functions through the @{Core.Scheduler#SCHEDULER.Schedule}() method. -- The Schedule() method returns the CallID that is the reference ID for each planned schedule. -- -- === @@ -30,7 +30,8 @@ -- ### Contributions: - -- ### Authors: FlightControl : Design & Programming -- --- @module ScheduleDispatcher +-- @module Core.ScheduleDispatcher +-- @image Core_Schedule_Dispatcher.JPG --- The SCHEDULEDISPATCHER structure -- @type SCHEDULEDISPATCHER diff --git a/Moose Development/Moose/Core/Scheduler.lua b/Moose Development/Moose/Core/Scheduler.lua index e576aa2aa..2b5e7e03b 100644 --- a/Moose Development/Moose/Core/Scheduler.lua +++ b/Moose Development/Moose/Core/Scheduler.lua @@ -1,15 +1,14 @@ ---- **Core** -- SCHEDULER prepares and handles the **execution of functions over scheduled time (intervals)**. +--- **Core** - Prepares and handles the execution of functions over scheduled time (intervals). -- --- ![Banner Image](..\Presentations\SCHEDULER\Dia1.JPG) --- -- === -- --- SCHEDULER manages the **scheduling of functions**: +-- ## Features: -- --- * optionally in an optional specified time interval, --- * optionally **repeating** with a specified time repeat interval, --- * optionally **randomizing** with a specified time interval randomization factor, --- * optionally **stop** the repeating after a specified time interval. +-- * Schedule functions over time, +-- * optionally in an optional specified time interval, +-- * optionally **repeating** with a specified time repeat interval, +-- * optionally **randomizing** with a specified time interval randomization factor, +-- * optionally **stop** the repeating after a specified time interval. -- -- === -- @@ -39,8 +38,8 @@ -- -- === -- --- @module Scheduler - +-- @module Core.Scheduler +-- @image Core_Scheduler.JPG --- The SCHEDULER class -- @type SCHEDULER @@ -48,9 +47,7 @@ -- @extends Core.Base#BASE ---- # SCHEDULER class, extends @{Base#BASE} --- --- The SCHEDULER class creates schedule. +--- Creates and handles schedules over time, which allow to execute code at specific time intervals with randomization. -- -- A SCHEDULER can manage **multiple** (repeating) schedules. Each planned or executing schedule has a unique **ScheduleID**. -- The ScheduleID is returned when the method @{#SCHEDULER.Schedule}() is called. diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index b02e76168..6acb294ab 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -1,24 +1,35 @@ ---- **Core** -- SET_ classes define **collections** of objects to perform **bulk actions** and logically **group** objects. --- --- ![Banner Image](..\Presentations\SET\Dia1.JPG) +--- **Core** - Define collections of objects to perform bulk actions and logically group objects. -- -- === -- --- SET_ classes group objects of the same type into a collection, which is either: +-- ## Features: -- --- * Manually managed using the **:Add...()** or **:Remove...()** methods. The initial SET can be filtered with the **@{#SET_BASE.FilterOnce}()** method +-- * Dynamically maintain collections of objects. +-- * Manually modify the collection, by adding or removing objects. +-- * Collections of different types. +-- * Validate the presence of objects in the collection. +-- * Perform bulk actions on collection. +-- +-- === +-- +-- Group objects or data of the same type into a collection, which is either: +-- +-- * Manually managed using the **:Add...()** or **:Remove...()** methods. The initial SET can be filtered with the **@{#SET_BASE.FilterOnce}()** method. -- * Dynamically updated when new objects are created or objects are destroyed using the **@{#SET_BASE.FilterStart}()** method. -- -- Various types of SET_ classes are available: -- --- * @{#SET_UNIT}: Defines a colleciton of @{Unit}s filtered by filter criteria. --- * @{#SET_GROUP}: Defines a collection of @{Group}s filtered by filter criteria. +-- * @{#SET_GROUP}: Defines a collection of @{Wrapper.Group}s filtered by filter criteria. +-- * @{#SET_UNIT}: Defines a colleciton of @{Wrapper.Unit}s filtered by filter criteria. +-- * @{#SET_STATIC}: Defines a collection of @{Wrapper.Static}s filtered by filter criteria. -- * @{#SET_CLIENT}: Defines a collection of @{Client}s filterd by filter criteria. --- * @{#SET_AIRBASE}: Defines a collection of @{Airbase}s filtered by filter criteria. +-- * @{#SET_AIRBASE}: Defines a collection of @{Wrapper.Airbase}s filtered by filter criteria. +-- * @{#SET_CARGO}: Defines a collection of @{Cargo.Cargo}s filtered by filter criteria. +-- * @{#SET_ZONE}: Defines a collection of @{Core.Zone}s filtered by filter criteria. -- --- These classes are derived from @{#SET_BASE}, which contains the main methods to manage SETs. +-- These classes are derived from @{#SET_BASE}, which contains the main methods to manage the collections. -- --- A multitude of other methods are available in SET_ classes that allow to: +-- A multitude of other methods are available in the individual set classes that allow to: -- -- * Validate the presence of objects in the SET. -- * Trigger events when objects in the SET change a zone presence. @@ -30,1317 +41,1495 @@ -- -- === -- --- @module Set +-- @module Core.Set +-- @image Core_Sets.JPG ---- @type SET_BASE --- @field #table Filter --- @field #table Set --- @field #table List --- @field Core.Scheduler#SCHEDULER CallScheduler --- @extends Core.Base#BASE +do -- SET_BASE - ---- # 1) SET_BASE class, extends @{Base#BASE} --- The @{Set#SET_BASE} class defines the core functions that define a collection of objects. --- A SET provides iterators to iterate the SET, but will **temporarily** yield the ForEach interator loop at defined **"intervals"** to the mail simulator loop. --- In this way, large loops can be done while not blocking the simulator main processing loop. --- The default **"yield interval"** is after 10 objects processed. --- The default **"time interval"** is after 0.001 seconds. --- --- ## 1.1) Add or remove objects from the SET --- --- Some key core functions are @{Set#SET_BASE.Add} and @{Set#SET_BASE.Remove} to add or remove objects from the SET in your logic. --- --- ## 1.2) Define the SET iterator **"yield interval"** and the **"time interval"** --- --- Modify the iterator intervals with the @{Set#SET_BASE.SetInteratorIntervals} method. --- You can set the **"yield interval"**, and the **"time interval"**. (See above). --- --- @field #SET_BASE SET_BASE -SET_BASE = { - ClassName = "SET_BASE", - Filter = {}, - Set = {}, - List = {}, - Index = {}, -} - - ---- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_BASE self --- @return #SET_BASE --- @usage --- -- Define a new SET_BASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. --- DBObject = SET_BASE:New() -function SET_BASE:New( Database ) - - -- Inherits from BASE - local self = BASE:Inherit( self, BASE:New() ) -- Core.Set#SET_BASE + --- @type SET_BASE + -- @field #table Filter + -- @field #table Set + -- @field #table List + -- @field Core.Scheduler#SCHEDULER CallScheduler + -- @extends Core.Base#BASE - self.Database = Database - - self.YieldInterval = 10 - self.TimeInterval = 0.001 - - self.Set = {} - self.Index = {} - self.CallScheduler = SCHEDULER:New( self ) - - self:SetEventPriority( 2 ) - - return self -end - ---- Finds an @{Base#BASE} object based on the object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return Core.Base#BASE The Object found. -function SET_BASE:_Find( ObjectName ) - - local ObjectFound = self.Set[ObjectName] - return ObjectFound -end - - ---- Gets the Set. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:GetSet() - self:F2() - - return self.Set -end - ---- Gets a list of the Names of the Objects in the Set. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:GetSetNames() -- R2.3 - self:F2() + --- The @{Core.Set#SET_BASE} class defines the core functions that define a collection of objects. + -- A SET provides iterators to iterate the SET, but will **temporarily** yield the ForEach interator loop at defined **"intervals"** to the mail simulator loop. + -- In this way, large loops can be done while not blocking the simulator main processing loop. + -- The default **"yield interval"** is after 10 objects processed. + -- The default **"time interval"** is after 0.001 seconds. + -- + -- ## Add or remove objects from the SET + -- + -- Some key core functions are @{Core.Set#SET_BASE.Add} and @{Core.Set#SET_BASE.Remove} to add or remove objects from the SET in your logic. + -- + -- ## Define the SET iterator **"yield interval"** and the **"time interval"** + -- + -- Modify the iterator intervals with the @{Core.Set#SET_BASE.SetInteratorIntervals} method. + -- You can set the **"yield interval"**, and the **"time interval"**. (See above). + -- + -- @field #SET_BASE SET_BASE + SET_BASE = { + ClassName = "SET_BASE", + Filter = {}, + Set = {}, + List = {}, + Index = {}, + } - local Names = {} - for Name, Object in pairs( self.Set ) do - table.insert( Names, Name ) + --- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. + -- @param #SET_BASE self + -- @return #SET_BASE + -- @usage + -- -- Define a new SET_BASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE. + -- DBObject = SET_BASE:New() + function SET_BASE:New( Database ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM:New() ) -- Core.Set#SET_BASE + + self.Database = Database + + self:SetStartState( "Started" ) + + --- Added Handler OnAfter for SET_BASE + -- @function [parent=#SET_BASE] OnAfterAdded + -- @param #SET_BASE self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #string ObjectName The name of the object. + -- @param Object The object. + + + self:AddTransition( "*", "Added", "*" ) + + --- Removed Handler OnAfter for SET_BASE + -- @function [parent=#SET_BASE] OnAfterRemoved + -- @param #SET_BASE self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param #string ObjectName The name of the object. + -- @param Object The object. + + self:AddTransition( "*", "Removed", "*" ) + + self.YieldInterval = 10 + self.TimeInterval = 0.001 + + self.Set = {} + self.Index = {} + + self.CallScheduler = SCHEDULER:New( self ) + + self:SetEventPriority( 2 ) + + return self end - return Names -end - - ---- Gets a list of the Objects in the Set. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:GetSetObjects() -- R2.3 - self:F2() + --- Finds an @{Core.Base#BASE} object based on the object Name. + -- @param #SET_BASE self + -- @param #string ObjectName + -- @return Core.Base#BASE The Object found. + function SET_BASE:_Find( ObjectName ) - local Objects = {} - - for Name, Object in pairs( self.Set ) do - table.insert( Objects, Object ) + local ObjectFound = self.Set[ObjectName] + return ObjectFound end - return Objects -end - - ---- Removes a @{Base#BASE} object from the @{Set#SET_BASE} and derived classes, based on the Object Name. --- @param #SET_BASE self --- @param #string ObjectName -function SET_BASE:Remove( ObjectName ) - - local Object = self.Set[ObjectName] - self:F3( { ObjectName, Object } ) - - if Object then - for Index, Key in ipairs( self.Index ) do - if Key == ObjectName then - table.remove( self.Index, Index ) - self.Set[ObjectName] = nil - break - end + --- Gets the Set. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:GetSet() + self:F2() + + return self.Set + end + + --- Gets a list of the Names of the Objects in the Set. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:GetSetNames() -- R2.3 + self:F2() + + local Names = {} + + for Name, Object in pairs( self.Set ) do + table.insert( Names, Name ) end + return Names end -end - - ---- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using a given ObjectName as the index. --- @param #SET_BASE self --- @param #string ObjectName --- @param Core.Base#BASE Object --- @return Core.Base#BASE The added BASE Object. -function SET_BASE:Add( ObjectName, Object ) - self:F3( { ObjectName = ObjectName, Object = Object } ) - - -- Ensure that the existing element is removed from the Set before a new one is inserted to the Set - if self.Set[ObjectName] then - self:Remove( ObjectName ) - end - self.Set[ObjectName] = Object - table.insert( self.Index, ObjectName ) -end - ---- Adds a @{Base#BASE} object in the @{Set#SET_BASE}, using the Object Name as the index. --- @param #SET_BASE self --- @param Wrapper.Object#OBJECT Object --- @return Core.Base#BASE The added BASE Object. -function SET_BASE:AddObject( Object ) - self:F2( Object.ObjectName ) - self:T( Object.UnitName ) - self:T( Object.ObjectName ) - self:Add( Object.ObjectName, Object ) - -end - - - - ---- Gets a @{Base#BASE} object from the @{Set#SET_BASE} and derived classes, based on the Object Name. --- @param #SET_BASE self --- @param #string ObjectName --- @return Core.Base#BASE -function SET_BASE:Get( ObjectName ) - self:F( ObjectName ) - - local Object = self.Set[ObjectName] - - self:T3( { ObjectName, Object } ) - return Object -end - ---- Gets the first object from the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return Core.Base#BASE -function SET_BASE:GetFirst() - - local ObjectName = self.Index[1] - local FirstObject = self.Set[ObjectName] - self:T3( { FirstObject } ) - return FirstObject -end - ---- Gets the last object from the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return Core.Base#BASE -function SET_BASE:GetLast() - - local ObjectName = self.Index[#self.Index] - local LastObject = self.Set[ObjectName] - self:T3( { LastObject } ) - return LastObject -end - ---- Gets a random object from the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return Core.Base#BASE -function SET_BASE:GetRandom() - - local RandomItem = self.Set[self.Index[math.random(#self.Index)]] - self:T3( { RandomItem } ) - return RandomItem -end - - ---- Retrieves the amount of objects in the @{Set#SET_BASE} and derived classes. --- @param #SET_BASE self --- @return #number Count -function SET_BASE:Count() - - return self.Index and #self.Index or 0 -end - - ---- Copies the Filter criteria from a given Set (for rebuilding a new Set based on an existing Set). --- @param #SET_BASE self --- @param #SET_BASE BaseSet --- @return #SET_BASE -function SET_BASE:SetDatabase( BaseSet ) - - -- Copy the filter criteria of the BaseSet - local OtherFilter = routines.utils.deepCopy( BaseSet.Filter ) - self.Filter = OtherFilter - - -- Now base the new Set on the BaseSet - self.Database = BaseSet:GetSet() - return self -end - - - ---- Define the SET iterator **"yield interval"** and the **"time interval"**. --- @param #SET_BASE self --- @param #number YieldInterval Sets the frequency when the iterator loop will yield after the number of objects processed. The default frequency is 10 objects processed. --- @param #number TimeInterval Sets the time in seconds when the main logic will resume the iterator loop. The default time is 0.001 seconds. --- @return #SET_BASE self -function SET_BASE:SetIteratorIntervals( YieldInterval, TimeInterval ) - - self.YieldInterval = YieldInterval - self.TimeInterval = TimeInterval - - return self -end - - ---- Filters for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterOnce() - - for ObjectName, Object in pairs( self.Database ) do - - if self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) + --- Gets a list of the Objects in the Set. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:GetSetObjects() -- R2.3 + self:F2() + + local Objects = {} + + for Name, Object in pairs( self.Set ) do + table.insert( Objects, Object ) end + + return Objects end - return self -end - ---- Starts the filtering for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:_FilterStart() - - for ObjectName, Object in pairs( self.Database ) do - - if self:IsIncludeObject( Object ) then - self:E( { "Adding Object:", ObjectName } ) - self:Add( ObjectName, Object ) - end - end - self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + --- Removes a @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name. + -- @param #SET_BASE self + -- @param #string ObjectName + -- @param NoTriggerEvent (optional) When `true`, the :Remove() method will not trigger a **Removed** event. + function SET_BASE:Remove( ObjectName, NoTriggerEvent ) + self:F2( { ObjectName = ObjectName } ) - -- Follow alive players and clients - --self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) - --self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) - - - return self -end - ---- Starts the filtering of the Dead events for the collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterDeads() --R2.1 allow deads to be filtered to automatically handle deads in the collection. - - self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) - - return self -end - ---- Starts the filtering of the Crash events for the collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterCrashes() --R2.1 allow crashes to be filtered to automatically handle crashes in the collection. - - self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) - - return self -end - ---- Stops the filtering for the defined collection. --- @param #SET_BASE self --- @return #SET_BASE self -function SET_BASE:FilterStop() - - self:UnHandleEvent( EVENTS.Birth ) - self:UnHandleEvent( EVENTS.Dead ) - self:UnHandleEvent( EVENTS.Crash ) - - return self -end - ---- Iterate the SET_BASE while identifying the nearest object from a @{Point#POINT_VEC2}. --- @param #SET_BASE self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest object in the set. --- @return Core.Base#BASE The closest object. -function SET_BASE:FindNearestObjectFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) - - local NearestObject = nil - local ClosestDistance = nil - - for ObjectID, ObjectData in pairs( self.Set ) do - if NearestObject == nil then - NearestObject = ObjectData - ClosestDistance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - else - local Distance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - if Distance < ClosestDistance then - NearestObject = ObjectData - ClosestDistance = Distance - end - end - end - - return NearestObject -end - - - ------ Private method that registers all alive players in the mission. ----- @param #SET_BASE self ----- @return #SET_BASE self ---function SET_BASE:_RegisterPlayers() --- --- local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } --- for CoalitionId, CoalitionData in pairs( CoalitionsData ) do --- for UnitId, UnitData in pairs( CoalitionData ) do --- self:T3( { "UnitData:", UnitData } ) --- if UnitData and UnitData:isExist() then --- local UnitName = UnitData:getName() --- if not self.PlayersAlive[UnitName] then --- self:E( { "Add player for unit:", UnitName, UnitData:getPlayerName() } ) --- self.PlayersAlive[UnitName] = UnitData:getPlayerName() --- end --- end --- end --- end --- --- return self ---end - ---- Events - ---- Handles the OnBirth event for the Set. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnBirth( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:AddInDatabase( Event ) - self:T3( ObjectName, Object ) - if Object and self:IsIncludeObject( Object ) then - self:Add( ObjectName, Object ) - --self:_EventOnPlayerEnterUnit( Event ) - end - end -end - ---- Handles the OnDead or OnCrash event for alive units set. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event -function SET_BASE:_EventOnDeadOrCrash( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:FindInDatabase( Event ) - if ObjectName then - self:Remove( ObjectName ) - end - end -end - ---- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event ---function SET_BASE:_EventOnPlayerEnterUnit( Event ) --- self:F3( { Event } ) --- --- if Event.IniDCSUnit then --- local ObjectName, Object = self:AddInDatabase( Event ) --- self:T3( ObjectName, Object ) --- if self:IsIncludeObject( Object ) then --- self:Add( ObjectName, Object ) --- --self:_EventOnPlayerEnterUnit( Event ) --- end --- end ---end - ---- Handles the OnPlayerLeaveUnit event to clean the active players table. --- @param #SET_BASE self --- @param Core.Event#EVENTDATA Event ---function SET_BASE:_EventOnPlayerLeaveUnit( Event ) --- self:F3( { Event } ) --- --- local ObjectName = Event.IniDCSUnit --- if Event.IniDCSUnit then --- if Event.IniDCSGroup then --- local GroupUnits = Event.IniDCSGroup:getUnits() --- local PlayerCount = 0 --- for _, DCSUnit in pairs( GroupUnits ) do --- if DCSUnit ~= Event.IniDCSUnit then --- if DCSUnit:getPlayerName() ~= nil then --- PlayerCount = PlayerCount + 1 --- end --- end --- end --- self:E(PlayerCount) --- if PlayerCount == 0 then --- self:Remove( Event.IniDCSGroupName ) --- end --- end --- end ---end - --- Iterators - ---- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters. --- @param #SET_BASE self --- @param #function IteratorFunction The function that will be called. --- @return #SET_BASE self -function SET_BASE:ForEach( IteratorFunction, arg, Set, Function, FunctionArguments ) - self:F3( arg ) - - Set = Set or self:GetSet() - arg = arg or {} - - local function CoRoutine() - local Count = 0 - for ObjectID, ObjectData in pairs( Set ) do - local Object = ObjectData - self:T3( Object ) - if Function then - if Function( unpack( FunctionArguments ), Object ) == true then - IteratorFunction( Object, unpack( arg ) ) - end - else - IteratorFunction( Object, unpack( arg ) ) + local Object = self.Set[ObjectName] + + if Object then + for Index, Key in ipairs( self.Index ) do + if Key == ObjectName then + table.remove( self.Index, Index ) + self.Set[ObjectName] = nil + break end - 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 call an interator function for each **alive** unit, providing the Unit and optional parameters. ----- @param #SET_BASE self ----- @param #function IteratorFunction The function that will be called when there is an alive unit in the SET_BASE. The function needs to accept a UNIT parameter. ----- @return #SET_BASE self ---function SET_BASE:ForEachDCSUnitAlive( IteratorFunction, ... ) --- self:F3( arg ) --- --- self:ForEach( IteratorFunction, arg, self.DCSUnitsAlive ) --- --- return self ---end --- ------ Iterate the SET_BASE and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. ----- @param #SET_BASE self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a UNIT parameter. ----- @return #SET_BASE self ---function SET_BASE:ForEachPlayer( IteratorFunction, ... ) --- self:F3( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_BASE and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_BASE self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a CLIENT parameter. ----- @return #SET_BASE self ---function SET_BASE:ForEachClient( IteratorFunction, ... ) --- self:F3( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- Decides whether to include the Object --- @param #SET_BASE self --- @param #table Object --- @return #SET_BASE self -function SET_BASE:IsIncludeObject( Object ) - self:F3( Object ) - - return true -end - ---- Gets a string with all the object names. --- @param #SET_BASE self --- @return #string A string with the names of the objects. -function SET_BASE:GetObjectNames() - self:F3() - - local ObjectNames = "" - for ObjectName, Object in pairs( self.Set ) do - ObjectNames = ObjectNames .. ObjectName .. ", " - end - - return ObjectNames -end - ---- Flushes the current SET_BASE contents in the log ... (for debugging reasons). --- @param #SET_BASE self --- @param Core.Base#BASE MasterObject (optional) The master object as a reference. --- @return #string A string with the names of the objects. -function SET_BASE:Flush( MasterObject ) - self:F3() - - local ObjectNames = "" - for ObjectName, Object in pairs( self.Set ) do - ObjectNames = ObjectNames .. ObjectName .. ", " - end - self:I( { MasterObject = MasterObject and MasterObject:GetClassNameAndID(), "Objects in Set:", ObjectNames } ) - - return ObjectNames -end - - ---- @type SET_GROUP --- @extends Core.Set#SET_BASE - ---- # SET_GROUP class, extends @{Set#SET_BASE} --- --- Mission designers can use the @{Set#SET_GROUP} class to build sets of groups belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Starting with certain prefix strings. --- --- ## 1. SET_GROUP constructor --- --- Create a new SET_GROUP object with the @{#SET_GROUP.New} method: --- --- * @{#SET_GROUP.New}: Creates a new SET_GROUP object. --- --- ## 2. Add or Remove GROUP(s) from SET_GROUP --- --- GROUPS can be added and removed using the @{Set#SET_GROUP.AddGroupsByName} and @{Set#SET_GROUP.RemoveGroupsByName} respectively. --- These methods take a single GROUP name or an array of GROUP names to be added or removed from SET_GROUP. --- --- ## 3. SET_GROUP filter criteria --- --- You can set filter criteria to define the set of groups within the SET_GROUP. --- Filter criteria are defined by: --- --- * @{#SET_GROUP.FilterCoalitions}: Builds the SET_GROUP with the groups belonging to the coalition(s). --- * @{#SET_GROUP.FilterCategories}: Builds the SET_GROUP with the groups belonging to the category(ies). --- * @{#SET_GROUP.FilterCountries}: Builds the SET_GROUP with the gruops belonging to the country(ies). --- * @{#SET_GROUP.FilterPrefixes}: Builds the SET_GROUP with the groups starting with the same prefix string(s). --- --- For the Category Filter, extra methods have been added: --- --- * @{#SET_GROUP.FilterCategoryAirplane}: Builds the SET_GROUP from airplanes. --- * @{#SET_GROUP.FilterCategoryHelicopter}: Builds the SET_GROUP from helicopters. --- * @{#SET_GROUP.FilterCategoryGround}: Builds the SET_GROUP from ground vehicles or infantry. --- * @{#SET_GROUP.FilterCategoryShip}: Builds the SET_GROUP from ships. --- * @{#SET_GROUP.FilterCategoryStructure}: Builds the SET_GROUP from structures. --- --- --- Once the filter criteria have been set for the SET_GROUP, you can start filtering using: --- --- * @{#SET_GROUP.FilterStart}: Starts the filtering of the groups within the SET_GROUP and add or remove GROUP objects **dynamically**. --- --- Planned filter criteria within development are (so these are not yet available): --- --- * @{#SET_GROUP.FilterZones}: Builds the SET_GROUP with the groups within a @{Zone#ZONE}. --- --- ## 4. SET_GROUP iterators --- --- Once the filters have been defined and the SET_GROUP has been built, you can iterate the SET_GROUP with the available iterator methods. --- The iterator methods will walk the SET_GROUP set, and call for each element within the set a function that you provide. --- The following iterator methods are currently available within the SET_GROUP: --- --- * @{#SET_GROUP.ForEachGroup}: Calls a function for each alive group it finds within the SET_GROUP. --- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. --- * @{#SET_GROUP.ForEachGroupPartlyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. --- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. --- --- === --- @field #SET_GROUP SET_GROUP -SET_GROUP = { - ClassName = "SET_GROUP", - Filter = { - Coalitions = nil, - Categories = nil, - Countries = nil, - GroupPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - Categories = { - plane = Group.Category.AIRPLANE, - helicopter = Group.Category.HELICOPTER, - ground = Group.Category.GROUND, -- R2.2 - ship = Group.Category.SHIP, - structure = Group.Category.STRUCTURE, - }, - }, -} - - ---- Creates a new SET_GROUP object, building a set of groups belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_GROUP self --- @return #SET_GROUP --- @usage --- -- Define a new SET_GROUP Object. This DBObject will contain a reference to all alive GROUPS. --- DBObject = SET_GROUP:New() -function SET_GROUP:New() - - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.GROUPS ) ) - - return self -end - ---- Gets the Set. --- @param #SET_GROUP self --- @return #SET_GROUP self -function SET_GROUP:GetAliveSet() - self:F2() - - local AliveSet = SET_GROUP:New() - - -- Clean the Set before returning with only the alive Groups. - for GroupName, GroupObject in pairs( self.Set ) do - if GroupObject then - if GroupObject:IsAlive() then - AliveSet:Add( GroupName, GroupObject ) + end + -- When NoTriggerEvent is true, then no Removed event will be triggered. + if not NoTriggerEvent then + self:Removed( ObjectName, Object ) end end end - return AliveSet.Set or {} -end - - ---- Add GROUP(s) to SET_GROUP. --- @param Core.Set#SET_GROUP self --- @param #string AddGroupNames A single name or an array of GROUP names. --- @return self -function SET_GROUP:AddGroupsByName( AddGroupNames ) - - local AddGroupNamesArray = ( type( AddGroupNames ) == "table" ) and AddGroupNames or { AddGroupNames } - for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do - self:Add( AddGroupName, GROUP:FindByName( AddGroupName ) ) - end + --- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using a given ObjectName as the index. + -- @param #SET_BASE self + -- @param #string ObjectName + -- @param Core.Base#BASE Object + -- @return Core.Base#BASE The added BASE Object. + function SET_BASE:Add( ObjectName, Object ) + self:F2( { ObjectName = ObjectName, Object = Object } ) + + -- Ensure that the existing element is removed from the Set before a new one is inserted to the Set + if self.Set[ObjectName] then + self:Remove( ObjectName, true ) + end + self.Set[ObjectName] = Object + table.insert( self.Index, ObjectName ) - return self -end - ---- Remove GROUP(s) from SET_GROUP. --- @param Core.Set#SET_GROUP self --- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names. --- @return self -function SET_GROUP:RemoveGroupsByName( RemoveGroupNames ) - - local RemoveGroupNamesArray = ( type( RemoveGroupNames ) == "table" ) and RemoveGroupNames or { RemoveGroupNames } - - for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do - self:Remove( RemoveGroupName.GroupName ) + self:Added( ObjectName, Object ) end + + --- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using the Object Name as the index. + -- @param #SET_BASE self + -- @param Wrapper.Object#OBJECT Object + -- @return Core.Base#BASE The added BASE Object. + function SET_BASE:AddObject( Object ) + self:F2( Object.ObjectName ) - return self -end - - - - ---- Finds a Group based on the Group Name. --- @param #SET_GROUP self --- @param #string GroupName --- @return Wrapper.Group#GROUP The found Group. -function SET_GROUP:FindGroup( GroupName ) - - local GroupFound = self.Set[GroupName] - return GroupFound -end - ---- Iterate the SET_GROUP while identifying the nearest object from a @{Point#POINT_VEC2}. --- @param #SET_GROUP self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest object in the set. --- @return Wrapper.Group#GROUP The closest group. -function SET_GROUP:FindNearestGroupFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) + self:T( Object.UnitName ) + self:T( Object.ObjectName ) + self:Add( Object.ObjectName, Object ) + + end - local NearestGroup = nil - local ClosestDistance = nil - for ObjectID, ObjectData in pairs( self.Set ) do - if NearestGroup == nil then - NearestGroup = ObjectData - ClosestDistance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - else - local Distance = PointVec2:DistanceFromVec2( ObjectData:GetVec2() ) - if Distance < ClosestDistance then - NearestGroup = ObjectData - ClosestDistance = Distance + + + --- Gets a @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name. + -- @param #SET_BASE self + -- @param #string ObjectName + -- @return Core.Base#BASE + function SET_BASE:Get( ObjectName ) + self:F( ObjectName ) + + local Object = self.Set[ObjectName] + + self:T3( { ObjectName, Object } ) + return Object + end + + --- Gets the first object from the @{Core.Set#SET_BASE} and derived classes. + -- @param #SET_BASE self + -- @return Core.Base#BASE + function SET_BASE:GetFirst() + + local ObjectName = self.Index[1] + local FirstObject = self.Set[ObjectName] + self:T3( { FirstObject } ) + return FirstObject + end + + --- Gets the last object from the @{Core.Set#SET_BASE} and derived classes. + -- @param #SET_BASE self + -- @return Core.Base#BASE + function SET_BASE:GetLast() + + local ObjectName = self.Index[#self.Index] + local LastObject = self.Set[ObjectName] + self:T3( { LastObject } ) + return LastObject + end + + --- Gets a random object from the @{Core.Set#SET_BASE} and derived classes. + -- @param #SET_BASE self + -- @return Core.Base#BASE + function SET_BASE:GetRandom() + + local RandomItem = self.Set[self.Index[math.random(#self.Index)]] + self:T3( { RandomItem } ) + return RandomItem + end + + + --- Retrieves the amount of objects in the @{Core.Set#SET_BASE} and derived classes. + -- @param #SET_BASE self + -- @return #number Count + function SET_BASE:Count() + + return self.Index and #self.Index or 0 + end + + + --- Copies the Filter criteria from a given Set (for rebuilding a new Set based on an existing Set). + -- @param #SET_BASE self + -- @param #SET_BASE BaseSet + -- @return #SET_BASE + function SET_BASE:SetDatabase( BaseSet ) + + -- Copy the filter criteria of the BaseSet + local OtherFilter = routines.utils.deepCopy( BaseSet.Filter ) + self.Filter = OtherFilter + + -- Now base the new Set on the BaseSet + self.Database = BaseSet:GetSet() + return self + end + + + + --- Define the SET iterator **"yield interval"** and the **"time interval"**. + -- @param #SET_BASE self + -- @param #number YieldInterval Sets the frequency when the iterator loop will yield after the number of objects processed. The default frequency is 10 objects processed. + -- @param #number TimeInterval Sets the time in seconds when the main logic will resume the iterator loop. The default time is 0.001 seconds. + -- @return #SET_BASE self + function SET_BASE:SetIteratorIntervals( YieldInterval, TimeInterval ) + + self.YieldInterval = YieldInterval + self.TimeInterval = TimeInterval + + return self + end + + + --- Filters for the defined collection. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:FilterOnce() + + for ObjectName, Object in pairs( self.Database ) do + + if self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + end + end + + return self + end + + --- Starts the filtering for the defined collection. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:_FilterStart() + + for ObjectName, Object in pairs( self.Database ) do + + if self:IsIncludeObject( Object ) then + self:E( { "Adding Object:", ObjectName } ) + self:Add( ObjectName, Object ) + end + end + + -- Follow alive players and clients + --self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) + --self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit ) + + + return self + end + + --- Starts the filtering of the Dead events for the collection. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:FilterDeads() --R2.1 allow deads to be filtered to automatically handle deads in the collection. + + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + + return self + end + + --- Starts the filtering of the Crash events for the collection. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:FilterCrashes() --R2.1 allow crashes to be filtered to automatically handle crashes in the collection. + + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + + return self + end + + --- Stops the filtering for the defined collection. + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:FilterStop() + + self:UnHandleEvent( EVENTS.Birth ) + self:UnHandleEvent( EVENTS.Dead ) + self:UnHandleEvent( EVENTS.Crash ) + + return self + end + + --- Iterate the SET_BASE while identifying the nearest object from a @{Core.Point#POINT_VEC2}. + -- @param #SET_BASE self + -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest object in the set. + -- @return Core.Base#BASE The closest object. + function SET_BASE:FindNearestObjectFromPointVec2( PointVec2 ) + self:F2( PointVec2 ) + + local NearestObject = nil + local ClosestDistance = nil + + for ObjectID, ObjectData in pairs( self.Set ) do + if NearestObject == nil then + NearestObject = ObjectData + ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) + else + local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) + if Distance < ClosestDistance then + NearestObject = ObjectData + ClosestDistance = Distance + end + end + end + + return NearestObject + end + + + + ----- Private method that registers all alive players in the mission. + ---- @param #SET_BASE self + ---- @return #SET_BASE self + --function SET_BASE:_RegisterPlayers() + -- + -- local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) } + -- for CoalitionId, CoalitionData in pairs( CoalitionsData ) do + -- for UnitId, UnitData in pairs( CoalitionData ) do + -- self:T3( { "UnitData:", UnitData } ) + -- if UnitData and UnitData:isExist() then + -- local UnitName = UnitData:getName() + -- if not self.PlayersAlive[UnitName] then + -- self:E( { "Add player for unit:", UnitName, UnitData:getPlayerName() } ) + -- self.PlayersAlive[UnitName] = UnitData:getPlayerName() + -- end + -- end + -- end + -- end + -- + -- return self + --end + + --- Events + + --- Handles the OnBirth event for the Set. + -- @param #SET_BASE self + -- @param Core.Event#EVENTDATA Event + function SET_BASE:_EventOnBirth( Event ) + self:F3( { Event } ) + + if Event.IniDCSUnit then + local ObjectName, Object = self:AddInDatabase( Event ) + self:T3( ObjectName, Object ) + if Object and self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + --self:_EventOnPlayerEnterUnit( Event ) end end end - return NearestGroup -end - - ---- Builds a set of groups of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_GROUP self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_GROUP self -function SET_GROUP:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of groups out of categories. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_GROUP self --- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". --- @return #SET_GROUP self -function SET_GROUP:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - ---- Builds a set of groups out of ground category. --- @param #SET_GROUP self --- @return #SET_GROUP self -function SET_GROUP:FilterCategoryGround() - self:FilterCategories( "ground" ) - return self -end - ---- Builds a set of groups out of airplane category. --- @param #SET_GROUP self --- @return #SET_GROUP self -function SET_GROUP:FilterCategoryAirplane() - self:FilterCategories( "plane" ) - return self -end - ---- Builds a set of groups out of helicopter category. --- @param #SET_GROUP self --- @return #SET_GROUP self -function SET_GROUP:FilterCategoryHelicopter() - self:FilterCategories( "helicopter" ) - return self -end - ---- Builds a set of groups out of ship category. --- @param #SET_GROUP self --- @return #SET_GROUP self -function SET_GROUP:FilterCategoryShip() - self:FilterCategories( "ship" ) - return self -end - ---- Builds a set of groups out of structure category. --- @param #SET_GROUP self --- @return #SET_GROUP self -function SET_GROUP:FilterCategoryStructure() - self:FilterCategories( "structure" ) - return self -end - - - ---- Builds a set of groups of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_GROUP self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_GROUP self -function SET_GROUP:FilterCountries( Countries ) - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- Builds a set of groups of defined GROUP prefixes. --- All the groups starting with the given prefixes will be included within the set. --- @param #SET_GROUP self --- @param #string Prefixes The prefix of which the group name starts with. --- @return #SET_GROUP self -function SET_GROUP:FilterPrefixes( Prefixes ) - if not self.Filter.GroupPrefixes then - self.Filter.GroupPrefixes = {} - end - if type( Prefixes ) ~= "table" then - Prefixes = { Prefixes } - end - for PrefixID, Prefix in pairs( Prefixes ) do - self.Filter.GroupPrefixes[Prefix] = Prefix - end - return self -end - - ---- Starts the filtering. --- @param #SET_GROUP self --- @return #SET_GROUP self -function SET_GROUP:FilterStart() - - if _DATABASE then - self:_FilterStart() - end + --- Handles the OnDead or OnCrash event for alive units set. + -- @param #SET_BASE self + -- @param Core.Event#EVENTDATA Event + function SET_BASE:_EventOnDeadOrCrash( Event ) + self:F( { Event } ) - - - return self -end - ---- Handles the OnDead or OnCrash event for alive groups set. --- Note: The GROUP object in the SET_GROUP collection will only be removed if the last unit is destroyed of the GROUP. --- @param #SET_GROUP self --- @param Core.Event#EVENTDATA Event -function SET_GROUP:_EventOnDeadOrCrash( Event ) - self:F3( { Event } ) - - if Event.IniDCSUnit then - local ObjectName, Object = self:FindInDatabase( Event ) - if ObjectName then - if Event.IniDCSGroup:getSize() == 1 then -- Only remove if the last unit of the group was destroyed. + if Event.IniDCSUnit then + local ObjectName, Object = self:FindInDatabase( Event ) + if ObjectName then self:Remove( ObjectName ) end end end -end - ---- Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_GROUP self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the GROUP --- @return #table The GROUP -function SET_GROUP:AddInDatabase( Event ) - self:F3( { Event } ) - - if Event.IniObjectCategory == 1 then - if not self.Database[Event.IniDCSGroupName] then - self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) - self:T3( self.Database[Event.IniDCSGroupName] ) - end - end - return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_GROUP self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the GROUP --- @return #table The GROUP -function SET_GROUP:FindInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] -end - ---- Iterate the SET_GROUP and call an iterator function for each GROUP object, providing the GROUP and optional parameters. --- @param #SET_GROUP self --- @param #function IteratorFunction The function that will be called 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:ForEachGroup( IteratorFunction, ... ) - self:F2( arg ) + --- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied). + -- @param #SET_BASE self + -- @param Core.Event#EVENTDATA Event + --function SET_BASE:_EventOnPlayerEnterUnit( Event ) + -- self:F3( { Event } ) + -- + -- if Event.IniDCSUnit then + -- local ObjectName, Object = self:AddInDatabase( Event ) + -- self:T3( ObjectName, Object ) + -- if self:IsIncludeObject( Object ) then + -- self:Add( ObjectName, Object ) + -- --self:_EventOnPlayerEnterUnit( Event ) + -- end + -- end + --end - self:ForEach( IteratorFunction, arg, self:GetSet() ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP object, providing the GROUP and optional parameters. --- @param #SET_GROUP self --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupAlive( IteratorFunction, ... ) - self:F2( arg ) + --- Handles the OnPlayerLeaveUnit event to clean the active players table. + -- @param #SET_BASE self + -- @param Core.Event#EVENTDATA Event + --function SET_BASE:_EventOnPlayerLeaveUnit( Event ) + -- self:F3( { Event } ) + -- + -- local ObjectName = Event.IniDCSUnit + -- if Event.IniDCSUnit then + -- if Event.IniDCSGroup then + -- local GroupUnits = Event.IniDCSGroup:getUnits() + -- local PlayerCount = 0 + -- for _, DCSUnit in pairs( GroupUnits ) do + -- if DCSUnit ~= Event.IniDCSUnit then + -- if DCSUnit:getPlayerName() ~= nil then + -- PlayerCount = PlayerCount + 1 + -- end + -- end + -- end + -- self:E(PlayerCount) + -- if PlayerCount == 0 then + -- self:Remove( Event.IniDCSGroupName ) + -- end + -- end + -- end + --end - self:ForEach( IteratorFunction, arg, self:GetAliveSet() ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupCompletelyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) + -- Iterators - self:ForEach( IteratorFunction, arg, self:GetSet(), - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsCompletelyInZone( ZoneObject ) then - return true - else - return false + --- 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:ForEach( IteratorFunction, arg, Set, Function, FunctionArguments ) + self:F3( arg ) + + Set = Set or self:GetSet() + arg = arg or {} + + local function CoRoutine() + local Count = 0 + for ObjectID, ObjectData in pairs( Set ) do + local Object = ObjectData + self:T3( Object ) + if Function then + if Function( unpack( FunctionArguments ), 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 - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupPartlyInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self:GetSet(), - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsPartlyInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. --- @return #SET_GROUP self -function SET_GROUP:ForEachGroupNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self:GetSet(), - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Group#GROUP GroupObject - function( ZoneObject, GroupObject ) - if GroupObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_GROUP and return true if all the @{Wrapper.Group#GROUP} are completely in the @{Core.Zone#ZONE} --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @return #boolean true if all the @{Wrapper.Group#GROUP} are completly in the @{Core.Zone#ZONE}, false otherwise --- @usage --- local MyZone = ZONE:New("Zone1") --- local MySetGroup = SET_GROUP:New() --- MySetGroup:AddGroupsByName({"Group1", "Group2"}) --- --- if MySetGroup:AllCompletelyInZone(MyZone) then --- MESSAGE:New("All the SET's GROUP are in zone !", 10):ToAll() --- else --- MESSAGE:New("Some or all SET's GROUP are outside zone !", 10):ToAll() --- end -function SET_GROUP:AllCompletelyInZone(Zone) - self:F2(Zone) - local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if not GroupData:IsCompletelyInZone(Zone) then - return false - end - end - return true -end - ---- Iterate the SET_GROUP and return true if at least one of the @{Wrapper.Group#GROUP} is completely inside the @{Core.Zone#ZONE} --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is completly inside the @{Core.Zone#ZONE}, false otherwise. --- @usage --- local MyZone = ZONE:New("Zone1") --- local MySetGroup = SET_GROUP:New() --- MySetGroup:AddGroupsByName({"Group1", "Group2"}) --- --- if MySetGroup:AnyCompletelyInZone(MyZone) then --- MESSAGE:New("At least one GROUP is completely in zone !", 10):ToAll() --- else --- MESSAGE:New("No GROUP is completely in zone !", 10):ToAll() --- end -function SET_GROUP:AnyCompletelyInZone(Zone) - self:F2(Zone) - local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if GroupData:IsCompletelyInZone(Zone) then return true end - end - return false -end - ---- Iterate the SET_GROUP and return true if at least one @{#UNIT} of one @{GROUP} of the @{SET_GROUP} is in @{ZONE} --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completly inside the @{Core.Zone#ZONE}, false otherwise. --- @usage --- local MyZone = ZONE:New("Zone1") --- local MySetGroup = SET_GROUP:New() --- MySetGroup:AddGroupsByName({"Group1", "Group2"}) --- --- if MySetGroup:AnyPartlyInZone(MyZone) then --- MESSAGE:New("At least one GROUP has at least one UNIT in zone !", 10):ToAll() --- else --- MESSAGE:New("No UNIT of any GROUP is in zone !", 10):ToAll() --- end -function SET_GROUP:AnyInZone(Zone) - self:F2(Zone) - local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if GroupData:IsPartlyInZone(Zone) or GroupData:IsCompletelyInZone(Zone) then - return true - end - end - return false -end - ---- Iterate the SET_GROUP and return true if at least one @{GROUP} of the @{SET_GROUP} is partly in @{ZONE}. --- Will return false if a @{GROUP} is fully in the @{ZONE} --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completly inside the @{Core.Zone#ZONE}, false otherwise. --- @usage --- local MyZone = ZONE:New("Zone1") --- local MySetGroup = SET_GROUP:New() --- MySetGroup:AddGroupsByName({"Group1", "Group2"}) --- --- if MySetGroup:AnyPartlyInZone(MyZone) then --- MESSAGE:New("At least one GROUP is partially in the zone, but none are fully in it !", 10):ToAll() --- else --- MESSAGE:New("No GROUP are in zone, or one (or more) GROUP is completely in it !", 10):ToAll() --- end -function SET_GROUP:AnyPartlyInZone(Zone) - self:F2(Zone) - local IsPartlyInZone = false - local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if GroupData:IsCompletelyInZone(Zone) then + + -- 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 - elseif GroupData:IsPartlyInZone(Zone) then - IsPartlyInZone = true -- at least one GROUP is partly in zone end + + --self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 ) + Schedule() + + return self end - if IsPartlyInZone then + + ----- Iterate the SET_BASE and call an interator function for each **alive** unit, providing the Unit and optional parameters. + ---- @param #SET_BASE self + ---- @param #function IteratorFunction The function that will be called when there is an alive unit in the SET_BASE. The function needs to accept a UNIT parameter. + ---- @return #SET_BASE self + --function SET_BASE:ForEachDCSUnitAlive( IteratorFunction, ... ) + -- self:F3( arg ) + -- + -- self:ForEach( IteratorFunction, arg, self.DCSUnitsAlive ) + -- + -- return self + --end + -- + ----- Iterate the SET_BASE and call an interator function for each **alive** player, providing the Unit of the player and optional parameters. + ---- @param #SET_BASE self + ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a UNIT parameter. + ---- @return #SET_BASE self + --function SET_BASE:ForEachPlayer( IteratorFunction, ... ) + -- self:F3( arg ) + -- + -- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) + -- + -- return self + --end + -- + -- + ----- Iterate the SET_BASE and call an interator function for each client, providing the Client to the function and optional parameters. + ---- @param #SET_BASE self + ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a CLIENT parameter. + ---- @return #SET_BASE self + --function SET_BASE:ForEachClient( IteratorFunction, ... ) + -- self:F3( arg ) + -- + -- self:ForEach( IteratorFunction, arg, self.Clients ) + -- + -- return self + --end + + + --- Decides whether to include the Object + -- @param #SET_BASE self + -- @param #table Object + -- @return #SET_BASE self + function SET_BASE:IsIncludeObject( Object ) + self:F3( Object ) + return true - else + end + + --- Gets a string with all the object names. + -- @param #SET_BASE self + -- @return #string A string with the names of the objects. + function SET_BASE:GetObjectNames() + self:F3() + + local ObjectNames = "" + for ObjectName, Object in pairs( self.Set ) do + ObjectNames = ObjectNames .. ObjectName .. ", " + end + + return ObjectNames + end + + --- Flushes the current SET_BASE contents in the log ... (for debugging reasons). + -- @param #SET_BASE self + -- @param Core.Base#BASE MasterObject (optional) The master object as a reference. + -- @return #string A string with the names of the objects. + function SET_BASE:Flush( MasterObject ) + self:F3() + + local ObjectNames = "" + for ObjectName, Object in pairs( self.Set ) do + ObjectNames = ObjectNames .. ObjectName .. ", " + end + self:F( { MasterObject = MasterObject and MasterObject:GetClassNameAndID(), "Objects in Set:", ObjectNames } ) + + return ObjectNames + end + +end + + +do -- SET_GROUP + + --- @type SET_GROUP + -- @extends Core.Set#SET_BASE + + --- Mission designers can use the @{Core.Set#SET_GROUP} class to build sets of groups belonging to certain: + -- + -- * Coalitions + -- * Categories + -- * Countries + -- * Starting with certain prefix strings. + -- + -- ## SET_GROUP constructor + -- + -- Create a new SET_GROUP object with the @{#SET_GROUP.New} method: + -- + -- * @{#SET_GROUP.New}: Creates a new SET_GROUP object. + -- + -- ## Add or Remove GROUP(s) from SET_GROUP + -- + -- GROUPS can be added and removed using the @{Core.Set#SET_GROUP.AddGroupsByName} and @{Core.Set#SET_GROUP.RemoveGroupsByName} respectively. + -- These methods take a single GROUP name or an array of GROUP names to be added or removed from SET_GROUP. + -- + -- ## SET_GROUP filter criteria + -- + -- You can set filter criteria to define the set of groups within the SET_GROUP. + -- Filter criteria are defined by: + -- + -- * @{#SET_GROUP.FilterCoalitions}: Builds the SET_GROUP with the groups belonging to the coalition(s). + -- * @{#SET_GROUP.FilterCategories}: Builds the SET_GROUP with the groups belonging to the category(ies). + -- * @{#SET_GROUP.FilterCountries}: Builds the SET_GROUP with the gruops belonging to the country(ies). + -- * @{#SET_GROUP.FilterPrefixes}: Builds the SET_GROUP with the groups starting with the same prefix string(s). + -- * @{#SET_GROUP.FilterActive}: Builds the SET_GROUP with the groups that are only active. Groups that are inactive (late activation) won't be included in the set! + -- + -- For the Category Filter, extra methods have been added: + -- + -- * @{#SET_GROUP.FilterCategoryAirplane}: Builds the SET_GROUP from airplanes. + -- * @{#SET_GROUP.FilterCategoryHelicopter}: Builds the SET_GROUP from helicopters. + -- * @{#SET_GROUP.FilterCategoryGround}: Builds the SET_GROUP from ground vehicles or infantry. + -- * @{#SET_GROUP.FilterCategoryShip}: Builds the SET_GROUP from ships. + -- * @{#SET_GROUP.FilterCategoryStructure}: Builds the SET_GROUP from structures. + -- + -- + -- Once the filter criteria have been set for the SET_GROUP, you can start filtering using: + -- + -- * @{#SET_GROUP.FilterStart}: Starts the filtering of the groups within the SET_GROUP and add or remove GROUP objects **dynamically**. + -- * @{#SET_GROUP.FilterOnce}: Filters of the groups **once**. + -- + -- Planned filter criteria within development are (so these are not yet available): + -- + -- * @{#SET_GROUP.FilterZones}: Builds the SET_GROUP with the groups within a @{Core.Zone#ZONE}. + -- + -- ## SET_GROUP iterators + -- + -- Once the filters have been defined and the SET_GROUP has been built, you can iterate the SET_GROUP with the available iterator methods. + -- The iterator methods will walk the SET_GROUP set, and call for each element within the set a function that you provide. + -- The following iterator methods are currently available within the SET_GROUP: + -- + -- * @{#SET_GROUP.ForEachGroup}: Calls a function for each alive group it finds within the SET_GROUP. + -- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- * @{#SET_GROUP.ForEachGroupPartlyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- + -- + -- ## SET_GROUP trigger events on the GROUP objects. + -- + -- The SET is derived from the FSM class, which provides extra capabilities to track the contents of the GROUP objects in the SET_GROUP. + -- + -- ### When a GROUP object crashes or is dead, the SET_GROUP will trigger a **Dead** event. + -- + -- You can handle the event using the OnBefore and OnAfter event handlers. + -- The event handlers need to have the paramters From, Event, To, GroupObject. + -- The GroupObject is the GROUP object that is dead and within the SET_GROUP, and is passed as a parameter to the event handler. + -- See the following example: + -- + -- -- Create the SetCarrier SET_GROUP collection. + -- + -- local SetHelicopter = SET_GROUP:New():FilterPrefixes( "Helicopter" ):FilterStart() + -- + -- -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset. + -- + -- function SetHelicopter:OnAfterDead( From, Event, To, GroupObject ) + -- self:F( { GroupObject = GroupObject:GetName() } ) + -- end + -- + -- While this is a good example, there is a catch. + -- Imageine you want to execute the code above, the the self would need to be from the object declared outside (above) the OnAfterDead method. + -- So, the self would need to contain another object. Fortunately, this can be done, but you must use then the **`.`** notation for the method. + -- See the modified example: + -- + -- -- Now we have a constructor of the class AI_CARGO_DISPATCHER, that receives the SetHelicopter as a parameter. + -- -- Within that constructor, we want to set an enclosed event handler OnAfterDead for SetHelicopter. + -- -- But within the OnAfterDead method, we want to refer to the self variable of the AI_CARGO_DISPATCHER. + -- + -- function AI_CARGO_DISPATCHER:New( SetCarrier, SetCargo, SetDeployZones ) + -- + -- local self = BASE:Inherit( self, FSM:New() ) -- #AI_CARGO_DISPATCHER + -- + -- -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset. + -- -- Note the "." notation, and the explicit declaration of SetHelicopter, which would be using the ":" notation the implicit self variable declaration. + -- + -- function SetHelicopter.OnAfterDead( SetHelicopter, From, Event, To, GroupObject ) + -- SetHelicopter:F( { GroupObject = GroupObject:GetName() } ) + -- self.PickupCargo[GroupObject] = nil -- So here I clear the PickupCargo table entry of the self object AI_CARGO_DISPATCHER. + -- self.CarrierHome[GroupObject] = nil + -- end + -- + -- end + -- + -- === + -- @field #SET_GROUP SET_GROUP + SET_GROUP = { + ClassName = "SET_GROUP", + Filter = { + Coalitions = nil, + Categories = nil, + Countries = nil, + GroupPrefixes = nil, + }, + FilterMeta = { + Coalitions = { + red = coalition.side.RED, + blue = coalition.side.BLUE, + neutral = coalition.side.NEUTRAL, + }, + Categories = { + plane = Group.Category.AIRPLANE, + helicopter = Group.Category.HELICOPTER, + ground = Group.Category.GROUND, -- R2.2 + ship = Group.Category.SHIP, + structure = Group.Category.STRUCTURE, + }, + }, + } + + + --- Creates a new SET_GROUP object, building a set of groups belonging to a coalitions, categories, countries, types or with defined prefix names. + -- @param #SET_GROUP self + -- @return #SET_GROUP + -- @usage + -- -- Define a new SET_GROUP Object. This DBObject will contain a reference to all alive GROUPS. + -- DBObject = SET_GROUP:New() + function SET_GROUP:New() + + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.GROUPS ) ) -- #SET_GROUP + + self:FilterActive( false ) + + return self + end + + --- Gets the Set. + -- @param #SET_GROUP self + -- @return #SET_GROUP self + function SET_GROUP:GetAliveSet() + self:F2() + + local AliveSet = SET_GROUP:New() + + -- Clean the Set before returning with only the alive Groups. + for GroupName, GroupObject in pairs( self.Set ) do + local GroupObject=GroupObject --Wrapper.Group#GROUP + if GroupObject then + if GroupObject:IsAlive() then + AliveSet:Add( GroupName, GroupObject ) + end + end + end + + return AliveSet.Set or {} + end + + --- Add a GROUP to SET_GROUP. + -- Note that for each unit in the group that is set, a default cargo bay limit is initialized. + -- @param Core.Set#SET_GROUP self + -- @param Wrapper.Group#GROUP group The group which should be added to the set. + -- @return self + function SET_GROUP:AddGroup( group ) + + self:Add( group:GetName(), group ) + + -- I set the default cargo bay weight limit each time a new group is added to the set. + for UnitID, UnitData in pairs( group:GetUnits() ) do + UnitData:SetCargoBayWeightLimit() + end + + return self + end + + --- Add GROUP(s) to SET_GROUP. + -- @param Core.Set#SET_GROUP self + -- @param #string AddGroupNames A single name or an array of GROUP names. + -- @return self + function SET_GROUP:AddGroupsByName( AddGroupNames ) + + local AddGroupNamesArray = ( type( AddGroupNames ) == "table" ) and AddGroupNames or { AddGroupNames } + + for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do + self:Add( AddGroupName, GROUP:FindByName( AddGroupName ) ) + end + + return self + end + + --- Remove GROUP(s) from SET_GROUP. + -- @param Core.Set#SET_GROUP self + -- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names. + -- @return self + function SET_GROUP:RemoveGroupsByName( RemoveGroupNames ) + + local RemoveGroupNamesArray = ( type( RemoveGroupNames ) == "table" ) and RemoveGroupNames or { RemoveGroupNames } + + for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do + self:Remove( RemoveGroupName ) + end + + return self + end + + + + + --- Finds a Group based on the Group Name. + -- @param #SET_GROUP self + -- @param #string GroupName + -- @return Wrapper.Group#GROUP The found Group. + function SET_GROUP:FindGroup( GroupName ) + + local GroupFound = self.Set[GroupName] + return GroupFound + end + + --- Iterate the SET_GROUP while identifying the nearest object from a @{Core.Point#POINT_VEC2}. + -- @param #SET_GROUP self + -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest object in the set. + -- @return Wrapper.Group#GROUP The closest group. + function SET_GROUP:FindNearestGroupFromPointVec2( PointVec2 ) + self:F2( PointVec2 ) + + local NearestGroup = nil --Wrapper.Group#GROUP + local ClosestDistance = nil + + for ObjectID, ObjectData in pairs( self.Set ) do + if NearestGroup == nil then + NearestGroup = ObjectData + ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) + else + local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) + if Distance < ClosestDistance then + NearestGroup = ObjectData + ClosestDistance = Distance + end + end + end + + return NearestGroup + end + + + --- Builds a set of groups of coalitions. + -- Possible current coalitions are red, blue and neutral. + -- @param #SET_GROUP self + -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". + -- @return #SET_GROUP self + function SET_GROUP:FilterCoalitions( Coalitions ) + if not self.Filter.Coalitions then + self.Filter.Coalitions = {} + end + if type( Coalitions ) ~= "table" then + Coalitions = { Coalitions } + end + for CoalitionID, Coalition in pairs( Coalitions ) do + self.Filter.Coalitions[Coalition] = Coalition + end + return self + end + + + --- Builds a set of groups out of categories. + -- Possible current categories are plane, helicopter, ground, ship. + -- @param #SET_GROUP self + -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". + -- @return #SET_GROUP self + function SET_GROUP:FilterCategories( Categories ) + if not self.Filter.Categories then + self.Filter.Categories = {} + end + if type( Categories ) ~= "table" then + Categories = { Categories } + end + for CategoryID, Category in pairs( Categories ) do + self.Filter.Categories[Category] = Category + end + return self + end + + --- Builds a set of groups out of ground category. + -- @param #SET_GROUP self + -- @return #SET_GROUP self + function SET_GROUP:FilterCategoryGround() + self:FilterCategories( "ground" ) + return self + end + + --- Builds a set of groups out of airplane category. + -- @param #SET_GROUP self + -- @return #SET_GROUP self + function SET_GROUP:FilterCategoryAirplane() + self:FilterCategories( "plane" ) + return self + end + + --- Builds a set of groups out of helicopter category. + -- @param #SET_GROUP self + -- @return #SET_GROUP self + function SET_GROUP:FilterCategoryHelicopter() + self:FilterCategories( "helicopter" ) + return self + end + + --- Builds a set of groups out of ship category. + -- @param #SET_GROUP self + -- @return #SET_GROUP self + function SET_GROUP:FilterCategoryShip() + self:FilterCategories( "ship" ) + return self + end + + --- Builds a set of groups out of structure category. + -- @param #SET_GROUP self + -- @return #SET_GROUP self + function SET_GROUP:FilterCategoryStructure() + self:FilterCategories( "structure" ) + return self + end + + + + --- Builds a set of groups of defined countries. + -- Possible current countries are those known within DCS world. + -- @param #SET_GROUP self + -- @param #string Countries Can take those country strings known within DCS world. + -- @return #SET_GROUP self + function SET_GROUP:FilterCountries( Countries ) + if not self.Filter.Countries then + self.Filter.Countries = {} + end + if type( Countries ) ~= "table" then + Countries = { Countries } + end + for CountryID, Country in pairs( Countries ) do + self.Filter.Countries[Country] = Country + end + return self + end + + + --- Builds a set of groups of defined GROUP prefixes. + -- All the groups starting with the given prefixes will be included within the set. + -- @param #SET_GROUP self + -- @param #string Prefixes The prefix of which the group name starts with. + -- @return #SET_GROUP self + function SET_GROUP:FilterPrefixes( Prefixes ) + if not self.Filter.GroupPrefixes then + self.Filter.GroupPrefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.GroupPrefixes[Prefix] = Prefix + end + return self + end + + --- Builds a set of groups that are only active. + -- Only the groups that are active will be included within the set. + -- @param #SET_GROUP self + -- @param #boolean Active (optional) Include only active groups to the set. + -- Include inactive groups if you provide false. + -- @return #SET_GROUP self + -- @usage + -- + -- -- Include only active groups to the set. + -- GroupSet = SET_GROUP:New():FilterActive():FilterStart() + -- + -- -- Include only active groups to the set of the blue coalition, and filter one time. + -- GroupSet = SET_GROUP:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() + -- + -- -- Include only active groups to the set of the blue coalition, and filter one time. + -- -- Later, reset to include back inactive groups to the set. + -- GroupSet = SET_GROUP:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() + -- ... logic ... + -- GroupSet = SET_GROUP:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce() + -- + function SET_GROUP:FilterActive( Active ) + Active = Active or not ( Active == false ) + self.Filter.Active = Active + return self + end + + + --- Starts the filtering. + -- @param #SET_GROUP self + -- @return #SET_GROUP self + function SET_GROUP:FilterStart() + + if _DATABASE then + self:_FilterStart() + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash ) + end + + + + return self + end + + --- Handles the OnDead or OnCrash event for alive groups set. + -- Note: The GROUP object in the SET_GROUP collection will only be removed if the last unit is destroyed of the GROUP. + -- @param #SET_GROUP self + -- @param Core.Event#EVENTDATA Event + function SET_GROUP:_EventOnDeadOrCrash( Event ) + self:F( { Event } ) + + if Event.IniDCSUnit then + local ObjectName, Object = self:FindInDatabase( Event ) + if ObjectName then + if Event.IniDCSGroup:getSize() == 1 then -- Only remove if the last unit of the group was destroyed. + self:Remove( ObjectName ) + end + end + end + end + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_GROUP self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the GROUP + -- @return #table The GROUP + function SET_GROUP:AddInDatabase( Event ) + self:F3( { Event } ) + + if Event.IniObjectCategory == 1 then + if not self.Database[Event.IniDCSGroupName] then + self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName ) + self:T3( self.Database[Event.IniDCSGroupName] ) + end + end + + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] + end + + --- Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_GROUP self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the GROUP + -- @return #table The GROUP + function SET_GROUP:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName] + end + + --- Iterate the SET_GROUP and call an iterator function for each GROUP object, providing the GROUP and optional parameters. + -- @param #SET_GROUP self + -- @param #function IteratorFunction The function that will be called 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:ForEachGroup( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP object, providing the GROUP and optional parameters. + -- @param #SET_GROUP self + -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForEachGroupAlive( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetAliveSet() ) + + return self + end + + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForEachGroupCompletelyInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Group#GROUP GroupObject + function( ZoneObject, GroupObject ) + if GroupObject:IsCompletelyInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForEachGroupPartlyInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Group#GROUP GroupObject + function( ZoneObject, GroupObject ) + if GroupObject:IsPartlyInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForEachGroupNotInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Group#GROUP GroupObject + function( ZoneObject, GroupObject ) + if GroupObject:IsNotInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- Iterate the SET_GROUP and return true if all the @{Wrapper.Group#GROUP} are completely in the @{Core.Zone#ZONE} + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #boolean true if all the @{Wrapper.Group#GROUP} are completly in the @{Core.Zone#ZONE}, false otherwise + -- @usage + -- local MyZone = ZONE:New("Zone1") + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) + -- + -- if MySetGroup:AllCompletelyInZone(MyZone) then + -- MESSAGE:New("All the SET's GROUP are in zone !", 10):ToAll() + -- else + -- MESSAGE:New("Some or all SET's GROUP are outside zone !", 10):ToAll() + -- end + function SET_GROUP:AllCompletelyInZone(Zone) + self:F2(Zone) + local Set = self:GetSet() + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + if not GroupData:IsCompletelyInZone(Zone) then + return false + end + end + return true + end + + --- Iterate the SET_GROUP and call an iterator function for each alive GROUP that has any unit in the @{Core.Zone}, providing the GROUP and optional parameters to the called function. + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter. + -- @return #SET_GROUP self + function SET_GROUP:ForEachGroupAnyInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Group#GROUP GroupObject + function( ZoneObject, GroupObject ) + if GroupObject:IsAnyInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + + --- Iterate the SET_GROUP and return true if at least one of the @{Wrapper.Group#GROUP} is completely inside the @{Core.Zone#ZONE} + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is completly inside the @{Core.Zone#ZONE}, false otherwise. + -- @usage + -- local MyZone = ZONE:New("Zone1") + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) + -- + -- if MySetGroup:AnyCompletelyInZone(MyZone) then + -- MESSAGE:New("At least one GROUP is completely in zone !", 10):ToAll() + -- else + -- MESSAGE:New("No GROUP is completely in zone !", 10):ToAll() + -- end + function SET_GROUP:AnyCompletelyInZone(Zone) + self:F2(Zone) + local Set = self:GetSet() + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + if GroupData:IsCompletelyInZone(Zone) then + return true + end + end return false end -end - ---- Iterate the SET_GROUP and return true if no @{GROUP} of the @{SET_GROUP} is in @{ZONE} --- This could also be achieved with `not SET_GROUP:AnyPartlyInZone(Zone)`, but it's easier for the --- mission designer to add a dedicated method --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @return #boolean true if no @{Wrapper.Group#GROUP} is inside the @{Core.Zone#ZONE} in any way, false otherwise. --- @usage --- local MyZone = ZONE:New("Zone1") --- local MySetGroup = SET_GROUP:New() --- MySetGroup:AddGroupsByName({"Group1", "Group2"}) --- --- if MySetGroup:NoneInZone(MyZone) then --- MESSAGE:New("No GROUP is completely in zone !", 10):ToAll() --- else --- MESSAGE:New("No UNIT of any GROUP is in zone !", 10):ToAll() --- end -function SET_GROUP:NoneInZone(Zone) - self:F2(Zone) - local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if not GroupData:IsNotInZone(Zone) then -- If the GROUP is in Zone in any way + + --- Iterate the SET_GROUP and return true if at least one @{#UNIT} of one @{GROUP} of the @{SET_GROUP} is in @{ZONE} + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completly inside the @{Core.Zone#ZONE}, false otherwise. + -- @usage + -- local MyZone = ZONE:New("Zone1") + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) + -- + -- if MySetGroup:AnyPartlyInZone(MyZone) then + -- MESSAGE:New("At least one GROUP has at least one UNIT in zone !", 10):ToAll() + -- else + -- MESSAGE:New("No UNIT of any GROUP is in zone !", 10):ToAll() + -- end + function SET_GROUP:AnyInZone(Zone) + self:F2(Zone) + local Set = self:GetSet() + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + if GroupData:IsPartlyInZone(Zone) or GroupData:IsCompletelyInZone(Zone) then + return true + end + end + return false + end + + --- Iterate the SET_GROUP and return true if at least one @{GROUP} of the @{SET_GROUP} is partly in @{ZONE}. + -- Will return false if a @{GROUP} is fully in the @{ZONE} + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completly inside the @{Core.Zone#ZONE}, false otherwise. + -- @usage + -- local MyZone = ZONE:New("Zone1") + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) + -- + -- if MySetGroup:AnyPartlyInZone(MyZone) then + -- MESSAGE:New("At least one GROUP is partially in the zone, but none are fully in it !", 10):ToAll() + -- else + -- MESSAGE:New("No GROUP are in zone, or one (or more) GROUP is completely in it !", 10):ToAll() + -- end + function SET_GROUP:AnyPartlyInZone(Zone) + self:F2(Zone) + local IsPartlyInZone = false + local Set = self:GetSet() + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + if GroupData:IsCompletelyInZone(Zone) then + return false + elseif GroupData:IsPartlyInZone(Zone) then + IsPartlyInZone = true -- at least one GROUP is partly in zone + end + end + + if IsPartlyInZone then + return true + else return false end end - return true -end - ---- Iterate the SET_GROUP and count how many GROUPs are completely in the Zone --- That could easily be done with SET_GROUP:ForEachGroupCompletelyInZone(), but this function --- provides an easy to use shortcut... --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @return #number the number of GROUPs completely in the Zone --- @usage --- local MyZone = ZONE:New("Zone1") --- local MySetGroup = SET_GROUP:New() --- MySetGroup:AddGroupsByName({"Group1", "Group2"}) --- --- MESSAGE:New("There are " .. MySetGroup:CountInZone(MyZone) .. " GROUPs in the Zone !", 10):ToAll() -function SET_GROUP:CountInZone(Zone) - self:F2(Zone) - local Count = 0 - local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - if GroupData:IsCompletelyInZone(Zone) then - Count = Count + 1 - end - end - return Count -end - ---- Iterate the SET_GROUP and count how many UNITs are completely in the Zone --- @param #SET_GROUP self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @return #number the number of GROUPs completely in the Zone --- @usage --- local MyZone = ZONE:New("Zone1") --- local MySetGroup = SET_GROUP:New() --- MySetGroup:AddGroupsByName({"Group1", "Group2"}) --- --- MESSAGE:New("There are " .. MySetGroup:CountUnitInZone(MyZone) .. " UNITs in the Zone !", 10):ToAll() -function SET_GROUP:CountUnitInZone(Zone) - self:F2(Zone) - local Count = 0 - local Set = self:GetSet() - for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP - Count = Count + GroupData:CountInZone(Zone) - end - return Count -end - ------ Iterate the SET_GROUP and call an interator function for each **alive** player, providing the Group of the player and optional parameters. ----- @param #SET_GROUP self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a GROUP parameter. ----- @return #SET_GROUP self ---function SET_GROUP:ForEachPlayer( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) --- --- return self ---end --- --- ------ Iterate the SET_GROUP and call an interator function for each client, providing the Client to the function and optional parameters. ----- @param #SET_GROUP self ----- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a CLIENT parameter. ----- @return #SET_GROUP self ---function SET_GROUP:ForEachClient( IteratorFunction, ... ) --- self:F2( arg ) --- --- self:ForEach( IteratorFunction, arg, self.Clients ) --- --- return self ---end - - ---- --- @param #SET_GROUP self --- @param Wrapper.Group#GROUP MooseGroup --- @return #SET_GROUP self -function SET_GROUP:IsIncludeObject( MooseGroup ) - self:F2( MooseGroup ) - local MooseGroupInclude = true - - if self.Filter.Coalitions then - local MooseGroupCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:T3( { "Coalition:", MooseGroup:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MooseGroup:GetCoalition() then - MooseGroupCoalition = true + + --- Iterate the SET_GROUP and return true if no @{GROUP} of the @{SET_GROUP} is in @{ZONE} + -- This could also be achieved with `not SET_GROUP:AnyPartlyInZone(Zone)`, but it's easier for the + -- mission designer to add a dedicated method + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #boolean true if no @{Wrapper.Group#GROUP} is inside the @{Core.Zone#ZONE} in any way, false otherwise. + -- @usage + -- local MyZone = ZONE:New("Zone1") + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) + -- + -- if MySetGroup:NoneInZone(MyZone) then + -- MESSAGE:New("No GROUP is completely in zone !", 10):ToAll() + -- else + -- MESSAGE:New("No UNIT of any GROUP is in zone !", 10):ToAll() + -- end + function SET_GROUP:NoneInZone(Zone) + self:F2(Zone) + local Set = self:GetSet() + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + if not GroupData:IsNotInZone(Zone) then -- If the GROUP is in Zone in any way + return false end end - MooseGroupInclude = MooseGroupInclude and MooseGroupCoalition + return true end - if self.Filter.Categories then - local MooseGroupCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T3( { "Category:", MooseGroup:GetCategory(), self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MooseGroup:GetCategory() then - MooseGroupCategory = true + --- Iterate the SET_GROUP and count how many GROUPs are completely in the Zone + -- That could easily be done with SET_GROUP:ForEachGroupCompletelyInZone(), but this function + -- provides an easy to use shortcut... + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #number the number of GROUPs completely in the Zone + -- @usage + -- local MyZone = ZONE:New("Zone1") + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) + -- + -- MESSAGE:New("There are " .. MySetGroup:CountInZone(MyZone) .. " GROUPs in the Zone !", 10):ToAll() + function SET_GROUP:CountInZone(Zone) + self:F2(Zone) + local Count = 0 + local Set = self:GetSet() + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + if GroupData:IsCompletelyInZone(Zone) then + Count = Count + 1 end end - MooseGroupInclude = MooseGroupInclude and MooseGroupCategory + return Count end - if self.Filter.Countries then - local MooseGroupCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T3( { "Country:", MooseGroup:GetCountry(), CountryName } ) - if country.id[CountryName] == MooseGroup:GetCountry() then - MooseGroupCountry = true + --- Iterate the SET_GROUP and count how many UNITs are completely in the Zone + -- @param #SET_GROUP self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @return #number the number of GROUPs completely in the Zone + -- @usage + -- local MyZone = ZONE:New("Zone1") + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:AddGroupsByName({"Group1", "Group2"}) + -- + -- MESSAGE:New("There are " .. MySetGroup:CountUnitInZone(MyZone) .. " UNITs in the Zone !", 10):ToAll() + function SET_GROUP:CountUnitInZone(Zone) + self:F2(Zone) + local Count = 0 + local Set = self:GetSet() + for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP + Count = Count + GroupData:CountInZone(Zone) + end + return Count + end + + ----- Iterate the SET_GROUP and call an interator function for each **alive** player, providing the Group of the player and optional parameters. + ---- @param #SET_GROUP self + ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a GROUP parameter. + ---- @return #SET_GROUP self + --function SET_GROUP:ForEachPlayer( IteratorFunction, ... ) + -- self:F2( arg ) + -- + -- self:ForEach( IteratorFunction, arg, self.PlayersAlive ) + -- + -- return self + --end + -- + -- + ----- Iterate the SET_GROUP and call an interator function for each client, providing the Client to the function and optional parameters. + ---- @param #SET_GROUP self + ---- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a CLIENT parameter. + ---- @return #SET_GROUP self + --function SET_GROUP:ForEachClient( IteratorFunction, ... ) + -- self:F2( arg ) + -- + -- self:ForEach( IteratorFunction, arg, self.Clients ) + -- + -- return self + --end + + + --- + -- @param #SET_GROUP self + -- @param Wrapper.Group#GROUP MGroup The group that is checked for inclusion. + -- @return #SET_GROUP self + function SET_GROUP:IsIncludeObject( MGroup ) + self:F2( MGroup ) + local MGroupInclude = true + + if self.Filter.Active ~= nil then + local MGroupActive = false + self:F( { Active = self.Filter.Active } ) + if self.Filter.Active == false or ( self.Filter.Active == true and MGroup:IsActive() == true ) then + MGroupActive = true + end + MGroupInclude = MGroupInclude and MGroupActive + end + + if self.Filter.Coalitions then + local MGroupCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + self:T3( { "Coalition:", MGroup:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MGroup:GetCoalition() then + MGroupCoalition = true + end + end + MGroupInclude = MGroupInclude and MGroupCoalition + end + + if self.Filter.Categories then + local MGroupCategory = false + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + self:T3( { "Category:", MGroup:GetCategory(), self.FilterMeta.Categories[CategoryName], CategoryName } ) + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MGroup:GetCategory() then + MGroupCategory = true + end + end + MGroupInclude = MGroupInclude and MGroupCategory + end + + if self.Filter.Countries then + local MGroupCountry = false + for CountryID, CountryName in pairs( self.Filter.Countries ) do + self:T3( { "Country:", MGroup:GetCountry(), CountryName } ) + if country.id[CountryName] == MGroup:GetCountry() then + MGroupCountry = true + end + end + MGroupInclude = MGroupInclude and MGroupCountry + end + + if self.Filter.GroupPrefixes then + local MGroupPrefix = false + for GroupPrefixId, GroupPrefix in pairs( self.Filter.GroupPrefixes ) do + self:T3( { "Prefix:", string.find( MGroup:GetName(), GroupPrefix, 1 ), GroupPrefix } ) + if string.find( MGroup:GetName(), GroupPrefix:gsub ("-", "%%-"), 1 ) then + MGroupPrefix = true + end + end + MGroupInclude = MGroupInclude and MGroupPrefix + end + + self:T2( MGroupInclude ) + return MGroupInclude + end + + + --- Iterate the SET_GROUP and set for each unit the default cargo bay weight limit. + -- Because within a group, the type of carriers can differ, each cargo bay weight limit is set on @{Wrapper.Unit} level. + -- @param #SET_GROUP self + -- @usage + -- -- Set the default cargo bay weight limits of the carrier units. + -- local MySetGroup = SET_GROUP:New() + -- MySetGroup:SetCargoBayWeightLimit() + function SET_GROUP:SetCargoBayWeightLimit() + local Set = self:GetSet() + for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP + for UnitName, UnitData in pairs( GroupData:GetUnits() ) do + --local UnitData = UnitData -- Wrapper.Unit#UNIT + UnitData:SetCargoBayWeightLimit() end end - MooseGroupInclude = MooseGroupInclude and MooseGroupCountry end - if self.Filter.GroupPrefixes then - local MooseGroupPrefix = false - for GroupPrefixId, GroupPrefix in pairs( self.Filter.GroupPrefixes ) do - self:T3( { "Prefix:", string.find( MooseGroup:GetName(), GroupPrefix, 1 ), GroupPrefix } ) - if string.find( MooseGroup:GetName(), GroupPrefix:gsub ("-", "%%-"), 1 ) then - MooseGroupPrefix = true - end - end - MooseGroupInclude = MooseGroupInclude and MooseGroupPrefix - end - - self:T2( MooseGroupInclude ) - return MooseGroupInclude end @@ -1349,9 +1538,7 @@ do -- SET_UNIT --- @type SET_UNIT -- @extends Core.Set#SET_BASE - --- # 3) SET_UNIT class, extends @{Set#SET_BASE} - -- - -- Mission designers can use the SET_UNIT class to build sets of units belonging to certain: + --- Mission designers can use the SET_UNIT class to build sets of units belonging to certain: -- -- * Coalitions -- * Categories @@ -1359,18 +1546,18 @@ do -- SET_UNIT -- * Unit types -- * Starting with certain prefix strings. -- - -- ## 3.1) SET_UNIT constructor + -- ## 1) SET_UNIT constructor -- -- Create a new SET_UNIT object with the @{#SET_UNIT.New} method: -- -- * @{#SET_UNIT.New}: Creates a new SET_UNIT object. -- - -- ## 3.2) Add or Remove UNIT(s) from SET_UNIT + -- ## 2) Add or Remove UNIT(s) from SET_UNIT -- - -- UNITs can be added and removed using the @{Set#SET_UNIT.AddUnitsByName} and @{Set#SET_UNIT.RemoveUnitsByName} respectively. + -- UNITs can be added and removed using the @{Core.Set#SET_UNIT.AddUnitsByName} and @{Core.Set#SET_UNIT.RemoveUnitsByName} respectively. -- These methods take a single UNIT name or an array of UNIT names to be added or removed from SET_UNIT. -- - -- ## 3.3) SET_UNIT filter criteria + -- ## 3) SET_UNIT filter criteria -- -- You can set filter criteria to define the set of units within the SET_UNIT. -- Filter criteria are defined by: @@ -1380,24 +1567,26 @@ do -- SET_UNIT -- * @{#SET_UNIT.FilterTypes}: Builds the SET_UNIT with the units belonging to the unit type(s). -- * @{#SET_UNIT.FilterCountries}: Builds the SET_UNIT with the units belonging to the country(ies). -- * @{#SET_UNIT.FilterPrefixes}: Builds the SET_UNIT with the units starting with the same prefix string(s). + -- * @{#SET_UNIT.FilterActive}: Builds the SET_UNIT with the units that are only active. Units that are inactive (late activation) won't be included in the set! -- -- Once the filter criteria have been set for the SET_UNIT, you can start filtering using: -- - -- * @{#SET_UNIT.FilterStart}: Starts the filtering of the units within the SET_UNIT. + -- * @{#SET_UNIT.FilterStart}: Starts the filtering of the units **dynamically**. + -- * @{#SET_UNIT.FilterOnce}: Filters of the units **once**. -- -- Planned filter criteria within development are (so these are not yet available): -- - -- * @{#SET_UNIT.FilterZones}: Builds the SET_UNIT with the units within a @{Zone#ZONE}. + -- * @{#SET_UNIT.FilterZones}: Builds the SET_UNIT with the units within a @{Core.Zone#ZONE}. -- - -- ## 3.4) SET_UNIT iterators + -- ## 4) SET_UNIT iterators -- -- Once the filters have been defined and the SET_UNIT has been built, you can iterate the SET_UNIT with the available iterator methods. -- The iterator methods will walk the SET_UNIT set, and call for each element within the set a function that you provide. -- The following iterator methods are currently available within the SET_UNIT: -- -- * @{#SET_UNIT.ForEachUnit}: Calls a function for each alive unit it finds within the SET_UNIT. - -- * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Zone}, providing the GROUP and optional parameters to the called function. - -- * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Zone}, providing the GROUP and optional parameters to the called function. + -- * @{#SET_UNIT.ForEachUnitInZone}: Iterate the SET_UNIT and call an iterator function for each **alive** UNIT object presence completely in a @{Zone}, providing the UNIT object and optional parameters to the called function. + -- * @{#SET_UNIT.ForEachUnitNotInZone}: Iterate the SET_UNIT and call an iterator function for each **alive** UNIT object presence not in a @{Zone}, providing the UNIT object and optional parameters to the called function. -- -- Planned iterators methods in development are (so these are not yet available): -- @@ -1405,12 +1594,55 @@ do -- SET_UNIT -- * @{#SET_UNIT.ForEachUnitCompletelyInZone}: Iterate and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function. -- * @{#SET_UNIT.ForEachUnitNotInZone}: Iterate and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function. -- - -- ## 3.5 ) SET_UNIT atomic methods + -- ## 5) SET_UNIT atomic methods -- -- Various methods exist for a SET_UNIT to perform actions or calculations and retrieve results from the SET_UNIT: -- - -- * @{#SET_UNIT.GetTypeNames}(): Retrieve the type names of the @{Unit}s in the SET, delimited by a comma. + -- * @{#SET_UNIT.GetTypeNames}(): Retrieve the type names of the @{Wrapper.Unit}s in the SET, delimited by a comma. -- + -- ## 6) SET_UNIT trigger events on the UNIT objects. + -- + -- The SET is derived from the FSM class, which provides extra capabilities to track the contents of the UNIT objects in the SET_UNIT. + -- + -- ### 6.1) When a UNIT object crashes or is dead, the SET_UNIT will trigger a **Dead** event. + -- + -- You can handle the event using the OnBefore and OnAfter event handlers. + -- The event handlers need to have the paramters From, Event, To, GroupObject. + -- The GroupObject is the UNIT object that is dead and within the SET_UNIT, and is passed as a parameter to the event handler. + -- See the following example: + -- + -- -- Create the SetCarrier SET_UNIT collection. + -- + -- local SetHelicopter = SET_UNIT:New():FilterPrefixes( "Helicopter" ):FilterStart() + -- + -- -- Put a Dead event handler on SetCarrier, to ensure that when a carrier unit is destroyed, that all internal parameters are reset. + -- + -- function SetHelicopter:OnAfterDead( From, Event, To, UnitObject ) + -- self:F( { UnitObject = UnitObject:GetName() } ) + -- end + -- + -- While this is a good example, there is a catch. + -- Imageine you want to execute the code above, the the self would need to be from the object declared outside (above) the OnAfterDead method. + -- So, the self would need to contain another object. Fortunately, this can be done, but you must use then the **`.`** notation for the method. + -- See the modified example: + -- + -- -- Now we have a constructor of the class AI_CARGO_DISPATCHER, that receives the SetHelicopter as a parameter. + -- -- Within that constructor, we want to set an enclosed event handler OnAfterDead for SetHelicopter. + -- -- But within the OnAfterDead method, we want to refer to the self variable of the AI_CARGO_DISPATCHER. + -- + -- function ACLASS:New( SetCarrier, SetCargo, SetDeployZones ) + -- + -- local self = BASE:Inherit( self, FSM:New() ) -- #AI_CARGO_DISPATCHER + -- + -- -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset. + -- -- Note the "." notation, and the explicit declaration of SetHelicopter, which would be using the ":" notation the implicit self variable declaration. + -- + -- function SetHelicopter.OnAfterDead( SetHelicopter, From, Event, To, UnitObject ) + -- SetHelicopter:F( { UnitObject = UnitObject:GetName() } ) + -- self.array[UnitObject] = nil -- So here I clear the array table entry of the self object ACLASS. + -- end + -- + -- end -- === -- @field #SET_UNIT SET_UNIT SET_UNIT = { @@ -1454,19 +1686,24 @@ do -- SET_UNIT function SET_UNIT:New() -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.UNITS ) ) -- Core.Set#SET_UNIT + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.UNITS ) ) -- #SET_UNIT + + self:FilterActive( false ) return self end --- Add UNIT(s) to SET_UNIT. -- @param #SET_UNIT self - -- @param #string AddUnit A single UNIT. + -- @param Wrapper.Unit#UNIT Unit A single UNIT. -- @return #SET_UNIT self - function SET_UNIT:AddUnit( AddUnit ) - self:F2( AddUnit:GetName() ) + function SET_UNIT:AddUnit( Unit ) + self:F2( Unit:GetName() ) - self:Add( AddUnit:GetName(), AddUnit ) + self:Add( Unit:GetName(), Unit ) + + -- Set the default cargo bay limit each time a new unit is added to the set. + Unit:SetCargoBayWeightLimit() return self end @@ -1609,6 +1846,32 @@ do -- SET_UNIT return self end + --- Builds a set of units that are only active. + -- Only the units that are active will be included within the set. + -- @param #SET_UNIT self + -- @param #boolean Active (optional) Include only active units to the set. + -- Include inactive units if you provide false. + -- @return #SET_UNIT self + -- @usage + -- + -- -- Include only active units to the set. + -- UnitSet = SET_UNIT:New():FilterActive():FilterStart() + -- + -- -- Include only active units to the set of the blue coalition, and filter one time. + -- UnitSet = SET_UNIT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() + -- + -- -- Include only active units to the set of the blue coalition, and filter one time. + -- -- Later, reset to include back inactive units to the set. + -- UnitSet = SET_UNIT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() + -- ... logic ... + -- UnitSet = SET_UNIT:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce() + -- + function SET_UNIT:FilterActive( Active ) + Active = Active or not ( Active == false ) + self.Filter.Active = Active + return self + end + --- Builds a set of units having a radar of give types. -- All the units having a radar of a given type will be included within the set. -- @param #SET_UNIT self @@ -1644,10 +1907,16 @@ do -- SET_UNIT if _DATABASE then self:_FilterStart() + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash ) end return self end + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! @@ -2060,7 +2329,7 @@ do -- SET_UNIT --- Returns if the @{Set} has targets having a radar (of a given type). -- @param #SET_UNIT self - -- @param Dcs.DCSWrapper.Unit#Unit.RadarType RadarType + -- @param DCS#Unit.RadarType RadarType -- @return #number The amount of radars in the Set with the given type function SET_UNIT:HasRadar( RadarType ) self:F2( RadarType ) @@ -2175,84 +2444,98 @@ do -- SET_UNIT -- @return #SET_UNIT self function SET_UNIT:IsIncludeObject( MUnit ) self:F2( MUnit ) - local MUnitInclude = true + + local MUnitInclude = false + + if MUnit:IsAlive() ~= nil then - if self.Filter.Coalitions then - local MUnitCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - self:F( { "Coalition:", MUnit:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MUnit:GetCoalition() then - MUnitCoalition = true - end - end - MUnitInclude = MUnitInclude and MUnitCoalition - end - - if self.Filter.Categories then - local MUnitCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - self:T3( { "Category:", MUnit:GetDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MUnit:GetDesc().category then - MUnitCategory = true - end - end - MUnitInclude = MUnitInclude and MUnitCategory - end - - if self.Filter.Types then - local MUnitType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T3( { "Type:", MUnit:GetTypeName(), TypeName } ) - if TypeName == MUnit:GetTypeName() then - MUnitType = true - end - end - MUnitInclude = MUnitInclude and MUnitType - end - - if self.Filter.Countries then - local MUnitCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - self:T3( { "Country:", MUnit:GetCountry(), CountryName } ) - if country.id[CountryName] == MUnit:GetCountry() then - MUnitCountry = true - end - end - MUnitInclude = MUnitInclude and MUnitCountry - end + MUnitInclude = true - if self.Filter.UnitPrefixes then - local MUnitPrefix = false - for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do - self:T3( { "Prefix:", string.find( MUnit:GetName(), UnitPrefix, 1 ), UnitPrefix } ) - if string.find( MUnit:GetName(), UnitPrefix, 1 ) then - MUnitPrefix = true + if self.Filter.Active ~= nil then + local MUnitActive = false + if self.Filter.Active == false or ( self.Filter.Active == true and MUnit:IsActive() == true ) then + MUnitActive = true end + MUnitInclude = MUnitInclude and MUnitActive end - MUnitInclude = MUnitInclude and MUnitPrefix - end - - if self.Filter.RadarTypes then - local MUnitRadar = false - for RadarTypeID, RadarType in pairs( self.Filter.RadarTypes ) do - self:T3( { "Radar:", RadarType } ) - if MUnit:HasSensors( Unit.SensorType.RADAR, RadarType ) == true then - if MUnit:GetRadar() == true then -- This call is necessary to evaluate the SEAD capability. - self:T3( "RADAR Found" ) + + if self.Filter.Coalitions then + local MUnitCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + self:F( { "Coalition:", MUnit:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MUnit:GetCoalition() then + MUnitCoalition = true end - MUnitRadar = true end + MUnitInclude = MUnitInclude and MUnitCoalition end - MUnitInclude = MUnitInclude and MUnitRadar - end - - if self.Filter.SEAD then - local MUnitSEAD = false - if MUnit:HasSEAD() == true then - self:T3( "SEAD Found" ) - MUnitSEAD = true + + if self.Filter.Categories then + local MUnitCategory = false + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + self:T3( { "Category:", MUnit:GetDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } ) + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MUnit:GetDesc().category then + MUnitCategory = true + end + end + MUnitInclude = MUnitInclude and MUnitCategory + end + + if self.Filter.Types then + local MUnitType = false + for TypeID, TypeName in pairs( self.Filter.Types ) do + self:T3( { "Type:", MUnit:GetTypeName(), TypeName } ) + if TypeName == MUnit:GetTypeName() then + MUnitType = true + end + end + MUnitInclude = MUnitInclude and MUnitType + end + + if self.Filter.Countries then + local MUnitCountry = false + for CountryID, CountryName in pairs( self.Filter.Countries ) do + self:T3( { "Country:", MUnit:GetCountry(), CountryName } ) + if country.id[CountryName] == MUnit:GetCountry() then + MUnitCountry = true + end + end + MUnitInclude = MUnitInclude and MUnitCountry + end + + if self.Filter.UnitPrefixes then + local MUnitPrefix = false + for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do + self:T3( { "Prefix:", string.find( MUnit:GetName(), UnitPrefix, 1 ), UnitPrefix } ) + if string.find( MUnit:GetName(), UnitPrefix, 1 ) then + MUnitPrefix = true + end + end + MUnitInclude = MUnitInclude and MUnitPrefix + end + + if self.Filter.RadarTypes then + local MUnitRadar = false + for RadarTypeID, RadarType in pairs( self.Filter.RadarTypes ) do + self:T3( { "Radar:", RadarType } ) + if MUnit:HasSensors( Unit.SensorType.RADAR, RadarType ) == true then + if MUnit:GetRadar() == true then -- This call is necessary to evaluate the SEAD capability. + self:T3( "RADAR Found" ) + end + MUnitRadar = true + end + end + MUnitInclude = MUnitInclude and MUnitRadar + end + + if self.Filter.SEAD then + local MUnitSEAD = false + if MUnit:HasSEAD() == true then + self:T3( "SEAD Found" ) + MUnitSEAD = true + end + MUnitInclude = MUnitInclude and MUnitSEAD end - MUnitInclude = MUnitInclude and MUnitSEAD end self:T2( MUnitInclude ) @@ -2260,10 +2543,10 @@ do -- SET_UNIT end - --- Retrieve the type names of the @{Unit}s in the SET, delimited by an optional delimiter. + --- Retrieve the type names of the @{Wrapper.Unit}s in the SET, delimited by an optional delimiter. -- @param #SET_UNIT self -- @param #string Delimiter (optional) The delimiter, which is default a comma. - -- @return #string The types of the @{Unit}s delimited. + -- @return #string The types of the @{Wrapper.Unit}s delimited. function SET_UNIT:GetTypeNames( Delimiter ) Delimiter = Delimiter or ", " @@ -2283,17 +2566,32 @@ do -- SET_UNIT return TypeReport:Text( Delimiter ) end + + --- Iterate the SET_UNIT and set for each unit the default cargo bay weight limit. + -- @param #SET_UNIT self + -- @usage + -- -- Set the default cargo bay weight limits of the carrier units. + -- local MySetUnit = SET_UNIT:New() + -- MySetUnit:SetCargoBayWeightLimit() + function SET_UNIT:SetCargoBayWeightLimit() + local Set = self:GetSet() + for UnitID, UnitData in pairs( Set ) do -- For each UNIT in SET_UNIT + --local UnitData = UnitData -- Wrapper.Unit#UNIT + UnitData:SetCargoBayWeightLimit() + end + end + + end + do -- SET_STATIC --- @type SET_STATIC -- @extends Core.Set#SET_BASE - --- # 3) SET_STATIC class, extends @{Set#SET_BASE} - -- - -- Mission designers can use the SET_STATIC class to build sets of Statics belonging to certain: + --- Mission designers can use the SET_STATIC class to build sets of Statics belonging to certain: -- -- * Coalitions -- * Categories @@ -2301,18 +2599,18 @@ do -- SET_STATIC -- * Static types -- * Starting with certain prefix strings. -- - -- ## 3.1) SET_STATIC constructor + -- ## SET_STATIC constructor -- -- Create a new SET_STATIC object with the @{#SET_STATIC.New} method: -- -- * @{#SET_STATIC.New}: Creates a new SET_STATIC object. -- - -- ## 3.2) Add or Remove STATIC(s) from SET_STATIC + -- ## Add or Remove STATIC(s) from SET_STATIC -- - -- STATICs can be added and removed using the @{Set#SET_STATIC.AddStaticsByName} and @{Set#SET_STATIC.RemoveStaticsByName} respectively. + -- STATICs can be added and removed using the @{Core.Set#SET_STATIC.AddStaticsByName} and @{Core.Set#SET_STATIC.RemoveStaticsByName} respectively. -- These methods take a single STATIC name or an array of STATIC names to be added or removed from SET_STATIC. -- - -- ## 3.3) SET_STATIC filter criteria + -- ## SET_STATIC filter criteria -- -- You can set filter criteria to define the set of units within the SET_STATIC. -- Filter criteria are defined by: @@ -2329,9 +2627,9 @@ do -- SET_STATIC -- -- Planned filter criteria within development are (so these are not yet available): -- - -- * @{#SET_STATIC.FilterZones}: Builds the SET_STATIC with the units within a @{Zone#ZONE}. + -- * @{#SET_STATIC.FilterZones}: Builds the SET_STATIC with the units within a @{Core.Zone#ZONE}. -- - -- ## 3.4) SET_STATIC iterators + -- ## SET_STATIC iterators -- -- Once the filters have been defined and the SET_STATIC has been built, you can iterate the SET_STATIC with the available iterator methods. -- The iterator methods will walk the SET_STATIC set, and call for each element within the set a function that you provide. @@ -2347,7 +2645,7 @@ do -- SET_STATIC -- * @{#SET_STATIC.ForEachStaticCompletelyInZone}: Iterate and call an iterator function for each **alive** STATIC presence completely in a @{Zone}, providing the STATIC and optional parameters to the called function. -- * @{#SET_STATIC.ForEachStaticNotInZone}: Iterate and call an iterator function for each **alive** STATIC presence not in a @{Zone}, providing the STATIC and optional parameters to the called function. -- - -- ## 3.5 ) SET_STATIC atomic methods + -- ## SET_STATIC atomic methods -- -- Various methods exist for a SET_STATIC to perform actions or calculations and retrieve results from the SET_STATIC: -- @@ -2560,6 +2858,9 @@ do -- SET_STATIC if _DATABASE then self:_FilterStart() + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) end return self @@ -2860,6 +3161,26 @@ do -- SET_STATIC end + --- Calculate the maxium A2G threat level of the SET_STATIC. + -- @param #SET_STATIC self + -- @return #number The maximum threatlevel + function SET_STATIC:CalculateThreatLevelA2G() + + local MaxThreatLevelA2G = 0 + local MaxThreatText = "" + for StaticName, StaticData in pairs( self:GetSet() ) do + local ThreatStatic = StaticData -- Wrapper.Static#STATIC + local ThreatLevelA2G, ThreatText = ThreatStatic:GetThreatLevel() + if ThreatLevelA2G > MaxThreatLevelA2G then + MaxThreatLevelA2G = ThreatLevelA2G + MaxThreatText = ThreatText + end + end + + self:F( { MaxThreatLevelA2G = MaxThreatLevelA2G, MaxThreatText = MaxThreatText } ) + return MaxThreatLevelA2G, MaxThreatText + + end --- -- @param #SET_STATIC self @@ -2956,1414 +3277,1976 @@ do -- SET_STATIC end ---- SET_CLIENT +do -- SET_CLIENT ---- @type SET_CLIENT --- @extends Core.Set#SET_BASE - - - ---- # 4) SET_CLIENT class, extends @{Set#SET_BASE} --- --- Mission designers can use the @{Set#SET_CLIENT} class to build sets of units belonging to certain: --- --- * Coalitions --- * Categories --- * Countries --- * Client types --- * Starting with certain prefix strings. --- --- ## 4.1) SET_CLIENT constructor --- --- Create a new SET_CLIENT object with the @{#SET_CLIENT.New} method: --- --- * @{#SET_CLIENT.New}: Creates a new SET_CLIENT object. --- --- ## 4.2) Add or Remove CLIENT(s) from SET_CLIENT --- --- CLIENTs can be added and removed using the @{Set#SET_CLIENT.AddClientsByName} and @{Set#SET_CLIENT.RemoveClientsByName} respectively. --- These methods take a single CLIENT name or an array of CLIENT names to be added or removed from SET_CLIENT. --- --- ## 4.3) SET_CLIENT filter criteria --- --- You can set filter criteria to define the set of clients within the SET_CLIENT. --- Filter criteria are defined by: --- --- * @{#SET_CLIENT.FilterCoalitions}: Builds the SET_CLIENT with the clients belonging to the coalition(s). --- * @{#SET_CLIENT.FilterCategories}: Builds the SET_CLIENT with the clients belonging to the category(ies). --- * @{#SET_CLIENT.FilterTypes}: Builds the SET_CLIENT with the clients belonging to the client type(s). --- * @{#SET_CLIENT.FilterCountries}: Builds the SET_CLIENT with the clients belonging to the country(ies). --- * @{#SET_CLIENT.FilterPrefixes}: Builds the SET_CLIENT with the clients starting with the same prefix string(s). --- --- Once the filter criteria have been set for the SET_CLIENT, you can start filtering using: --- --- * @{#SET_CLIENT.FilterStart}: Starts the filtering of the clients within the SET_CLIENT. --- --- Planned filter criteria within development are (so these are not yet available): --- --- * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Zone#ZONE}. --- --- ## 4.4) SET_CLIENT iterators --- --- Once the filters have been defined and the SET_CLIENT has been built, you can iterate the SET_CLIENT with the available iterator methods. --- The iterator methods will walk the SET_CLIENT set, and call for each element within the set a function that you provide. --- The following iterator methods are currently available within the SET_CLIENT: --- --- * @{#SET_CLIENT.ForEachClient}: Calls a function for each alive client it finds within the SET_CLIENT. --- --- === --- @field #SET_CLIENT SET_CLIENT -SET_CLIENT = { - ClassName = "SET_CLIENT", - Clients = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - ClientPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, + --- @type SET_CLIENT + -- @extends Core.Set#SET_BASE + + + + --- Mission designers can use the @{Core.Set#SET_CLIENT} class to build sets of units belonging to certain: + -- + -- * Coalitions + -- * Categories + -- * Countries + -- * Client types + -- * Starting with certain prefix strings. + -- + -- ## 1) SET_CLIENT constructor + -- + -- Create a new SET_CLIENT object with the @{#SET_CLIENT.New} method: + -- + -- * @{#SET_CLIENT.New}: Creates a new SET_CLIENT object. + -- + -- ## 2) Add or Remove CLIENT(s) from SET_CLIENT + -- + -- CLIENTs can be added and removed using the @{Core.Set#SET_CLIENT.AddClientsByName} and @{Core.Set#SET_CLIENT.RemoveClientsByName} respectively. + -- These methods take a single CLIENT name or an array of CLIENT names to be added or removed from SET_CLIENT. + -- + -- ## 3) SET_CLIENT filter criteria + -- + -- You can set filter criteria to define the set of clients within the SET_CLIENT. + -- Filter criteria are defined by: + -- + -- * @{#SET_CLIENT.FilterCoalitions}: Builds the SET_CLIENT with the clients belonging to the coalition(s). + -- * @{#SET_CLIENT.FilterCategories}: Builds the SET_CLIENT with the clients belonging to the category(ies). + -- * @{#SET_CLIENT.FilterTypes}: Builds the SET_CLIENT with the clients belonging to the client type(s). + -- * @{#SET_CLIENT.FilterCountries}: Builds the SET_CLIENT with the clients belonging to the country(ies). + -- * @{#SET_CLIENT.FilterPrefixes}: Builds the SET_CLIENT with the clients starting with the same prefix string(s). + -- * @{#SET_CLIENT.FilterActive}: Builds the SET_CLIENT with the units that are only active. Units that are inactive (late activation) won't be included in the set! + -- + -- Once the filter criteria have been set for the SET_CLIENT, you can start filtering using: + -- + -- * @{#SET_CLIENT.FilterStart}: Starts the filtering of the clients **dynamically**. + -- * @{#SET_CLIENT.FilterOnce}: Filters the clients **once**. + -- + -- Planned filter criteria within development are (so these are not yet available): + -- + -- * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Core.Zone#ZONE}. + -- + -- ## 4) SET_CLIENT iterators + -- + -- Once the filters have been defined and the SET_CLIENT has been built, you can iterate the SET_CLIENT with the available iterator methods. + -- The iterator methods will walk the SET_CLIENT set, and call for each element within the set a function that you provide. + -- The following iterator methods are currently available within the SET_CLIENT: + -- + -- * @{#SET_CLIENT.ForEachClient}: Calls a function for each alive client it finds within the SET_CLIENT. + -- + -- === + -- @field #SET_CLIENT SET_CLIENT + SET_CLIENT = { + ClassName = "SET_CLIENT", + Clients = {}, + Filter = { + Coalitions = nil, + Categories = nil, + Types = nil, + Countries = nil, + ClientPrefixes = nil, }, - Categories = { - plane = Unit.Category.AIRPLANE, - helicopter = Unit.Category.HELICOPTER, - ground = Unit.Category.GROUND_UNIT, - ship = Unit.Category.SHIP, - structure = Unit.Category.STRUCTURE, + FilterMeta = { + Coalitions = { + red = coalition.side.RED, + blue = coalition.side.BLUE, + neutral = coalition.side.NEUTRAL, + }, + Categories = { + plane = Unit.Category.AIRPLANE, + helicopter = Unit.Category.HELICOPTER, + ground = Unit.Category.GROUND_UNIT, + ship = Unit.Category.SHIP, + structure = Unit.Category.STRUCTURE, + }, }, - }, -} - - ---- Creates a new SET_CLIENT object, building a set of clients belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_CLIENT self --- @return #SET_CLIENT --- @usage --- -- Define a new SET_CLIENT Object. This DBObject will contain a reference to all Clients. --- DBObject = SET_CLIENT:New() -function SET_CLIENT:New() - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CLIENTS ) ) - - return self -end - ---- Add CLIENT(s) to SET_CLIENT. --- @param Core.Set#SET_CLIENT self --- @param #string AddClientNames A single name or an array of CLIENT names. --- @return self -function SET_CLIENT:AddClientsByName( AddClientNames ) - - local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } + } - for AddClientID, AddClientName in pairs( AddClientNamesArray ) do - self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) + + --- Creates a new SET_CLIENT object, building a set of clients belonging to a coalitions, categories, countries, types or with defined prefix names. + -- @param #SET_CLIENT self + -- @return #SET_CLIENT + -- @usage + -- -- Define a new SET_CLIENT Object. This DBObject will contain a reference to all Clients. + -- DBObject = SET_CLIENT:New() + function SET_CLIENT:New() + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CLIENTS ) ) -- #SET_CLIENT + + self:FilterActive( false ) + + return self end + + --- Add CLIENT(s) to SET_CLIENT. + -- @param Core.Set#SET_CLIENT self + -- @param #string AddClientNames A single name or an array of CLIENT names. + -- @return self + function SET_CLIENT:AddClientsByName( AddClientNames ) + + local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } - return self -end - ---- Remove CLIENT(s) from SET_CLIENT. --- @param Core.Set#SET_CLIENT self --- @param Wrapper.Client#CLIENT RemoveClientNames A single name or an array of CLIENT names. --- @return self -function SET_CLIENT:RemoveClientsByName( RemoveClientNames ) - - local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } - - for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do - self:Remove( RemoveClientName.ClientName ) + for AddClientID, AddClientName in pairs( AddClientNamesArray ) do + self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) + end + + return self end + + --- Remove CLIENT(s) from SET_CLIENT. + -- @param Core.Set#SET_CLIENT self + -- @param Wrapper.Client#CLIENT RemoveClientNames A single name or an array of CLIENT names. + -- @return self + function SET_CLIENT:RemoveClientsByName( RemoveClientNames ) + + local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } - return self -end - - ---- Finds a Client based on the Client Name. --- @param #SET_CLIENT self --- @param #string ClientName --- @return Wrapper.Client#CLIENT The found Client. -function SET_CLIENT:FindClient( ClientName ) - - local ClientFound = self.Set[ClientName] - return ClientFound -end - - - ---- Builds a set of clients of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_CLIENT self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_CLIENT self -function SET_CLIENT:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of clients out of categories. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_CLIENT self --- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". --- @return #SET_CLIENT self -function SET_CLIENT:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - - ---- Builds a set of clients of defined client types. --- Possible current types are those types known within DCS world. --- @param #SET_CLIENT self --- @param #string Types Can take those type strings known within DCS world. --- @return #SET_CLIENT self -function SET_CLIENT:FilterTypes( Types ) - if not self.Filter.Types then - self.Filter.Types = {} - end - if type( Types ) ~= "table" then - Types = { Types } - end - for TypeID, Type in pairs( Types ) do - self.Filter.Types[Type] = Type - end - return self -end - - ---- Builds a set of clients of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_CLIENT self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_CLIENT self -function SET_CLIENT:FilterCountries( Countries ) - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- Builds a set of clients of defined client prefixes. --- All the clients starting with the given prefixes will be included within the set. --- @param #SET_CLIENT self --- @param #string Prefixes The prefix of which the client name starts with. --- @return #SET_CLIENT self -function SET_CLIENT:FilterPrefixes( Prefixes ) - if not self.Filter.ClientPrefixes then - self.Filter.ClientPrefixes = {} - end - if type( Prefixes ) ~= "table" then - Prefixes = { Prefixes } - end - for PrefixID, Prefix in pairs( Prefixes ) do - self.Filter.ClientPrefixes[Prefix] = Prefix - end - return self -end - - - - ---- Starts the filtering. --- @param #SET_CLIENT self --- @return #SET_CLIENT self -function SET_CLIENT:FilterStart() - - if _DATABASE then - self:_FilterStart() + for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do + self:Remove( RemoveClientName.ClientName ) + end + + return self end - return self -end - ---- Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_CLIENT self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the CLIENT --- @return #table The CLIENT -function SET_CLIENT:AddInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_CLIENT self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the CLIENT --- @return #table The CLIENT -function SET_CLIENT:FindInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Iterate the SET_CLIENT and call an interator function for each **alive** CLIENT, providing the CLIENT and optional parameters. --- @param #SET_CLIENT self --- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. --- @return #SET_CLIENT self -function SET_CLIENT:ForEachClient( IteratorFunction, ... ) - self:F2( arg ) - self:ForEach( IteratorFunction, arg, self:GetSet() ) + --- Finds a Client based on the Client Name. + -- @param #SET_CLIENT self + -- @param #string ClientName + -- @return Wrapper.Client#CLIENT The found Client. + function SET_CLIENT:FindClient( ClientName ) + + local ClientFound = self.Set[ClientName] + return ClientFound + end + + + + --- Builds a set of clients of coalitions. + -- Possible current coalitions are red, blue and neutral. + -- @param #SET_CLIENT self + -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". + -- @return #SET_CLIENT self + function SET_CLIENT:FilterCoalitions( Coalitions ) + if not self.Filter.Coalitions then + self.Filter.Coalitions = {} + end + if type( Coalitions ) ~= "table" then + Coalitions = { Coalitions } + end + for CoalitionID, Coalition in pairs( Coalitions ) do + self.Filter.Coalitions[Coalition] = Coalition + end + return self + end + + + --- Builds a set of clients out of categories. + -- Possible current categories are plane, helicopter, ground, ship. + -- @param #SET_CLIENT self + -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". + -- @return #SET_CLIENT self + function SET_CLIENT:FilterCategories( Categories ) + if not self.Filter.Categories then + self.Filter.Categories = {} + end + if type( Categories ) ~= "table" then + Categories = { Categories } + end + for CategoryID, Category in pairs( Categories ) do + self.Filter.Categories[Category] = Category + end + return self + end + + + --- Builds a set of clients of defined client types. + -- Possible current types are those types known within DCS world. + -- @param #SET_CLIENT self + -- @param #string Types Can take those type strings known within DCS world. + -- @return #SET_CLIENT self + function SET_CLIENT:FilterTypes( Types ) + if not self.Filter.Types then + self.Filter.Types = {} + end + if type( Types ) ~= "table" then + Types = { Types } + end + for TypeID, Type in pairs( Types ) do + self.Filter.Types[Type] = Type + end + return self + end + + + --- Builds a set of clients of defined countries. + -- Possible current countries are those known within DCS world. + -- @param #SET_CLIENT self + -- @param #string Countries Can take those country strings known within DCS world. + -- @return #SET_CLIENT self + function SET_CLIENT:FilterCountries( Countries ) + if not self.Filter.Countries then + self.Filter.Countries = {} + end + if type( Countries ) ~= "table" then + Countries = { Countries } + end + for CountryID, Country in pairs( Countries ) do + self.Filter.Countries[Country] = Country + end + return self + end + + + --- Builds a set of clients of defined client prefixes. + -- All the clients starting with the given prefixes will be included within the set. + -- @param #SET_CLIENT self + -- @param #string Prefixes The prefix of which the client name starts with. + -- @return #SET_CLIENT self + function SET_CLIENT:FilterPrefixes( Prefixes ) + if not self.Filter.ClientPrefixes then + self.Filter.ClientPrefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.ClientPrefixes[Prefix] = Prefix + end + return self + end + + --- Builds a set of clients that are only active. + -- Only the clients that are active will be included within the set. + -- @param #SET_CLIENT self + -- @param #boolean Active (optional) Include only active clients to the set. + -- Include inactive clients if you provide false. + -- @return #SET_CLIENT self + -- @usage + -- + -- -- Include only active clients to the set. + -- ClientSet = SET_CLIENT:New():FilterActive():FilterStart() + -- + -- -- Include only active clients to the set of the blue coalition, and filter one time. + -- ClientSet = SET_CLIENT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() + -- + -- -- Include only active clients to the set of the blue coalition, and filter one time. + -- -- Later, reset to include back inactive clients to the set. + -- ClientSet = SET_CLIENT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce() + -- ... logic ... + -- ClientSet = SET_CLIENT:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce() + -- + function SET_CLIENT:FilterActive( Active ) + Active = Active or not ( Active == false ) + self.Filter.Active = Active + return self + end + + + + --- Starts the filtering. + -- @param #SET_CLIENT self + -- @return #SET_CLIENT self + function SET_CLIENT:FilterStart() + + if _DATABASE then + self:_FilterStart() + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + end + + return self + end + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_CLIENT self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the CLIENT + -- @return #table The CLIENT + function SET_CLIENT:AddInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_CLIENT self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the CLIENT + -- @return #table The CLIENT + function SET_CLIENT:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Iterate the SET_CLIENT and call an interator function for each **alive** CLIENT, providing the CLIENT and optional parameters. + -- @param #SET_CLIENT self + -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. + -- @return #SET_CLIENT self + function SET_CLIENT:ForEachClient( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + --- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence completely in a @{Zone}, providing the CLIENT and optional parameters to the called function. + -- @param #SET_CLIENT self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. + -- @return #SET_CLIENT self + function SET_CLIENT:ForEachClientInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Client#CLIENT ClientObject + function( ZoneObject, ClientObject ) + if ClientObject:IsInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence not in a @{Zone}, providing the CLIENT and optional parameters to the called function. + -- @param #SET_CLIENT self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. + -- @return #SET_CLIENT self + function SET_CLIENT:ForEachClientNotInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Client#CLIENT ClientObject + function( ZoneObject, ClientObject ) + if ClientObject:IsNotInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- + -- @param #SET_CLIENT self + -- @param Wrapper.Client#CLIENT MClient + -- @return #SET_CLIENT self + function SET_CLIENT:IsIncludeObject( MClient ) + self:F2( MClient ) + + local MClientInclude = true + + if MClient then + local MClientName = MClient.UnitName + + if self.Filter.Active ~= nil then + local MClientActive = false + if self.Filter.Active == false or ( self.Filter.Active == true and MClient:IsActive() == true ) then + MClientActive = true + end + MClientInclude = MClientInclude and MClientActive + end + + if self.Filter.Coalitions then + local MClientCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName ) + self:T3( { "Coalition:", ClientCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == ClientCoalitionID then + MClientCoalition = true + end + end + self:T( { "Evaluated Coalition", MClientCoalition } ) + MClientInclude = MClientInclude and MClientCoalition + end + + if self.Filter.Categories then + local MClientCategory = false + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName ) + self:T3( { "Category:", ClientCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == ClientCategoryID then + MClientCategory = true + end + end + self:T( { "Evaluated Category", MClientCategory } ) + MClientInclude = MClientInclude and MClientCategory + end + + if self.Filter.Types then + local MClientType = false + for TypeID, TypeName in pairs( self.Filter.Types ) do + self:T3( { "Type:", MClient:GetTypeName(), TypeName } ) + if TypeName == MClient:GetTypeName() then + MClientType = true + end + end + self:T( { "Evaluated Type", MClientType } ) + MClientInclude = MClientInclude and MClientType + end + + if self.Filter.Countries then + local MClientCountry = false + for CountryID, CountryName in pairs( self.Filter.Countries ) do + local ClientCountryID = _DATABASE:GetCountryFromClientTemplate(MClientName) + self:T3( { "Country:", ClientCountryID, country.id[CountryName], CountryName } ) + if country.id[CountryName] and country.id[CountryName] == ClientCountryID then + MClientCountry = true + end + end + self:T( { "Evaluated Country", MClientCountry } ) + MClientInclude = MClientInclude and MClientCountry + end + + if self.Filter.ClientPrefixes then + local MClientPrefix = false + for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do + self:T3( { "Prefix:", string.find( MClient.UnitName, ClientPrefix, 1 ), ClientPrefix } ) + if string.find( MClient.UnitName, ClientPrefix, 1 ) then + MClientPrefix = true + end + end + self:T( { "Evaluated Prefix", MClientPrefix } ) + MClientInclude = MClientInclude and MClientPrefix + end + end + + self:T2( MClientInclude ) + return MClientInclude + end - return self end ---- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence completely in a @{Zone}, providing the CLIENT and optional parameters to the called function. --- @param #SET_CLIENT self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. --- @return #SET_CLIENT self -function SET_CLIENT:ForEachClientInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) + +do -- SET_PLAYER + + --- @type SET_PLAYER + -- @extends Core.Set#SET_BASE - self:ForEach( IteratorFunction, arg, self:GetSet(), - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Client#CLIENT ClientObject - function( ZoneObject, ClientObject ) - if ClientObject:IsInZone( ZoneObject ) then - return true + + + --- Mission designers can use the @{Core.Set#SET_PLAYER} class to build sets of units belonging to alive players: + -- + -- ## SET_PLAYER constructor + -- + -- Create a new SET_PLAYER object with the @{#SET_PLAYER.New} method: + -- + -- * @{#SET_PLAYER.New}: Creates a new SET_PLAYER object. + -- + -- ## SET_PLAYER filter criteria + -- + -- You can set filter criteria to define the set of clients within the SET_PLAYER. + -- Filter criteria are defined by: + -- + -- * @{#SET_PLAYER.FilterCoalitions}: Builds the SET_PLAYER with the clients belonging to the coalition(s). + -- * @{#SET_PLAYER.FilterCategories}: Builds the SET_PLAYER with the clients belonging to the category(ies). + -- * @{#SET_PLAYER.FilterTypes}: Builds the SET_PLAYER with the clients belonging to the client type(s). + -- * @{#SET_PLAYER.FilterCountries}: Builds the SET_PLAYER with the clients belonging to the country(ies). + -- * @{#SET_PLAYER.FilterPrefixes}: Builds the SET_PLAYER with the clients starting with the same prefix string(s). + -- + -- Once the filter criteria have been set for the SET_PLAYER, you can start filtering using: + -- + -- * @{#SET_PLAYER.FilterStart}: Starts the filtering of the clients within the SET_PLAYER. + -- + -- Planned filter criteria within development are (so these are not yet available): + -- + -- * @{#SET_PLAYER.FilterZones}: Builds the SET_PLAYER with the clients within a @{Core.Zone#ZONE}. + -- + -- ## SET_PLAYER iterators + -- + -- Once the filters have been defined and the SET_PLAYER has been built, you can iterate the SET_PLAYER with the available iterator methods. + -- The iterator methods will walk the SET_PLAYER set, and call for each element within the set a function that you provide. + -- The following iterator methods are currently available within the SET_PLAYER: + -- + -- * @{#SET_PLAYER.ForEachClient}: Calls a function for each alive client it finds within the SET_PLAYER. + -- + -- === + -- @field #SET_PLAYER SET_PLAYER + SET_PLAYER = { + ClassName = "SET_PLAYER", + Clients = {}, + Filter = { + Coalitions = nil, + Categories = nil, + Types = nil, + Countries = nil, + ClientPrefixes = nil, + }, + FilterMeta = { + Coalitions = { + red = coalition.side.RED, + blue = coalition.side.BLUE, + neutral = coalition.side.NEUTRAL, + }, + Categories = { + plane = Unit.Category.AIRPLANE, + helicopter = Unit.Category.HELICOPTER, + ground = Unit.Category.GROUND_UNIT, + ship = Unit.Category.SHIP, + structure = Unit.Category.STRUCTURE, + }, + }, + } + + + --- Creates a new SET_PLAYER object, building a set of clients belonging to a coalitions, categories, countries, types or with defined prefix names. + -- @param #SET_PLAYER self + -- @return #SET_PLAYER + -- @usage + -- -- Define a new SET_PLAYER Object. This DBObject will contain a reference to all Clients. + -- DBObject = SET_PLAYER:New() + function SET_PLAYER:New() + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.PLAYERS ) ) + + return self + end + + --- Add CLIENT(s) to SET_PLAYER. + -- @param Core.Set#SET_PLAYER self + -- @param #string AddClientNames A single name or an array of CLIENT names. + -- @return self + function SET_PLAYER:AddClientsByName( AddClientNames ) + + local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } + + for AddClientID, AddClientName in pairs( AddClientNamesArray ) do + self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) + end + + return self + end + + --- Remove CLIENT(s) from SET_PLAYER. + -- @param Core.Set#SET_PLAYER self + -- @param Wrapper.Client#CLIENT RemoveClientNames A single name or an array of CLIENT names. + -- @return self + function SET_PLAYER:RemoveClientsByName( RemoveClientNames ) + + local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } + + for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do + self:Remove( RemoveClientName.ClientName ) + end + + return self + end + + + --- Finds a Client based on the Player Name. + -- @param #SET_PLAYER self + -- @param #string PlayerName + -- @return Wrapper.Client#CLIENT The found Client. + function SET_PLAYER:FindClient( PlayerName ) + + local ClientFound = self.Set[PlayerName] + return ClientFound + end + + + + --- Builds a set of clients of coalitions joined by specific players. + -- Possible current coalitions are red, blue and neutral. + -- @param #SET_PLAYER self + -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". + -- @return #SET_PLAYER self + function SET_PLAYER:FilterCoalitions( Coalitions ) + if not self.Filter.Coalitions then + self.Filter.Coalitions = {} + end + if type( Coalitions ) ~= "table" then + Coalitions = { Coalitions } + end + for CoalitionID, Coalition in pairs( Coalitions ) do + self.Filter.Coalitions[Coalition] = Coalition + end + return self + end + + + --- Builds a set of clients out of categories joined by players. + -- Possible current categories are plane, helicopter, ground, ship. + -- @param #SET_PLAYER self + -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". + -- @return #SET_PLAYER self + function SET_PLAYER:FilterCategories( Categories ) + if not self.Filter.Categories then + self.Filter.Categories = {} + end + if type( Categories ) ~= "table" then + Categories = { Categories } + end + for CategoryID, Category in pairs( Categories ) do + self.Filter.Categories[Category] = Category + end + return self + end + + + --- Builds a set of clients of defined client types joined by players. + -- Possible current types are those types known within DCS world. + -- @param #SET_PLAYER self + -- @param #string Types Can take those type strings known within DCS world. + -- @return #SET_PLAYER self + function SET_PLAYER:FilterTypes( Types ) + if not self.Filter.Types then + self.Filter.Types = {} + end + if type( Types ) ~= "table" then + Types = { Types } + end + for TypeID, Type in pairs( Types ) do + self.Filter.Types[Type] = Type + end + return self + end + + + --- Builds a set of clients of defined countries. + -- Possible current countries are those known within DCS world. + -- @param #SET_PLAYER self + -- @param #string Countries Can take those country strings known within DCS world. + -- @return #SET_PLAYER self + function SET_PLAYER:FilterCountries( Countries ) + if not self.Filter.Countries then + self.Filter.Countries = {} + end + if type( Countries ) ~= "table" then + Countries = { Countries } + end + for CountryID, Country in pairs( Countries ) do + self.Filter.Countries[Country] = Country + end + return self + end + + + --- Builds a set of clients of defined client prefixes. + -- All the clients starting with the given prefixes will be included within the set. + -- @param #SET_PLAYER self + -- @param #string Prefixes The prefix of which the client name starts with. + -- @return #SET_PLAYER self + function SET_PLAYER:FilterPrefixes( Prefixes ) + if not self.Filter.ClientPrefixes then + self.Filter.ClientPrefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.ClientPrefixes[Prefix] = Prefix + end + return self + end + + + + + --- Starts the filtering. + -- @param #SET_PLAYER self + -- @return #SET_PLAYER self + function SET_PLAYER:FilterStart() + + if _DATABASE then + self:_FilterStart() + self:HandleEvent( EVENTS.Birth, self._EventOnBirth ) + self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash ) + self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash ) + end + + return self + end + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_PLAYER self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the CLIENT + -- @return #table The CLIENT + function SET_PLAYER:AddInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_PLAYER self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the CLIENT + -- @return #table The CLIENT + function SET_PLAYER:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Iterate the SET_PLAYER and call an interator function for each **alive** CLIENT, providing the CLIENT and optional parameters. + -- @param #SET_PLAYER self + -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter. + -- @return #SET_PLAYER self + function SET_PLAYER:ForEachPlayer( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + --- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT presence completely in a @{Zone}, providing the CLIENT and optional parameters to the called function. + -- @param #SET_PLAYER self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter. + -- @return #SET_PLAYER self + function SET_PLAYER:ForEachPlayerInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Client#CLIENT ClientObject + function( ZoneObject, ClientObject ) + if ClientObject:IsInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT presence not in a @{Zone}, providing the CLIENT and optional parameters to the called function. + -- @param #SET_PLAYER self + -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. + -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter. + -- @return #SET_PLAYER self + function SET_PLAYER:ForEachPlayerNotInZone( ZoneObject, IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet(), + --- @param Core.Zone#ZONE_BASE ZoneObject + -- @param Wrapper.Client#CLIENT ClientObject + function( ZoneObject, ClientObject ) + if ClientObject:IsNotInZone( ZoneObject ) then + return true + else + return false + end + end, { ZoneObject } ) + + return self + end + + --- + -- @param #SET_PLAYER self + -- @param Wrapper.Client#CLIENT MClient + -- @return #SET_PLAYER self + function SET_PLAYER:IsIncludeObject( MClient ) + self:F2( MClient ) + + local MClientInclude = true + + if MClient then + local MClientName = MClient.UnitName + + if self.Filter.Coalitions then + local MClientCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName ) + self:T3( { "Coalition:", ClientCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == ClientCoalitionID then + MClientCoalition = true + end + end + self:T( { "Evaluated Coalition", MClientCoalition } ) + MClientInclude = MClientInclude and MClientCoalition + end + + if self.Filter.Categories then + local MClientCategory = false + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName ) + self:T3( { "Category:", ClientCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == ClientCategoryID then + MClientCategory = true + end + end + self:T( { "Evaluated Category", MClientCategory } ) + MClientInclude = MClientInclude and MClientCategory + end + + if self.Filter.Types then + local MClientType = false + for TypeID, TypeName in pairs( self.Filter.Types ) do + self:T3( { "Type:", MClient:GetTypeName(), TypeName } ) + if TypeName == MClient:GetTypeName() then + MClientType = true + end + end + self:T( { "Evaluated Type", MClientType } ) + MClientInclude = MClientInclude and MClientType + end + + if self.Filter.Countries then + local MClientCountry = false + for CountryID, CountryName in pairs( self.Filter.Countries ) do + local ClientCountryID = _DATABASE:GetCountryFromClientTemplate(MClientName) + self:T3( { "Country:", ClientCountryID, country.id[CountryName], CountryName } ) + if country.id[CountryName] and country.id[CountryName] == ClientCountryID then + MClientCountry = true + end + end + self:T( { "Evaluated Country", MClientCountry } ) + MClientInclude = MClientInclude and MClientCountry + end + + if self.Filter.ClientPrefixes then + local MClientPrefix = false + for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do + self:T3( { "Prefix:", string.find( MClient.UnitName, ClientPrefix, 1 ), ClientPrefix } ) + if string.find( MClient.UnitName, ClientPrefix, 1 ) then + MClientPrefix = true + end + end + self:T( { "Evaluated Prefix", MClientPrefix } ) + MClientInclude = MClientInclude and MClientPrefix + end + end + + self:T2( MClientInclude ) + return MClientInclude + end + +end + + +do -- SET_AIRBASE + + --- @type SET_AIRBASE + -- @extends Core.Set#SET_BASE + + --- Mission designers can use the @{Core.Set#SET_AIRBASE} class to build sets of airbases optionally belonging to certain: + -- + -- * Coalitions + -- + -- ## SET_AIRBASE constructor + -- + -- Create a new SET_AIRBASE object with the @{#SET_AIRBASE.New} method: + -- + -- * @{#SET_AIRBASE.New}: Creates a new SET_AIRBASE object. + -- + -- ## Add or Remove AIRBASEs from SET_AIRBASE + -- + -- AIRBASEs can be added and removed using the @{Core.Set#SET_AIRBASE.AddAirbasesByName} and @{Core.Set#SET_AIRBASE.RemoveAirbasesByName} respectively. + -- These methods take a single AIRBASE name or an array of AIRBASE names to be added or removed from SET_AIRBASE. + -- + -- ## SET_AIRBASE filter criteria + -- + -- You can set filter criteria to define the set of clients within the SET_AIRBASE. + -- Filter criteria are defined by: + -- + -- * @{#SET_AIRBASE.FilterCoalitions}: Builds the SET_AIRBASE with the airbases belonging to the coalition(s). + -- + -- Once the filter criteria have been set for the SET_AIRBASE, you can start filtering using: + -- + -- * @{#SET_AIRBASE.FilterStart}: Starts the filtering of the airbases within the SET_AIRBASE. + -- + -- ## SET_AIRBASE iterators + -- + -- Once the filters have been defined and the SET_AIRBASE has been built, you can iterate the SET_AIRBASE with the available iterator methods. + -- The iterator methods will walk the SET_AIRBASE set, and call for each airbase within the set a function that you provide. + -- The following iterator methods are currently available within the SET_AIRBASE: + -- + -- * @{#SET_AIRBASE.ForEachAirbase}: Calls a function for each airbase it finds within the SET_AIRBASE. + -- + -- === + -- @field #SET_AIRBASE SET_AIRBASE + SET_AIRBASE = { + ClassName = "SET_AIRBASE", + Airbases = {}, + Filter = { + Coalitions = nil, + }, + FilterMeta = { + Coalitions = { + red = coalition.side.RED, + blue = coalition.side.BLUE, + neutral = coalition.side.NEUTRAL, + }, + Categories = { + airdrome = Airbase.Category.AIRDROME, + helipad = Airbase.Category.HELIPAD, + ship = Airbase.Category.SHIP, + }, + }, + } + + + --- Creates a new SET_AIRBASE object, building a set of airbases belonging to a coalitions and categories. + -- @param #SET_AIRBASE self + -- @return #SET_AIRBASE self + -- @usage + -- -- Define a new SET_AIRBASE Object. The DatabaseSet will contain a reference to all Airbases. + -- DatabaseSet = SET_AIRBASE:New() + function SET_AIRBASE:New() + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.AIRBASES ) ) + + return self + end + + --- Add an AIRBASE object to SET_AIRBASE. + -- @param Core.Set#SET_AIRBASE self + -- @param Wrapper.Airbase#AIRBASE airbase Airbase that should be added to the set. + -- @return self + function SET_AIRBASE:AddAirbase( airbase ) + + self:Add( airbase:GetName(), airbase ) + + return self + end + + --- Add AIRBASEs to SET_AIRBASE. + -- @param Core.Set#SET_AIRBASE self + -- @param #string AddAirbaseNames A single name or an array of AIRBASE names. + -- @return self + function SET_AIRBASE:AddAirbasesByName( AddAirbaseNames ) + + local AddAirbaseNamesArray = ( type( AddAirbaseNames ) == "table" ) and AddAirbaseNames or { AddAirbaseNames } + + for AddAirbaseID, AddAirbaseName in pairs( AddAirbaseNamesArray ) do + self:Add( AddAirbaseName, AIRBASE:FindByName( AddAirbaseName ) ) + end + + return self + end + + --- Remove AIRBASEs from SET_AIRBASE. + -- @param Core.Set#SET_AIRBASE self + -- @param Wrapper.Airbase#AIRBASE RemoveAirbaseNames A single name or an array of AIRBASE names. + -- @return self + function SET_AIRBASE:RemoveAirbasesByName( RemoveAirbaseNames ) + + local RemoveAirbaseNamesArray = ( type( RemoveAirbaseNames ) == "table" ) and RemoveAirbaseNames or { RemoveAirbaseNames } + + for RemoveAirbaseID, RemoveAirbaseName in pairs( RemoveAirbaseNamesArray ) do + self:Remove( RemoveAirbaseName ) + end + + return self + end + + + --- Finds a Airbase based on the Airbase Name. + -- @param #SET_AIRBASE self + -- @param #string AirbaseName + -- @return Wrapper.Airbase#AIRBASE The found Airbase. + function SET_AIRBASE:FindAirbase( AirbaseName ) + + local AirbaseFound = self.Set[AirbaseName] + return AirbaseFound + end + + + --- Finds an Airbase in range of a coordinate. + -- @param #SET_AIRBASE self + -- @param Core.Point#COORDINATE Coordinate + -- @param #number Range + -- @return Wrapper.Airbase#AIRBASE The found Airbase. + function SET_AIRBASE:FindAirbaseInRange( Coordinate, Range ) + + local AirbaseFound = nil + + for AirbaseName, AirbaseObject in pairs( self.Set ) do + + local AirbaseCoordinate = AirbaseObject:GetCoordinate() + local Distance = Coordinate:Get2DDistance( AirbaseCoordinate ) + + self:F({Distance=Distance}) + + if Distance <= Range then + AirbaseFound = AirbaseObject + break + end + + end + + return AirbaseFound + end + + + --- Finds a random Airbase in the set. + -- @param #SET_AIRBASE self + -- @return Wrapper.Airbase#AIRBASE The found Airbase. + function SET_AIRBASE:GetRandomAirbase() + + local RandomAirbase = self:GetRandom() + self:F( { RandomAirbase = RandomAirbase:GetName() } ) + + return RandomAirbase + end + + + + --- Builds a set of airbases of coalitions. + -- Possible current coalitions are red, blue and neutral. + -- @param #SET_AIRBASE self + -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". + -- @return #SET_AIRBASE self + function SET_AIRBASE:FilterCoalitions( Coalitions ) + if not self.Filter.Coalitions then + self.Filter.Coalitions = {} + end + if type( Coalitions ) ~= "table" then + Coalitions = { Coalitions } + end + for CoalitionID, Coalition in pairs( Coalitions ) do + self.Filter.Coalitions[Coalition] = Coalition + end + return self + end + + + --- Builds a set of airbases out of categories. + -- Possible current categories are plane, helicopter, ground, ship. + -- @param #SET_AIRBASE self + -- @param #string Categories Can take the following values: "airdrome", "helipad", "ship". + -- @return #SET_AIRBASE self + function SET_AIRBASE:FilterCategories( Categories ) + if not self.Filter.Categories then + self.Filter.Categories = {} + end + if type( Categories ) ~= "table" then + Categories = { Categories } + end + for CategoryID, Category in pairs( Categories ) do + self.Filter.Categories[Category] = Category + end + return self + end + + --- Starts the filtering. + -- @param #SET_AIRBASE self + -- @return #SET_AIRBASE self + function SET_AIRBASE:FilterStart() + + if _DATABASE then + + -- We use the BaseCaptured event, which is generated by DCS when a base got captured. + self:HandleEvent( EVENTS.BaseCaptured ) + + -- We initialize the first set. + for ObjectName, Object in pairs( self.Database ) do + if self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + else + self:RemoveAirbasesByName( ObjectName ) + end + end + end + + return self + end + + --- Starts the filtering. + -- @param #SET_AIRBASE self + -- @param Core.Event#EVENT EventData + -- @return #SET_AIRBASE self + function SET_AIRBASE:OnEventBaseCaptured(EventData) + + -- When a base got captured, we reevaluate the set. + for ObjectName, Object in pairs( self.Database ) do + if self:IsIncludeObject( Object ) then + -- We add captured bases on yet in the set. + self:Add( ObjectName, Object ) else - return false + -- We remove captured bases that are not anymore part of the set. + self:RemoveAirbasesByName( ObjectName ) end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence not in a @{Zone}, providing the CLIENT and optional parameters to the called function. --- @param #SET_CLIENT self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter. --- @return #SET_CLIENT self -function SET_CLIENT:ForEachClientNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self:GetSet(), - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Client#CLIENT ClientObject - function( ZoneObject, ClientObject ) - if ClientObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- --- @param #SET_CLIENT self --- @param Wrapper.Client#CLIENT MClient --- @return #SET_CLIENT self -function SET_CLIENT:IsIncludeObject( MClient ) - self:F2( MClient ) - - local MClientInclude = true - - if MClient then - local MClientName = MClient.UnitName - - if self.Filter.Coalitions then - local MClientCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName ) - self:T3( { "Coalition:", ClientCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == ClientCoalitionID then - MClientCoalition = true - end - end - self:T( { "Evaluated Coalition", MClientCoalition } ) - MClientInclude = MClientInclude and MClientCoalition - end - - if self.Filter.Categories then - local MClientCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName ) - self:T3( { "Category:", ClientCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == ClientCategoryID then - MClientCategory = true - end - end - self:T( { "Evaluated Category", MClientCategory } ) - MClientInclude = MClientInclude and MClientCategory - end - - if self.Filter.Types then - local MClientType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T3( { "Type:", MClient:GetTypeName(), TypeName } ) - if TypeName == MClient:GetTypeName() then - MClientType = true - end - end - self:T( { "Evaluated Type", MClientType } ) - MClientInclude = MClientInclude and MClientType - end - - if self.Filter.Countries then - local MClientCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - local ClientCountryID = _DATABASE:GetCountryFromClientTemplate(MClientName) - self:T3( { "Country:", ClientCountryID, country.id[CountryName], CountryName } ) - if country.id[CountryName] and country.id[CountryName] == ClientCountryID then - MClientCountry = true - end - end - self:T( { "Evaluated Country", MClientCountry } ) - MClientInclude = MClientInclude and MClientCountry end - if self.Filter.ClientPrefixes then - local MClientPrefix = false - for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do - self:T3( { "Prefix:", string.find( MClient.UnitName, ClientPrefix, 1 ), ClientPrefix } ) - if string.find( MClient.UnitName, ClientPrefix, 1 ) then - MClientPrefix = true - end - end - self:T( { "Evaluated Prefix", MClientPrefix } ) - MClientInclude = MClientInclude and MClientPrefix - end end - self:T2( MClientInclude ) - return MClientInclude + --- Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_AIRBASE self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the AIRBASE + -- @return #table The AIRBASE + function SET_AIRBASE: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_AIRBASE self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the AIRBASE + -- @return #table The AIRBASE + function SET_AIRBASE:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Iterate the SET_AIRBASE and call an interator function for each AIRBASE, providing the AIRBASE and optional parameters. + -- @param #SET_AIRBASE self + -- @param #function IteratorFunction The function that will be called when there is an alive AIRBASE in the SET_AIRBASE. The function needs to accept a AIRBASE parameter. + -- @return #SET_AIRBASE self + function SET_AIRBASE:ForEachAirbase( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + --- Iterate the SET_AIRBASE while identifying the nearest @{Wrapper.Airbase#AIRBASE} from a @{Core.Point#POINT_VEC2}. + -- @param #SET_AIRBASE self + -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest @{Wrapper.Airbase#AIRBASE}. + -- @return Wrapper.Airbase#AIRBASE The closest @{Wrapper.Airbase#AIRBASE}. + function SET_AIRBASE:FindNearestAirbaseFromPointVec2( PointVec2 ) + self:F2( PointVec2 ) + + local NearestAirbase = self:FindNearestObjectFromPointVec2( PointVec2 ) + return NearestAirbase + end + + + + --- + -- @param #SET_AIRBASE self + -- @param Wrapper.Airbase#AIRBASE MAirbase + -- @return #SET_AIRBASE self + function SET_AIRBASE:IsIncludeObject( MAirbase ) + self:F2( MAirbase ) + + local MAirbaseInclude = true + + if MAirbase then + local MAirbaseName = MAirbase:GetName() + + if self.Filter.Coalitions then + local MAirbaseCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + local AirbaseCoalitionID = _DATABASE:GetCoalitionFromAirbase( MAirbaseName ) + self:T3( { "Coalition:", AirbaseCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == AirbaseCoalitionID then + MAirbaseCoalition = true + end + end + self:T( { "Evaluated Coalition", MAirbaseCoalition } ) + MAirbaseInclude = MAirbaseInclude and MAirbaseCoalition + end + + if self.Filter.Categories then + local MAirbaseCategory = false + for CategoryID, CategoryName in pairs( self.Filter.Categories ) do + local AirbaseCategoryID = _DATABASE:GetCategoryFromAirbase( MAirbaseName ) + self:T3( { "Category:", AirbaseCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) + if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == AirbaseCategoryID then + MAirbaseCategory = true + end + end + self:T( { "Evaluated Category", MAirbaseCategory } ) + MAirbaseInclude = MAirbaseInclude and MAirbaseCategory + end + end + + self:T2( MAirbaseInclude ) + return MAirbaseInclude + end + end ---- SET_PLAYER +do -- SET_CARGO ---- @type SET_PLAYER --- @extends Core.Set#SET_BASE - - - ---- # 4) SET_PLAYER class, extends @{Set#SET_BASE} --- --- Mission designers can use the @{Set#SET_PLAYER} class to build sets of units belonging to alive players: --- --- ## 4.1) SET_PLAYER constructor --- --- Create a new SET_PLAYER object with the @{#SET_PLAYER.New} method: --- --- * @{#SET_PLAYER.New}: Creates a new SET_PLAYER object. --- --- ## 4.3) SET_PLAYER filter criteria --- --- You can set filter criteria to define the set of clients within the SET_PLAYER. --- Filter criteria are defined by: --- --- * @{#SET_PLAYER.FilterCoalitions}: Builds the SET_PLAYER with the clients belonging to the coalition(s). --- * @{#SET_PLAYER.FilterCategories}: Builds the SET_PLAYER with the clients belonging to the category(ies). --- * @{#SET_PLAYER.FilterTypes}: Builds the SET_PLAYER with the clients belonging to the client type(s). --- * @{#SET_PLAYER.FilterCountries}: Builds the SET_PLAYER with the clients belonging to the country(ies). --- * @{#SET_PLAYER.FilterPrefixes}: Builds the SET_PLAYER with the clients starting with the same prefix string(s). --- --- Once the filter criteria have been set for the SET_PLAYER, you can start filtering using: --- --- * @{#SET_PLAYER.FilterStart}: Starts the filtering of the clients within the SET_PLAYER. --- --- Planned filter criteria within development are (so these are not yet available): --- --- * @{#SET_PLAYER.FilterZones}: Builds the SET_PLAYER with the clients within a @{Zone#ZONE}. --- --- ## 4.4) SET_PLAYER iterators --- --- Once the filters have been defined and the SET_PLAYER has been built, you can iterate the SET_PLAYER with the available iterator methods. --- The iterator methods will walk the SET_PLAYER set, and call for each element within the set a function that you provide. --- The following iterator methods are currently available within the SET_PLAYER: --- --- * @{#SET_PLAYER.ForEachClient}: Calls a function for each alive client it finds within the SET_PLAYER. --- --- === --- @field #SET_PLAYER SET_PLAYER -SET_PLAYER = { - ClassName = "SET_PLAYER", - Clients = {}, - Filter = { - Coalitions = nil, - Categories = nil, - Types = nil, - Countries = nil, - ClientPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, + --- @type SET_CARGO + -- @extends Core.Set#SET_BASE + + --- Mission designers can use the @{Core.Set#SET_CARGO} class to build sets of cargos optionally belonging to certain: + -- + -- * Coalitions + -- * Types + -- * Name or Prefix + -- + -- ## SET_CARGO constructor + -- + -- Create a new SET_CARGO object with the @{#SET_CARGO.New} method: + -- + -- * @{#SET_CARGO.New}: Creates a new SET_CARGO object. + -- + -- ## Add or Remove CARGOs from SET_CARGO + -- + -- CARGOs can be added and removed using the @{Core.Set#SET_CARGO.AddCargosByName} and @{Core.Set#SET_CARGO.RemoveCargosByName} respectively. + -- These methods take a single CARGO name or an array of CARGO names to be added or removed from SET_CARGO. + -- + -- ## SET_CARGO filter criteria + -- + -- You can set filter criteria to automatically maintain the SET_CARGO contents. + -- Filter criteria are defined by: + -- + -- * @{#SET_CARGO.FilterCoalitions}: Builds the SET_CARGO with the cargos belonging to the coalition(s). + -- * @{#SET_CARGO.FilterPrefixes}: Builds the SET_CARGO with the cargos containing the prefix string(s). + -- * @{#SET_CARGO.FilterTypes}: Builds the SET_CARGO with the cargos belonging to the cargo type(s). + -- * @{#SET_CARGO.FilterCountries}: Builds the SET_CARGO with the cargos belonging to the country(ies). + -- + -- Once the filter criteria have been set for the SET_CARGO, you can start filtering using: + -- + -- * @{#SET_CARGO.FilterStart}: Starts the filtering of the cargos within the SET_CARGO. + -- + -- ## SET_CARGO iterators + -- + -- Once the filters have been defined and the SET_CARGO has been built, you can iterate the SET_CARGO with the available iterator methods. + -- The iterator methods will walk the SET_CARGO set, and call for each cargo within the set a function that you provide. + -- The following iterator methods are currently available within the SET_CARGO: + -- + -- * @{#SET_CARGO.ForEachCargo}: Calls a function for each cargo it finds within the SET_CARGO. + -- + -- @field #SET_CARGO SET_CARGO + -- + SET_CARGO = { + ClassName = "SET_CARGO", + Cargos = {}, + Filter = { + Coalitions = nil, + Types = nil, + Countries = nil, + ClientPrefixes = nil, }, - Categories = { - plane = Unit.Category.AIRPLANE, - helicopter = Unit.Category.HELICOPTER, - ground = Unit.Category.GROUND_UNIT, - ship = Unit.Category.SHIP, - structure = Unit.Category.STRUCTURE, + FilterMeta = { + Coalitions = { + red = coalition.side.RED, + blue = coalition.side.BLUE, + neutral = coalition.side.NEUTRAL, + }, }, - }, -} - - ---- Creates a new SET_PLAYER object, building a set of clients belonging to a coalitions, categories, countries, types or with defined prefix names. --- @param #SET_PLAYER self --- @return #SET_PLAYER --- @usage --- -- Define a new SET_PLAYER Object. This DBObject will contain a reference to all Clients. --- DBObject = SET_PLAYER:New() -function SET_PLAYER:New() - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.PLAYERS ) ) - - return self -end - ---- Add CLIENT(s) to SET_PLAYER. --- @param Core.Set#SET_PLAYER self --- @param #string AddClientNames A single name or an array of CLIENT names. --- @return self -function SET_PLAYER:AddClientsByName( AddClientNames ) - - local AddClientNamesArray = ( type( AddClientNames ) == "table" ) and AddClientNames or { AddClientNames } + } - for AddClientID, AddClientName in pairs( AddClientNamesArray ) do - self:Add( AddClientName, CLIENT:FindByName( AddClientName ) ) + + --- Creates a new SET_CARGO object, building a set of cargos belonging to a coalitions and categories. + -- @param #SET_CARGO self + -- @return #SET_CARGO + -- @usage + -- -- Define a new SET_CARGO Object. The DatabaseSet will contain a reference to all Cargos. + -- DatabaseSet = SET_CARGO:New() + function SET_CARGO:New() --R2.1 + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CARGOS ) ) -- #SET_CARGO + + return self end + + + --- (R2.1) Add CARGO to SET_CARGO. + -- @param Core.Set#SET_CARGO self + -- @param Cargo.Cargo#CARGO Cargo A single cargo. + -- @return self + function SET_CARGO:AddCargo( Cargo ) --R2.4 + + self:Add( Cargo:GetName(), Cargo ) + + return self + end + + + --- (R2.1) Add CARGOs to SET_CARGO. + -- @param Core.Set#SET_CARGO self + -- @param #string AddCargoNames A single name or an array of CARGO names. + -- @return self + function SET_CARGO:AddCargosByName( AddCargoNames ) --R2.1 + + local AddCargoNamesArray = ( type( AddCargoNames ) == "table" ) and AddCargoNames or { AddCargoNames } - return self -end - ---- Remove CLIENT(s) from SET_PLAYER. --- @param Core.Set#SET_PLAYER self --- @param Wrapper.Client#CLIENT RemoveClientNames A single name or an array of CLIENT names. --- @return self -function SET_PLAYER:RemoveClientsByName( RemoveClientNames ) - - local RemoveClientNamesArray = ( type( RemoveClientNames ) == "table" ) and RemoveClientNames or { RemoveClientNames } - - for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do - self:Remove( RemoveClientName.ClientName ) + for AddCargoID, AddCargoName in pairs( AddCargoNamesArray ) do + self:Add( AddCargoName, CARGO:FindByName( AddCargoName ) ) + end + + return self end + + --- (R2.1) Remove CARGOs from SET_CARGO. + -- @param Core.Set#SET_CARGO self + -- @param Wrapper.Cargo#CARGO RemoveCargoNames A single name or an array of CARGO names. + -- @return self + function SET_CARGO:RemoveCargosByName( RemoveCargoNames ) --R2.1 + + local RemoveCargoNamesArray = ( type( RemoveCargoNames ) == "table" ) and RemoveCargoNames or { RemoveCargoNames } - return self -end - - ---- Finds a Client based on the Player Name. --- @param #SET_PLAYER self --- @param #string PlayerName --- @return Wrapper.Client#CLIENT The found Client. -function SET_PLAYER:FindClient( PlayerName ) - - local ClientFound = self.Set[PlayerName] - return ClientFound -end - - - ---- Builds a set of clients of coalitions joined by specific players. --- Possible current coalitions are red, blue and neutral. --- @param #SET_PLAYER self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_PLAYER self -function SET_PLAYER:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of clients out of categories joined by players. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_PLAYER self --- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". --- @return #SET_PLAYER self -function SET_PLAYER:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - - ---- Builds a set of clients of defined client types joined by players. --- Possible current types are those types known within DCS world. --- @param #SET_PLAYER self --- @param #string Types Can take those type strings known within DCS world. --- @return #SET_PLAYER self -function SET_PLAYER:FilterTypes( Types ) - if not self.Filter.Types then - self.Filter.Types = {} - end - if type( Types ) ~= "table" then - Types = { Types } - end - for TypeID, Type in pairs( Types ) do - self.Filter.Types[Type] = Type - end - return self -end - - ---- Builds a set of clients of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_PLAYER self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_PLAYER self -function SET_PLAYER:FilterCountries( Countries ) - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- Builds a set of clients of defined client prefixes. --- All the clients starting with the given prefixes will be included within the set. --- @param #SET_PLAYER self --- @param #string Prefixes The prefix of which the client name starts with. --- @return #SET_PLAYER self -function SET_PLAYER:FilterPrefixes( Prefixes ) - if not self.Filter.ClientPrefixes then - self.Filter.ClientPrefixes = {} - end - if type( Prefixes ) ~= "table" then - Prefixes = { Prefixes } - end - for PrefixID, Prefix in pairs( Prefixes ) do - self.Filter.ClientPrefixes[Prefix] = Prefix - end - return self -end - - - - ---- Starts the filtering. --- @param #SET_PLAYER self --- @return #SET_PLAYER self -function SET_PLAYER:FilterStart() - - if _DATABASE then - self:_FilterStart() + for RemoveCargoID, RemoveCargoName in pairs( RemoveCargoNamesArray ) do + self:Remove( RemoveCargoName.CargoName ) + end + + return self end - return self -end - ---- Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_PLAYER self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the CLIENT --- @return #table The CLIENT -function SET_PLAYER:AddInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_PLAYER self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the CLIENT --- @return #table The CLIENT -function SET_PLAYER:FindInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Iterate the SET_PLAYER and call an interator function for each **alive** CLIENT, providing the CLIENT and optional parameters. --- @param #SET_PLAYER self --- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter. --- @return #SET_PLAYER self -function SET_PLAYER:ForEachPlayer( IteratorFunction, ... ) - self:F2( arg ) - self:ForEach( IteratorFunction, arg, self:GetSet() ) - - return self -end - ---- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT presence completely in a @{Zone}, providing the CLIENT and optional parameters to the called function. --- @param #SET_PLAYER self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter. --- @return #SET_PLAYER self -function SET_PLAYER:ForEachPlayerInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) + --- (R2.1) Finds a Cargo based on the Cargo Name. + -- @param #SET_CARGO self + -- @param #string CargoName + -- @return Wrapper.Cargo#CARGO The found Cargo. + function SET_CARGO:FindCargo( CargoName ) --R2.1 - self:ForEach( IteratorFunction, arg, self:GetSet(), - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Client#CLIENT ClientObject - function( ZoneObject, ClientObject ) - if ClientObject:IsInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT presence not in a @{Zone}, providing the CLIENT and optional parameters to the called function. --- @param #SET_PLAYER self --- @param Core.Zone#ZONE ZoneObject The Zone to be tested for. --- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter. --- @return #SET_PLAYER self -function SET_PLAYER:ForEachPlayerNotInZone( ZoneObject, IteratorFunction, ... ) - self:F2( arg ) + local CargoFound = self.Set[CargoName] + return CargoFound + end - self:ForEach( IteratorFunction, arg, self:GetSet(), - --- @param Core.Zone#ZONE_BASE ZoneObject - -- @param Wrapper.Client#CLIENT ClientObject - function( ZoneObject, ClientObject ) - if ClientObject:IsNotInZone( ZoneObject ) then - return true - else - return false - end - end, { ZoneObject } ) - - return self -end - ---- --- @param #SET_PLAYER self --- @param Wrapper.Client#CLIENT MClient --- @return #SET_PLAYER self -function SET_PLAYER:IsIncludeObject( MClient ) - self:F2( MClient ) - - local MClientInclude = true - - if MClient then - local MClientName = MClient.UnitName - if self.Filter.Coalitions then - local MClientCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName ) - self:T3( { "Coalition:", ClientCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == ClientCoalitionID then - MClientCoalition = true - end - end - self:T( { "Evaluated Coalition", MClientCoalition } ) - MClientInclude = MClientInclude and MClientCoalition + + --- (R2.1) Builds a set of cargos of coalitions. + -- Possible current coalitions are red, blue and neutral. + -- @param #SET_CARGO self + -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". + -- @return #SET_CARGO self + function SET_CARGO:FilterCoalitions( Coalitions ) --R2.1 + if not self.Filter.Coalitions then + self.Filter.Coalitions = {} + end + if type( Coalitions ) ~= "table" then + Coalitions = { Coalitions } + end + for CoalitionID, Coalition in pairs( Coalitions ) do + self.Filter.Coalitions[Coalition] = Coalition + end + return self + end + + --- (R2.1) Builds a set of cargos of defined cargo types. + -- Possible current types are those types known within DCS world. + -- @param #SET_CARGO self + -- @param #string Types Can take those type strings known within DCS world. + -- @return #SET_CARGO self + function SET_CARGO:FilterTypes( Types ) --R2.1 + if not self.Filter.Types then + self.Filter.Types = {} + end + if type( Types ) ~= "table" then + Types = { Types } + end + for TypeID, Type in pairs( Types ) do + self.Filter.Types[Type] = Type + end + return self + end + + + --- (R2.1) Builds a set of cargos of defined countries. + -- Possible current countries are those known within DCS world. + -- @param #SET_CARGO self + -- @param #string Countries Can take those country strings known within DCS world. + -- @return #SET_CARGO self + function SET_CARGO:FilterCountries( Countries ) --R2.1 + if not self.Filter.Countries then + self.Filter.Countries = {} + end + if type( Countries ) ~= "table" then + Countries = { Countries } + end + for CountryID, Country in pairs( Countries ) do + self.Filter.Countries[Country] = Country + end + return self + end + + + --- (R2.1) Builds a set of cargos of defined cargo prefixes. + -- All the cargos starting with the given prefixes will be included within the set. + -- @param #SET_CARGO self + -- @param #string Prefixes The prefix of which the cargo name starts with. + -- @return #SET_CARGO self + function SET_CARGO:FilterPrefixes( Prefixes ) --R2.1 + if not self.Filter.CargoPrefixes then + self.Filter.CargoPrefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.CargoPrefixes[Prefix] = Prefix + end + return self + end + + + + --- (R2.1) Starts the filtering. + -- @param #SET_CARGO self + -- @return #SET_CARGO self + function SET_CARGO:FilterStart() --R2.1 + + if _DATABASE then + self:_FilterStart() + self:HandleEvent( EVENTS.NewCargo ) + self:HandleEvent( EVENTS.DeleteCargo ) end - if self.Filter.Categories then - local MClientCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName ) - self:T3( { "Category:", ClientCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == ClientCategoryID then - MClientCategory = true - end - end - self:T( { "Evaluated Category", MClientCategory } ) - MClientInclude = MClientInclude and MClientCategory - end - - if self.Filter.Types then - local MClientType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T3( { "Type:", MClient:GetTypeName(), TypeName } ) - if TypeName == MClient:GetTypeName() then - MClientType = true - end - end - self:T( { "Evaluated Type", MClientType } ) - MClientInclude = MClientInclude and MClientType - end - - if self.Filter.Countries then - local MClientCountry = false - for CountryID, CountryName in pairs( self.Filter.Countries ) do - local ClientCountryID = _DATABASE:GetCountryFromClientTemplate(MClientName) - self:T3( { "Country:", ClientCountryID, country.id[CountryName], CountryName } ) - if country.id[CountryName] and country.id[CountryName] == ClientCountryID then - MClientCountry = true - end - end - self:T( { "Evaluated Country", MClientCountry } ) - MClientInclude = MClientInclude and MClientCountry - end + return self + end - if self.Filter.ClientPrefixes then - local MClientPrefix = false - for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do - self:T3( { "Prefix:", string.find( MClient.UnitName, ClientPrefix, 1 ), ClientPrefix } ) - if string.find( MClient.UnitName, ClientPrefix, 1 ) then - MClientPrefix = true - end + --- Stops the filtering for the defined collection. + -- @param #SET_CARGO self + -- @return #SET_CARGO self + function SET_CARGO:FilterStop() + + self:UnHandleEvent( EVENTS.NewCargo ) + self:UnHandleEvent( EVENTS.DeleteCargo ) + + return self + end + + + --- (R2.1) Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_CARGO self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the CARGO + -- @return #table The CARGO + function SET_CARGO:AddInDatabase( Event ) --R2.1 + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- (R2.1) Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_CARGO self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the CARGO + -- @return #table The CARGO + function SET_CARGO:FindInDatabase( Event ) --R2.1 + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- (R2.1) Iterate the SET_CARGO and call an interator function for each CARGO, providing the CARGO and optional parameters. + -- @param #SET_CARGO self + -- @param #function IteratorFunction The function that will be called when there is an alive CARGO in the SET_CARGO. The function needs to accept a CARGO parameter. + -- @return #SET_CARGO self + function SET_CARGO:ForEachCargo( IteratorFunction, ... ) --R2.1 + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + --- (R2.1) Iterate the SET_CARGO while identifying the nearest @{Cargo.Cargo#CARGO} from a @{Core.Point#POINT_VEC2}. + -- @param #SET_CARGO self + -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest @{Cargo.Cargo#CARGO}. + -- @return Wrapper.Cargo#CARGO The closest @{Cargo.Cargo#CARGO}. + function SET_CARGO:FindNearestCargoFromPointVec2( PointVec2 ) --R2.1 + self:F2( PointVec2 ) + + local NearestCargo = self:FindNearestObjectFromPointVec2( PointVec2 ) + return NearestCargo + end + + function SET_CARGO:FirstCargoWithState( State ) + + local FirstCargo = nil + + for CargoName, Cargo in pairs( self.Set ) do + if Cargo:Is( State ) then + FirstCargo = Cargo + break + end + end + + return FirstCargo + end + + function SET_CARGO:FirstCargoWithStateAndNotDeployed( State ) + + local FirstCargo = nil + + for CargoName, Cargo in pairs( self.Set ) do + if Cargo:Is( State ) and not Cargo:IsDeployed() then + FirstCargo = Cargo + break + end + end + + return FirstCargo + end + + + --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is UnLoaded. + -- @param #SET_CARGO self + -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. + function SET_CARGO:FirstCargoUnLoaded() + local FirstCargo = self:FirstCargoWithState( "UnLoaded" ) + return FirstCargo + end + + + --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is UnLoaded and not Deployed. + -- @param #SET_CARGO self + -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. + function SET_CARGO:FirstCargoUnLoadedAndNotDeployed() + local FirstCargo = self:FirstCargoWithStateAndNotDeployed( "UnLoaded" ) + return FirstCargo + end + + + --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is Loaded. + -- @param #SET_CARGO self + -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. + function SET_CARGO:FirstCargoLoaded() + local FirstCargo = self:FirstCargoWithState( "Loaded" ) + return FirstCargo + end + + + --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is Deployed. + -- @param #SET_CARGO self + -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}. + function SET_CARGO:FirstCargoDeployed() + local FirstCargo = self:FirstCargoWithState( "Deployed" ) + return FirstCargo + end + + + + + --- (R2.1) + -- @param #SET_CARGO self + -- @param AI.AI_Cargo#AI_CARGO MCargo + -- @return #SET_CARGO self + function SET_CARGO:IsIncludeObject( MCargo ) --R2.1 + self:F2( MCargo ) + + local MCargoInclude = true + + if MCargo then + local MCargoName = MCargo:GetName() + + if self.Filter.Coalitions then + local MCargoCoalition = false + for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do + local CargoCoalitionID = MCargo:GetCoalition() + self:T3( { "Coalition:", CargoCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) + if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == CargoCoalitionID then + MCargoCoalition = true + end + end + self:F( { "Evaluated Coalition", MCargoCoalition } ) + MCargoInclude = MCargoInclude and MCargoCoalition + end + + if self.Filter.Types then + local MCargoType = false + for TypeID, TypeName in pairs( self.Filter.Types ) do + self:T3( { "Type:", MCargo:GetType(), TypeName } ) + if TypeName == MCargo:GetType() then + MCargoType = true + end + end + self:F( { "Evaluated Type", MCargoType } ) + MCargoInclude = MCargoInclude and MCargoType + end + + if self.Filter.CargoPrefixes then + local MCargoPrefix = false + for CargoPrefixId, CargoPrefix in pairs( self.Filter.CargoPrefixes ) do + self:T3( { "Prefix:", string.find( MCargo.Name, CargoPrefix, 1 ), CargoPrefix } ) + if string.find( MCargo.Name, CargoPrefix, 1 ) then + MCargoPrefix = true + end + end + self:F( { "Evaluated Prefix", MCargoPrefix } ) + MCargoInclude = MCargoInclude and MCargoPrefix + end + end + + self:T2( MCargoInclude ) + return MCargoInclude + end + + --- (R2.1) Handles the OnEventNewCargo event for the Set. + -- @param #SET_CARGO self + -- @param Core.Event#EVENTDATA EventData + function SET_CARGO:OnEventNewCargo( EventData ) --R2.1 + + self:F( { "New Cargo", EventData } ) + + if EventData.Cargo then + if EventData.Cargo and self:IsIncludeObject( EventData.Cargo ) then + self:Add( EventData.Cargo.Name , EventData.Cargo ) end - self:T( { "Evaluated Prefix", MClientPrefix } ) - MClientInclude = MClientInclude and MClientPrefix end end - self:T2( MClientInclude ) - return MClientInclude + --- (R2.1) Handles the OnDead or OnCrash event for alive units set. + -- @param #SET_CARGO self + -- @param Core.Event#EVENTDATA EventData + function SET_CARGO:OnEventDeleteCargo( EventData ) --R2.1 + self:F3( { EventData } ) + + if EventData.Cargo then + local Cargo = _DATABASE:FindCargo( EventData.Cargo.Name ) + if Cargo and Cargo.Name then + + -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. + -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. + -- And this is a problem because it will remove all entries from the SET_CARGOs. + -- To prevent this from happening, the Cargo object has a flag NoDestroy. + -- When true, the SET_CARGO won't Remove the Cargo object from the set. + -- This flag is switched off after the event handlers have been called in the EVENT class. + self:F( { CargoNoDestroy=Cargo.NoDestroy } ) + if Cargo.NoDestroy then + else + self:Remove( Cargo.Name ) + end + end + end + end + end ---- @type SET_AIRBASE --- @extends Core.Set#SET_BASE ---- # 5) SET_AIRBASE class, extends @{Set#SET_BASE} --- --- Mission designers can use the @{Set#SET_AIRBASE} class to build sets of airbases optionally belonging to certain: --- --- * Coalitions --- --- ## 5.1) SET_AIRBASE constructor --- --- Create a new SET_AIRBASE object with the @{#SET_AIRBASE.New} method: --- --- * @{#SET_AIRBASE.New}: Creates a new SET_AIRBASE object. --- --- ## 5.2) Add or Remove AIRBASEs from SET_AIRBASE --- --- AIRBASEs can be added and removed using the @{Set#SET_AIRBASE.AddAirbasesByName} and @{Set#SET_AIRBASE.RemoveAirbasesByName} respectively. --- These methods take a single AIRBASE name or an array of AIRBASE names to be added or removed from SET_AIRBASE. --- --- ## 5.3) SET_AIRBASE filter criteria --- --- You can set filter criteria to define the set of clients within the SET_AIRBASE. --- Filter criteria are defined by: --- --- * @{#SET_AIRBASE.FilterCoalitions}: Builds the SET_AIRBASE with the airbases belonging to the coalition(s). --- --- Once the filter criteria have been set for the SET_AIRBASE, you can start filtering using: --- --- * @{#SET_AIRBASE.FilterStart}: Starts the filtering of the airbases within the SET_AIRBASE. --- --- ## 5.4) SET_AIRBASE iterators --- --- Once the filters have been defined and the SET_AIRBASE has been built, you can iterate the SET_AIRBASE with the available iterator methods. --- The iterator methods will walk the SET_AIRBASE set, and call for each airbase within the set a function that you provide. --- The following iterator methods are currently available within the SET_AIRBASE: --- --- * @{#SET_AIRBASE.ForEachAirbase}: Calls a function for each airbase it finds within the SET_AIRBASE. --- --- === --- @field #SET_AIRBASE SET_AIRBASE -SET_AIRBASE = { - ClassName = "SET_AIRBASE", - Airbases = {}, - Filter = { - Coalitions = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, +do -- SET_ZONE + + --- @type SET_ZONE + -- @extends Core.Set#SET_BASE + + --- Mission designers can use the @{Core.Set#SET_ZONE} class to build sets of zones of various types. + -- + -- ## SET_ZONE constructor + -- + -- Create a new SET_ZONE object with the @{#SET_ZONE.New} method: + -- + -- * @{#SET_ZONE.New}: Creates a new SET_ZONE object. + -- + -- ## Add or Remove ZONEs from SET_ZONE + -- + -- ZONEs can be added and removed using the @{Core.Set#SET_ZONE.AddZonesByName} and @{Core.Set#SET_ZONE.RemoveZonesByName} respectively. + -- These methods take a single ZONE name or an array of ZONE names to be added or removed from SET_ZONE. + -- + -- ## SET_ZONE filter criteria + -- + -- You can set filter criteria to build the collection of zones in SET_ZONE. + -- Filter criteria are defined by: + -- + -- * @{#SET_ZONE.FilterPrefixes}: Builds the SET_ZONE with the zones having a certain text pattern of prefix. + -- + -- Once the filter criteria have been set for the SET_ZONE, you can start filtering using: + -- + -- * @{#SET_ZONE.FilterStart}: Starts the filtering of the zones within the SET_ZONE. + -- + -- ## SET_ZONE iterators + -- + -- Once the filters have been defined and the SET_ZONE has been built, you can iterate the SET_ZONE with the available iterator methods. + -- The iterator methods will walk the SET_ZONE set, and call for each airbase within the set a function that you provide. + -- The following iterator methods are currently available within the SET_ZONE: + -- + -- * @{#SET_ZONE.ForEachZone}: Calls a function for each zone it finds within the SET_ZONE. + -- + -- === + -- @field #SET_ZONE SET_ZONE + SET_ZONE = { + ClassName = "SET_ZONE", + Zones = {}, + Filter = { + Prefixes = nil, }, - Categories = { - airdrome = Airbase.Category.AIRDROME, - helipad = Airbase.Category.HELIPAD, - ship = Airbase.Category.SHIP, + FilterMeta = { }, - }, -} - - ---- Creates a new SET_AIRBASE object, building a set of airbases belonging to a coalitions and categories. --- @param #SET_AIRBASE self --- @return #SET_AIRBASE self --- @usage --- -- Define a new SET_AIRBASE Object. The DatabaseSet will contain a reference to all Airbases. --- DatabaseSet = SET_AIRBASE:New() -function SET_AIRBASE:New() - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.AIRBASES ) ) - - return self -end - ---- Add AIRBASEs to SET_AIRBASE. --- @param Core.Set#SET_AIRBASE self --- @param #string AddAirbaseNames A single name or an array of AIRBASE names. --- @return self -function SET_AIRBASE:AddAirbasesByName( AddAirbaseNames ) - - local AddAirbaseNamesArray = ( type( AddAirbaseNames ) == "table" ) and AddAirbaseNames or { AddAirbaseNames } + } - for AddAirbaseID, AddAirbaseName in pairs( AddAirbaseNamesArray ) do - self:Add( AddAirbaseName, AIRBASE:FindByName( AddAirbaseName ) ) + + --- Creates a new SET_ZONE object, building a set of zones. + -- @param #SET_ZONE self + -- @return #SET_ZONE self + -- @usage + -- -- Define a new SET_ZONE Object. The DatabaseSet will contain a reference to all Zones. + -- DatabaseSet = SET_ZONE:New() + function SET_ZONE:New() + -- Inherits from BASE + local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.ZONES ) ) + + return self end + + --- Add ZONEs by a search name to SET_ZONE. + -- @param Core.Set#SET_ZONE self + -- @param #string AddZoneNames A single name or an array of ZONE_BASE names. + -- @return self + function SET_ZONE:AddZonesByName( AddZoneNames ) + + local AddZoneNamesArray = ( type( AddZoneNames ) == "table" ) and AddZoneNames or { AddZoneNames } - return self -end - ---- Remove AIRBASEs from SET_AIRBASE. --- @param Core.Set#SET_AIRBASE self --- @param Wrapper.Airbase#AIRBASE RemoveAirbaseNames A single name or an array of AIRBASE names. --- @return self -function SET_AIRBASE:RemoveAirbasesByName( RemoveAirbaseNames ) - - local RemoveAirbaseNamesArray = ( type( RemoveAirbaseNames ) == "table" ) and RemoveAirbaseNames or { RemoveAirbaseNames } - - for RemoveAirbaseID, RemoveAirbaseName in pairs( RemoveAirbaseNamesArray ) do - self:Remove( RemoveAirbaseName.AirbaseName ) + for AddAirbaseID, AddZoneName in pairs( AddZoneNamesArray ) do + self:Add( AddZoneName, ZONE:FindByName( AddZoneName ) ) + end + + return self end + + --- Add ZONEs to SET_ZONE. + -- @param Core.Set#SET_ZONE self + -- @param Core.Zone#ZONE_BASE Zone A ZONE_BASE object. + -- @return self + function SET_ZONE:AddZone( Zone ) + + self:Add( Zone:GetName(), Zone ) + + return self + end + + + --- Remove ZONEs from SET_ZONE. + -- @param Core.Set#SET_ZONE self + -- @param Core.Zone#ZONE_BASE RemoveZoneNames A single name or an array of ZONE_BASE names. + -- @return self + function SET_ZONE:RemoveZonesByName( RemoveZoneNames ) + + local RemoveZoneNamesArray = ( type( RemoveZoneNames ) == "table" ) and RemoveZoneNames or { RemoveZoneNames } - return self -end - - ---- Finds a Airbase based on the Airbase Name. --- @param #SET_AIRBASE self --- @param #string AirbaseName --- @return Wrapper.Airbase#AIRBASE The found Airbase. -function SET_AIRBASE:FindAirbase( AirbaseName ) - - local AirbaseFound = self.Set[AirbaseName] - return AirbaseFound -end - - - ---- Builds a set of airbases of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_AIRBASE self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_AIRBASE self -function SET_AIRBASE:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - - ---- Builds a set of airbases out of categories. --- Possible current categories are plane, helicopter, ground, ship. --- @param #SET_AIRBASE self --- @param #string Categories Can take the following values: "airdrome", "helipad", "ship". --- @return #SET_AIRBASE self -function SET_AIRBASE:FilterCategories( Categories ) - if not self.Filter.Categories then - self.Filter.Categories = {} - end - if type( Categories ) ~= "table" then - Categories = { Categories } - end - for CategoryID, Category in pairs( Categories ) do - self.Filter.Categories[Category] = Category - end - return self -end - ---- Starts the filtering. --- @param #SET_AIRBASE self --- @return #SET_AIRBASE self -function SET_AIRBASE:FilterStart() - - if _DATABASE then - self:_FilterStart() + for RemoveZoneID, RemoveZoneName in pairs( RemoveZoneNamesArray ) do + self:Remove( RemoveZoneName ) + end + + return self end - return self -end - - ---- Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_AIRBASE self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the AIRBASE --- @return #table The AIRBASE -function SET_AIRBASE: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_AIRBASE self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the AIRBASE --- @return #table The AIRBASE -function SET_AIRBASE:FindInDatabase( Event ) - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- Iterate the SET_AIRBASE and call an interator function for each AIRBASE, providing the AIRBASE and optional parameters. --- @param #SET_AIRBASE self --- @param #function IteratorFunction The function that will be called when there is an alive AIRBASE in the SET_AIRBASE. The function needs to accept a AIRBASE parameter. --- @return #SET_AIRBASE self -function SET_AIRBASE:ForEachAirbase( IteratorFunction, ... ) - self:F2( arg ) - self:ForEach( IteratorFunction, arg, self:GetSet() ) - - return self -end - ---- Iterate the SET_AIRBASE while identifying the nearest @{Airbase#AIRBASE} from a @{Point#POINT_VEC2}. --- @param #SET_AIRBASE self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest @{Airbase#AIRBASE}. --- @return Wrapper.Airbase#AIRBASE The closest @{Airbase#AIRBASE}. -function SET_AIRBASE:FindNearestAirbaseFromPointVec2( PointVec2 ) - self:F2( PointVec2 ) + --- Finds a Zone based on the Zone Name. + -- @param #SET_ZONE self + -- @param #string ZoneName + -- @return Core.Zone#ZONE_BASE The found Zone. + function SET_ZONE:FindZone( ZoneName ) - local NearestAirbase = self:FindNearestObjectFromPointVec2( PointVec2 ) - return NearestAirbase -end - - - ---- --- @param #SET_AIRBASE self --- @param Wrapper.Airbase#AIRBASE MAirbase --- @return #SET_AIRBASE self -function SET_AIRBASE:IsIncludeObject( MAirbase ) - self:F2( MAirbase ) - - local MAirbaseInclude = true - - if MAirbase then - local MAirbaseName = MAirbase:GetName() + local ZoneFound = self.Set[ZoneName] + return ZoneFound + end - if self.Filter.Coalitions then - local MAirbaseCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - local AirbaseCoalitionID = _DATABASE:GetCoalitionFromAirbase( MAirbaseName ) - self:T3( { "Coalition:", AirbaseCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == AirbaseCoalitionID then - MAirbaseCoalition = true + + --- Get a random zone from the set. + -- @param #SET_ZONE self + -- @return Core.Zone#ZONE_BASE The random Zone. + -- @return #nil if no zone in the collection. + function SET_ZONE: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 self + -- @param #string ZoneName The name of the zone. + function SET_ZONE:SetZoneProbability( ZoneName, ZoneProbability ) + local Zone = self:FindZone( ZoneName ) + Zone:SetZoneProbability( ZoneProbability ) + end + + + + + --- Builds a set of zones of defined zone prefixes. + -- All the zones starting with the given prefixes will be included within the set. + -- @param #SET_ZONE self + -- @param #string Prefixes The prefix of which the zone name starts with. + -- @return #SET_ZONE self + function SET_ZONE:FilterPrefixes( Prefixes ) + if not self.Filter.Prefixes then + self.Filter.Prefixes = {} + end + if type( Prefixes ) ~= "table" then + Prefixes = { Prefixes } + end + for PrefixID, Prefix in pairs( Prefixes ) do + self.Filter.Prefixes[Prefix] = Prefix + end + return self + end + + + --- Starts the filtering. + -- @param #SET_ZONE self + -- @return #SET_ZONE self + function SET_ZONE:FilterStart() + + if _DATABASE then + + -- We initialize the first set. + for ObjectName, Object in pairs( self.Database ) do + if self:IsIncludeObject( Object ) then + self:Add( ObjectName, Object ) + else + self:RemoveZonesByName( ObjectName ) end end - self:T( { "Evaluated Coalition", MAirbaseCoalition } ) - MAirbaseInclude = MAirbaseInclude and MAirbaseCoalition end + + self:HandleEvent( EVENTS.NewZone ) + self:HandleEvent( EVENTS.DeleteZone ) - if self.Filter.Categories then - local MAirbaseCategory = false - for CategoryID, CategoryName in pairs( self.Filter.Categories ) do - local AirbaseCategoryID = _DATABASE:GetCategoryFromAirbase( MAirbaseName ) - self:T3( { "Category:", AirbaseCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } ) - if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == AirbaseCategoryID then - MAirbaseCategory = true + return self + end + + --- Stops the filtering for the defined collection. + -- @param #SET_ZONE self + -- @return #SET_ZONE self + function SET_ZONE:FilterStop() + + self:UnHandleEvent( EVENTS.NewZone ) + self:UnHandleEvent( EVENTS.DeleteZone ) + + return self + end + + --- Handles the Database to check on an event (birth) that the Object was added in the Database. + -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! + -- @param #SET_ZONE self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the AIRBASE + -- @return #table The AIRBASE + function SET_ZONE:AddInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Handles the Database to check on any event that Object exists in the Database. + -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! + -- @param #SET_ZONE self + -- @param Core.Event#EVENTDATA Event + -- @return #string The name of the AIRBASE + -- @return #table The AIRBASE + function SET_ZONE:FindInDatabase( Event ) + self:F3( { Event } ) + + return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] + end + + --- Iterate the SET_ZONE and call an interator function for each ZONE, providing the ZONE and optional parameters. + -- @param #SET_ZONE self + -- @param #function IteratorFunction The function that will be called when there is an alive ZONE in the SET_ZONE. The function needs to accept a AIRBASE parameter. + -- @return #SET_ZONE self + function SET_ZONE:ForEachZone( IteratorFunction, ... ) + self:F2( arg ) + + self:ForEach( IteratorFunction, arg, self:GetSet() ) + + return self + end + + + --- + -- @param #SET_ZONE self + -- @param Core.Zone#ZONE_BASE MZone + -- @return #SET_ZONE self + function SET_ZONE:IsIncludeObject( MZone ) + self:F2( MZone ) + + local MZoneInclude = true + + if MZone then + local MZoneName = MZone:GetName() + + if self.Filter.Prefixes then + local MZonePrefix = false + for ZonePrefixId, ZonePrefix in pairs( self.Filter.Prefixes ) do + self:T3( { "Prefix:", string.find( MZoneName, ZonePrefix, 1 ), ZonePrefix } ) + if string.find( MZoneName, ZonePrefix, 1 ) then + MZonePrefix = true + end + end + self:T( { "Evaluated Prefix", MZonePrefix } ) + MZoneInclude = MZoneInclude and MZonePrefix + end + end + + self:T2( MZoneInclude ) + return MZoneInclude + end + + --- Handles the OnEventNewZone event for the Set. + -- @param #SET_ZONE self + -- @param Core.Event#EVENTDATA EventData + function SET_ZONE:OnEventNewZone( EventData ) --R2.1 + + self:F( { "New Zone", EventData } ) + + if EventData.Zone then + if EventData.Zone and self:IsIncludeObject( EventData.Zone ) then + self:Add( EventData.Zone.ZoneName , EventData.Zone ) + end + end + end + + --- Handles the OnDead or OnCrash event for alive units set. + -- @param #SET_ZONE self + -- @param Core.Event#EVENTDATA EventData + function SET_ZONE:OnEventDeleteZone( EventData ) --R2.1 + self:F3( { EventData } ) + + if EventData.Zone then + local Zone = _DATABASE:FindZone( EventData.Zone.ZoneName ) + if Zone and Zone.ZoneName then + + -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD. + -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call. + -- And this is a problem because it will remove all entries from the SET_ZONEs. + -- To prevent this from happening, the Zone object has a flag NoDestroy. + -- When true, the SET_ZONE won't Remove the Zone object from the set. + -- This flag is switched off after the event handlers have been called in the EVENT class. + self:F( { ZoneNoDestroy=Zone.NoDestroy } ) + if Zone.NoDestroy then + else + self:Remove( Zone.ZoneName ) end end - self:T( { "Evaluated Category", MAirbaseCategory } ) - MAirbaseInclude = MAirbaseInclude and MAirbaseCategory end end - - self:T2( MAirbaseInclude ) - return MAirbaseInclude -end - ---- @type SET_CARGO --- @extends Core.Set#SET_BASE - ---- # (R2.1) SET_CARGO class, extends @{Set#SET_BASE} --- --- Mission designers can use the @{Set#SET_CARGO} class to build sets of cargos optionally belonging to certain: --- --- * Coalitions --- * Types --- * Name or Prefix --- --- ## SET_CARGO constructor --- --- Create a new SET_CARGO object with the @{#SET_CARGO.New} method: --- --- * @{#SET_CARGO.New}: Creates a new SET_CARGO object. --- --- ## Add or Remove CARGOs from SET_CARGO --- --- CARGOs can be added and removed using the @{Set#SET_CARGO.AddCargosByName} and @{Set#SET_CARGO.RemoveCargosByName} respectively. --- These methods take a single CARGO name or an array of CARGO names to be added or removed from SET_CARGO. --- --- ## SET_CARGO filter criteria --- --- You can set filter criteria to automatically maintain the SET_CARGO contents. --- Filter criteria are defined by: --- --- * @{#SET_CARGO.FilterCoalitions}: Builds the SET_CARGO with the cargos belonging to the coalition(s). --- * @{#SET_CARGO.FilterPrefixes}: Builds the SET_CARGO with the cargos containing the prefix string(s). --- * @{#SET_CARGO.FilterTypes}: Builds the SET_CARGO with the cargos belonging to the cargo type(s). --- * @{#SET_CARGO.FilterCountries}: Builds the SET_CARGO with the cargos belonging to the country(ies). --- --- Once the filter criteria have been set for the SET_CARGO, you can start filtering using: --- --- * @{#SET_CARGO.FilterStart}: Starts the filtering of the cargos within the SET_CARGO. --- --- ## SET_CARGO iterators --- --- Once the filters have been defined and the SET_CARGO has been built, you can iterate the SET_CARGO with the available iterator methods. --- The iterator methods will walk the SET_CARGO set, and call for each cargo within the set a function that you provide. --- The following iterator methods are currently available within the SET_CARGO: --- --- * @{#SET_CARGO.ForEachCargo}: Calls a function for each cargo it finds within the SET_CARGO. --- --- @field #SET_CARGO SET_CARGO --- -SET_CARGO = { - ClassName = "SET_CARGO", - Cargos = {}, - Filter = { - Coalitions = nil, - Types = nil, - Countries = nil, - ClientPrefixes = nil, - }, - FilterMeta = { - Coalitions = { - red = coalition.side.RED, - blue = coalition.side.BLUE, - neutral = coalition.side.NEUTRAL, - }, - }, -} - - ---- (R2.1) Creates a new SET_CARGO object, building a set of cargos belonging to a coalitions and categories. --- @param #SET_CARGO self --- @return #SET_CARGO --- @usage --- -- Define a new SET_CARGO Object. The DatabaseSet will contain a reference to all Cargos. --- DatabaseSet = SET_CARGO:New() -function SET_CARGO:New() --R2.1 - -- Inherits from BASE - local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CARGOS ) ) -- #SET_CARGO - - return self -end - ---- (R2.1) Add CARGOs to SET_CARGO. --- @param Core.Set#SET_CARGO self --- @param #string AddCargoNames A single name or an array of CARGO names. --- @return self -function SET_CARGO:AddCargosByName( AddCargoNames ) --R2.1 - - local AddCargoNamesArray = ( type( AddCargoNames ) == "table" ) and AddCargoNames or { AddCargoNames } - for AddCargoID, AddCargoName in pairs( AddCargoNamesArray ) do - self:Add( AddCargoName, CARGO:FindByName( AddCargoName ) ) - end - - return self -end - ---- (R2.1) Remove CARGOs from SET_CARGO. --- @param Core.Set#SET_CARGO self --- @param Wrapper.Cargo#CARGO RemoveCargoNames A single name or an array of CARGO names. --- @return self -function SET_CARGO:RemoveCargosByName( RemoveCargoNames ) --R2.1 - - local RemoveCargoNamesArray = ( type( RemoveCargoNames ) == "table" ) and RemoveCargoNames or { RemoveCargoNames } + --- Validate if a coordinate is in one of the zones in the set. + -- Returns the ZONE object where the coordiante is located. + -- If zones overlap, the first zone that validates the test is returned. + -- @param #SET_ZONE self + -- @param Core.Point#COORDINATE Coordinate The coordinate to be searched. + -- @return Core.Zone#ZONE_BASE The zone that validates the coordinate location. + -- @return #nil No zone has been found. + function SET_ZONE:IsCoordinateInZone( Coordinate ) - for RemoveCargoID, RemoveCargoName in pairs( RemoveCargoNamesArray ) do - self:Remove( RemoveCargoName.CargoName ) - end - - return self -end - - ---- (R2.1) Finds a Cargo based on the Cargo Name. --- @param #SET_CARGO self --- @param #string CargoName --- @return Wrapper.Cargo#CARGO The found Cargo. -function SET_CARGO:FindCargo( CargoName ) --R2.1 - - local CargoFound = self.Set[CargoName] - return CargoFound -end - - - ---- (R2.1) Builds a set of cargos of coalitions. --- Possible current coalitions are red, blue and neutral. --- @param #SET_CARGO self --- @param #string Coalitions Can take the following values: "red", "blue", "neutral". --- @return #SET_CARGO self -function SET_CARGO:FilterCoalitions( Coalitions ) --R2.1 - if not self.Filter.Coalitions then - self.Filter.Coalitions = {} - end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end - for CoalitionID, Coalition in pairs( Coalitions ) do - self.Filter.Coalitions[Coalition] = Coalition - end - return self -end - ---- (R2.1) Builds a set of cargos of defined cargo types. --- Possible current types are those types known within DCS world. --- @param #SET_CARGO self --- @param #string Types Can take those type strings known within DCS world. --- @return #SET_CARGO self -function SET_CARGO:FilterTypes( Types ) --R2.1 - if not self.Filter.Types then - self.Filter.Types = {} - end - if type( Types ) ~= "table" then - Types = { Types } - end - for TypeID, Type in pairs( Types ) do - self.Filter.Types[Type] = Type - end - return self -end - - ---- (R2.1) Builds a set of cargos of defined countries. --- Possible current countries are those known within DCS world. --- @param #SET_CARGO self --- @param #string Countries Can take those country strings known within DCS world. --- @return #SET_CARGO self -function SET_CARGO:FilterCountries( Countries ) --R2.1 - if not self.Filter.Countries then - self.Filter.Countries = {} - end - if type( Countries ) ~= "table" then - Countries = { Countries } - end - for CountryID, Country in pairs( Countries ) do - self.Filter.Countries[Country] = Country - end - return self -end - - ---- (R2.1) Builds a set of cargos of defined cargo prefixes. --- All the cargos starting with the given prefixes will be included within the set. --- @param #SET_CARGO self --- @param #string Prefixes The prefix of which the cargo name starts with. --- @return #SET_CARGO self -function SET_CARGO:FilterPrefixes( Prefixes ) --R2.1 - if not self.Filter.CargoPrefixes then - self.Filter.CargoPrefixes = {} - end - if type( Prefixes ) ~= "table" then - Prefixes = { Prefixes } - end - for PrefixID, Prefix in pairs( Prefixes ) do - self.Filter.CargoPrefixes[Prefix] = Prefix - end - return self -end - - - ---- (R2.1) Starts the filtering. --- @param #SET_CARGO self --- @return #SET_CARGO self -function SET_CARGO:FilterStart() --R2.1 - - if _DATABASE then - self:_FilterStart() - end - - self:HandleEvent( EVENTS.NewCargo ) - self:HandleEvent( EVENTS.DeleteCargo ) - - return self -end - - ---- (R2.1) Handles the Database to check on an event (birth) that the Object was added in the Database. --- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event! --- @param #SET_CARGO self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the CARGO --- @return #table The CARGO -function SET_CARGO:AddInDatabase( Event ) --R2.1 - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- (R2.1) Handles the Database to check on any event that Object exists in the Database. --- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa! --- @param #SET_CARGO self --- @param Core.Event#EVENTDATA Event --- @return #string The name of the CARGO --- @return #table The CARGO -function SET_CARGO:FindInDatabase( Event ) --R2.1 - self:F3( { Event } ) - - return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName] -end - ---- (R2.1) Iterate the SET_CARGO and call an interator function for each CARGO, providing the CARGO and optional parameters. --- @param #SET_CARGO self --- @param #function IteratorFunction The function that will be called when there is an alive CARGO in the SET_CARGO. The function needs to accept a CARGO parameter. --- @return #SET_CARGO self -function SET_CARGO:ForEachCargo( IteratorFunction, ... ) --R2.1 - self:F2( arg ) - - self:ForEach( IteratorFunction, arg, self:GetSet() ) - - return self -end - ---- (R2.1) Iterate the SET_CARGO while identifying the nearest @{Cargo#CARGO} from a @{Point#POINT_VEC2}. --- @param #SET_CARGO self --- @param Core.Point#POINT_VEC2 PointVec2 A @{Point#POINT_VEC2} object from where to evaluate the closest @{Cargo#CARGO}. --- @return Wrapper.Cargo#CARGO The closest @{Cargo#CARGO}. -function SET_CARGO:FindNearestCargoFromPointVec2( PointVec2 ) --R2.1 - self:F2( PointVec2 ) - - local NearestCargo = self:FindNearestObjectFromPointVec2( PointVec2 ) - return NearestCargo -end - - - ---- (R2.1) --- @param #SET_CARGO self --- @param AI.AI_Cargo#AI_CARGO MCargo --- @return #SET_CARGO self -function SET_CARGO:IsIncludeObject( MCargo ) --R2.1 - self:F2( MCargo ) - - local MCargoInclude = true - - if MCargo then - local MCargoName = MCargo:GetName() - - if self.Filter.Coalitions then - local MCargoCoalition = false - for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do - local CargoCoalitionID = MCargo:GetCoalition() - self:T3( { "Coalition:", CargoCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } ) - if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == CargoCoalitionID then - MCargoCoalition = true - end + for _, Zone in pairs( self:GetSet() ) do + local Zone = Zone -- Core.Zone#ZONE_BASE + if Zone:IsCoordinateInZone( Coordinate ) then + return Zone end - self:T( { "Evaluated Coalition", MCargoCoalition } ) - MCargoInclude = MCargoInclude and MCargoCoalition - end - - if self.Filter.Types then - local MCargoType = false - for TypeID, TypeName in pairs( self.Filter.Types ) do - self:T3( { "Type:", MCargo:GetType(), TypeName } ) - if TypeName == MCargo:GetType() then - MCargoType = true - end - end - self:T( { "Evaluated Type", MCargoType } ) - MCargoInclude = MCargoInclude and MCargoType - end - - if self.Filter.CargoPrefixes then - local MCargoPrefix = false - for CargoPrefixId, CargoPrefix in pairs( self.Filter.CargoPrefixes ) do - self:T3( { "Prefix:", string.find( MCargo.Name, CargoPrefix, 1 ), CargoPrefix } ) - if string.find( MCargo.Name, CargoPrefix, 1 ) then - MCargoPrefix = true - end - end - self:T( { "Evaluated Prefix", MCargoPrefix } ) - MCargoInclude = MCargoInclude and MCargoPrefix end + + return nil end - - self:T2( MCargoInclude ) - return MCargoInclude -end - ---- (R2.1) Handles the OnEventNewCargo event for the Set. --- @param #SET_CARGO self --- @param Core.Event#EVENTDATA EventData -function SET_CARGO:OnEventNewCargo( EventData ) --R2.1 - - if EventData.Cargo then - if EventData.Cargo and self:IsIncludeObject( EventData.Cargo ) then - self:Add( EventData.Cargo.Name , EventData.Cargo ) - end - end -end - ---- (R2.1) Handles the OnDead or OnCrash event for alive units set. --- @param #SET_CARGO self --- @param Core.Event#EVENTDATA EventData -function SET_CARGO:OnEventDeleteCargo( EventData ) --R2.1 - self:F3( { EventData } ) - - if EventData.Cargo then - local Cargo = _DATABASE:FindCargo( EventData.Cargo.Name ) - if Cargo and Cargo.Name then - self:Remove( Cargo.Name ) - end - end -end +end \ No newline at end of file diff --git a/Moose Development/Moose/Core/Settings.lua b/Moose Development/Moose/Core/Settings.lua index 8b8a2c601..1f504271d 100644 --- a/Moose Development/Moose/Core/Settings.lua +++ b/Moose Development/Moose/Core/Settings.lua @@ -1,9 +1,19 @@ ---- **Core** -- Manages various settings for MOOSE classes. --- --- ![Banner Image](..\Presentations\SETTINGS\Dia1.JPG) +--- **Core** - Manages various settings for running missions, consumed by moose classes and provides a menu system for players to tweak settings in running missions. -- -- === -- +-- ## Features: +-- +-- * Provide a settings menu system to the players. +-- * Provide a player settings menu and an overall mission settings menu. +-- * Mission settings provide default settings, while player settings override mission settings. +-- * Provide a menu to select between different coordinate formats for A2G coordinates. +-- * Provide a menu to select between different coordinate formats for A2A coordinates. +-- * Provide a menu to select between different message time duration options. +-- * Provide a menu to select between different metric systems. +-- +-- === +-- -- The documentation of the SETTINGS class can be found further in this document. -- -- === @@ -16,21 +26,17 @@ -- -- * **FlightControl**: Design & Programming -- --- @module Settings +-- @module Core.Settings +-- @image Core_Settings.JPG --- @type SETTINGS -- @extends Core.Base#BASE ---- # SETTINGS class, extends @{Base#BASE} --- The SETTINGS class takes care of various settings that influence the behaviour of certain functionalities and classes within the MOOSE framework. +--- Takes care of various settings that influence the behaviour of certain functionalities and classes within the MOOSE framework. -- -- === -- --- ![Banner Image](..\Presentations\SETTINGS\Dia1.JPG) --- --- === --- -- 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: -- @@ -39,29 +45,29 @@ -- -- 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 +-- # 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 +-- # 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 +-- ## 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 +-- ## 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 +-- ## 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. @@ -75,14 +81,14 @@ -- -- But only when a player exits and reenters the slot these settings will have effect! -- -- --- ## 3. Settings +-- # 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) **A2G coordinates** display formatting -- --- #### 3.1.1. A2G coordinates setting **types** +-- ### 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. -- @@ -91,11 +97,11 @@ -- - 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** +-- ### 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** +-- ### 3.1.3) A2G coordinates setting **methods** -- -- There are different methods that can be used to change the **System settings** using the \_SETTINGS object. -- @@ -104,14 +110,14 @@ -- - @{#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 +-- ### 3.1.4) A2G coordinates setting - additional notes -- -- One additional note on BR. In a situation when a BR coordinate should be given, -- but there isn't any player context (no player unit to reference from), the MGRS formatting will be applied! -- --- ### 3.2. **A2A coordinates** formatting +-- ## 3.2) **A2A coordinates** formatting -- --- #### 3.2.1. A2A coordinates setting **types** +-- ### 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. -- @@ -121,11 +127,11 @@ -- - 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** +-- ### 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** +-- ### 3.2.3) A2A coordinates setting **methods** -- -- There are different methods that can be used to change the **System settings** using the \_SETTINGS object. -- @@ -135,34 +141,34 @@ -- - @{#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 +-- ### 3.2.4) A2A coordinates settings - additional notes -- -- One additional note on BRAA. In a situation when a BRAA coordinate should be given, -- but there isn't any player context (no player unit to reference from), the MGRS formatting will be applied! -- --- ### 3.3. **Measurements** formatting +-- ## 3.3) **Measurements** formatting -- --- #### 3.3.1. Measurements setting **types** +-- ### 3.3.1) Measurements setting **types** -- -- Will customize the measurements system being used as part as part of the Command Center communications. -- -- - **Metrics** system: Applies the [Metrics system](https://en.wikipedia.org/wiki/Metric_system) ... -- - **Imperial** system: Applies the [Imperial system](https://en.wikipedia.org/wiki/Imperial_units) ... -- --- #### 3.3.2. Measurements setting **menu** +-- ### 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** +-- ### 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) **Message** display times -- --- #### 3.4.1. Message setting **types** +-- ### 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. -- @@ -172,7 +178,7 @@ -- - **Overview report**: Provides a short report overview, the summary of the report. -- - **Detailed report**: Provides a complete report. -- --- #### 3.4.2. Message setting **menu** +-- ### 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. -- @@ -181,7 +187,7 @@ -- 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** +-- ### 3.4.3) Message setting **methods** -- -- There are different methods that can be used to change the **System settings** using the \_SETTINGS object. -- @@ -370,7 +376,6 @@ do -- SETTINGS -- @param #SETTINGS self -- @return #boolean true if BRA function SETTINGS:IsA2A_BRAA() - self:E( { BRA = ( self.A2ASystem and self.A2ASystem == "BRAA" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_BRAA() ) } ) return ( self.A2ASystem and self.A2ASystem == "BRAA" ) or ( not self.A2ASystem and _SETTINGS:IsA2A_BRAA() ) end @@ -729,7 +734,7 @@ do -- SETTINGS end --- Removes the player menu from the PlayerUnit. - --- @param #SETTINGS self + -- @param #SETTINGS self -- @param Wrapper.Client#CLIENT PlayerUnit -- @return #SETTINGS self function SETTINGS:RemovePlayerMenu( PlayerUnit ) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 5342a6e92..0d4156260 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -1,10 +1,32 @@ ---- **Core** -- SPAWN class dynamically spawns new groups of units in your missions. +--- **Core** - Spawn dynamically new groups of units in running missions. -- --- ![Banner Image](..\Presentations\SPAWN\SPAWN.JPG) --- -- === -- --- The documentation of the SPAWN class can be found further in this document. +-- ## Features: +-- +-- * Spawn new groups in running missions. +-- * Schedule spawning of new groups. +-- * Put limits on the amount of groups that can be spawned, and the amount of units that can be alive at the same time. +-- * Randomize the spawning location between different zones. +-- * Randomize the intial positions within the zones. +-- * Spawn in array formation. +-- * Spawn uncontrolled (for planes or helos only). +-- * Clean up inactive helicopters that "crashed". +-- * Place a hook to capture a spawn event, and tailor with customer code. +-- * Spawn late activated. +-- * Spawn with or without an initial delay. +-- * Respawn after landing, on the runway or at the ramp after engine shutdown. +-- * Spawn with custom heading. +-- * Spawn with different skills. +-- * Spawn with different liveries. +-- * Spawn with an inner and outer radius to set the initial position. +-- * Spawn with a randomize route. +-- * Spawn with a randomized template. +-- * Spawn with a randomized start points on a route. +-- * Spawn with an alternative name. +-- * Spawn and keep the unit names. +-- * Spawn with a different coalition and country. +-- * Enquiry methods to check on spawn status. -- -- === -- @@ -21,7 +43,8 @@ -- -- === -- --- @module Spawn +-- @module Core.Spawn +-- @image Core_Spawn.JPG --- SPAWN Class @@ -37,13 +60,8 @@ -- @extends Core.Base#BASE ---- # SPAWN class, extends @{Base#BASE} +--- Allows to spawn dynamically new @{Core.Group}s. -- --- -- ![Banner Image](..\Presentations\SPAWN\SPAWN.JPG) --- --- === --- --- The SPAWN class allows to spawn dynamically new groups. -- Each SPAWN object needs to be have related **template groups** setup in the Mission Editor (ME), -- which is a normal group with the **Late Activation** flag set. -- This template group will never be activated in your mission. @@ -67,8 +85,8 @@ -- **Limits** can be set on how many groups can be spawn in each SPAWN object, -- using the method @{#SPAWN.InitLimit}. SPAWN has 2 kind of limits: -- --- * The maximum amount of @{Unit}s that can be **alive** at the same time... --- * The maximum amount of @{Group}s that can be **spawned**... This is more of a **resource**-type of limit. +-- * The maximum amount of @{Wrapper.Unit}s that can be **alive** at the same time... +-- * The maximum amount of @{Wrapper.Group}s that can be **spawned**... This is more of a **resource**-type of limit. -- -- When new groups get spawned using the **Spawn** methods, -- it will be evaluated whether any limits have been reached. @@ -82,7 +100,7 @@ -- with unlimited resources = :InitLimit( 100, 0 ) and 10 groups are alive, but two groups have only one unit alive in the group, -- then a sequent Spawn(Scheduled) will allow a new group to be spawned!!! -- --- ### IMPORTANT!! If a limit has been reached, it is possible that a **Spawn** method returns **nil**, meaning, no @{Group} had been spawned!!! +-- ### IMPORTANT!! If a limit has been reached, it is possible that a **Spawn** method returns **nil**, meaning, no @{Wrapper.Group} had been spawned!!! -- -- Spawned groups get **the same name** as the name of the template group. -- Spawned units in those groups keep _by default_ **the same name** as the name of the template group. @@ -117,7 +135,7 @@ -- Create a new SPAWN object with the @{#SPAWN.New}() or the @{#SPAWN.NewWithAlias}() methods: -- -- * @{#SPAWN.New}(): Creates a new SPAWN object taking the name of the group that represents the GROUP template (definition). --- * @{#SPAWN.NewWithAlias}(): Creates a new SPAWN object taking the name of the group that represents the GROUP template (definition), and gives each spawned @{Group} an different name. +-- * @{#SPAWN.NewWithAlias}(): Creates a new SPAWN object taking the name of the group that represents the GROUP template (definition), and gives each spawned @{Wrapper.Group} an different name. -- -- It is important to understand how the SPAWN class works internally. The SPAWN object created will contain internally a list of groups that will be spawned and that are already spawned. -- The initialization methods will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons. @@ -149,15 +167,15 @@ -- -- ### Position randomization -- --- * @{#SPAWN.InitRandomizePosition}(): Randomizes the position of @{Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. --- * @{#SPAWN.InitRandomizeUnits}(): Randomizes the @{Unit}s in the @{Group} that is spawned within a **radius band**, given an Outer and Inner radius. +-- * @{#SPAWN.InitRandomizePosition}(): Randomizes the position of @{Wrapper.Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. +-- * @{#SPAWN.InitRandomizeUnits}(): Randomizes the @{Wrapper.Unit}s in the @{Wrapper.Group} that is spawned within a **radius band**, given an Outer and Inner radius. -- * @{#SPAWN.InitRandomizeZones}(): Randomizes the spawning between a predefined list of @{Zone}s that are declared using this function. Each zone can be given a probability factor. -- --- ### Enable / Disable AI when spawning a new @{Group} +-- ### Enable / Disable AI when spawning a new @{Wrapper.Group} -- --- * @{#SPAWN.InitAIOn}(): Turns the AI On when spawning the new @{Group} object. --- * @{#SPAWN.InitAIOff}(): Turns the AI Off when spawning the new @{Group} object. --- * @{#SPAWN.InitAIOnOff}(): Turns the AI On or Off when spawning the new @{Group} object. +-- * @{#SPAWN.InitAIOn}(): Turns the AI On when spawning the new @{Wrapper.Group} object. +-- * @{#SPAWN.InitAIOff}(): Turns the AI Off when spawning the new @{Wrapper.Group} object. +-- * @{#SPAWN.InitAIOnOff}(): Turns the AI On or Off when spawning the new @{Wrapper.Group} object. -- -- ### Limit scheduled spawning -- @@ -165,11 +183,11 @@ -- -- ### Delay initial scheduled spawn -- --- * @{#SPAWN.InitDelayOnOff}(): Turns the inital delay On/Off when scheduled spawning the first @{Group} object. --- * @{#SPAWN.InitDelayOn}(): Turns the inital delay On when scheduled spawning the first @{Group} object. --- * @{#SPAWN.InitDelayOff}(): Turns the inital delay Off when scheduled spawning the first @{Group} object. +-- * @{#SPAWN.InitDelayOnOff}(): Turns the inital delay On/Off when scheduled spawning the first @{Wrapper.Group} object. +-- * @{#SPAWN.InitDelayOn}(): Turns the inital delay On when scheduled spawning the first @{Wrapper.Group} object. +-- * @{#SPAWN.InitDelayOff}(): Turns the inital delay Off when scheduled spawning the first @{Wrapper.Group} object. -- --- ### Repeat spawned @{Group}s upon landing +-- ### Repeat spawned @{Wrapper.Group}s upon landing -- -- * @{#SPAWN.InitRepeat}() or @{#SPAWN.InitRepeatOnLanding}(): This method is used to re-spawn automatically the same group after it has landed. -- * @{#SPAWN.InitRepeatOnEngineShutDown}(): This method is used to re-spawn automatically the same group after it has landed and it shuts down the engines at the ramp. @@ -186,11 +204,11 @@ -- * @{#SPAWN.SpawnFromVec3}(): Spawn a new group from a Vec3 coordinate. (The group will can be spawned at a point in the air). -- * @{#SPAWN.SpawnFromVec2}(): Spawn a new group from a Vec2 coordinate. (The group will be spawned at land height ). -- * @{#SPAWN.SpawnFromStatic}(): Spawn a new group from a structure, taking the position of a @{Static}. --- * @{#SPAWN.SpawnFromUnit}(): Spawn a new group taking the position of a @{Unit}. +-- * @{#SPAWN.SpawnFromUnit}(): Spawn a new group taking the position of a @{Wrapper.Unit}. -- * @{#SPAWN.SpawnInZone}(): Spawn a new group in a @{Zone}. --- * @{#SPAWN.SpawnAtAirbase}(): Spawn a new group at an @{Airbase}, which can be an airdrome, ship or helipad. +-- * @{#SPAWN.SpawnAtAirbase}(): Spawn a new group at an @{Wrapper.Airbase}, which can be an airdrome, ship or helipad. -- --- Note that @{#SPAWN.Spawn} and @{#SPAWN.ReSpawn} return a @{GROUP#GROUP.New} object, that contains a reference to the DCSGroup object. +-- Note that @{#SPAWN.Spawn} and @{#SPAWN.ReSpawn} return a @{Wrapper.Group#GROUP.New} object, that contains a reference to the DCSGroup object. -- You can use the @{GROUP} object to do further actions with the DCSGroup. -- -- ### **Scheduled** spawning methods @@ -226,21 +244,21 @@ -- This models AI that has succesfully returned to their airbase, to restart their combat activities. -- Check the @{#SPAWN.InitCleanUp}() for further info. -- --- ## Catch the @{Group} Spawn Event in a callback function! +-- ## Catch the @{Wrapper.Group} Spawn Event in a callback function! -- --- When using the @{#SPAWN.SpawnScheduled)() method, new @{Group}s are created following the spawn time interval parameters. --- When a new @{Group} is spawned, you maybe want to execute actions with that group spawned at the spawn event. +-- When using the @{#SPAWN.SpawnScheduled)() method, new @{Wrapper.Group}s are created following the spawn time interval parameters. +-- When a new @{Wrapper.Group} is spawned, you maybe want to execute actions with that group spawned at the spawn event. -- The SPAWN class supports this functionality through the method @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ), -- which takes a function as a parameter that you can define locally. --- Whenever a new @{Group} is spawned, the given function is called, and the @{Group} that was just spawned, is given as a parameter. --- As a result, your spawn event handling function requires one parameter to be declared, which will contain the spawned @{Group} object. +-- Whenever a new @{Wrapper.Group} is spawned, the given function is called, and the @{Wrapper.Group} that was just spawned, is given as a parameter. +-- As a result, your spawn event handling function requires one parameter to be declared, which will contain the spawned @{Wrapper.Group} object. -- A coding example is provided at the description of the @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ) method. -- -- ## Delay the initial spawning -- --- When using the @{#SPAWN.SpawnScheduled)() method, the default behaviour of this method will be that it will spawn the initial (first) @{Group} +-- When using the @{#SPAWN.SpawnScheduled)() method, the default behaviour of this method will be that it will spawn the initial (first) @{Wrapper.Group} -- immediately when :SpawnScheduled() is initiated. The methods @{#SPAWN.InitDelayOnOff}() and @{#SPAWN.InitDelayOn}() can be used to --- activate a delay before the first @{Group} is spawned. For completeness, a method @{#SPAWN.InitDelayOff}() is also available, that +-- 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. -- @@ -270,7 +288,7 @@ SPAWN.Takeoff = { -- @list SpawnZone ---- Creates the main object to spawn a @{Group} defined in the DCS ME. +--- Creates the main object to spawn a @{Wrapper.Group} defined in the DCS ME. -- @param #SPAWN self -- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. Each new group will have the name starting with SpawnTemplatePrefix. -- @return #SPAWN @@ -288,20 +306,22 @@ function SPAWN:New( SpawnTemplatePrefix ) self.SpawnIndex = 0 self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. + self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! - self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. - self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. - self.SpawnInitLimit = false -- By default, no InitLimit - self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. - self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. - self.AIOnOff = true -- The AI is on by default when spawning a group. + self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. + self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. + self.SpawnInitLimit = false -- By default, no InitLimit + self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. + self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. + self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. + self.AIOnOff = true -- The AI is on by default when spawning a group. self.SpawnUnControlled = false - self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. - self.DelayOnOff = false -- No intial delay when spawning the first group. - self.Grouping = nil -- No grouping + self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. + self.DelayOnOff = false -- No intial delay when spawning the first group. + self.Grouping = nil -- No grouping. + self.SpawnInitLivery = nil -- No special livery. + self.SpawnInitSkill = nil -- No special skill. self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else @@ -334,20 +354,22 @@ function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) self.SpawnIndex = 0 self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. self.AliveUnits = 0 -- Contains the counter how many units are currently alive - self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. + self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! - self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. - self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. - self.SpawnInitLimit = false -- By default, no InitLimit - self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. - self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. - self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. - self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. - self.AIOnOff = true -- The AI is on by default when spawning a group. + self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. + self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. + self.SpawnInitLimit = false -- By default, no InitLimit + self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. + self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. + self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. + self.AIOnOff = true -- The AI is on by default when spawning a group. self.SpawnUnControlled = false - self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. - self.DelayOnOff = false -- No intial delay when spawning the first group. - self.Grouping = nil + self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. + self.DelayOnOff = false -- No intial delay when spawning the first group. + self.Grouping = nil -- No grouping. + self.SpawnInitLivery = nil -- No special livery. + self.SpawnInitSkill = nil -- No special skill. self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. else @@ -361,6 +383,57 @@ function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix ) end +--- Creates a new SPAWN instance to create new groups based on the provided template. +-- @param #SPAWN self +-- @param #table SpawnTemplate is the Template of the Group. This must be a valid Group Template structure! +-- @param #string SpawnTemplatePrefix is the name of the Group that will be given at each spawn. +-- @param #string SpawnAliasPrefix (optional) is the name that will be given to the Group at runtime. +-- @return #SPAWN +-- @usage +-- -- Create a new SPAWN object based on a Group Template defined from scratch. +-- Spawn_BE_KA50 = SPAWN:NewWithAlias( 'BE KA-50@RAMP-Ground Defense', 'Helicopter Attacking a City' ) +-- @usage +-- -- Create a new CSAR_Spawn object based on a normal Group Template to spawn a soldier. +-- local CSAR_Spawn = SPAWN:NewWithFromTemplate( Template, "CSAR", "Pilot" ) +function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPrefix ) + local self = BASE:Inherit( self, BASE:New() ) + self:F( { SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPrefix } ) + + if SpawnTemplate then + self.SpawnTemplate = SpawnTemplate -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!! + self.SpawnTemplatePrefix = SpawnTemplatePrefix + self.SpawnAliasPrefix = SpawnAliasPrefix + self.SpawnIndex = 0 + self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart. + self.AliveUnits = 0 -- Contains the counter how many units are currently alive + self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not. + self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning. + self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts. + self.SpawnInitLimit = false -- By default, no InitLimit. + self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time. + self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned. + self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false. + self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned. + self.AIOnOff = true -- The AI is on by default when spawning a group. + self.SpawnUnControlled = false + self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name. + self.DelayOnOff = false -- No intial delay when spawning the first group. + self.Grouping = nil -- No grouping. + self.SpawnInitLivery = nil -- No special livery. + self.SpawnInitSkill = nil -- No special skill. + + self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned. + else + error( "There is no template provided for SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) + end + + self:SetEventPriority( 5 ) + self.SpawnHookScheduler = SCHEDULER:New( nil ) + + return self +end + + --- Limits the Maximum amount of Units that can be alive at the same time, and the maximum amount of groups that can be spawned. -- 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... @@ -395,11 +468,131 @@ end -- and any spaces before and after the resulting name are removed. -- IMPORTANT! This method MUST be the first used after :New !!! -- @param #SPAWN self +-- @param #boolean KeepUnitNames (optional) If true, the unit names are kept, false or not provided to make new unit names. -- @return #SPAWN self -function SPAWN:InitKeepUnitNames() +function SPAWN:InitKeepUnitNames( KeepUnitNames ) self:F( ) - self.SpawnInitKeepUnitNames = true + self.SpawnInitKeepUnitNames = KeepUnitNames or true + + return self +end + + +--- Flags that the spawned groups must be spawned late activated. +-- @param #SPAWN self +-- @param #boolean LateActivated (optional) If true, the spawned groups are late activated. +-- @return #SPAWN self +function SPAWN:InitLateActivated( LateActivated ) + self:F( ) + + self.LateActivated = LateActivated or true + + return self +end + + +--- Defines the Heading for the new spawned units. +-- The heading can be given as one fixed degree, or can be randomized between minimum and maximum degrees. +-- @param #SPAWN self +-- @param #number HeadingMin The minimum or fixed heading in degrees. +-- @param #number HeadingMax (optional) The maximum heading in degrees. This there is no maximum heading, then the heading will be fixed for all units using minimum heading. +-- @return #SPAWN self +-- @usage +-- +-- Spawn = SPAWN:New( ... ) +-- +-- -- Spawn the units pointing to 100 degrees. +-- Spawn:InitHeading( 100 ) +-- +-- -- Spawn the units pointing between 100 and 150 degrees. +-- Spawn:InitHeading( 100, 150 ) +-- +function SPAWN:InitHeading( HeadingMin, HeadingMax ) + self:F( ) + + self.SpawnInitHeadingMin = HeadingMin + self.SpawnInitHeadingMax = HeadingMax + + return self +end + + +--- Sets the coalition of the spawned group. Note that it might be necessary to also set the country explicitly! +-- @param #SPAWN self +-- @param DCS#coalition.side Coalition Coalition of the group as number of enumerator: +-- +-- * @{DCS#coaliton.side.NEUTRAL} +-- * @{DCS#coaliton.side.RED} +-- * @{DCS#coalition.side.BLUE} +-- +-- @return #SPAWN self +function SPAWN:InitCoalition( Coalition ) + self:F({coalition=Coalition}) + + self.SpawnInitCoalition = Coalition + + return self +end + +--- Sets the country of the spawn group. Note that the country determins the coalition of the group depending on which country is defined to be on which side for each specific mission! +-- @param #SPAWN self +-- @param #DCS.country Country Country id as number or enumerator: +-- +-- * @{DCS#country.id.RUSSIA} +-- * @{DCS#county.id.USA} +-- +-- @return #SPAWN self +function SPAWN:InitCountry( Country ) + self:F( ) + + self.SpawnInitCountry = Country + + return self +end + + +--- Sets category ID of the group. +-- @param #SPAWN self +-- @param #number Category Category id. +-- @return #SPAWN self +function SPAWN:InitCategory( Category ) + self:F( ) + + self.SpawnInitCategory = Category + + return self +end + +--- Sets livery of the group. +-- @param #SPAWN self +-- @param #string Livery Livery name. Note that this is not necessarily the same name as displayed in the mission edior. +-- @return #SPAWN self +function SPAWN:InitLivery( Livery ) + self:F({livery=Livery} ) + + self.SpawnInitLivery = Livery + + return self +end + +--- Sets skill of the group. +-- @param #SPAWN self +-- @param #string Skill Skill, possible values "Average", "Good", "High", "Excellent" or "Random". +-- @return #SPAWN self +function SPAWN:InitSkill( Skill ) + self:F({skill=Skill}) + if Skill:lower()=="average" then + self.SpawnInitSkill="Average" + elseif Skill:lower()=="good" then + self.SpawnInitSkill="Good" + elseif Skill:lower()=="excellent" then + self.SpawnInitSkill="Excellent" + elseif Skill:lower()=="random" then + self.SpawnInitSkill="Random" + else + self.SpawnInitSkill="High" + end return self end @@ -436,11 +629,11 @@ function SPAWN:InitRandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius, return self end ---- Randomizes the position of @{Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. +--- Randomizes the position of @{Wrapper.Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. -- @param #SPAWN self --- @param #boolean RandomizePosition If true, SPAWN will perform the randomization of the @{Group}s position between a given outer and inner radius. --- @param Dcs.DCSTypes#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. --- @param Dcs.DCSTypes#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. +-- @param #boolean RandomizePosition If true, SPAWN will perform the randomization of the @{Wrapper.Group}s position between a given outer and inner radius. +-- @param DCS#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. +-- @param DCS#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. -- @return #SPAWN function SPAWN:InitRandomizePosition( RandomizePosition, OuterRadius, InnerRadius ) self:F( { self.SpawnTemplatePrefix, RandomizePosition, OuterRadius, InnerRadius } ) @@ -460,8 +653,8 @@ end --- Randomizes the UNITs that are spawned within a radius band given an Outer and Inner radius. -- @param #SPAWN self -- @param #boolean RandomizeUnits If true, SPAWN will perform the randomization of the @{UNIT}s position within the group between a given outer and inner radius. --- @param Dcs.DCSTypes#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. --- @param Dcs.DCSTypes#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. +-- @param DCS#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned. +-- @param DCS#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned. -- @return #SPAWN -- @usage -- -- NATO helicopters engaging in the battle field. @@ -596,14 +789,20 @@ end ---TODO: Add example. --- This method provides the functionality to randomize the spawning of the Groups at a given list of zones of different types. -- @param #SPAWN self -- @param #table SpawnZoneTable A table with @{Zone} objects. If this table is given, then each spawn will be executed within the given list of @{Zone}s objects. -- @return #SPAWN -- @usage --- -- NATO Tank Platoons invading Gori. --- -- Choose between 3 different zones for each new SPAWN the Group to be executed, regardless of the zone type. +-- -- Create a zone table of the 2 zones. +-- ZoneTable = { ZONE:New( "Zone1" ), ZONE:New( "Zone2" ) } +-- +-- Spawn_Vehicle_1 = SPAWN:New( "Spawn Vehicle 1" ) +-- :InitLimit( 10, 10 ) +-- :InitRandomizeRoute( 1, 1, 200 ) +-- :InitRandomizeZones( ZoneTable ) +-- :SpawnScheduled( 5, .5 ) +-- function SPAWN:InitRandomizeZones( SpawnZoneTable ) self:F( { self.SpawnTemplatePrefix, SpawnZoneTable } ) @@ -722,10 +921,10 @@ 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. -- @param #SPAWN self --- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. --- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. --- @param #number SpawnDeltaX The space between each Group on the X-axis. --- @param #number SpawnDeltaY The space between each Group on the Y-axis. +-- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned. +-- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis. +-- @param #number SpawnDeltaX The space between each Group on the X-axis. +-- @param #number SpawnDeltaY The space between each Group on the Y-axis. -- @return #SPAWN self -- @usage -- -- Define an array of Groups. @@ -771,6 +970,7 @@ function SPAWN:InitArray( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) self:HandleEvent( EVENTS.Birth, self._OnBirth ) self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash ) self:HandleEvent( EVENTS.Crash, self._OnDeadOrCrash ) + self:HandleEvent( EVENTS.RemoveUnit, self._OnDeadOrCrash ) if self.Repeat then self:HandleEvent( EVENTS.Takeoff, self._OnTakeOff ) self:HandleEvent( EVENTS.Land, self._OnLand ) @@ -789,7 +989,7 @@ function SPAWN:InitArray( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY ) end do -- AI methods - --- Turns the AI On or Off for the @{Group} when spawning. + --- Turns the AI On or Off for the @{Wrapper.Group} when spawning. -- @param #SPAWN self -- @param #boolean AIOnOff A value of true sets the AI On, a value of false sets the AI Off. -- @return #SPAWN The SPAWN object @@ -799,7 +999,7 @@ do -- AI methods return self end - --- Turns the AI On for the @{Group} when spawning. + --- Turns the AI On for the @{Wrapper.Group} when spawning. -- @param #SPAWN self -- @return #SPAWN The SPAWN object function SPAWN:InitAIOn() @@ -807,7 +1007,7 @@ do -- AI methods return self:InitAIOnOff( true ) end - --- Turns the AI Off for the @{Group} when spawning. + --- Turns the AI Off for the @{Wrapper.Group} when spawning. -- @param #SPAWN self -- @return #SPAWN The SPAWN object function SPAWN:InitAIOff() @@ -818,8 +1018,8 @@ do -- AI methods end -- AI methods do -- Delay methods - --- Turns the Delay On or Off for the first @{Group} scheduled spawning. - -- The default value is that for scheduled spawning, there is an initial delay when spawning the first @{Group}. + --- Turns the Delay On or Off for the first @{Wrapper.Group} scheduled spawning. + -- The default value is that for scheduled spawning, there is an initial delay when spawning the first @{Wrapper.Group}. -- @param #SPAWN self -- @param #boolean DelayOnOff A value of true sets the Delay On, a value of false sets the Delay Off. -- @return #SPAWN The SPAWN object @@ -829,7 +1029,7 @@ do -- Delay methods return self end - --- Turns the Delay On for the @{Group} when spawning. + --- Turns the Delay On for the @{Wrapper.Group} when spawning. -- @param #SPAWN self -- @return #SPAWN The SPAWN object function SPAWN:InitDelayOn() @@ -837,7 +1037,7 @@ do -- Delay methods return self:InitDelayOnOff( true ) end - --- Turns the Delay Off for the @{Group} when spawning. + --- Turns the Delay Off for the @{Wrapper.Group} when spawning. -- @param #SPAWN self -- @return #SPAWN The SPAWN object function SPAWN:InitDelayOff() @@ -941,6 +1141,45 @@ function SPAWN:SpawnWithIndex( SpawnIndex ) end end + -- 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 + + -- If Heading is given, point all the units towards the given Heading. + 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].psi = -SpawnTemplate.units[UnitID].heading + end + end + + -- Set livery. + if self.SpawnInitLivery then + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].livery_id = self.SpawnInitLivery + end + end + + -- Set skill. + if self.SpawnInitSkill then + for UnitID = 1, #SpawnTemplate.units do + SpawnTemplate.units[UnitID].skill = self.SpawnInitSkill + end + end + + -- Set country, coaliton and categroy. + SpawnTemplate.CategoryID = self.SpawnInitCategory or SpawnTemplate.CategoryID + SpawnTemplate.CountryID = self.SpawnInitCountry or SpawnTemplate.CountryID + SpawnTemplate.CoalitionID = self.SpawnInitCoalition or SpawnTemplate.CoalitionID + + if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then if SpawnTemplate.route.points[1].type == "TakeOffParking" then SpawnTemplate.uncontrolled = self.SpawnUnControlled @@ -951,6 +1190,7 @@ function SPAWN:SpawnWithIndex( SpawnIndex ) self:HandleEvent( EVENTS.Birth, self._OnBirth ) self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash ) self:HandleEvent( EVENTS.Crash, self._OnDeadOrCrash ) + self:HandleEvent( EVENTS.RemoveUnit, self._OnDeadOrCrash ) if self.Repeat then self:HandleEvent( EVENTS.Takeoff, self._OnTakeOff ) self:HandleEvent( EVENTS.Land, self._OnLand ) @@ -1045,22 +1285,23 @@ end --- Allows to place a CallFunction hook when a new group spawns. -- The provided method will be called when a new group is spawned, including its given parameters. --- The first parameter of the SpawnFunction is the @{Group#GROUP} that was spawned. +-- The first parameter of the SpawnFunction is the @{Wrapper.Group#GROUP} that was spawned. -- @param #SPAWN self -- @param #function SpawnCallBackFunction The function to be called when a group spawns. -- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns. -- @return #SPAWN -- @usage --- -- Declare SpawnObject and call a function when a new Group is spawned. --- local SpawnObject = SPAWN --- :New( "SpawnObject" ) --- :InitLimit( 2, 10 ) --- :OnSpawnGroup( --- function( SpawnGroup ) --- SpawnGroup:E( "I am spawned" ) --- end --- ) --- :SpawnScheduled( 300, 0.3 ) +-- -- Declare SpawnObject and call a function when a new Group is spawned. +-- local SpawnObject = SPAWN +-- :New( "SpawnObject" ) +-- :InitLimit( 2, 10 ) +-- :OnSpawnGroup( +-- function( SpawnGroup ) +-- SpawnGroup:E( "I am spawned" ) +-- end +-- ) +-- :SpawnScheduled( 300, 0.3 ) +-- function SPAWN:OnSpawnGroup( SpawnCallBackFunction, ... ) self:F( "OnSpawnGroup" ) @@ -1073,32 +1314,34 @@ function SPAWN:OnSpawnGroup( SpawnCallBackFunction, ... ) return self end ---- Will spawn a group at an @{Airbase}. +--- Will spawn a group at an @{Wrapper.Airbase}. -- This method is mostly advisable to be used if you want to simulate spawning units at an airbase. -- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. -- You can use the returned group to further define the route to be followed. -- --- The @{Airbase#AIRBASE} object must refer to a valid airbase known in the sim. +-- 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: -- --- * @{Airbase#AIRBASE.Caucasus}: The airbases on the Caucasus map. --- * @{Airbase#AIRBASE.Nevada}: The airbases on the Nevada (NTTR) map. --- * @{Airbase#AIRBASE.Normandy}: The airbases on the Normandy map. +-- * @{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 @{Airbase#AIRBASE.FindByName}() to retrieve the airbase object. +-- 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 @{Airbase#AIRBASE} enumeration defined. --- You need to provide the **exact name** of the airbase as the parameter to the @{Airbase#AIRBASE.FindByName}() method! +-- 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 @{Airbase} where to spawn the group. +-- @param Wrapper.Airbase#AIRBASE SpawnAirbase The @{Wrapper.Airbase} where to spawn the group. -- @param #SPAWN.Takeoff Takeoff (optional) The location and takeoff method. Default is Hot. -- @param #number TakeoffAltitude (optional) The altitude above the ground. --- @return Wrapper.Group#GROUP that was spawned. --- @return #nil Nothing was spawned. +-- @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. -- @usage -- Spawn_Plane = SPAWN:New( "Plane" ) -- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Cold ) @@ -1115,34 +1358,59 @@ end -- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Air" ), SPAWN.Takeoff.Air ) -- -- Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "Carrier" ), SPAWN.Takeoff.Cold ) +-- +-- Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Cold, nil, AIRBASE.TerminalType.OpenBig ) -- -function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude ) -- R2.2 - self:F( { self.SpawnTemplatePrefix, SpawnAirbase, Takeoff, TakeoffAltitude } ) +function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalType, EmergencyAirSpawn, Parkingdata ) -- R2.2, R2.4 + self:F( { self.SpawnTemplatePrefix, SpawnAirbase, Takeoff, TakeoffAltitude, TerminalType } ) - local PointVec3 = SpawnAirbase:GetPointVec3() + -- Get position of airbase. + local PointVec3 = SpawnAirbase:GetCoordinate() self:T2(PointVec3) + -- Set take off type. Default is hot. Takeoff = Takeoff or SPAWN.Takeoff.Hot + -- By default, groups are spawned in air if no parking spot is available. + if EmergencyAirSpawn==nil then + EmergencyAirSpawn=true + end + if self:_GetSpawnIndex( self.SpawnIndex + 1 ) then + -- Get group template. local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate if SpawnTemplate then + -- 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 - local SpawnPoint = SpawnTemplate.route.points[1] + -- First waypoint of the group. + local SpawnPoint = SpawnTemplate.route.points[1] - -- These are only for ships. + -- 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:GetDesc().category self:F( { AirbaseCategory = AirbaseCategory } ) + -- Set airdromeId. if AirbaseCategory == Airbase.Category.SHIP then SpawnPoint.linkUnit = AirbaseID SpawnPoint.helipadId = AirbaseID @@ -1153,78 +1421,309 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude ) -- R2.2 SpawnPoint.airdromeId = AirbaseID end - SpawnPoint.alt = 0 - - SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + -- 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 then + + -- Number of free parking spots. + local nfree=0 + + -- Set terminal type. + local termtype=TerminalType + if spawnonrunway then + termtype=AIRBASE.TerminalType.Runway + end + + -- Scan options. Might make that input somehow. + local scanradius=50 + local scanunits=true + local scanstatics=true + local scanscenery=false + local verysafe=false + + -- Number of free parking spots at the airbase. + if spawnonship or spawnonfarp or spawnonrunway then + -- These places work procedural and have some kind of build in queue ==> Less effort. + self:T(string.format("Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + nfree=SpawnAirbase:GetFreeParkingSpotsNumber(termtype, true) + spots=SpawnAirbase:GetFreeParkingSpotsTable(termtype, true) + elseif Parkingdata~=nil then + -- Parking data explicitly set by user as input parameter. + nfree=#Parkingdata + spots=Parkingdata + else + if ishelo then + if termtype==nil then + -- Helo is spawned. Try exclusive helo spots first. + self:T(string.format("Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterOnly)) + spots=SpawnAirbase:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits) + nfree=#spots + if nfree=1 then + + -- All units get the same spot. DCS takes care of the rest. + for i=1,nunits do + table.insert(parkingspots, spots[1].Coordinate) + table.insert(parkingindex, spots[1].TerminalID) + end + -- This is actually used... + PointVec3=spots[1].Coordinate + + else + -- If there is absolutely no spot ==> air start! + _notenough=true + end + + elseif spawnonairport then + + if nfree>=nunits then + + for i=1,nunits do + table.insert(parkingspots, spots[i].Coordinate) + table.insert(parkingindex, spots[i].TerminalID) + end + + else + -- Not enough spots for the whole group ==> air start! + _notenough=true + end + end + + -- Not enough spots ==> Prepare airstart. + if _notenough then + + if EmergencyAirSpawn and not self.SpawnUnControlled then + self:E(string.format("WARNING: Group %s has no parking spots at %s ==> air start!", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + -- Not enough parking spots at the airport ==> Spawn in air. + spawnonground=false + spawnonship=false + spawnonfarp=false + spawnonrunway=false + + -- Set waypoint type/action to turning point. + SpawnPoint.type = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][1] -- type = Turning Point + SpawnPoint.action = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][2] -- action = Turning Point + + -- Adjust altitude to be 500-1000 m above the airbase. + PointVec3.x=PointVec3.x+math.random(-500,500) + PointVec3.z=PointVec3.z+math.random(-500,500) + if ishelo then + PointVec3.y=PointVec3:GetLandHeight()+math.random(100,1000) + else + -- Randomize position so that multiple AC wont be spawned on top even in air. + PointVec3.y=PointVec3:GetLandHeight()+math.random(500,2500) + end + + Takeoff=GROUP.Takeoff.Air + else + self:E(string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + return nil + end + end + + else + + -- Air start requested initially ==> Set altitude. + if TakeoffAltitude then + PointVec3.y=TakeoffAltitude + else + if ishelo then + PointVec3.y=PointVec3:GetLandHeight()+math.random(100,1000) + else + -- Randomize position so that multiple AC wont be spawned on top even in air. + PointVec3.y=PointVec3:GetLandHeight()+math.random(500,2500) + end + end + + end -- Translate the position of the Group Template to the Vec3. - for UnitID = 1, #SpawnTemplate.units do - self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) - - -- These cause a lot of confusion. + 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] - - UnitTemplate.parking = nil - UnitTemplate.parking_id = nil - UnitTemplate.alt = 0 - + + -- Tranlate position and preserve the relative position/formation of all aircraft. local SX = UnitTemplate.x local SY = UnitTemplate.y - local BX = SpawnPoint.x - local BY = SpawnPoint.y - local TX = PointVec3.x + ( SX - BX ) - local TY = PointVec3.z + ( SY - BY ) - - UnitTemplate.x = TX - UnitTemplate.y = TY - - if Takeoff == GROUP.Takeoff.Air then - UnitTemplate.alt = PointVec3.y + ( TakeoffAltitude or 200 ) - --else - -- UnitTemplate.alt = PointVec3.y + 10 - end - self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. UnitTemplate.y ) - end - - SpawnPoint.x = PointVec3.x - SpawnPoint.y = PointVec3.z - - if Takeoff == GROUP.Takeoff.Air then - SpawnPoint.alt = PointVec3.y + ( TakeoffAltitude or 200 ) - --else - -- SpawnPoint.alt = PointVec3.y + 10 - end + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = PointVec3.x + (SX-BX) + local TY = PointVec3.z + (SY-BY) + + if spawnonground then + + -- Ships and FARPS seem to have a build in queue. + if spawnonship or spawnonfarp or spawnonrunway then + + self:T(string.format("Group %s spawning at farp, ship or runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + -- Spawn on ship. We take only the position of the ship. + SpawnTemplate.units[UnitID].x = PointVec3.x --TX + SpawnTemplate.units[UnitID].y = PointVec3.z --TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + + else + + self:T(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + + -- Get coordinates of parking spot. + SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x + SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z + SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y + + --parkingspots[UnitID]:MarkToAll(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID])) + end + + else + + self:T(string.format("Group %s spawning in air at %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName())) + + -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. + SpawnTemplate.units[UnitID].x = TX + SpawnTemplate.units[UnitID].y = TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + + end + + -- Parking spot id. + UnitTemplate.parking = nil + UnitTemplate.parking_id = nil + if parkingindex[UnitID] then + UnitTemplate.parking = parkingindex[UnitID] + end + + -- Debug output. + self:T2(string.format("Group %s unit number %d: Parking = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking))) + self:T2(string.format("Group %s unit number %d: Parking ID = %s",self.SpawnTemplatePrefix, UnitID, tostring(UnitTemplate.parking_id))) + self:T2('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + end + + -- 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 - + + -- 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() } , 1 ) + SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() } , 5 ) end end - - return GroupSpawned + + -- 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 - - --- Will spawn a group from a Vec3 in 3D space. -- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes. -- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. -- You can use the returned group to further define the route to be followed. -- @param #SPAWN self --- @param Dcs.DCSTypes#Vec3 Vec3 The Vec3 coordinates where to spawn the group. +-- @param DCS#Vec3 Vec3 The Vec3 coordinates where to spawn the group. -- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. -- @return Wrapper.Group#GROUP that was spawned. -- @return #nil Nothing was spawned. @@ -1247,14 +1746,20 @@ function SPAWN:SpawnFromVec3( Vec3, SpawnIndex ) self:T( { "Current point of ", self.SpawnTemplatePrefix, Vec3 } ) - local TemplateHeight = SpawnTemplate.route.points[1].alt + local TemplateHeight = SpawnTemplate.route and SpawnTemplate.route.points[1].alt or nil + + SpawnTemplate.route = SpawnTemplate.route or {} + SpawnTemplate.route.points = SpawnTemplate.route.points or {} + SpawnTemplate.route.points[1] = SpawnTemplate.route.points[1] or {} + SpawnTemplate.route.points[1].x = SpawnTemplate.route.points[1].x or 0 + SpawnTemplate.route.points[1].y = SpawnTemplate.route.points[1].y or 0 -- Translate the position of the Group Template to the Vec3. for UnitID = 1, #SpawnTemplate.units do - self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) + --self:T( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) local UnitTemplate = SpawnTemplate.units[UnitID] - local SX = UnitTemplate.x - local SY = UnitTemplate.y + local SX = UnitTemplate.x or 0 + local SY = UnitTemplate.y or 0 local BX = SpawnTemplate.route.points[1].x local BY = SpawnTemplate.route.points[1].y local TX = Vec3.x + ( SX - BX ) @@ -1266,7 +1771,6 @@ function SPAWN:SpawnFromVec3( Vec3, SpawnIndex ) end self:T( 'After Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y ) end - SpawnTemplate.route.points[1].x = Vec3.x SpawnTemplate.route.points[1].y = Vec3.z if SpawnTemplate.CategoryID ~= Group.Category.SHIP then @@ -1283,12 +1787,53 @@ function SPAWN:SpawnFromVec3( Vec3, SpawnIndex ) return nil end + +--- Will spawn a group from a Coordinate in 3D space. +-- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Core.Point#Coordinate Coordinate The Coordinate coordinates where to spawn the group. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +function SPAWN:SpawnFromCoordinate( Coordinate, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + + return self:SpawnFromVec3( Coordinate:GetVec3(), SpawnIndex ) +end + + + +--- Will spawn a group from a PointVec3 in 3D space. +-- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Core.Point#POINT_VEC3 PointVec3 The PointVec3 coordinates where to spawn the group. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +-- @usage +-- +-- local SpawnPointVec3 = ZONE:New( ZoneName ):GetPointVec3( 2000 ) -- Get the center of the ZONE object at 2000 meters from the ground. +-- +-- -- Spawn at the zone center position at 2000 meters from the ground! +-- SpawnAirplanes:SpawnFromPointVec3( SpawnPointVec3 ) +-- +function SPAWN:SpawnFromPointVec3( PointVec3, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, SpawnIndex } ) + + return self:SpawnFromVec3( PointVec3:GetVec3(), SpawnIndex ) +end + + --- Will spawn a group from a Vec2 in 3D space. -- This method is mostly advisable to be used if you want to simulate spawning groups on the ground from air units, like vehicles. -- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. -- You can use the returned group to further define the route to be followed. -- @param #SPAWN self --- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 coordinates where to spawn the group. +-- @param DCS#Vec2 Vec2 The Vec2 coordinates where to spawn the group. -- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone. -- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone. -- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. @@ -1317,6 +1862,35 @@ function SPAWN:SpawnFromVec2( Vec2, MinHeight, MaxHeight, SpawnIndex ) end +--- Will spawn a group from a POINT_VEC2 in 3D space. +-- This method is mostly advisable to be used if you want to simulate spawning groups on the ground from air units, like vehicles. +-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. +-- You can use the returned group to further define the route to be followed. +-- @param #SPAWN self +-- @param Core.Point#POINT_VEC2 PointVec2 The PointVec2 coordinates where to spawn the group. +-- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone. +-- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone. +-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. +-- @return Wrapper.Group#GROUP that was spawned. +-- @return #nil Nothing was spawned. +-- @usage +-- +-- local SpawnPointVec2 = ZONE:New( ZoneName ):GetPointVec2() +-- +-- -- Spawn at the zone center position at the height specified in the ME of the group template! +-- SpawnAirplanes:SpawnFromPointVec2( SpawnPointVec2 ) +-- +-- -- Spawn from the static position at the height randomized between 2000 and 4000 meters. +-- SpawnAirplanes:SpawnFromPointVec2( SpawnPointVec2, 2000, 4000 ) +-- +function SPAWN:SpawnFromPointVec2( PointVec2, MinHeight, MaxHeight, SpawnIndex ) + self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } ) + + return self:SpawnFromVec2( PointVec2:GetVec2(), MinHeight, MaxHeight, SpawnIndex ) +end + + + --- Will spawn a group from a hosting unit. This method is mostly advisable to be used if you want to simulate spawning from air units, like helicopters, which are dropping infantry into a defined Landing Zone. -- Note that each point in the route assigned to the spawning group is reset to the point of the spawn. -- You can use the returned group to further define the route to be followed. @@ -1377,12 +1951,12 @@ function SPAWN:SpawnFromStatic( HostStatic, MinHeight, MaxHeight, SpawnIndex ) end --- Will spawn a Group within a given @{Zone}. --- The @{Zone} can be of any type derived from @{Zone#ZONE_BASE}. --- Once the @{Group} is spawned within the zone, the @{Group} will continue on its route. +-- The @{Zone} can be of any type derived from @{Core.Zone#ZONE_BASE}. +-- Once the @{Wrapper.Group} is spawned within the zone, the @{Wrapper.Group} will continue on its route. -- The **first waypoint** (where the group is spawned) is replaced with the zone location coordinates. -- @param #SPAWN self -- @param Core.Zone#ZONE Zone The zone where the group is to be spawned. --- @param #boolean RandomizeGroup (optional) Randomization of the @{Group} position in the zone. +-- @param #boolean RandomizeGroup (optional) Randomization of the @{Wrapper.Group} position in the zone. -- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone. -- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone. -- @param #number SpawnIndex (optional) The index which group to spawn within the given zone. @@ -1431,10 +2005,10 @@ end function SPAWN:InitUnControlled( UnControlled ) self:F2( { self.SpawnTemplatePrefix, UnControlled } ) - self.SpawnUnControlled = UnControlled + self.SpawnUnControlled = UnControlled or true for SpawnGroupID = 1, self.SpawnMaxGroups do - self.SpawnGroups[SpawnGroupID].UnControlled = UnControlled + self.SpawnGroups[SpawnGroupID].UnControlled = self.SpawnUnControlled end return self @@ -1478,12 +2052,12 @@ function SPAWN:SpawnGroupName( SpawnIndex ) end ---- Will find the first alive @{Group} it has spawned, and return the alive @{Group} object and the first Index where the first alive @{Group} object has been found. +--- Will find the first alive @{Wrapper.Group} it has spawned, and return the alive @{Wrapper.Group} object and the first Index where the first alive @{Wrapper.Group} object has been found. -- @param #SPAWN self --- @return Wrapper.Group#GROUP, #number The @{Group} object found, the new Index where the group was found. +-- @return Wrapper.Group#GROUP, #number The @{Wrapper.Group} object found, the new Index where the group was found. -- @return #nil, #nil When no group is found, #nil is returned. -- @usage --- -- Find the first alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. +-- -- Find the first alive @{Wrapper.Group} object of the SpawnPlanes SPAWN object @{Wrapper.Group} collection that it has spawned during the mission. -- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() -- while GroupPlane ~= nil do -- -- Do actions with the GroupPlane object. @@ -1503,13 +2077,13 @@ function SPAWN:GetFirstAliveGroup() end ---- Will find the next alive @{Group} object from a given Index, and return a reference to the alive @{Group} object and the next Index where the alive @{Group} has been found. +--- Will find the next alive @{Wrapper.Group} object from a given Index, and return a reference to the alive @{Wrapper.Group} object and the next Index where the alive @{Wrapper.Group} has been found. -- @param #SPAWN self --- @param #number SpawnIndexStart A Index holding the start position to search from. This method can also be used to find the first alive @{Group} object from the given Index. --- @return Wrapper.Group#GROUP, #number The next alive @{Group} object found, the next Index where the next alive @{Group} object was found. --- @return #nil, #nil When no alive @{Group} object is found from the start Index position, #nil is returned. +-- @param #number SpawnIndexStart A Index holding the start position to search from. This method can also be used to find the first alive @{Wrapper.Group} object from the given Index. +-- @return Wrapper.Group#GROUP, #number The next alive @{Wrapper.Group} object found, the next Index where the next alive @{Wrapper.Group} object was found. +-- @return #nil, #nil When no alive @{Wrapper.Group} object is found from the start Index position, #nil is returned. -- @usage --- -- Find the first alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. +-- -- Find the first alive @{Wrapper.Group} object of the SpawnPlanes SPAWN object @{Wrapper.Group} collection that it has spawned during the mission. -- local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup() -- while GroupPlane ~= nil do -- -- Do actions with the GroupPlane object. @@ -1529,12 +2103,12 @@ function SPAWN:GetNextAliveGroup( SpawnIndexStart ) return nil, nil end ---- Will find the last alive @{Group} object, and will return a reference to the last live @{Group} object and the last Index where the last alive @{Group} object has been found. +--- Will find the last alive @{Wrapper.Group} object, and will return a reference to the last live @{Wrapper.Group} object and the last Index where the last alive @{Wrapper.Group} object has been found. -- @param #SPAWN self --- @return Wrapper.Group#GROUP, #number The last alive @{Group} object found, the last Index where the last alive @{Group} object was found. --- @return #nil, #nil When no alive @{Group} object is found, #nil is returned. +-- @return Wrapper.Group#GROUP, #number The last alive @{Wrapper.Group} object found, the last Index where the last alive @{Wrapper.Group} object was found. +-- @return #nil, #nil When no alive @{Wrapper.Group} object is found, #nil is returned. -- @usage --- -- Find the last alive @{Group} object of the SpawnPlanes SPAWN object @{Group} collection that it has spawned during the mission. +-- -- Find the last alive @{Wrapper.Group} object of the SpawnPlanes SPAWN object @{Wrapper.Group} collection that it has spawned during the mission. -- local GroupPlane, Index = SpawnPlanes:GetLastAliveGroup() -- if GroupPlane then -- GroupPlane can be nil!!! -- -- Do actions with the GroupPlane object. @@ -1583,7 +2157,7 @@ end -- The method will search for a #-mark, and will return the text before the #-mark. -- It will return nil of no prefix was found. -- @param #SPAWN self --- @param Dcs.DCSWrapper.Unit#UNIT DCSUnit The @{DCSUnit} to be searched. +-- @param DCS#UNIT DCSUnit The @{DCSUnit} to be searched. -- @return #string The prefix -- @return #nil Nothing found function SPAWN:_GetPrefixFromGroup( SpawnGroup ) @@ -1605,7 +2179,7 @@ end --- Get the index from a given group. -- The function will search the name of the group for a #, and will return the number behind the #-mark. function SPAWN:GetSpawnIndexFromGroup( SpawnGroup ) - self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) + self:F2( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } ) local IndexString = string.match( SpawnGroup:GetName(), "#(%d*)$" ):sub( 2 ) local Index = tonumber( IndexString ) @@ -1693,7 +2267,10 @@ function SPAWN:_GetTemplate( SpawnTemplatePrefix ) local SpawnTemplate = nil - SpawnTemplate = routines.utils.deepCopy( _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template ) + local Template = _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template + self:F( { Template = Template } ) + + SpawnTemplate = UTILS.DeepCopy( _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template ) if SpawnTemplate == nil then error( 'No Template returned for SpawnTemplatePrefix = ' .. SpawnTemplatePrefix ) @@ -1715,12 +2292,17 @@ end function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) --R2.2 self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } ) - local SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) +-- if not self.SpawnTemplate then +-- self.SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) +-- end + + local SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix ) + --local SpawnTemplate = self.SpawnTemplate SpawnTemplate.name = self:SpawnGroupName( SpawnIndex ) SpawnTemplate.groupId = nil --SpawnTemplate.lateActivation = false - SpawnTemplate.lateActivation = false + SpawnTemplate.lateActivation = self.LateActivated or false if SpawnTemplate.CategoryID == Group.Category.GROUND then self:T3( "For ground units, visible needs to be false..." ) @@ -1808,7 +2390,7 @@ function SPAWN:_RandomizeTemplate( SpawnIndex ) if self.SpawnRandomizeTemplate then self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[ math.random( 1, #self.SpawnTemplatePrefixTable ) ] self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex ) - self.SpawnGroups[SpawnIndex].SpawnTemplate.route = routines.utils.deepCopy( self.SpawnTemplate.route ) + self.SpawnGroups[SpawnIndex].SpawnTemplate.route = UTILS.DeepCopy( self.SpawnTemplate.route ) self.SpawnGroups[SpawnIndex].SpawnTemplate.x = self.SpawnTemplate.x self.SpawnGroups[SpawnIndex].SpawnTemplate.y = self.SpawnTemplate.y self.SpawnGroups[SpawnIndex].SpawnTemplate.start_time = self.SpawnTemplate.start_time @@ -2055,7 +2637,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(self, self.ReSpawn, {SpawnGroupIndex}, 3) end end end diff --git a/Moose Development/Moose/Core/SpawnStatic.lua b/Moose Development/Moose/Core/SpawnStatic.lua index 92a816257..e5a7b4e59 100644 --- a/Moose Development/Moose/Core/SpawnStatic.lua +++ b/Moose Development/Moose/Core/SpawnStatic.lua @@ -1,10 +1,14 @@ ---- **Core** -- Spawn dynamically new STATICs in your missions. +--- **Core** - Spawn new statics in your running missions. -- --- ![Banner Image](..\Presentations\SPAWNSTATIC\Dia1.JPG) --- -- === -- --- SPAWNSTATIC spawns static structures in your missions dynamically. See below the SPAWNSTATIC class documentation. +-- ## Features: +-- +-- * Spawn new statics from a static already defined using the mission editor. +-- * Spawn new statics from a given template. +-- * Spawn new statics from a given type. +-- * Spawn with a custom heading and location. +-- * Spawn within a zone. -- -- === -- @@ -29,7 +33,8 @@ -- -- === -- --- @module SpawnStatic +-- @module Core.SpawnStatic +-- @image Core_Spawnstatic.JPG @@ -37,9 +42,7 @@ -- @extends Core.Base#BASE ---- # SPAWNSTATIC class, extends @{Base#BASE} --- --- The SPAWNSTATIC class allows to spawn dynamically new @{Static}s. +--- Allows to spawn dynamically new @{Static}s. -- Through creating a copy of an existing static object template as defined in the Mission Editor (ME), -- SPAWNSTATIC can retireve the properties of the defined static object template (like type, category etc), and "copy" -- these properties to create a new static object and place it at the desired coordinate. @@ -80,18 +83,22 @@ SPAWNSTATIC = { --- Creates the main object to spawn a @{Static} defined in the ME. -- @param #SPAWNSTATIC self -- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template. Each new group will have the name starting with SpawnTemplatePrefix. +-- @param DCS#country.id SpawnCountryID The ID of the country. +-- @param DCS#coalition.side SpawnCoalitionID The ID of the coalition. -- @return #SPAWNSTATIC -function SPAWNSTATIC:NewFromStatic( SpawnTemplatePrefix, CountryID ) --R2.1 +function SPAWNSTATIC:NewFromStatic( SpawnTemplatePrefix, SpawnCountryID, SpawnCoalitionID ) local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC self:F( { SpawnTemplatePrefix } ) - local TemplateStatic = StaticObject.getByName( SpawnTemplatePrefix ) + local TemplateStatic, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( SpawnTemplatePrefix ) if TemplateStatic then self.SpawnTemplatePrefix = SpawnTemplatePrefix - self.CountryID = CountryID + self.CountryID = SpawnCountryID or CountryID + self.CategoryID = CategoryID + self.CoalitionID = SpawnCoalitionID or CoalitionID self.SpawnIndex = 0 else - error( "SPAWNSTATIC:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) + error( "SPAWNSTATIC:New: There is no static declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" ) end self:SetEventPriority( 5 ) @@ -103,12 +110,13 @@ end -- @param #SPAWNSTATIC self -- @param #string SpawnTypeName is the name of the type. -- @return #SPAWNSTATIC -function SPAWNSTATIC:NewFromType( SpawnTypeName, SpawnShapeName, SpawnCategory, CountryID ) --R2.1 +function SPAWNSTATIC:NewFromType( SpawnTypeName, SpawnShapeName, SpawnCategory, SpawnCountryID, SpawnCoalitionID ) local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC self:F( { SpawnTypeName } ) self.SpawnTypeName = SpawnTypeName - self.CountryID = CountryID + self.CountryID = SpawnCountryID + self.CoalitionID = SpawnCoalitionID self.SpawnIndex = 0 self:SetEventPriority( 5 ) @@ -116,6 +124,7 @@ function SPAWNSTATIC:NewFromType( SpawnTypeName, SpawnShapeName, SpawnCategory, return self end + --- Creates a new @{Static} at the original position. -- @param #SPAWNSTATIC self -- @param #number Heading The heading of the static, which is a number in degrees from 0 to 360. @@ -124,26 +133,28 @@ end function SPAWNSTATIC:Spawn( Heading, NewName ) --R2.3 self:F( { Heading, NewName } ) - local CountryName = _DATABASE.COUNTRY_NAME[self.CountryID] + local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix ) - local StaticTemplate = _DATABASE:GetStaticUnitTemplate( self.SpawnTemplatePrefix ) + if StaticTemplate then - StaticTemplate.name = NewName or string.format("%s#%05d", self.SpawnTemplatePrefix, self.SpawnIndex ) - StaticTemplate.heading = ( Heading / 180 ) * math.pi + local StaticUnitTemplate = StaticTemplate.units[1] - StaticTemplate.CountryID = nil - StaticTemplate.CoalitionID = nil - StaticTemplate.CategoryID = nil - - local Static = coalition.addStaticObject( self.CountryID, StaticTemplate ) - - self.SpawnIndex = self.SpawnIndex + 1 + StaticTemplate.name = NewName or string.format("%s#%05d", self.SpawnTemplatePrefix, self.SpawnIndex ) + StaticTemplate.heading = ( Heading / 180 ) * math.pi + + _DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, CategoryID, CountryID ) - return Static + local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] ) + + self.SpawnIndex = self.SpawnIndex + 1 + + return _DATABASE:FindStatic(Static:getName()) + end + + return nil end - --- Creates a new @{Static} from a POINT_VEC2. -- @param #SPAWNSTATIC self -- @param Core.Point#POINT_VEC2 PointVec2 The 2D coordinate where to spawn the static. @@ -153,32 +164,129 @@ end function SPAWNSTATIC:SpawnFromPointVec2( PointVec2, Heading, NewName ) --R2.1 self:F( { PointVec2, Heading, NewName } ) - local CountryName = _DATABASE.COUNTRY_NAME[self.CountryID] + local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix ) - local StaticTemplate = _DATABASE:GetStaticUnitTemplate( self.SpawnTemplatePrefix ) + if StaticTemplate then - StaticTemplate.x = PointVec2.x - StaticTemplate.y = PointVec2.z - - StaticTemplate.units = nil - StaticTemplate.route = nil - StaticTemplate.groupId = nil + local StaticUnitTemplate = StaticTemplate.units[1] + + StaticUnitTemplate.x = PointVec2.x + StaticUnitTemplate.y = PointVec2.z + + StaticTemplate.route = nil + StaticTemplate.groupId = nil - - StaticTemplate.name = NewName or string.format("%s#%05d", self.SpawnTemplatePrefix, self.SpawnIndex ) - StaticTemplate.heading = ( Heading / 180 ) * math.pi - - StaticTemplate.CountryID = nil - StaticTemplate.CoalitionID = nil - StaticTemplate.CategoryID = nil - - local Static = coalition.addStaticObject( self.CountryID, StaticTemplate ) - - self.SpawnIndex = self.SpawnIndex + 1 + 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}) - return Static + local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] ) + + self.SpawnIndex = self.SpawnIndex + 1 + + return _DATABASE:FindStatic(Static:getName()) + end + + return nil end + +--- Creates a new @{Static} from a COORDINATE. +-- @param #SPAWNSTATIC self +-- @param Core.Point#COORDINATE Coordinate The 3D coordinate where to spawn the static. +-- @param #number Heading (Optional) Heading The heading of the static, 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: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 + + return nil +end + + +--- Respawns the original @{Static}. +-- @param #SPAWNSTATIC self +-- @return #SPAWNSTATIC +function SPAWNSTATIC:ReSpawn() + + 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 + + +--- Creates the original @{Static} at a POINT_VEC2. +-- @param #SPAWNSTATIC self +-- @param Core.Point#COORDINATE Coordinate The 2D coordinate where to spawn the static. +-- @param #number Heading The heading of the static, which is a number in degrees from 0 to 360. +-- @return #SPAWNSTATIC +function SPAWNSTATIC:ReSpawnAt( Coordinate, Heading ) + + local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix ) + + if StaticTemplate then + + local StaticUnitTemplate = StaticTemplate.units[1] + + StaticUnitTemplate.x = Coordinate.x + StaticUnitTemplate.y = Coordinate.z + + StaticUnitTemplate.heading = Heading and ( ( Heading / 180 ) * math.pi ) or StaticTemplate.heading + + local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] ) + + return _DATABASE:FindStatic(Static:getName()) + end + + return nil +end + + --- Creates a new @{Static} from a @{Zone}. -- @param #SPAWNSTATIC self -- @param Core.Zone#ZONE_BASE Zone The Zone where to spawn the static. diff --git a/Moose Development/Moose/Core/Spot.lua b/Moose Development/Moose/Core/Spot.lua index 10edb6cee..dbc3ea683 100644 --- a/Moose Development/Moose/Core/Spot.lua +++ b/Moose Development/Moose/Core/Spot.lua @@ -1,14 +1,13 @@ ---- **Core** -- Management of SPOT logistics, that can be transported from and to transportation carriers. --- --- ![Banner Image](..\Presentations\SPOT\Dia1.JPG) +--- **Core** - Management of spotting logistics, that can be activated and deactivated upon command. -- -- === -- -- SPOT implements the DCS Spot class functionality, but adds additional luxury to be able to: -- -- * Spot for a defined duration. --- * wiggle the spot at the target. --- * Provide a @{Unit} as a target, instead of a point. +-- * Updates of laer spot position every 0.2 seconds for moving targets. +-- * Wiggle the spot at the target. +-- * Provide a @{Wrapper.Unit} as a target, instead of a point. -- * Implement a status machine, LaseOn, LaseOff. -- -- === @@ -38,7 +37,8 @@ -- -- === -- --- @module Spot +-- @module Core.Spot +-- @image Core_Spot.JPG do @@ -47,13 +47,12 @@ do -- @extends Core.Fsm#FSM - --- # SPOT class, extends @{Fsm#FSM} - -- - -- SPOT implements the DCS Spot class functionality, but adds additional luxury to be able to: + --- Implements the target spotting or marking functionality, but adds additional luxury to be able to: -- -- * Mark targets for a defined duration. - -- * wiggle the spot at the target. - -- * Provide a @{Unit} as a target, instead of a point. + -- * Updates of laer spot position every 0.2 seconds for moving targets. + -- * Wiggle the spot at the target. + -- * Provide a @{Wrapper.Unit} as a target, instead of a point. -- * Implement a status machine, LaseOn, LaseOff. -- -- ## 1. SPOT constructor @@ -88,9 +87,7 @@ do --- SPOT Constructor. -- @param #SPOT self - -- @param Wrapper.Unit#UNIT Recce - -- @param #number LaserCode - -- @param #number Duration + -- @param Wrapper.Unit#UNIT Recce Unit that is lasing -- @return #SPOT function SPOT:New( Recce ) @@ -118,12 +115,17 @@ do --- LaseOn Trigger for SPOT -- @function [parent=#SPOT] LaseOn -- @param #SPOT self + -- @param Wrapper.Positionable#POSITIONABLE Target + -- @param #number LaserCode Laser code. + -- @param #number Duration Duration of lasing in seconds. --- LaseOn Asynchronous Trigger for SPOT -- @function [parent=#SPOT] __LaseOn -- @param #SPOT self -- @param #number Delay - + -- @param Wrapper.Positionable#POSITIONABLE Target + -- @param #number LaserCode Laser code. + -- @param #number Duration Duration of lasing in seconds. self:AddTransition( "On", "Lasing", "On" ) @@ -196,9 +198,9 @@ do -- @param From -- @param Event -- @param To - -- @param Wrapper.Positionable#POSITIONABLE Target - -- @param #number LaserCode - -- @param #number Duration + -- @param Wrapper.Positionable#POSITIONABLE Target Unit that is being lased. + -- @param #number LaserCode Laser code. + -- @param #number Duration Duration of lasing in seconds. function SPOT:onafterLaseOn( From, Event, To, Target, LaserCode, Duration ) self:F( { "LaseOn", Target, LaserCode, Duration } ) diff --git a/Moose Development/Moose/Core/UserFlag.lua b/Moose Development/Moose/Core/UserFlag.lua index 05fc7b29e..bef5cefff 100644 --- a/Moose Development/Moose/Core/UserFlag.lua +++ b/Moose Development/Moose/Core/UserFlag.lua @@ -1,8 +1,10 @@ ---- **Core (WIP)** -- Manage user flags. +--- **Core** - Manage user flags to interact with the mission editor trigger system and server side scripts. -- -- === -- --- Management of DCS User Flags. +-- ## Features: +-- +-- * Set or get DCS user flags within running missions. -- -- === -- @@ -10,7 +12,9 @@ -- -- === -- --- @module UserFlag +-- @module Core.UserFlag +-- @image Core_Userflag.JPG +-- do -- UserFlag @@ -18,11 +22,9 @@ do -- UserFlag -- @extends Core.Base#BASE - --- # USERFLAG class, extends @{Base#BASE} + --- Management of DCS User Flags. -- - -- Management of DCS User Flags. - -- - -- ## 1. USERFLAG constructor + -- # 1. USERFLAG constructor -- -- * @{#USERFLAG.New}(): Creates a new USERFLAG object. -- @@ -55,8 +57,6 @@ do -- UserFlag -- function USERFLAG:Set( Number ) --R2.3 - self:F( { Number = Number } ) - trigger.action.setUserFlag( self.UserFlagName, Number ) return self @@ -70,7 +70,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 808b78a5f..b0f6fb393 100644 --- a/Moose Development/Moose/Core/UserSound.lua +++ b/Moose Development/Moose/Core/UserSound.lua @@ -1,7 +1,13 @@ ---- **Core (WIP)** -- Manage user sound. +--- **Core** - Manage user sound. -- -- === -- +-- ## Features: +-- +-- * Play sounds wihtin running missions. +-- +-- === +-- -- Management of DCS User Sound. -- -- === @@ -10,7 +16,8 @@ -- -- === -- --- @module UserSound +-- @module Core.UserSound +-- @image Core_Usersound.JPG do -- UserSound @@ -18,11 +25,9 @@ do -- UserSound -- @extends Core.Base#BASE - --- # USERSOUND class, extends @{Base#BASE} + --- Management of DCS User Sound. -- - -- Management of DCS User Sound. - -- - -- ## 1. USERSOUND constructor + -- ## USERSOUND constructor -- -- * @{#USERSOUND.New}(): Creates a new USERSOUND object. -- @@ -80,7 +85,7 @@ do -- UserSound --- Play the usersound to the given coalition. -- @param #USERSOUND self - -- @param Dcs.DCScoalition#coalition Coalition The coalition to play the usersound to. + -- @param DCS#coalition Coalition The coalition to play the usersound to. -- @return #USERSOUND The usersound instance. -- @usage -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) @@ -96,7 +101,7 @@ do -- UserSound --- Play the usersound to the given country. -- @param #USERSOUND self - -- @param Dcs.DCScountry#country Country The country to play the usersound to. + -- @param DCS#country Country The country to play the usersound to. -- @return #USERSOUND The usersound instance. -- @usage -- local BlueVictory = USERSOUND:New( "BlueVictory.ogg" ) @@ -110,18 +115,24 @@ do -- UserSound end - --- Play the usersound to the given @{Group}. + --- Play the usersound to the given @{Wrapper.Group}. -- @param #USERSOUND self - -- @param Wrapper.Group#GROUP Group The @{Group} to play the usersound to. + -- @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/Velocity.lua b/Moose Development/Moose/Core/Velocity.lua index 88d047126..5f86f6696 100644 --- a/Moose Development/Moose/Core/Velocity.lua +++ b/Moose Development/Moose/Core/Velocity.lua @@ -1,13 +1,22 @@ ---- **Core** -- VELOCITY models a speed, which can be expressed in various formats according the Settings. +--- **Core** - Models a velocity or speed, which can be expressed in various formats according the settings. -- -- === -- +-- ## Features: +-- +-- * Convert velocity in various metric systems. +-- * Set the velocity. +-- * Create a text in a specific format of a velocity. +-- +-- === +-- -- ### Author: **FlightControl** -- ### Contributions: -- -- === -- --- @module Velocity +-- @module Core.Velocity +-- @image MOOSE.JPG do -- Velocity @@ -15,11 +24,9 @@ do -- Velocity -- @extends Core.Base#BASE - --- # VELOCITY class, extends @{Base#BASE} + --- VELOCITY models a speed, which can be expressed in various formats according the Settings. -- - -- VELOCITY models a speed, which can be expressed in various formats according the Settings. - -- - -- ## 1. VELOCITY constructor + -- ## VELOCITY constructor -- -- * @{#VELOCITY.New}(): Creates a new VELOCITY object. -- @@ -125,7 +132,7 @@ do -- VELOCITY_POSITIONABLE -- @extends Core.Base#BASE - --- # VELOCITY_POSITIONABLE class, extends @{Base#BASE} + --- # VELOCITY_POSITIONABLE class, extends @{Core.Base#BASE} -- -- VELOCITY_POSITIONABLE monitors the speed of an @{Positionable} in the simulation, which can be expressed in various formats according the Settings. -- diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 72317d2ba..2c00f48f5 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -1,9 +1,24 @@ ---- **Core** -- ZONE classes define **zones** within your mission of **various forms**, with **various capabilities**. --- --- ![Banner Image](..\Presentations\ZONE\Dia1.JPG) +--- **Core** - Define zones within your mission of various forms, with various capabilities. -- -- === -- +-- ## Features: +-- +-- * Create radius zones. +-- * Create trigger zones. +-- * Create polygon zones. +-- * Create moving zones around a unit. +-- * Create moving zones around a group. +-- * Provide the zone behaviour. Some zones are static, while others are moveable. +-- * Enquiry if a coordinate is within a zone. +-- * Smoke zones. +-- * Set a zone probability to control zone selection. +-- * Get zone coordinates. +-- * Get zone properties. +-- * Get zone bounding box. +-- * Set/get zone name. +-- +-- -- There are essentially two core functions that zones accomodate: -- -- * Test if an object is within the zone boundaries. @@ -12,7 +27,7 @@ -- The object classes are using the zone classes to test the zone boundaries, which can take various forms: -- -- * Test if completely within the zone. --- * Test if partly within the zone (for @{Group#GROUP} objects). +-- * Test if partly within the zone (for @{Wrapper.Group#GROUP} objects). -- * Test if not in the zone. -- * Distance to the nearest intersecting point of the zone. -- * Distance to the center of the zone. @@ -23,9 +38,9 @@ -- * @{#ZONE_BASE}: The ZONE_BASE class defining the base for all other zone classes. -- * @{#ZONE_RADIUS}: The ZONE_RADIUS class defined by a zone name, a location and a radius. -- * @{#ZONE}: The ZONE class, defined by the zone name as defined within the Mission Editor. --- * @{#ZONE_UNIT}: The ZONE_UNIT class defines by a zone around a @{Unit#UNIT} with a radius. --- * @{#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Group#GROUP} with a radius. --- * @{#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- * @{#ZONE_UNIT}: The ZONE_UNIT class defines by a zone around a @{Wrapper.Unit#UNIT} with a radius. +-- * @{#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. +-- * @{#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. -- -- === -- @@ -34,7 +49,8 @@ -- -- === -- --- @module Zone +-- @module Core.Zone +-- @image Core_Zones.JPG --- @type ZONE_BASE @@ -43,9 +59,7 @@ -- @extends Core.Base#BASE ---- # ZONE_BASE class, extends @{Base#BASE} --- --- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. +--- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. -- -- ## Each zone has a name: -- @@ -53,7 +67,7 @@ -- * @{#ZONE_BASE.SetName}(): Sets the name of the zone. -- -- --- ## Each zone implements two polymorphic functions defined in @{Zone#ZONE_BASE}: +-- ## Each zone implements two polymorphic functions defined in @{Core.Zone#ZONE_BASE}: -- -- * @{#ZONE_BASE.IsVec2InZone}(): Returns if a 2D vector is within the zone. -- * @{#ZONE_BASE.IsVec3InZone}(): Returns if a 3D vector is within the zone. @@ -95,10 +109,10 @@ ZONE_BASE = { --- The ZONE_BASE.BoundingSquare -- @type ZONE_BASE.BoundingSquare --- @field Dcs.DCSTypes#Distance x1 The lower x coordinate (left down) --- @field Dcs.DCSTypes#Distance y1 The lower y coordinate (left down) --- @field Dcs.DCSTypes#Distance x2 The higher x coordinate (right up) --- @field Dcs.DCSTypes#Distance y2 The higher y coordinate (right up) +-- @field DCS#Distance x1 The lower x coordinate (left down) +-- @field DCS#Distance y1 The lower y coordinate (left down) +-- @field DCS#Distance x2 The higher x coordinate (right up) +-- @field DCS#Distance y2 The higher y coordinate (right up) --- ZONE_BASE constructor @@ -114,6 +128,8 @@ function ZONE_BASE:New( ZoneName ) return self end + + --- Returns the name of the zone. -- @param #ZONE_BASE self -- @return #string The name of the zone. @@ -136,7 +152,7 @@ end --- Returns if a Vec2 is within the zone. -- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Vec2 Vec2 The Vec2 to test. +-- @param DCS#Vec2 Vec2 The Vec2 to test. -- @return #boolean true if the Vec2 is within the zone. function ZONE_BASE:IsVec2InZone( Vec2 ) self:F2( Vec2 ) @@ -146,13 +162,19 @@ end --- Returns if a Vec3 is within the zone. -- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. +-- @param DCS#Vec3 Vec3 The point to test. -- @return #boolean true if the Vec3 is within the zone. function ZONE_BASE:IsVec3InZone( Vec3 ) - self:F2( Vec3 ) - local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } ) + return InZone +end +--- Returns if a Coordinate is within the zone. +-- @param #ZONE_BASE self +-- @param Core.Point#COORDINATE Coordinate The coordinate to test. +-- @return #boolean true if the coordinate is within the zone. +function ZONE_BASE:IsCoordinateInZone( Coordinate ) + local InZone = self:IsVec2InZone( Coordinate:GetVec2() ) return InZone end @@ -161,10 +183,7 @@ end -- @param Core.Point#POINT_VEC2 PointVec2 The PointVec2 to test. -- @return #boolean true if the PointVec2 is within the zone. function ZONE_BASE:IsPointVec2InZone( PointVec2 ) - self:F2( PointVec2 ) - local InZone = self:IsVec2InZone( PointVec2:GetVec2() ) - return InZone end @@ -173,26 +192,21 @@ end -- @param Core.Point#POINT_VEC3 PointVec3 The PointVec3 to test. -- @return #boolean true if the PointVec3 is within the zone. function ZONE_BASE:IsPointVec3InZone( PointVec3 ) - self:F2( PointVec3 ) - local InZone = self:IsPointVec2InZone( PointVec3 ) - return InZone end ---- Returns the @{DCSTypes#Vec2} coordinate of the zone. +--- Returns the @{DCS#Vec2} coordinate of the zone. -- @param #ZONE_BASE self -- @return #nil. function ZONE_BASE:GetVec2() - self:F2( self.ZoneName ) - return nil end ---- Returns a @{Point#POINT_VEC2} of the zone. +--- Returns a @{Core.Point#POINT_VEC2} of the zone. -- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. +-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located. -- @return Core.Point#POINT_VEC2 The PointVec2 of the zone. function ZONE_BASE:GetPointVec2() self:F2( self.ZoneName ) @@ -207,7 +221,7 @@ function ZONE_BASE:GetPointVec2() end ---- Returns a @{Point#COORDINATE} of the zone. +--- Returns a @{Core.Point#COORDINATE} of the zone. -- @param #ZONE_BASE self -- @return Core.Point#COORDINATE The Coordinate of the zone. function ZONE_BASE:GetCoordinate() @@ -223,10 +237,10 @@ function ZONE_BASE:GetCoordinate() end ---- Returns the @{DCSTypes#Vec3} of the zone. +--- Returns the @{DCS#Vec3} of the zone. -- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Dcs.DCSTypes#Vec3 The Vec3 of the zone. +-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located. +-- @return DCS#Vec3 The Vec3 of the zone. function ZONE_BASE:GetVec3( Height ) self:F2( self.ZoneName ) @@ -241,9 +255,9 @@ function ZONE_BASE:GetVec3( Height ) return Vec3 end ---- Returns a @{Point#POINT_VEC3} of the zone. +--- Returns a @{Core.Point#POINT_VEC3} of the zone. -- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. +-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located. -- @return Core.Point#POINT_VEC3 The PointVec3 of the zone. function ZONE_BASE:GetPointVec3( Height ) self:F2( self.ZoneName ) @@ -257,9 +271,9 @@ function ZONE_BASE:GetPointVec3( Height ) return PointVec3 end ---- Returns a @{Point#COORDINATE} of the zone. +--- Returns a @{Core.Point#COORDINATE} of the zone. -- @param #ZONE_BASE self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. +-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located. -- @return Core.Point#COORDINATE The Coordinate of the zone. function ZONE_BASE:GetCoordinate( Height ) --R2.1 self:F2( self.ZoneName ) @@ -274,21 +288,21 @@ function ZONE_BASE:GetCoordinate( Height ) --R2.1 end ---- Define a random @{DCSTypes#Vec2} within the zone. +--- Define a random @{DCS#Vec2} within the zone. -- @param #ZONE_BASE self --- @return Dcs.DCSTypes#Vec2 The Vec2 coordinates. +-- @return DCS#Vec2 The Vec2 coordinates. function ZONE_BASE:GetRandomVec2() return nil end ---- Define a random @{Point#POINT_VEC2} within the zone. +--- Define a random @{Core.Point#POINT_VEC2} within the zone. -- @param #ZONE_BASE self -- @return Core.Point#POINT_VEC2 The PointVec2 coordinates. function ZONE_BASE:GetRandomPointVec2() return nil end ---- Define a random @{Point#POINT_VEC3} within the zone. +--- Define a random @{Core.Point#POINT_VEC3} within the zone. -- @param #ZONE_BASE self -- @return Core.Point#POINT_VEC3 The PointVec3 coordinates. function ZONE_BASE:GetRandomPointVec3() @@ -315,14 +329,15 @@ end -- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. function ZONE_BASE:SmokeZone( SmokeColor ) self:F2( SmokeColor ) - + end --- Set the randomization probability of a zone to be selected. -- @param #ZONE_BASE self --- @param ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. +-- @param #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability. +-- @return #ZONE_BASE self function ZONE_BASE:SetZoneProbability( ZoneProbability ) - self:F2( ZoneProbability ) + self:F( { self:GetName(), ZoneProbability = ZoneProbability } ) self.ZoneProbability = ZoneProbability or 1 return self @@ -332,7 +347,7 @@ end -- @param #ZONE_BASE self -- @return #number A value between 0 and 1. 0 = 0% and 1 = 100% probability. function ZONE_BASE:GetZoneProbability() - self:F2() + self:F2() return self.ZoneProbability end @@ -341,6 +356,27 @@ end -- @param #ZONE_BASE self -- @return #ZONE_BASE The zone is selected taking into account the randomization probability factor. -- @return #nil The zone is not selected taking into account the randomization probability factor. +-- @usage +-- +-- local ZoneArray = { ZONE:New( "Zone1" ), ZONE:New( "Zone2" ) } +-- +-- -- We set a zone probability of 70% to the first zone and 30% to the second zone. +-- ZoneArray[1]:SetZoneProbability( 0.5 ) +-- ZoneArray[2]:SetZoneProbability( 0.5 ) +-- +-- local ZoneSelected = nil +-- +-- while ZoneSelected == nil do +-- for _, Zone in pairs( ZoneArray ) do +-- ZoneSelected = Zone:GetZoneMaybe() +-- if ZoneSelected ~= nil then +-- break +-- end +-- end +-- end +-- +-- -- The result should be that Zone1 would be more probable selected than Zone2. +-- function ZONE_BASE:GetZoneMaybe() self:F2() @@ -355,13 +391,11 @@ end --- The ZONE_RADIUS class, defined by a zone name, a location and a radius. -- @type ZONE_RADIUS --- @field Dcs.DCSTypes#Vec2 Vec2 The current location of the zone. --- @field Dcs.DCSTypes#Distance Radius The radius of the zone. +-- @field DCS#Vec2 Vec2 The current location of the zone. +-- @field DCS#Distance Radius The radius of the zone. -- @extends #ZONE_BASE ---- # ZONE_RADIUS class, extends @{Zone#ZONE_BASE} --- --- The ZONE_RADIUS class defined by a zone name, a location and a radius. +--- The ZONE_RADIUS class defined by a zone name, a location and a radius. -- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties. -- -- ## ZONE_RADIUS constructor @@ -375,17 +409,17 @@ end -- -- ## Manage the location of the zone -- --- * @{#ZONE_RADIUS.SetVec2}(): Sets the @{DCSTypes#Vec2} of the zone. --- * @{#ZONE_RADIUS.GetVec2}(): Returns the @{DCSTypes#Vec2} of the zone. --- * @{#ZONE_RADIUS.GetVec3}(): Returns the @{DCSTypes#Vec3} of the zone, taking an additional height parameter. +-- * @{#ZONE_RADIUS.SetVec2}(): Sets the @{DCS#Vec2} of the zone. +-- * @{#ZONE_RADIUS.GetVec2}(): Returns the @{DCS#Vec2} of the zone. +-- * @{#ZONE_RADIUS.GetVec3}(): Returns the @{DCS#Vec3} of the zone, taking an additional height parameter. -- -- ## Zone point randomization -- -- Various functions exist to find random points within the zone. -- -- * @{#ZONE_RADIUS.GetRandomVec2}(): Gets a random 2D point in the zone. --- * @{#ZONE_RADIUS.GetRandomPointVec2}(): Gets a @{Point#POINT_VEC2} object representing a random 2D point in the zone. --- * @{#ZONE_RADIUS.GetRandomPointVec3}(): Gets a @{Point#POINT_VEC3} object representing a random 3D point in the zone. Note that the height of the point is at landheight. +-- * @{#ZONE_RADIUS.GetRandomPointVec2}(): Gets a @{Core.Point#POINT_VEC2} object representing a random 2D point in the zone. +-- * @{#ZONE_RADIUS.GetRandomPointVec3}(): Gets a @{Core.Point#POINT_VEC3} object representing a random 3D point in the zone. Note that the height of the point is at landheight. -- -- @field #ZONE_RADIUS ZONE_RADIUS = { @@ -395,8 +429,8 @@ ZONE_RADIUS = { --- Constructor of @{#ZONE_RADIUS}, taking the zone name, the zone location and a radius. -- @param #ZONE_RADIUS self -- @param #string ZoneName Name of the zone. --- @param Dcs.DCSTypes#Vec2 Vec2 The location of the zone. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. +-- @param DCS#Vec2 Vec2 The location of the zone. +-- @param DCS#Distance Radius The radius of the zone. -- @return #ZONE_RADIUS self function ZONE_RADIUS:New( ZoneName, Vec2, Radius ) local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_RADIUS @@ -410,8 +444,9 @@ end --- Bounds the zone with tires. -- @param #ZONE_RADIUS self --- @param #number Points (optional) The amount of points in the circle. --- @param #boolean UnBound If true the tyres will be destroyed. +-- @param #number Points (optional) The amount of points in the circle. Default 360. +-- @param DCS#country.id CountryID The country id of the tire objects, e.g. country.id.USA for blue or country.id.RUSSIA for red. +-- @param #boolean UnBound (Optional) If true the tyres will be destroyed. -- @return #ZONE_RADIUS self function ZONE_RADIUS:BoundZone( Points, CountryID, UnBound ) @@ -490,7 +525,7 @@ end -- @param #ZONE_RADIUS self -- @param Utilities.Utils#FLARECOLOR FlareColor The flare color. -- @param #number Points (optional) The amount of points in the circle. --- @param Dcs.DCSTypes#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. +-- @param DCS#Azimuth Azimuth (optional) Azimuth The azimuth of the flare. -- @param #number AddHeight (optional) The height to be added for the smoke. -- @return #ZONE_RADIUS self function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth, AddHeight ) @@ -500,7 +535,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 @@ -518,7 +553,7 @@ end --- Returns the radius of the zone. -- @param #ZONE_RADIUS self --- @return Dcs.DCSTypes#Distance The radius of the zone. +-- @return DCS#Distance The radius of the zone. function ZONE_RADIUS:GetRadius() self:F2( self.ZoneName ) @@ -529,8 +564,8 @@ end --- Sets the radius of the zone. -- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. --- @return Dcs.DCSTypes#Distance The radius of the zone. +-- @param DCS#Distance Radius The radius of the zone. +-- @return DCS#Distance The radius of the zone. function ZONE_RADIUS:SetRadius( Radius ) self:F2( self.ZoneName ) @@ -540,9 +575,9 @@ function ZONE_RADIUS:SetRadius( Radius ) return self.Radius end ---- Returns the @{DCSTypes#Vec2} of the zone. +--- Returns the @{DCS#Vec2} of the zone. -- @param #ZONE_RADIUS self --- @return Dcs.DCSTypes#Vec2 The location of the zone. +-- @return DCS#Vec2 The location of the zone. function ZONE_RADIUS:GetVec2() self:F2( self.ZoneName ) @@ -551,10 +586,10 @@ function ZONE_RADIUS:GetVec2() return self.Vec2 end ---- Sets the @{DCSTypes#Vec2} of the zone. +--- Sets the @{DCS#Vec2} of the zone. -- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec2 Vec2 The new location of the zone. --- @return Dcs.DCSTypes#Vec2 The new location of the zone. +-- @param DCS#Vec2 Vec2 The new location of the zone. +-- @return DCS#Vec2 The new location of the zone. function ZONE_RADIUS:SetVec2( Vec2 ) self:F2( self.ZoneName ) @@ -565,10 +600,10 @@ function ZONE_RADIUS:SetVec2( Vec2 ) return self.Vec2 end ---- Returns the @{DCSTypes#Vec3} of the ZONE_RADIUS. +--- Returns the @{DCS#Vec3} of the ZONE_RADIUS. -- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Dcs.DCSTypes#Vec3 The point of the zone. +-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located. +-- @return DCS#Vec3 The point of the zone. function ZONE_RADIUS:GetVec3( Height ) self:F2( { self.ZoneName, Height } ) @@ -583,6 +618,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: -- @@ -594,15 +632,16 @@ end -- @{#ZONE_RADIUS. -- @param #ZONE_RADIUS self -- @param ObjectCategories --- @param Coalition +-- @param UnitCategories -- @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 = {} self.ScanData.Scenery = {} + self.ScanData.Units = {} local ZoneCoord = self:GetCoordinate() local ZoneRadius = self:GetRadius() @@ -624,15 +663,31 @@ function ZONE_RADIUS:Scan( ObjectCategories ) 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:F( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) + local Include = false + if not UnitCategories then + Include = true + else + 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() + self.ScanData.Coalitions[CoalitionDCSUnit] = true + self.ScanData.Units[ZoneObject] = ZoneObject + self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) + end end if ObjectCategory == Object.Category.SCENERY then local SceneryType = ZoneObject:getTypeName() local SceneryName = ZoneObject:getName() self.ScanData.Scenery[SceneryType] = self.ScanData.Scenery[SceneryType] or {} self.ScanData.Scenery[SceneryType][SceneryName] = SCENERY:Register( SceneryName, ZoneObject ) - self:F( { SCENERY = self.ScanData.Scenery[SceneryType][SceneryName] } ) + self:F2( { SCENERY = self.ScanData.Scenery[SceneryType][SceneryName] } ) end end return true @@ -643,6 +698,12 @@ function ZONE_RADIUS:Scan( ObjectCategories ) end +function ZONE_RADIUS:GetScannedUnits() + + return self.ScanData.Units +end + + function ZONE_RADIUS:CountScannedCoalitions() local Count = 0 @@ -804,7 +865,7 @@ end --- Returns if a location is within the zone. -- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. +-- @param DCS#Vec2 Vec2 The location to test. -- @return #boolean true if the location is within the zone. function ZONE_RADIUS:IsVec2InZone( Vec2 ) self:F2( Vec2 ) @@ -822,7 +883,7 @@ end --- Returns if a point is within the zone. -- @param #ZONE_RADIUS self --- @param Dcs.DCSTypes#Vec3 Vec3 The point to test. +-- @param DCS#Vec3 Vec3 The point to test. -- @return #boolean true if the point is within the zone. function ZONE_RADIUS:IsVec3InZone( Vec3 ) self:F2( Vec3 ) @@ -836,7 +897,7 @@ end -- @param #ZONE_RADIUS self -- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. -- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Dcs.DCSTypes#Vec2 The random location within the zone. +-- @return DCS#Vec2 The random location within the zone. function ZONE_RADIUS:GetRandomVec2( inner, outer ) self:F( self.ZoneName, inner, outer ) @@ -854,11 +915,11 @@ function ZONE_RADIUS:GetRandomVec2( inner, outer ) return Point end ---- Returns a @{Point#POINT_VEC2} object reflecting a random 2D location within the zone. +--- Returns a @{Core.Point#POINT_VEC2} object reflecting a random 2D location within the zone. -- @param #ZONE_RADIUS self -- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. -- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#POINT_VEC2 The @{Point#POINT_VEC2} object reflecting the random 3D location within the zone. +-- @return Core.Point#POINT_VEC2 The @{Core.Point#POINT_VEC2} object reflecting the random 3D location within the zone. function ZONE_RADIUS:GetRandomPointVec2( inner, outer ) self:F( self.ZoneName, inner, outer ) @@ -873,7 +934,7 @@ end -- @param #ZONE_RADIUS self -- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. -- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Dcs.DCSTypes#Vec3 The random location within the zone. +-- @return DCS#Vec3 The random location within the zone. function ZONE_RADIUS:GetRandomVec3( inner, outer ) self:F( self.ZoneName, inner, outer ) @@ -885,11 +946,11 @@ function ZONE_RADIUS:GetRandomVec3( inner, outer ) end ---- Returns a @{Point#POINT_VEC3} object reflecting a random 3D location within the zone. +--- Returns a @{Core.Point#POINT_VEC3} object reflecting a random 3D location within the zone. -- @param #ZONE_RADIUS self -- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. -- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#POINT_VEC3 The @{Point#POINT_VEC3} object reflecting the random 3D location within the zone. +-- @return Core.Point#POINT_VEC3 The @{Core.Point#POINT_VEC3} object reflecting the random 3D location within the zone. function ZONE_RADIUS:GetRandomPointVec3( inner, outer ) self:F( self.ZoneName, inner, outer ) @@ -901,7 +962,7 @@ function ZONE_RADIUS:GetRandomPointVec3( inner, outer ) end ---- Returns a @{Point#COORDINATE} object reflecting a random 3D location within the zone. +--- Returns a @{Core.Point#COORDINATE} object reflecting a random 3D location within the zone. -- @param #ZONE_RADIUS self -- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. -- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. @@ -909,7 +970,7 @@ end function ZONE_RADIUS:GetRandomCoordinate( inner, outer ) self:F( self.ZoneName, inner, outer ) - local Coordinate = COORDINATE:NewFromVec2( self:GetRandomVec2() ) + local Coordinate = COORDINATE:NewFromVec2( self:GetRandomVec2(inner, outer) ) self:T3( { Coordinate = Coordinate } ) @@ -922,11 +983,33 @@ end -- @extends #ZONE_RADIUS ---- # ZONE class, extends @{Zone#ZONE_RADIUS} --- --- The ZONE class, defined by the zone name as defined within the Mission Editor. +--- The ZONE class, defined by the zone name as defined within the Mission Editor. -- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. -- +-- ## ZONE constructor +-- +-- * @{#ZONE.New}(): Constructor. This will search for a trigger zone with the name given, and will return for you a ZONE object. +-- +-- ## Declare a ZONE directly in the DCS mission editor! +-- +-- You can declare a ZONE using the DCS mission editor by adding a trigger zone in the mission editor. +-- +-- Then during mission startup, when loading Moose.lua, this trigger zone will be detected as a ZONE declaration. +-- Within the background, a ZONE object will be created within the @{Core.Database}. +-- The ZONE name will be the trigger zone name. +-- +-- So, you can search yourself for the ZONE object by using the @{#ZONE.FindByName}() method. +-- In this example, `local TriggerZone = ZONE:FindByName( "DefenseZone" )` would return the ZONE object +-- that was created at mission startup, and reference it into the `TriggerZone` local object. +-- +-- Refer to mission `ZON-110` for a demonstration. +-- +-- This is especially handy if you want to quickly setup a SET_ZONE... +-- So when you would declare `local SetZone = SET_ZONE:New():FilterPrefixes( "Defense" ):FilterStart()`, +-- then SetZone would contain the ZONE object `DefenseZone` as part of the zone collection, +-- without much scripting overhead!!! +-- +-- -- @field #ZONE ZONE = { ClassName="ZONE", @@ -954,14 +1037,26 @@ function ZONE:New( ZoneName ) return self end +--- Find a zone in the _DATABASE using the name of the zone. +-- @param #ZONE_BASE self +-- @param #string ZoneName The name of the zone. +-- @return #ZONE_BASE self +function ZONE:FindByName( ZoneName ) + + local ZoneFound = _DATABASE:FindZone( ZoneName ) + return ZoneFound +end + + --- @type ZONE_UNIT -- @field Wrapper.Unit#UNIT ZoneUNIT -- @extends Core.Zone#ZONE_RADIUS + --- # ZONE_UNIT class, extends @{Zone#ZONE_RADIUS} -- --- The ZONE_UNIT class defined by a zone around a @{Unit#UNIT} with a radius. +-- The ZONE_UNIT class defined by a zone attached to a @{Wrapper.Unit#UNIT} with a radius and optional offsets. -- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties. -- -- @field #ZONE_UNIT @@ -969,31 +1064,79 @@ ZONE_UNIT = { ClassName="ZONE_UNIT", } ---- Constructor to create a ZONE_UNIT instance, taking the zone name, a zone unit and a radius. +--- Constructor to create a ZONE_UNIT instance, taking the zone name, a zone unit and a radius and optional offsets in X and Y directions. -- @param #ZONE_UNIT self -- @param #string ZoneName Name of the zone. -- @param Wrapper.Unit#UNIT ZoneUNIT The unit as the center of the zone. -- @param Dcs.DCSTypes#Distance Radius The radius of the zone. +-- @param #table Offset A table specifying the offset. The offset table may have the following elements: +-- dx The offset in X direction, +x is north. +-- dy The offset in Y direction, +y is east. +-- rho The distance of the zone from the unit +-- theta The azimuth of the zone relative to unit +-- relative_to_unit If true, theta is measured clockwise from unit's direction else clockwise from north. If using dx, dy setting this to true makes +x parallel to unit heading. +-- dx, dy OR rho, theta may be used, not both. -- @return #ZONE_UNIT self -function ZONE_UNIT:New( ZoneName, ZoneUNIT, Radius ) +function ZONE_UNIT:New( ZoneName, ZoneUNIT, Radius, Offset) + + if Offset then + -- check if the inputs was reasonable, either (dx, dy) or (rho, theta) can be given, else raise an exception. + if (Offset.dx or Offset.dy) and (Offset.rho or Offset.theta) then + error("Cannot use (dx, dy) with (rho, theta)") + end + + self.dy = Offset.dy or 0.0 + self.dx = Offset.dx or 0.0 + self.rho = Offset.rho or 0.0 + self.theta = (Offset.theta or 0.0) * math.pi / 180.0 + self.relative_to_unit = Offset.relative_to_unit or false + end + local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneUNIT:GetVec2(), Radius ) ) + self:F( { ZoneName, ZoneUNIT:GetVec2(), Radius } ) self.ZoneUNIT = ZoneUNIT self.LastVec2 = ZoneUNIT:GetVec2() + -- Zone objects are added to the _DATABASE and SET_ZONE objects. + _EVENTDISPATCHER:CreateEventNewZone( self ) + return self end ---- Returns the current location of the @{Unit#UNIT}. +--- Returns the current location of the @{Wrapper.Unit#UNIT}. -- @param #ZONE_UNIT self --- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Unit#UNIT}location. +-- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Unit#UNIT}location and the offset, if any. function ZONE_UNIT:GetVec2() self:F2( self.ZoneName ) local ZoneVec2 = self.ZoneUNIT:GetVec2() if ZoneVec2 then + + local heading + if self.relative_to_unit then + heading = ( self.ZoneUNIT:GetHeading() or 0.0 ) * math.pi / 180.0 + else + heading = 0.0 + end + + -- update the zone position with the offsets. + if (self.dx or self.dy) then + + -- use heading to rotate offset relative to unit using rotation matrix in 2D. + -- see: https://en.wikipedia.org/wiki/Rotation_matrix + ZoneVec2.x = ZoneVec2.x + self.dx * math.cos( -heading ) + self.dy * math.sin( -heading ) + ZoneVec2.y = ZoneVec2.y - self.dx * math.sin( -heading ) + self.dy * math.cos( -heading ) + end + + -- if using the polar coordinates + if (self.rho or self.theta) then + ZoneVec2.x = ZoneVec2.x + self.rho * math.cos( self.theta + heading ) + ZoneVec2.y = ZoneVec2.y + self.rho * math.sin( self.theta + heading ) + end + self.LastVec2 = ZoneVec2 return ZoneVec2 else @@ -1007,12 +1150,13 @@ end --- Returns a random location within the zone. -- @param #ZONE_UNIT self --- @return Dcs.DCSTypes#Vec2 The random location within the zone. +-- @return DCS#Vec2 The random location within the zone. function ZONE_UNIT:GetRandomVec2() self:F( self.ZoneName ) local RandomVec2 = {} - local Vec2 = self.ZoneUNIT:GetVec2() + --local Vec2 = self.ZoneUNIT:GetVec2() -- FF: This does not take care of the new offset feature! + local Vec2 = self:GetVec2() if not Vec2 then Vec2 = self.LastVec2 @@ -1027,10 +1171,10 @@ function ZONE_UNIT:GetRandomVec2() return RandomVec2 end ---- Returns the @{DCSTypes#Vec3} of the ZONE_UNIT. +--- Returns the @{DCS#Vec3} of the ZONE_UNIT. -- @param #ZONE_UNIT self --- @param Dcs.DCSTypes#Distance Height The height to add to the land height where the center of the zone is located. --- @return Dcs.DCSTypes#Vec3 The point of the zone. +-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located. +-- @return DCS#Vec3 The point of the zone. function ZONE_UNIT:GetVec3( Height ) self:F2( self.ZoneName ) @@ -1049,48 +1193,57 @@ end -- @extends #ZONE_RADIUS ---- # ZONE_GROUP class, extends @{Zone#ZONE_RADIUS} --- --- The ZONE_GROUP class defines by a zone around a @{Group#GROUP} with a radius. The current leader of the group defines the center of the zone. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. +--- The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. The current leader of the group defines the center of the zone. +-- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. -- -- @field #ZONE_GROUP ZONE_GROUP = { ClassName="ZONE_GROUP", } ---- Constructor to create a ZONE_GROUP instance, taking the zone name, a zone @{Group#GROUP} and a radius. +--- Constructor to create a ZONE_GROUP instance, taking the zone name, a zone @{Wrapper.Group#GROUP} and a radius. -- @param #ZONE_GROUP self -- @param #string ZoneName Name of the zone. --- @param Wrapper.Group#GROUP ZoneGROUP The @{Group} as the center of the zone. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone. +-- @param Wrapper.Group#GROUP ZoneGROUP The @{Wrapper.Group} as the center of the zone. +-- @param DCS#Distance Radius The radius of the zone. -- @return #ZONE_GROUP self function ZONE_GROUP:New( ZoneName, ZoneGROUP, Radius ) local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneGROUP:GetVec2(), Radius ) ) self:F( { ZoneName, ZoneGROUP:GetVec2(), Radius } ) self._.ZoneGROUP = ZoneGROUP + self._.ZoneVec2Cache = self._.ZoneGROUP:GetVec2() + + -- Zone objects are added to the _DATABASE and SET_ZONE objects. + _EVENTDISPATCHER:CreateEventNewZone( self ) return self end ---- Returns the current location of the @{Group}. +--- Returns the current location of the @{Wrapper.Group}. -- @param #ZONE_GROUP self --- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Group} location. +-- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. function ZONE_GROUP:GetVec2() self:F( self.ZoneName ) - local ZoneVec2 = self._.ZoneGROUP:GetVec2() + local ZoneVec2 = nil + + if self._.ZoneGROUP:IsAlive() then + ZoneVec2 = self._.ZoneGROUP:GetVec2() + self._.ZoneVec2Cache = ZoneVec2 + else + ZoneVec2 = self._.ZoneVec2Cache + end self:T( { ZoneVec2 } ) return ZoneVec2 end ---- Returns a random location within the zone of the @{Group}. +--- Returns a random location within the zone of the @{Wrapper.Group}. -- @param #ZONE_GROUP self --- @return Dcs.DCSTypes#Vec2 The random location of the zone based on the @{Group} location. +-- @return DCS#Vec2 The random location of the zone based on the @{Wrapper.Group} location. function ZONE_GROUP:GetRandomVec2() self:F( self.ZoneName ) @@ -1106,11 +1259,11 @@ function ZONE_GROUP:GetRandomVec2() return Point end ---- Returns a @{Point#POINT_VEC2} object reflecting a random 2D location within the zone. +--- Returns a @{Core.Point#POINT_VEC2} object reflecting a random 2D location within the zone. -- @param #ZONE_GROUP self -- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. -- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. --- @return Core.Point#POINT_VEC2 The @{Point#POINT_VEC2} object reflecting the random 3D location within the zone. +-- @return Core.Point#POINT_VEC2 The @{Core.Point#POINT_VEC2} object reflecting the random 3D location within the zone. function ZONE_GROUP:GetRandomPointVec2( inner, outer ) self:F( self.ZoneName, inner, outer ) @@ -1123,14 +1276,12 @@ end --- @type ZONE_POLYGON_BASE --- --@field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCSTypes#Vec2}. +-- --@field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCS#Vec2}. -- @extends #ZONE_BASE ---- # ZONE_POLYGON_BASE class, extends @{Zone#ZONE_BASE} --- --- The ZONE_POLYGON_BASE class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. +--- The ZONE_POLYGON_BASE class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. -- This class is an abstract BASE class for derived classes, and is not meant to be instantiated. -- -- ## Zone point randomization @@ -1138,8 +1289,8 @@ end -- Various functions exist to find random points within the zone. -- -- * @{#ZONE_POLYGON_BASE.GetRandomVec2}(): Gets a random 2D point in the zone. --- * @{#ZONE_POLYGON_BASE.GetRandomPointVec2}(): Return a @{Point#POINT_VEC2} object representing a random 2D point within the zone. --- * @{#ZONE_POLYGON_BASE.GetRandomPointVec3}(): Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. +-- * @{#ZONE_POLYGON_BASE.GetRandomPointVec2}(): Return a @{Core.Point#POINT_VEC2} object representing a random 2D point within the zone. +-- * @{#ZONE_POLYGON_BASE.GetRandomPointVec3}(): Return a @{Core.Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. -- -- @field #ZONE_POLYGON_BASE ZONE_POLYGON_BASE = { @@ -1148,13 +1299,13 @@ ZONE_POLYGON_BASE = { --- A points array. -- @type ZONE_POLYGON_BASE.ListVec2 --- @list +-- @list ---- Constructor to create a ZONE_POLYGON_BASE instance, taking the zone name and an array of @{DCSTypes#Vec2}, forming a polygon. --- The @{Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected. +--- Constructor to create a ZONE_POLYGON_BASE instance, taking the zone name and an array of @{DCS#Vec2}, forming a polygon. +-- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected. -- @param #ZONE_POLYGON_BASE self -- @param #string ZoneName Name of the zone. --- @param #ZONE_POLYGON_BASE.ListVec2 PointsArray An array of @{DCSTypes#Vec2}, forming a polygon.. +-- @param #ZONE_POLYGON_BASE.ListVec2 PointsArray An array of @{DCS#Vec2}, forming a polygon.. -- @return #ZONE_POLYGON_BASE self function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) @@ -1175,7 +1326,7 @@ end --- Returns the center location of the polygon. -- @param #ZONE_GROUP self --- @return Dcs.DCSTypes#Vec2 The location of the zone based on the @{Group} location. +-- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. function ZONE_POLYGON_BASE:GetVec2() self:F( self.ZoneName ) @@ -1247,16 +1398,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] } ) @@ -1277,12 +1427,48 @@ 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. -- Source learned and taken from: https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html -- @param #ZONE_POLYGON_BASE self --- @param Dcs.DCSTypes#Vec2 Vec2 The location to test. +-- @param DCS#Vec2 Vec2 The location to test. -- @return #boolean true if the location is within the zone. function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) self:F2( Vec2 ) @@ -1310,9 +1496,9 @@ function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 ) return InPolygon end ---- Define a random @{DCSTypes#Vec2} within the zone. +--- Define a random @{DCS#Vec2} within the zone. -- @param #ZONE_POLYGON_BASE self --- @return Dcs.DCSTypes#Vec2 The Vec2 coordinate. +-- @return DCS#Vec2 The Vec2 coordinate. function ZONE_POLYGON_BASE:GetRandomVec2() self:F2() @@ -1336,9 +1522,9 @@ function ZONE_POLYGON_BASE:GetRandomVec2() return Vec2 end ---- Return a @{Point#POINT_VEC2} object representing a random 2D point at landheight within the zone. +--- Return a @{Core.Point#POINT_VEC2} object representing a random 2D point at landheight within the zone. -- @param #ZONE_POLYGON_BASE self --- @return @{Point#POINT_VEC2} +-- @return @{Core.Point#POINT_VEC2} function ZONE_POLYGON_BASE:GetRandomPointVec2() self:F2() @@ -1349,9 +1535,9 @@ function ZONE_POLYGON_BASE:GetRandomPointVec2() return PointVec2 end ---- Return a @{Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. +--- Return a @{Core.Point#POINT_VEC3} object representing a random 3D point at landheight within the zone. -- @param #ZONE_POLYGON_BASE self --- @return @{Point#POINT_VEC3} +-- @return @{Core.Point#POINT_VEC3} function ZONE_POLYGON_BASE:GetRandomPointVec3() self:F2() @@ -1363,7 +1549,7 @@ function ZONE_POLYGON_BASE:GetRandomPointVec3() end ---- Return a @{Point#COORDINATE} object representing a random 3D point at landheight within the zone. +--- Return a @{Core.Point#COORDINATE} object representing a random 3D point at landheight within the zone. -- @param #ZONE_POLYGON_BASE self -- @return Core.Point#COORDINATE function ZONE_POLYGON_BASE:GetRandomCoordinate() @@ -1404,18 +1590,36 @@ end -- @extends #ZONE_POLYGON_BASE ---- # ZONE_POLYGON class, extends @{Zone#ZONE_POLYGON_BASE} +--- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon. +-- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. -- --- The ZONE_POLYGON class defined by a sequence of @{Group#GROUP} waypoints within the Mission Editor, forming a polygon. --- This class implements the inherited functions from @{Zone#ZONE_RADIUS} taking into account the own zone format and properties. +-- ## Declare a ZONE_POLYGON directly in the DCS mission editor! +-- +-- You can declare a ZONE_POLYGON using the DCS mission editor by adding the ~ZONE_POLYGON tag in the group name. +-- +-- So, imagine you have a group declared in the mission editor, with group name `DefenseZone~ZONE_POLYGON`. +-- Then during mission startup, when loading Moose.lua, this group will be detected as a ZONE_POLYGON declaration. +-- Within the background, a ZONE_POLYGON object will be created within the @{Core.Database} using the properties of the group. +-- The ZONE_POLYGON name will be the group name without the ~ZONE_POLYGON tag. +-- +-- So, you can search yourself for the ZONE_POLYGON by using the @{#ZONE_POLYGON.FindByName}() method. +-- In this example, `local PolygonZone = ZONE_POLYGON:FindByName( "DefenseZone" )` would return the ZONE_POLYGON object +-- that was created at mission startup, and reference it into the `PolygonZone` local object. +-- +-- Mission `ZON-510` shows a demonstration of this feature or method. +-- +-- This is especially handy if you want to quickly setup a SET_ZONE... +-- So when you would declare `local SetZone = SET_ZONE:New():FilterPrefixes( "Defense" ):FilterStart()`, +-- then SetZone would contain the ZONE_POLYGON object `DefenseZone` as part of the zone collection, +-- without much scripting overhead!!! -- -- @field #ZONE_POLYGON ZONE_POLYGON = { ClassName="ZONE_POLYGON", } ---- Constructor to create a ZONE_POLYGON instance, taking the zone name and the @{Group#GROUP} defined within the Mission Editor. --- The @{Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected by ZONE_POLYGON. +--- Constructor to create a ZONE_POLYGON instance, taking the zone name and the @{Wrapper.Group#GROUP} defined within the Mission Editor. +-- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected by ZONE_POLYGON. -- @param #ZONE_POLYGON self -- @param #string ZoneName Name of the zone. -- @param Wrapper.Group#GROUP ZoneGroup The GROUP waypoints as defined within the Mission Editor define the polygon shape. @@ -1427,14 +1631,16 @@ function ZONE_POLYGON:New( ZoneName, ZoneGroup ) local self = BASE:Inherit( self, ZONE_POLYGON_BASE:New( ZoneName, GroupPoints ) ) self:F( { ZoneName, ZoneGroup, self._.Polygon } ) + -- Zone objects are added to the _DATABASE and SET_ZONE objects. + _EVENTDISPATCHER:CreateEventNewZone( self ) + return self end ---- Constructor to create a ZONE_POLYGON instance, taking the zone name and the **name** of the @{Group#GROUP} defined within the Mission Editor. --- The @{Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected by ZONE_POLYGON. +--- Constructor to create a ZONE_POLYGON instance, taking the zone name and the **name** of the @{Wrapper.Group#GROUP} defined within the Mission Editor. +-- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected by ZONE_POLYGON. -- @param #ZONE_POLYGON self --- @param #string ZoneName Name of the zone. -- @param #string GroupName The group name of the GROUP defining the waypoints within the Mission Editor to define the polygon shape. -- @return #ZONE_POLYGON self function ZONE_POLYGON:NewFromGroupName( GroupName ) @@ -1446,6 +1652,120 @@ function ZONE_POLYGON:NewFromGroupName( GroupName ) local self = BASE:Inherit( self, ZONE_POLYGON_BASE:New( GroupName, GroupPoints ) ) self:F( { GroupName, ZoneGroup, self._.Polygon } ) + -- Zone objects are added to the _DATABASE and SET_ZONE objects. + _EVENTDISPATCHER:CreateEventNewZone( self ) + return self end + +--- Find a polygon zone in the _DATABASE using the name of the polygon zone. +-- @param #ZONE_POLYGON self +-- @param #string ZoneName The name of the polygon zone. +-- @return #ZONE_POLYGON self +function ZONE_POLYGON:FindByName( ZoneName ) + + local ZoneFound = _DATABASE:FindZone( ZoneName ) + return ZoneFound +end + +do -- ZONE_AIRBASE + + --- @type ZONE_AIRBASE + -- @extends #ZONE_RADIUS + + + --- The ZONE_AIRBASE class defines by a zone around a @{Wrapper.Airbase#AIRBASE} with a radius. + -- This class implements the inherited functions from @{Core.Zone#ZONE_RADIUS} taking into account the own zone format and properties. + -- + -- @field #ZONE_AIRBASE + ZONE_AIRBASE = { + ClassName="ZONE_AIRBASE", + } + + + + --- Constructor to create a ZONE_AIRBASE instance, taking the zone name, a zone @{Wrapper.Airbase#AIRBASE} and a radius. + -- @param #ZONE_AIRBASE self + -- @param #string AirbaseName Name of the airbase. + -- @param DCS#Distance Radius (Optional)The radius of the zone in meters. Default 4000 meters. + -- @return #ZONE_AIRBASE self + function ZONE_AIRBASE:New( AirbaseName, Radius ) + + Radius=Radius or 4000 + + local Airbase = AIRBASE:FindByName( AirbaseName ) + + local self = BASE:Inherit( self, ZONE_RADIUS:New( AirbaseName, Airbase:GetVec2(), Radius ) ) + + self._.ZoneAirbase = Airbase + self._.ZoneVec2Cache = self._.ZoneAirbase:GetVec2() + + -- Zone objects are added to the _DATABASE and SET_ZONE objects. + _EVENTDISPATCHER:CreateEventNewZone( self ) + + return self + end + + --- Get the airbase as part of the ZONE_AIRBASE object. + -- @param #ZONE_AIRBASE self + -- @return Wrapper.Airbase#AIRBASE The airbase. + function ZONE_AIRBASE:GetAirbase() + return self._.ZoneAirbase + end + + --- Returns the current location of the @{Wrapper.Group}. + -- @param #ZONE_AIRBASE self + -- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location. + function ZONE_AIRBASE:GetVec2() + self:F( self.ZoneName ) + + local ZoneVec2 = nil + + if self._.ZoneAirbase:IsAlive() then + ZoneVec2 = self._.ZoneAirbase:GetVec2() + self._.ZoneVec2Cache = ZoneVec2 + else + ZoneVec2 = self._.ZoneVec2Cache + end + + self:T( { ZoneVec2 } ) + + return ZoneVec2 + end + + --- Returns a random location within the zone of the @{Wrapper.Group}. + -- @param #ZONE_AIRBASE self + -- @return DCS#Vec2 The random location of the zone based on the @{Wrapper.Group} location. + function ZONE_AIRBASE:GetRandomVec2() + self:F( self.ZoneName ) + + local Point = {} + local Vec2 = self._.ZoneAirbase:GetVec2() + + local angle = math.random() * math.pi*2; + Point.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius(); + Point.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius(); + + self:T( { Point } ) + + return Point + end + + --- Returns a @{Core.Point#POINT_VEC2} object reflecting a random 2D location within the zone. + -- @param #ZONE_AIRBASE self + -- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0. + -- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone. + -- @return Core.Point#POINT_VEC2 The @{Core.Point#POINT_VEC2} object reflecting the random 3D location within the zone. + function ZONE_AIRBASE:GetRandomPointVec2( inner, outer ) + self:F( self.ZoneName, inner, outer ) + + local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() ) + + self:T3( { PointVec2 } ) + + return PointVec2 + end + + +end diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua new file mode 100644 index 000000000..002ea566d --- /dev/null +++ b/Moose Development/Moose/DCS.lua @@ -0,0 +1,1324 @@ +--- 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. +-- @module DCS +-- @image MOOSE.JPG + +do -- world + + --- [DCS Enum world](https://wiki.hoggitworld.com/view/DCS_enum_world) + -- @type world + -- @field #world.event event [https://wiki.hoggitworld.com/view/DCS_enum_world](https://wiki.hoggitworld.com/view/DCS_enum_world) + -- @field #world.BirthPlace BirthPlace The birthplace enumerator is used to define where an aircraft or helicopter has spawned in association with birth events. + -- @field #world.VolumeType VolumeType The volumeType enumerator defines the types of 3d geometery used within the [world.searchObjects](https://wiki.hoggitworld.com/view/DCS_func_searchObjects) function. + + --- The world singleton contains functions centered around two different but extremely useful functions. + -- * Events and event handlers are all governed within world. + -- * A number of functions to get information about the game world. + -- + -- See [https://wiki.hoggitworld.com/view/DCS_singleton_world](https://wiki.hoggitworld.com/view/DCS_singleton_world) + -- @field #world world + world = {} + + --- [https://wiki.hoggitworld.com/view/DCS_enum_world](https://wiki.hoggitworld.com/view/DCS_enum_world) + -- @type world.event + -- @field S_EVENT_INVALID + -- @field S_EVENT_SHOT [https://wiki.hoggitworld.com/view/DCS_event_shot](https://wiki.hoggitworld.com/view/DCS_event_shot) + -- @field S_EVENT_HIT [https://wiki.hoggitworld.com/view/DCS_event_hit](https://wiki.hoggitworld.com/view/DCS_event_hit) + -- @field S_EVENT_TAKEOFF [https://wiki.hoggitworld.com/view/DCS_event_takeoff](https://wiki.hoggitworld.com/view/DCS_event_takeoff) + -- @field S_EVENT_LAND [https://wiki.hoggitworld.com/view/DCS_event_land](https://wiki.hoggitworld.com/view/DCS_event_land) + -- @field S_EVENT_CRASH [https://wiki.hoggitworld.com/view/DCS_event_crash](https://wiki.hoggitworld.com/view/DCS_event_crash) + -- @field S_EVENT_EJECTION [https://wiki.hoggitworld.com/view/DCS_event_ejection](https://wiki.hoggitworld.com/view/DCS_event_ejection) + -- @field S_EVENT_REFUELING [https://wiki.hoggitworld.com/view/DCS_event_refueling](https://wiki.hoggitworld.com/view/DCS_event_refueling) + -- @field S_EVENT_DEAD [https://wiki.hoggitworld.com/view/DCS_event_dead](https://wiki.hoggitworld.com/view/DCS_event_dead) + -- @field S_EVENT_PILOT_DEAD [https://wiki.hoggitworld.com/view/DCS_event_pilot_dead](https://wiki.hoggitworld.com/view/DCS_event_pilot_dead) + -- @field S_EVENT_BASE_CAPTURED [https://wiki.hoggitworld.com/view/DCS_event_base_captured](https://wiki.hoggitworld.com/view/DCS_event_base_captured) + -- @field S_EVENT_MISSION_START [https://wiki.hoggitworld.com/view/DCS_event_mission_start](https://wiki.hoggitworld.com/view/DCS_event_mission_start) + -- @field S_EVENT_MISSION_END [https://wiki.hoggitworld.com/view/DCS_event_mission_end](https://wiki.hoggitworld.com/view/DCS_event_mission_end) + -- @field S_EVENT_TOOK_CONTROL + -- @field S_EVENT_REFUELING_STOP [https://wiki.hoggitworld.com/view/DCS_event_refueling_stop](https://wiki.hoggitworld.com/view/DCS_event_refueling_stop) + -- @field S_EVENT_BIRTH [https://wiki.hoggitworld.com/view/DCS_event_birth](https://wiki.hoggitworld.com/view/DCS_event_birth) + -- @field S_EVENT_HUMAN_FAILURE [https://wiki.hoggitworld.com/view/DCS_event_human_failure](https://wiki.hoggitworld.com/view/DCS_event_human_failure) + -- @field S_EVENT_ENGINE_STARTUP [https://wiki.hoggitworld.com/view/DCS_event_engine_startup](https://wiki.hoggitworld.com/view/DCS_event_engine_startup) + -- @field S_EVENT_ENGINE_SHUTDOWN [https://wiki.hoggitworld.com/view/DCS_event_engine_shutdown](https://wiki.hoggitworld.com/view/DCS_event_engine_shutdown) + -- @field S_EVENT_PLAYER_ENTER_UNIT [https://wiki.hoggitworld.com/view/DCS_event_player_enter_unit](https://wiki.hoggitworld.com/view/DCS_event_player_enter_unit) + -- @field S_EVENT_PLAYER_LEAVE_UNIT [https://wiki.hoggitworld.com/view/DCS_event_player_leave_unit](https://wiki.hoggitworld.com/view/DCS_event_player_leave_unit) + -- @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_MAX + + --- The birthplace enumerator is used to define where an aircraft or helicopter has spawned in association with birth events. + -- @type world.BirthPlace + -- @field wsBirthPlace_Air + -- @field wsBirthPlace_RunWay + -- @field wsBirthPlace_Park + -- @field wsBirthPlace_Heliport_Hot + -- @field wsBirthPlace_Heliport_Cold + + --- The volumeType enumerator defines the types of 3d geometery used within the #world.searchObjects function. + -- @type world.VolumeType + -- @field SEGMENT + -- @field BOX + -- @field SPHERE + -- @field PYRAMID + + --- Adds a function as an event handler that executes whenever a simulator event occurs. See [hoggit](https://wiki.hoggitworld.com/view/DCS_func_addEventHandler). + -- @function [parent=#world] addEventHandler + -- @param #table handler Event handler table. + + --- Removes the specified event handler from handling events. + -- @function [parent=#world] removeEventHandler + -- @param #table handler Event handler table. + + --- Returns a table of the single unit object in the game who's skill level is set as "Player". See [hoggit](https://wiki.hoggitworld.com/view/DCS_func_getPlayer). + -- There is only a single player unit in a mission and in single player the user will always spawn into this unit automatically unless other client or Combined Arms slots are available. + -- @function [parent=#world] getPlayer + -- @return DCS#Unit + + --- Searches a defined volume of 3d space for the specified objects within it and then can run function on each returned object. See [hoggit](https://wiki.hoggitworld.com/view/DCS_func_searchObjects). + -- @function [parent=#world] searchObjects + -- @param DCS#Object.Category objectcategory Category (can be a table) of objects to search. + -- @param DCS#word.VolumeType volume Shape of the search area/volume. + -- @param ObjectSeachHandler handler A function that handles the search. + -- @param #table any Additional data. + -- @return DCS#Unit + + --- Returns a table of mark panels indexed numerically that are present within the mission. See [hoggit](https://wiki.hoggitworld.com/view/DCS_func_getMarkPanels) + -- @function [parent=#world] getMarkPanels + -- @return #table Table of marks. + +end -- world + + +do -- env + + --- [DCS Singleton env](https://wiki.hoggitworld.com/view/DCS_singleton_env) + -- @type env + + --- Add message to simulator log with caption "INFO". Message box is optional. + -- @function [parent=#env] info + -- @param #string message message string to add to log. + -- @param #boolean showMessageBox If the parameter is true Message Box will appear. Optional. + + --- Add message to simulator log with caption "WARNING". Message box is optional. + -- @function [parent=#env] warning + -- @param #string message message string to add to log. + -- @param #boolean showMessageBox If the parameter is true Message Box will appear. Optional. + + --- Add message to simulator log with caption "ERROR". Message box is optional. + -- @function [parent=#env] error + -- @param #string message message string to add to log. + -- @param #boolean showMessageBox If the parameter is true Message Box will appear. Optional. + + --- Enables/disables appearance of message box each time lua error occurs. + -- @function [parent=#env] setErrorMessageBoxEnabled + -- @param #boolean on if true message box appearance is enabled. + + --- [DCS Singleton env](https://wiki.hoggitworld.com/view/DCS_singleton_env) + env = {} --#env + +end -- env + + +do -- timer + + --- [DCS Singleton timer](https://wiki.hoggitworld.com/view/DCS_singleton_timer) + -- @type timer + + --- Returns model time in seconds. + -- @function [parent=#timer] getTime + -- @return #Time + + --- Returns mission time in seconds. + -- @function [parent=#timer] getAbsTime + -- @return #Time + + --- Returns mission start time in seconds. + -- @function [parent=#timer] getTime0 + -- @return #Time + + --- Schedules function to call at desired model time. + -- Time function FunctionToCall(any argument, Time time) + -- + -- ... + -- + -- return ... + -- + -- end + -- + -- Must return model time of next call or nil. Note that the DCS scheduler calls the function in protected mode and any Lua errors in the called function will be trapped and not reported. If the function triggers a Lua error then it will be terminated and not scheduled to run again. + -- @function [parent=#timer] scheduleFunction + -- @param #FunctionToCall functionToCall Lua-function to call. Must have prototype of FunctionToCall. + -- @param functionArgument Function argument of any type to pass to functionToCall. + -- @param #Time time Model time of the function call. + -- @return functionId + + --- Re-schedules function to call at another model time. + -- @function [parent=#timer] setFunctionTime + -- @param functionId Lua-function to call. Must have prototype of FunctionToCall. + -- @param #Time time Model time of the function call. + + + --- Removes the function from schedule. + -- @function [parent=#timer] removeFunction + -- @param functionId Function identifier to remove from schedule + + --- [DCS Singleton timer](https://wiki.hoggitworld.com/view/DCS_singleton_timer) + timer = {} --#timer + +end + + +do -- land + + --- [DCS Singleton land](https://wiki.hoggitworld.com/view/DCS_singleton_land) + -- @type land + -- @field #land.SurfaceType SurfaceType + + --- [Type of surface enumerator](https://wiki.hoggitworld.com/view/DCS_singleton_land) + -- @type land.SurfaceType + -- @field LAND + -- @field SHALLOW_WATER + -- @field WATER + -- @field ROAD + -- @field RUNWAY + + --- Returns altitude MSL of the point. + -- @function [parent=#land] getHeight + -- @param #Vec2 point point on the ground. + -- @return #Distance + + --- returns surface type at the given point. + -- @function [parent=#land] getSurfaceType + -- @param #Vec2 point Point on the land. + -- @return #land.SurfaceType + + --- [DCS Singleton land](https://wiki.hoggitworld.com/view/DCS_singleton_land) + land = {} --#land + +end -- land + +do -- country + + --- [DCS Enum country](https://wiki.hoggitworld.com/view/DCS_enum_country) + -- @type country + -- @field #country.id id + + + --- [DCS enumerator country](https://wiki.hoggitworld.com/view/DCS_enum_country) + -- @type country.id + -- @field RUSSIA + -- @field UKRAINE + -- @field USA + -- @field TURKEY + -- @field UK + -- @field FRANCE + -- @field GERMANY + -- @field AGGRESSORS + -- @field CANADA + -- @field SPAIN + -- @field THE_NETHERLANDS + -- @field BELGIUM + -- @field NORWAY + -- @field DENMARK + -- @field ISRAEL + -- @field GEORGIA + -- @field INSURGENTS + -- @field ABKHAZIA + -- @field SOUTH_OSETIA + -- @field ITALY + -- @field AUSTRALIA + -- @field SWITZERLAND + -- @field AUSTRIA + -- @field BELARUS + -- @field BULGARIA + -- @field CHEZH_REPUBLIC + -- @field CHINA + -- @field CROATIA + -- @field EGYPT + -- @field FINLAND + -- @field GREECE + -- @field HUNGARY + -- @field INDIA + -- @field IRAN + -- @field IRAQ + -- @field JAPAN + -- @field KAZAKHSTAN + -- @field NORTH_KOREA + -- @field PAKISTAN + -- @field POLAND + -- @field ROMANIA + -- @field SAUDI_ARABIA + -- @field SERBIA + -- @field SLOVAKIA + -- @field SOUTH_KOREA + -- @field SWEDEN + -- @field SYRIA + -- @field YEMEN + -- @field VIETNAM + -- @field VENEZUELA + -- @field TUNISIA + -- @field THAILAND + -- @field SUDAN + -- @field PHILIPPINES + -- @field MOROCCO + -- @field MEXICO + -- @field MALAYSIA + -- @field LIBYA + -- @field JORDAN + -- @field INDONESIA + -- @field HONDURAS + -- @field ETHIOPIA + -- @field CHILE + -- @field BRAZIL + -- @field BAHRAIN + -- @field THIRDREICH + -- @field YUGOSLAVIA + -- @field USSR + -- @field ITALIAN_SOCIAL_REPUBLIC + -- @field ALGERIA + -- @field KUWAIT + -- @field QATAR + -- @field OMAN + -- @field UNITED_ARAB_EMIRATES + + country = {} --#country + +end -- country + + +do -- Command + + --- @type Command + -- @field #string id + -- @field #Command.params params + + --- @type Command.params + +end -- Command + +do -- coalition + + --- [DCS Enum coalition](https://wiki.hoggitworld.com/view/DCS_enum_coalition) + -- @type coalition + -- @field #coalition.side side + + --- [DCS Enum coalition.side](https://wiki.hoggitworld.com/view/DCS_enum_coalition) + -- @type coalition.side + -- @field NEUTRAL + -- @field RED + -- @field BLUE + + --- @function [parent=#coalition] getCountryCoalition + -- @param #number countryId + -- @return #number coalitionId + + coalition = {} -- #coalition + +end -- coalition + + +do -- Types + + --- @type Desc + -- @field #TypeName typeName type name + -- @field #string displayName localized display name + -- @field #table attributes object type attributes + + --- A distance type + -- @type Distance + + --- An angle type + -- @type Angle + + --- Time is given in seconds. + -- @type Time + -- @extends #number + + --- Model time is the time that drives the simulation. Model time may be stopped, accelerated and decelerated relative real time. + -- @type ModelTime + -- @extends #number + + --- Mission time is a model time plus time of the mission start. + -- @type MissionTime + -- @extends #number + + + --- Distance is given in meters. + -- @type Distance + -- @extends #number + + --- Angle is given in radians. + -- @type Angle + -- @extends #number + + --- Azimuth is an angle of rotation around world axis y counter-clockwise. + -- @type Azimuth + -- @extends #number + + --- Mass is given in kilograms. + -- @type Mass + -- @extends #number + + --- Vec3 type is a 3D-vector. + -- DCS world has 3-dimensional coordinate system. DCS ground is an infinite plain. + -- @type Vec3 + -- @field #Distance x is directed to the north + -- @field #Distance z is directed to the east + -- @field #Distance y is directed up + + --- Vec2 is a 2D-vector for the ground plane as a reference plane. + -- @type Vec2 + -- @field #Distance x Vec2.x = Vec3.x + -- @field #Distance y Vec2.y = Vec3.z + + --- Position is a composite structure. It consists of both coordinate vector and orientation matrix. Position3 (also known as "Pos3" for short) is a table that has following format: + -- @type Position3 + -- @field #Vec3 p + -- @field #Vec3 x + -- @field #Vec3 y + -- @field #Vec3 z + + --- 3-dimensional box. + -- @type Box3 + -- @field #Vec3 min + -- @field #Vec3 max + + --- Each object belongs to a type. Object type is a named couple of properties those independent of mission and common for all units of the same type. Name of unit type is a string. Samples of unit type: "Su-27", "KAMAZ" and "M2 Bradley". + -- @type TypeName + -- @extends #string + + --- AttributeName = string + -- Each object type may have attributes. + -- Attributes are enlisted in ./Scripts/Database/db_attributes.Lua. + -- To know what attributes the object type has, look for the unit type script in sub-directories planes/, helicopter/s, vehicles, navy/ of ./Scripts/Database/ directory. + -- @type AttributeName + -- @extends #string + + --- List of @{#AttributeName} + -- @type AttributeNameArray + -- @list <#AttributeName> + + --- @type Zone + -- @field DCSVec3#Vec3 point + -- @field #number radius + + Zone = {} + + --- @type ModelTime + -- @extends #number + + --- @type Time + -- @extends #number + + --- A task descriptor (internal structure for DCS World). See [https://wiki.hoggitworld.com/view/Category:Tasks](https://wiki.hoggitworld.com/view/Category:Tasks). + -- In MOOSE, these tasks can be accessed via @{Wrapper.Controllable#CONTROLLABLE}. + -- @type Task + -- @field #string id + -- @field #Task.param param + + --- @type Task.param + + --- List of @{#Task} + -- @type TaskArray + -- @list <#Task> + + +end -- + + + +do -- Object + + --- [DCS Class Object](https://wiki.hoggitworld.com/view/DCS_Class_Object) + -- @type Object + -- @field #Object.Category Category + -- @field #Object.Desc Desc + + --- [DCS Enum Object.Category](https://wiki.hoggitworld.com/view/DCS_Class_Object) + -- @type Object.Category + -- @field UNIT + -- @field WEAPON + -- @field STATIC + -- @field SCENERY + -- @field BASE + + --- @type Object.Desc + -- @extends #Desc + -- @field #number life initial life level + -- @field #Box3 box bounding box of collision geometry + + --- @function [parent=#Object] isExist + -- @param #Object self + -- @return #boolean + + --- @function [parent=#Object] destroy + -- @param #Object self + + --- @function [parent=#Object] getCategory + -- @param #Object self + -- @return #Object.Category + + --- Returns type name of the Object. + -- @function [parent=#Object] getTypeName + -- @param #Object self + -- @return #string + + --- Returns object descriptor. + -- @function [parent=#Object] getDesc + -- @param #Object self + -- @return #Object.Desc + + --- Returns true if the object belongs to the category. + -- @function [parent=#Object] hasAttribute + -- @param #Object self + -- @param #AttributeName attributeName Attribute name to check. + -- @return #boolean + + --- Returns name of the object. This is the name that is assigned to the object in the Mission Editor. + -- @function [parent=#Object] getName + -- @param #Object self + -- @return #string + + --- Returns object coordinates for current time. + -- @function [parent=#Object] getPoint + -- @param #Object self + -- @return #Vec3 + + --- Returns object position for current time. + -- @function [parent=#Object] getPosition + -- @param #Object self + -- @return #Position3 + + --- Returns the unit's velocity vector. + -- @function [parent=#Object] getVelocity + -- @param #Object self + -- @return #Vec3 + + --- Returns true if the unit is in air. + -- @function [parent=#Object] inAir + -- @param #Object self + -- @return #boolean + + Object = {} --#Object + +end -- Object + +do -- CoalitionObject + + --- [DCS Class CoalitionObject](https://wiki.hoggitworld.com/view/DCS_Class_Coalition_Object) + -- @type CoalitionObject + -- @extends #Object + + --- Returns coalition of the object. + -- @function [parent=#CoalitionObject] getCoalition + -- @param #CoalitionObject self + -- @return #coalition.side + + --- Returns object country. + -- @function [parent=#CoalitionObject] getCountry + -- @param #CoalitionObject self + -- @return #country.id + + CoalitionObject = {} --#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) + -- Represents airbases: airdromes, helipads and ships with flying decks or landing pads. + -- @type Airbase + -- @extends #CoalitionObject + -- @field #Airbase.ID ID Identifier of an airbase. It assigned to an airbase by the Mission Editor automatically. This identifier is used in AI tasks to refer an airbase that exists (spawned and not dead) or not. + -- @field #Airbase.Category Category enum contains identifiers of airbase categories. + -- @field #Airbase.Desc Desc Airbase descriptor. Airdromes are unique and their types are unique, but helipads and ships are not always unique and may have the same type. + + --- Enum contains identifiers of airbase categories. + -- @type Airbase.Category + -- @field AIRDROME + -- @field HELIPAD + -- @field SHIP + + --- Airbase descriptor. Airdromes are unique and their types are unique, but helipads and ships are not always unique and may have the same type. + -- @type Airbase.Desc + -- @extends #Desc + -- @field #Airbase.Category category Category of the airbase type. + + --- Returns airbase by its name. If no airbase found the function will return nil. + -- @function [parent=#Airbase] getByName + -- @param #string name + -- @return #Airbase + + --- Returns airbase descriptor by type name. If no descriptor is found the function will return nil. + -- @function [parent=#Airbase] getDescByName + -- @param #TypeName typeName Airbase type name. + -- @return #Airbase.Desc + + --- Returns Unit that is corresponded to the airbase. Works only for ships. + -- @function [parent=#Airbase] getUnit + -- @param self + -- @return #Unit + + --- Returns identifier of the airbase. + -- @function [parent=#Airbase] getID + -- @param self + -- @return #Airbase.ID + + --- Returns the airbase's callsign - the localized string. + -- @function [parent=#Airbase] getCallsign + -- @param self + -- @return #string + + --- Returns descriptor of the airbase. + -- @function [parent=#Airbase] getDesc + -- @param self + -- @return #Airbase.Desc + + Airbase = {} --#Airbase + +end -- Airbase + + + +do -- Controller + --- Controller is an object that performs A.I.-routines. Other words controller is an instance of A.I.. Controller stores current main task, active enroute tasks and behavior options. Controller performs commands. Please, read DCS A-10C GUI Manual EN.pdf chapter "Task Planning for Unit Groups", page 91 to understand A.I. system of DCS:A-10C. + -- + -- This class has 2 types of functions: + -- + -- * Tasks + -- * Commands: Commands are instant actions those required zero time to perform. Commands may be used both for control unit/group behavior and control game mechanics. + -- @type Controller + -- @field #Controller.Detection Detection Enum contains identifiers of surface types. + + --- Enables and disables the controller. + -- Note: Now it works only for ground / naval groups! + -- @function [parent=#Controller] setOnOff + -- @param self + -- @param #boolean value Enable / Disable. + + -- Tasks + + --- Resets current task and then sets the task to the controller. Task is a table that contains task identifier and task parameters. + -- @function [parent=#Controller] setTask + -- @param self + -- @param #Task task + + --- Resets current task of the controller. + -- @function [parent=#Controller] resetTask + -- @param self + + --- Pushes the task to the front of the queue and makes the task active. Further call of function Controller.setTask() function will stop current task, clear the queue and set the new task active. If the task queue is empty the function will work like function Controller.setTask() function. + -- @function [parent=#Controller] pushTask + -- @param self + -- @param #Task task + + --- Pops current (front) task from the queue and makes active next task in the queue (if exists). If no more tasks in the queue the function works like function Controller.resetTask() function. Does nothing if the queue is empty. + -- @function [parent=#Controller] popTask + -- @param self + + --- Returns true if the controller has a task. + -- @function [parent=#Controller] hasTask + -- @param self + -- @return #boolean + + -- Commands + + --TODO: describe #Command structure + --- Sets the command to perform by controller. + -- @function [parent=#Controller] setCommand + -- @param self + -- @param #Command command Table that contains command identifier and command parameters. + + + -- Behaviours + + --- Sets the option to the controller. + -- Option is a pair of identifier and value. Behavior options are global parameters those affect controller behavior in all tasks it performs. + -- Option identifiers and values are stored in table AI.Option in subtables Air, Ground and Naval. + -- + -- OptionId = @{#AI.Option.Air.id} or @{#AI.Option.Ground.id} or @{#AI.Option.Naval.id} + -- OptionValue = AI.Option.Air.val[optionName] or AI.Option.Ground.val[optionName] or AI.Option.Naval.val[optionName] + -- + -- @function [parent=#Controller] setOption + -- @param self + -- @param #OptionId optionId Option identifier. + -- @param #OptionValue optionValue Value of the option. + + + -- Detection + + --- Enum contains identifiers of surface types. + -- @type Controller.Detection + -- @field VISUAL + -- @field OPTIC + -- @field RADAR + -- @field IRST + -- @field RWR + -- @field DLINK + + --- Detected target. + -- @type DetectedTarget + -- @field Wrapper.Object#Object object The target + -- @field #boolean visible The target is visible + -- @field #boolean type The target type is known + -- @field #boolean distance Distance to the target is known + + + --- Checks if the target is detected or not. If one or more detection method is specified the function will return true if the target is detected by at least one of these methods. If no detection methods are specified the function will return true if the target is detected by any method. + -- @function [parent=#Controller] isTargetDetected + -- @param self + -- @param Wrapper.Object#Object target Target to check + -- @param #Controller.Detection detection Controller.Detection detection1, Controller.Detection detection2, ... Controller.Detection detectionN + -- @return #boolean detected True if the target is detected. + -- @return #boolean visible Has effect only if detected is true. True if the target is visible now. + -- @return #ModelTime lastTime Has effect only if visible is false. Last time when target was seen. + -- @return #boolean type Has effect only if detected is true. True if the target type is known. + -- @return #boolean distance Has effect only if detected is true. True if the distance to the target is known. + -- @return #Vec3 lastPos Has effect only if visible is false. Last position of the target when it was seen. + -- @return #Vec3 lastVel Has effect only if visible is false. Last velocity of the target when it was seen. + + + --- Returns list of detected targets. If one or more detection method is specified the function will return targets which were detected by at least one of these methods. If no detection methods are specified the function will return targets which were detected by any method. + -- @function [parent=#Controller] getDetectedTargets + -- @param self + -- @param #Controller.Detection detection Controller.Detection detection1, Controller.Detection detection2, ... Controller.Detection detectionN + -- @return #list<#DetectedTarget> array of DetectedTarget + + --- Know a target. + -- @function [parent=#Controller] knowTarget + -- @param self + -- @param Wrapper.Object#Object object The target. + -- @param #boolean type Target type is known. + -- @param #boolean distance Distance to target is known. + + + Controller = {} --#Controller + +end -- Controller + + +do -- Unit + + --- @type Unit + -- @extends #CoalitionObject + -- @field ID Identifier of an unit. It assigned to an unit by the Mission Editor automatically. + -- @field #Unit.Category Category + -- @field #Unit.RefuelingSystem RefuelingSystem + -- @field #Unit.SensorType SensorType + -- @field #Unit.OpticType OpticType + -- @field #Unit.RadarType RadarType + -- @field #Unit.Desc Desc + -- @field #Unit.DescAircraft DescAircraft + -- @field #Unit.DescAirplane DescAirplane + -- @field #Unit.DescHelicopter DescHelicopter + -- @field #Unit.DescVehicle DescVehicle + -- @field #Unit.DescShip DescShip + -- @field #Unit.AmmoItem AmmoItem + -- @field #list<#Unit.AmmoItem> Ammo + -- @field #Unit.Sensor Sensor + -- @field #Unit.Optic Optic + -- @field #Unit.Radar Radar + -- @field #Unit.IRST IRST + + + --- Enum that stores unit categories. + -- @type Unit.Category + -- @field AIRPLANE + -- @field HELICOPTER + -- @field GROUND_UNIT + -- @field SHIP + -- @field STRUCTURE + + --- Enum that stores aircraft refueling system types. + -- @type Unit.RefuelingSystem + -- @field BOOM_AND_RECEPTACLE + -- @field PROBE_AND_DROGUE + + --- Enum that stores sensor types. + -- @type Unit.SensorType + -- @field OPTIC + -- @field RADAR + -- @field IRST + -- @field RWR + + --- Enum that stores types of optic sensors. + -- @type Unit.OpticType + -- @field TV TV-sensor + -- @field LLTV Low-level TV-sensor + -- @field IR Infra-Red optic sensor + + --- Enum that stores radar types. + -- @type Unit.RadarType + -- @field AS air search radar + -- @field SS surface/land search radar + + + --- A unit descriptor. + -- @type Unit.Desc + -- @extends #Object.Desc + -- @field #Unit.Category category Unit Category + -- @field #Mass massEmpty mass of empty unit + -- @field #number speedMax istance / Time, --maximal velocity + + --- An aircraft descriptor. + -- @type Unit.DescAircraft + -- @extends #Unit.Desc + -- @field #Mass fuelMassMax maximal inner fuel mass + -- @field #Distance range Operational range + -- @field #Distance Hmax Ceiling + -- @field #number VyMax #Distance / #Time, --maximal climb rate + -- @field #number NyMin minimal safe acceleration + -- @field #number NyMax maximal safe acceleration + -- @field #Unit.RefuelingSystem tankerType refueling system type + + --- An airplane descriptor. + -- @type Unit.DescAirplane + -- @extends #Unit.DescAircraft + -- @field #number speedMax0 Distance / Time maximal TAS at ground level + -- @field #number speedMax10K Distance / Time maximal TAS at altitude of 10 km + + --- A helicopter descriptor. + -- @type Unit.DescHelicopter + -- @extends #Unit.DescAircraft + -- @field #Distance HmaxStat static ceiling + + --- A vehicle descriptor. + -- @type Unit.DescVehicle + -- @extends #Unit.Desc + -- @field #Angle maxSlopeAngle maximal slope angle + -- @field #boolean riverCrossing can the vehicle cross a rivers + + --- A ship descriptor. + -- @type Unit.DescShip + -- @extends #Unit.Desc + + --- ammunition item: "type-count" pair. + -- @type Unit.AmmoItem + -- @field #Weapon.Desc desc ammunition descriptor + -- @field #number count ammunition count + + --- A unit sensor. + -- @type Unit.Sensor + -- @field #TypeName typeName + -- @field #Unit.SensorType type + + --- An optic sensor. + -- @type Unit.Optic + -- @extends #Unit.Sensor + -- @field #Unit.OpticType opticType + + --- A radar. + -- @type Unit.Radar + -- @extends #Unit.Sensor + -- @field #Distance detectionDistanceRBM detection distance for RCS=1m^2 in real-beam mapping mode, nil if radar doesn't support surface/land search + -- @field #Distance detectionDistanceHRM detection distance for RCS=1m^2 in high-resolution mapping mode, nil if radar has no HRM + -- @field #Unit.Radar.detectionDistanceAir detectionDistanceAir detection distance for RCS=1m^2 airborne target, nil if radar doesn't support air search + + --- @type Unit.Radar.detectionDistanceAir + -- @field #Unit.Radar.detectionDistanceAir.upperHemisphere upperHemisphere + -- @field #Unit.Radar.detectionDistanceAir.lowerHemisphere lowerHemisphere + + --- @type Unit.Radar.detectionDistanceAir.upperHemisphere + -- @field #Distance headOn + -- @field #Distance tailOn + + --- @type Unit.Radar.detectionDistanceAir.lowerHemisphere + -- @field #Distance headOn + -- @field #Distance tailOn + + --- An IRST. + -- @type Unit.IRST + -- @extends #Unit.Sensor + -- @field #Distance detectionDistanceIdle detection of tail-on target with heat signature = 1 in upper hemisphere, engines are in idle + -- @field #Distance detectionDistanceMaximal ..., engines are in maximal mode + -- @field #Distance detectionDistanceAfterburner ..., engines are in afterburner mode + + --- An RWR. + -- @type Unit.RWR + -- @extends #Unit.Sensor + + --- table that stores all unit sensors. + -- TODO @type Sensors + -- + + + --- Returns unit object by the name assigned to the unit in Mission Editor. If there is unit with such name or the unit is destroyed the function will return nil. The function provides access to non-activated units too. + -- @function [parent=#Unit] getByName + -- @param #string name + -- @return #Unit + + --- Returns if the unit is activated. + -- @function [parent=#Unit] isActive + -- @param #Unit self + -- @return #boolean + + --- Returns name of the player that control the unit or nil if the unit is controlled by A.I. + -- @function [parent=#Unit] getPlayerName + -- @param #Unit self + -- @return #string + + --- returns the unit's unique identifier. + -- @function [parent=#Unit] getID + -- @param #Unit self + -- @return #Unit.ID + + + --- Returns the unit's number in the group. The number is the same number the unit has in ME. It may not be changed during the mission. If any unit in the group is destroyed, the numbers of another units will not be changed. + -- @function [parent=#Unit] getNumber + -- @param #Unit self + -- @return #number + + --- Returns controller of the unit if it exist and nil otherwise + -- @function [parent=#Unit] getController + -- @param #Unit self + -- @return #Controller + + --- Returns the unit's group if it exist and nil otherwise + -- @function [parent=#Unit] getGroup + -- @param #Unit self + -- @return #Group + + --- Returns the unit's callsign - the localized string. + -- @function [parent=#Unit] getCallsign + -- @param #Unit self + -- @return #string + + --- Returns the unit's health. Dead units has health <= 1.0 + -- @function [parent=#Unit] getLife + -- @param #Unit self + -- @return #number + + --- returns the unit's initial health. + -- @function [parent=#Unit] getLife0 + -- @param #Unit self + -- @return #number + + --- 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. + -- @function [parent=#Unit] getFuel + -- @param #Unit self + -- @return #number + + --- Returns the unit ammunition. + -- @function [parent=#Unit] getAmmo + -- @param #Unit self + -- @return #Unit.Ammo + + --- Returns the unit sensors. + -- @function [parent=#Unit] getSensors + -- @param #Unit self + -- @return #Unit.Sensors + + --- Returns true if the unit has specified types of sensors. This function is more preferable than Unit.getSensors() if you don't want to get information about all the unit's sensors, and just want to check if the unit has specified types of sensors. + -- @function [parent=#Unit] hasSensors + -- @param #Unit self + -- @param #Unit.SensorType sensorType (= nil) Sensor type. + -- @param ... Additional parameters. + -- @return #boolean + -- @usage + -- If sensorType is Unit.SensorType.OPTIC, additional parameters are optic sensor types. Following example checks if the unit has LLTV or IR optics: + -- unit:hasSensors(Unit.SensorType.OPTIC, Unit.OpticType.LLTV, Unit.OpticType.IR) + -- If sensorType is Unit.SensorType.RADAR, additional parameters are radar types. Following example checks if the unit has air search radars: + -- unit:hasSensors(Unit.SensorType.RADAR, Unit.RadarType.AS) + -- If no additional parameters are specified the function returns true if the unit has at least one sensor of specified type. + -- If sensor type is not specified the function returns true if the unit has at least one sensor of any type. + -- + + --- returns two values: + -- First value indicates if at least one of the unit's radar(s) is on. + -- Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. + -- @function [parent=#Unit] getRadar + -- @param #Unit self + -- @return #boolean, Wrapper.Object#Object + + --- Returns unit descriptor. Descriptor type depends on unit category. + -- @function [parent=#Unit] getDesc + -- @param #Unit self + -- @return #Unit.Desc + + + Unit = {} --#Unit + +end -- Unit + + +do -- Group + + --- Represents group of Units. + -- @type Group + -- @field #ID ID Identifier of a group. It is assigned to a group by Mission Editor automatically. + -- @field #Group.Category Category Enum contains identifiers of group types. + + --- Enum contains identifiers of group types. + -- @type Group.Category + -- @field AIRPLANE + -- @field HELICOPTER + -- @field GROUND + -- @field SHIP + -- @field TRAIN + + -- Static Functions + + --- Returns group by the name assigned to the group in Mission Editor. + -- @function [parent=#Group] getByName + -- @param #string name + -- @return #Group + + -- Member Functions + + --- returns true if the group exist or false otherwise. + -- @function [parent=#Group] isExist + -- @param #Group self + -- @return #boolean + + --- Destroys the group and all of its units. + -- @function [parent=#Group] destroy + -- @param #Group self + + --- Returns category of the group. + -- @function [parent=#Group] getCategory + -- @param #Group self + -- @return #Group.Category + + --- Returns the coalition of the group. + -- @function [parent=#Group] getCoalition + -- @param #Group self + -- @return #coalition.side + + --- Returns the group's name. This is the same name assigned to the group in Mission Editor. + -- @function [parent=#Group] getName + -- @param #Group self + -- @return #string + + --- Returns the group identifier. + -- @function [parent=#Group] getID + -- @param #Group self + -- @return #ID + + --- Returns the unit with number unitNumber. If the unit is not exists the function will return nil. + -- @function [parent=#Group] getUnit + -- @param #Group self + -- @param #number unitNumber + -- @return #Unit + + --- Returns current size of the group. If some of the units will be destroyed, As units are destroyed the size of the group will be changed. + -- @function [parent=#Group] getSize + -- @param #Group self + -- @return #number + + --- Returns initial size of the group. If some of the units will be destroyed, initial size of the group will not be changed. Initial size limits the unitNumber parameter for Group.getUnit() function. + -- @function [parent=#Group] getInitialSize + -- @param #Group self + -- @return #number + + --- Returns array of the units present in the group now. Destroyed units will not be enlisted at all. + -- @function [parent=#Group] getUnits + -- @param #Group self + -- @return #list<#Unit> array of Units + + --- Returns controller of the group. + -- @function [parent=#Group] getController + -- @param #Group self + -- @return #Controller + + Group = {} --#Group + +end -- Group + + +do -- AI + + --- [https://wiki.hoggitworld.com/view/DCS_enum_AI](https://wiki.hoggitworld.com/view/DCS_enum_AI) + -- @type AI + -- @field #AI.Skill Skill + -- @field #AI.Task Task + -- @field #AI.Option Option + + --- [https://wiki.hoggitworld.com/view/DCS_enum_AI](https://wiki.hoggitworld.com/view/DCS_enum_AI) + -- @type AI.Skill + -- @field AVERAGE + -- @field GOOD + -- @field HIGH + -- @field EXCELLENT + -- @field PLAYER + -- @field CLIENT + + --- [https://wiki.hoggitworld.com/view/DCS_enum_AI](https://wiki.hoggitworld.com/view/DCS_enum_AI) + -- @type AI.Task + -- @field #AI.Task.WeaponExpend WeaponExpend + -- @field #AI.Task.OrbitPattern OrbitPattern + -- @field #AI.Task.Designation Designation + -- @field #AI.Task.WaypointType WaypointType + -- @field #AI.Task.TurnMethod TurnMethod + -- @field #AI.Task.AltitudeType AltitudeType + -- @field #AI.Task.VehicleFormation VehicleFormation + + --- [https://wiki.hoggitworld.com/view/DCS_enum_AI](https://wiki.hoggitworld.com/view/DCS_enum_AI) + -- @type AI.Task.WeaponExpend + -- @field ONE + -- @field TWO + -- @field FOUR + -- @field QUARTER + -- @field HALF + -- @field ALL + + --- [https://wiki.hoggitworld.com/view/DCS_enum_AI](https://wiki.hoggitworld.com/view/DCS_enum_AI) + -- @type AI.Task.OrbitPattern + -- @field CIRCLE + -- @field RACE_TRACK + + --- [https://wiki.hoggitworld.com/view/DCS_enum_AI](https://wiki.hoggitworld.com/view/DCS_enum_AI) + -- @type AI.Task.Designation + -- @field NO + -- @field AUTO + -- @field WP + -- @field IR_POINTER + -- @field LASER + + --- @type AI.Task.WaypointType + -- @field TAKEOFF + -- @field TAKEOFF_PARKING + -- @field TURNING_POINT + -- @field LAND + + --- @type AI.Task.TurnMethod + -- @field FLY_OVER_POINT + -- @field FIN_POINT + + --- @type AI.Task.AltitudeType + -- @field BARO + -- @field RADIO + + --- @type AI.Task.VehicleFormation + -- @field OFF_ROAD + -- @field ON_ROAD + -- @field RANK + -- @field CONE + -- @field DIAMOND + -- @field VEE + -- @field ECHELON_LEFT + -- @field ECHELON_RIGHT + + --- @type AI.Option + -- @field #AI.Option.Air Air + -- @field #AI.Option.Ground Ground + -- @field #AI.Option.Naval Naval + + --- @type AI.Option.Air + -- @field #AI.Option.Air.id id + -- @field #AI.Option.Air.val val + + --- @type AI.Option.Ground + -- @field #AI.Option.Ground.id id + -- @field #AI.Option.Ground.val val + + --- @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 + -- @field REACTION_ON_THREAT + -- @field RADAR_USING + -- @field FLARE_USING + -- @field FORMATION + -- @field RTB_ON_BINGO + -- @field SILENCE + + --- @type AI.Option.Air.val + -- @field #AI.Option.Air.val.ROE ROE + -- @field #AI.Option.Air.val.REACTION_ON_THREAT REACTION_ON_THREAT + -- @field #AI.Option.Air.val.RADAR_USING RADAR_USING + -- @field #AI.Option.Air.val.FLARE_USING FLARE_USING + + --- @type AI.Option.Air.val.ROE + -- @field WEAPON_FREE + -- @field OPEN_FIRE_WEAPON_FREE + -- @field OPEN_FIRE + -- @field RETURN_FIRE + -- @field WEAPON_HOLD + + --- @type AI.Option.Air.val.REACTION_ON_THREAT + -- @field NO_REACTION + -- @field PASSIVE_DEFENCE + -- @field EVADE_FIRE + -- @field BYPASS_AND_ESCAPE + -- @field ALLOW_ABORT_MISSION + + --- @type AI.Option.Air.val.RADAR_USING + -- @field NEVER + -- @field FOR_ATTACK_ONLY + -- @field FOR_SEARCH_IF_REQUIRED + -- @field FOR_CONTINUOUS_SEARCH + + --- @type AI.Option.Air.val.FLARE_USING + -- @field NEVER + -- @field AGAINST_FIRED_MISSILE + -- @field WHEN_FLYING_IN_SAM_WEZ + -- @field WHEN_FLYING_NEAR_ENEMIES + + --- @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} + + --- @type AI.Option.Ground.val + -- @field #AI.Option.Ground.val.ROE ROE + -- @field #AI.Option.Ground.val.ALARM_STATE ALARM_STATE + + --- @type AI.Option.Ground.val.ROE + -- @field OPEN_FIRE + -- @field RETURN_FIRE + -- @field WEAPON_HOLD + + --- @type AI.Option.Ground.val.ALARM_STATE + -- @field AUTO + -- @field GREEN + -- @field RED + + --- @type AI.Option.Naval.id + -- @field NO_OPTION + -- @field ROE + + --- @type AI.Option.Naval.val + -- @field #AI.Option.Naval.val.ROE ROE + + --- @type AI.Option.Naval.val.ROE + -- @field OPEN_FIRE + -- @field RETURN_FIRE + -- @field WEAPON_HOLD + + AI = {} --#AI + +end -- AI + + + diff --git a/Moose Development/Moose/Dcs/DCSAirbase.lua b/Moose Development/Moose/Dcs/DCSAirbase.lua deleted file mode 100644 index d47ddc067..000000000 --- a/Moose Development/Moose/Dcs/DCSAirbase.lua +++ /dev/null @@ -1,54 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCSAirbase - - ---- Represents airbases: airdromes, helipads and ships with flying decks or landing pads. --- @type Airbase --- @extends Dcs.DCSCoalitionWrapper.Object#CoalitionObject --- @field #Airbase.ID ID Identifier of an airbase. It assigned to an airbase by the Mission Editor automatically. This identifier is used in AI tasks to refer an airbase that exists (spawned and not dead) or not. --- @field #Airbase.Category Category enum contains identifiers of airbase categories. --- @field #Airbase.Desc Desc Airbase descriptor. Airdromes are unique and their types are unique, but helipads and ships are not always unique and may have the same type. - ---- Enum contains identifiers of airbase categories. --- @type Airbase.Category --- @field AIRDROME --- @field HELIPAD --- @field SHIP - ---- Airbase descriptor. Airdromes are unique and their types are unique, but helipads and ships are not always unique and may have the same type. --- @type Airbase.Desc --- @extends #Desc --- @field #Airbase.Category category Category of the airbase type. - ---- Returns airbase by its name. If no airbase found the function will return nil. --- @function [parent=#Airbase] getByName --- @param #string name --- @return #Airbase - ---- Returns airbase descriptor by type name. If no descriptor is found the function will return nil. --- @function [parent=#Airbase] getDescByName --- @param #TypeName typeName Airbase type name. --- @return #Airbase.Desc - ---- Returns Unit that is corresponded to the airbase. Works only for ships. --- @function [parent=#Airbase] getUnit --- @param self --- @return Wrapper.Unit#Unit - ---- Returns identifier of the airbase. --- @function [parent=#Airbase] getID --- @param self --- @return #Airbase.ID - ---- Returns the airbase's callsign - the localized string. --- @function [parent=#Airbase] getCallsign --- @param self --- @return #string - ---- Returns descriptor of the airbase. --- @function [parent=#Airbase] getDesc --- @param self --- @return #Airbase.Desc - - -Airbase = {} --#Airbase diff --git a/Moose Development/Moose/Dcs/DCSCoalitionObject.lua b/Moose Development/Moose/Dcs/DCSCoalitionObject.lua deleted file mode 100644 index be7dc106b..000000000 --- a/Moose Development/Moose/Dcs/DCSCoalitionObject.lua +++ /dev/null @@ -1,20 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCSCoalitionObject - ---- @type CoalitionObject --- @extends Dcs.DCSWrapper.Object#Object - -coalition = {} --#coalition - ---- Returns coalition of the object. --- @function [parent=#CoalitionObject] getCoalition --- @param #CoalitionObject self --- @return Dcs.DCSTypes#coalition.side - ---- Returns object country. --- @function [parent=#CoalitionObject] getCountry --- @param #CoalitionObject self --- @return #country.id - - -CoalitionObject = {} --#CoalitionObject diff --git a/Moose Development/Moose/Dcs/DCSCommand.lua b/Moose Development/Moose/Dcs/DCSCommand.lua deleted file mode 100644 index 5e0040332..000000000 --- a/Moose Development/Moose/Dcs/DCSCommand.lua +++ /dev/null @@ -1,10 +0,0 @@ ---- @module DCSCommand - - ---- @type Command --- @field #string id --- @field #Command.params params - ---- @type Command.params - -env.info( "Command defined" ) \ No newline at end of file diff --git a/Moose Development/Moose/Dcs/DCSController.lua b/Moose Development/Moose/Dcs/DCSController.lua deleted file mode 100644 index 09b8d4b1e..000000000 --- a/Moose Development/Moose/Dcs/DCSController.lua +++ /dev/null @@ -1,115 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCSController - ---- Controller is an object that performs A.I.-routines. Other words controller is an instance of A.I.. Controller stores current main task, active enroute tasks and behavior options. Controller performs commands. Please, read DCS A-10C GUI Manual EN.pdf chapter "Task Planning for Unit Groups", page 91 to understand A.I. system of DCS:A-10C. --- --- This class has 2 types of functions: --- --- * Tasks --- * Commands: Commands are instant actions those required zero time to perform. Commands may be used both for control unit/group behavior and control game mechanics. --- @type Controller --- @field #Controller.Detection Detection Enum contains identifiers of surface types. - ---- Enables and disables the controller. --- Note: Now it works only for ground / naval groups! --- @function [parent=#Controller] setOnOff --- @param self --- @param #boolean value Enable / Disable. - --- Tasks - ---- Resets current task and then sets the task to the controller. Task is a table that contains task identifier and task parameters. --- @function [parent=#Controller] setTask --- @param self --- @param #Task task - ---- Resets current task of the controller. --- @function [parent=#Controller] resetTask --- @param self - ---- Pushes the task to the front of the queue and makes the task active. Further call of function Controller.setTask() function will stop current task, clear the queue and set the new task active. If the task queue is empty the function will work like function Controller.setTask() function. --- @function [parent=#Controller] pushTask --- @param self --- @param #Task task - ---- Pops current (front) task from the queue and makes active next task in the queue (if exists). If no more tasks in the queue the function works like function Controller.resetTask() function. Does nothing if the queue is empty. --- @function [parent=#Controller] popTask --- @param self - ---- Returns true if the controller has a task. --- @function [parent=#Controller] hasTask --- @param self --- @return #boolean - --- Commands - ---TODO: describe #Command structure ---- Sets the command to perform by controller. --- @function [parent=#Controller] setCommand --- @param self --- @param #Command command Table that contains command identifier and command parameters. - - --- Behaviours - ---- Sets the option to the controller. --- Option is a pair of identifier and value. Behavior options are global parameters those affect controller behavior in all tasks it performs. --- Option identifiers and values are stored in table AI.Option in subtables Air, Ground and Naval. --- --- OptionId = @{#AI.Option.Air.id} or @{#AI.Option.Ground.id} or @{#AI.Option.Naval.id} --- OptionValue = AI.Option.Air.val[optionName] or AI.Option.Ground.val[optionName] or AI.Option.Naval.val[optionName] --- --- @function [parent=#Controller] setOption --- @param self --- @param #OptionId optionId Option identifier. --- @param #OptionValue optionValue Value of the option. - - --- Detection - ---- Enum contains identifiers of surface types. --- @type Controller.Detection --- @field VISUAL --- @field OPTIC --- @field RADAR --- @field IRST --- @field RWR --- @field DLINK - ---- Detected target. --- @type DetectedTarget --- @field Wrapper.Object#Object object The target --- @field #boolean visible The target is visible --- @field #boolean type The target type is known --- @field #boolean distance Distance to the target is known - - ---- Checks if the target is detected or not. If one or more detection method is specified the function will return true if the target is detected by at least one of these methods. If no detection methods are specified the function will return true if the target is detected by any method. --- @function [parent=#Controller] isTargetDetected --- @param self --- @param Wrapper.Object#Object target Target to check --- @param #Controller.Detection detection Controller.Detection detection1, Controller.Detection detection2, ... Controller.Detection detectionN --- @return #boolean detected True if the target is detected. --- @return #boolean visible Has effect only if detected is true. True if the target is visible now. --- @return #ModelTime lastTime Has effect only if visible is false. Last time when target was seen. --- @return #boolean type Has effect only if detected is true. True if the target type is known. --- @return #boolean distance Has effect only if detected is true. True if the distance to the target is known. --- @return #Vec3 lastPos Has effect only if visible is false. Last position of the target when it was seen. --- @return #Vec3 lastVel Has effect only if visible is false. Last velocity of the target when it was seen. - - ---- Returns list of detected targets. If one or more detection method is specified the function will return targets which were detected by at least one of these methods. If no detection methods are specified the function will return targets which were detected by any method. --- @function [parent=#Controller] getDetectedTargets --- @param self --- @param #Controller.Detection detection Controller.Detection detection1, Controller.Detection detection2, ... Controller.Detection detectionN --- @return #list<#DetectedTarget> array of DetectedTarget - ---- Know a target. --- @function [parent=#Controller] knowTarget --- @param self --- @param Wrapper.Object#Object object The target. --- @param #boolean type Target type is known. --- @param #boolean distance Distance to target is known. - - -Controller = {} --#Controller \ No newline at end of file diff --git a/Moose Development/Moose/Dcs/DCSGroup.lua b/Moose Development/Moose/Dcs/DCSGroup.lua deleted file mode 100644 index 04f7818bf..000000000 --- a/Moose Development/Moose/Dcs/DCSGroup.lua +++ /dev/null @@ -1,83 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCSGroup - ---- Represents group of Units. --- @type Group --- @field #ID ID Identifier of a group. It is assigned to a group by Mission Editor automatically. --- @field #Group.Category Category Enum contains identifiers of group types. - ---- Enum contains identifiers of group types. --- @type Group.Category --- @field AIRPLANE --- @field HELICOPTER --- @field GROUND --- @field SHIP - --- Static Functions - ---- Returns group by the name assigned to the group in Mission Editor. --- @function [parent=#Group] getByName --- @param #string name --- @return #Group - --- Member Functions - ---- returns true if the group exist or false otherwise. --- @function [parent=#Group] isExist --- @param #Group self --- @return #boolean - ---- Destroys the group and all of its units. --- @function [parent=#Group] destroy --- @param #Group self - ---- Returns category of the group. --- @function [parent=#Group] getCategory --- @param #Group self --- @return #Group.Category - ---TODO check coalition.side - ---- Returns the coalition of the group. --- @function [parent=#Group] getCoalition --- @param #Group self --- @return Dcs.DCSCoalitionWrapper.Object#coalition.side - ---- Returns the group's name. This is the same name assigned to the group in Mission Editor. --- @function [parent=#Group] getName --- @param #Group self --- @return #string - ---- Returns the group identifier. --- @function [parent=#Group] getID --- @param #Group self --- @return #ID - ---- Returns the unit with number unitNumber. If the unit is not exists the function will return nil. --- @function [parent=#Group] getUnit --- @param #Group self --- @param #number unitNumber --- @return Dcs.DCSWrapper.Unit#Unit - ---- Returns current size of the group. If some of the units will be destroyed, As units are destroyed the size of the group will be changed. --- @function [parent=#Group] getSize --- @param #Group self --- @return #number - ---- Returns initial size of the group. If some of the units will be destroyed, initial size of the group will not be changed. Initial size limits the unitNumber parameter for Group.getUnit() function. --- @function [parent=#Group] getInitialSize --- @param #Group self --- @return #number - ---- Returns array of the units present in the group now. Destroyed units will not be enlisted at all. --- @function [parent=#Group] getUnits --- @param #Group self --- @return #list array of Units - ---- Returns controller of the group. --- @function [parent=#Group] getController --- @param #Group self --- @return Controller#Controller - -Group = {} --#Group - diff --git a/Moose Development/Moose/Dcs/DCSObject.lua b/Moose Development/Moose/Dcs/DCSObject.lua deleted file mode 100644 index 281e2781a..000000000 --- a/Moose Development/Moose/Dcs/DCSObject.lua +++ /dev/null @@ -1,73 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCSObject - ---- @type Object --- @field #Object.Category Category --- @field #Object.Desc Desc - ---- @type Object.Category --- @field UNIT --- @field WEAPON --- @field STATIC --- @field SCENERY --- @field BASE - ---- @type Object.Desc --- @extends #Desc --- @field #number life initial life level --- @field #Box3 box bounding box of collision geometry - ---- @function [parent=#Object] isExist --- @param #Object self --- @return #boolean - ---- @function [parent=#Object] destroy --- @param #Object self - ---- @function [parent=#Object] getCategory --- @param #Object self --- @return #Object.Category - ---- Returns type name of the Object. --- @function [parent=#Object] getTypeName --- @param #Object self --- @return #string - ---- Returns object descriptor. --- @function [parent=#Object] getDesc --- @param #Object self --- @return #Object.Desc - ---- Returns true if the object belongs to the category. --- @function [parent=#Object] hasAttribute --- @param #Object self --- @param #AttributeName attributeName Attribute name to check. --- @return #boolean - ---- Returns name of the object. This is the name that is assigned to the object in the Mission Editor. --- @function [parent=#Object] getName --- @param #Object self --- @return #string - ---- Returns object coordinates for current time. --- @function [parent=#Object] getPoint --- @param #Object self --- @return #Vec3 - ---- Returns object position for current time. --- @function [parent=#Object] getPosition --- @param #Object self --- @return #Position3 - ---- Returns the unit's velocity vector. --- @function [parent=#Object] getVelocity --- @param #Object self --- @return #Vec3 - ---- Returns true if the unit is in air. --- @function [parent=#Object] inAir --- @param #Object self --- @return #boolean - -Object = {} --#Object - diff --git a/Moose Development/Moose/Dcs/DCSStaticObject.lua b/Moose Development/Moose/Dcs/DCSStaticObject.lua deleted file mode 100644 index 5dc220412..000000000 --- a/Moose Development/Moose/Dcs/DCSStaticObject.lua +++ /dev/null @@ -1,34 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCSStaticObject - - -------------------------------------------------------------------------------- --- @module StaticObject --- @extends CoalitionWrapper.Object#CoalitionObject - ---- Represents static object added in the Mission Editor. --- @type StaticObject --- @field #StaticObject.ID ID Identifier of a StaticObject. It assigned to an StaticObject by the Mission Editor automatically. --- @field #StaticObject.Desc Desc Descriptor of StaticObject and Unit are equal. StaticObject is just a passive variant of Unit. - ---- StaticObject descriptor. Airdromes are unique and their types are unique, but helipads and ships are not always unique and may have the same type. --- @type StaticObject.Desc --- @extends Wrapper.Unit#Unit.Desc - ---- Returns static object by its name. If no static object found nil will be returned. --- @function [parent=#StaticObject] getByName --- @param #string name Name of static object to find. --- @return #StaticObject - ---- returns identifier of the static object. --- @function [parent=#StaticObject] getID --- @param #StaticObject self --- @return #StaticObject.ID - ---- Returns descriptor of the StaticObject. --- @function [parent=#StaticObject] getDesc --- @param #StaticObject self --- @return #StaticObject.Desc - - -StaticObject = {} --#StaticObject diff --git a/Moose Development/Moose/Dcs/DCSTask.lua b/Moose Development/Moose/Dcs/DCSTask.lua deleted file mode 100644 index 4b4cd277a..000000000 --- a/Moose Development/Moose/Dcs/DCSTask.lua +++ /dev/null @@ -1,15 +0,0 @@ ---- @module DCSTask - - ---- A task descriptor (internal structure for DCS World) --- @type Task --- @field #string id --- @field #Task.param param - ---- @type Task.param - ---- List of @{#Task} --- @type TaskArray --- @list <#Task> - -env.info( "Task defined" ) diff --git a/Moose Development/Moose/Dcs/DCSTime.lua b/Moose Development/Moose/Dcs/DCSTime.lua deleted file mode 100644 index d70d337ec..000000000 --- a/Moose Development/Moose/Dcs/DCSTime.lua +++ /dev/null @@ -1,8 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCSTime - ---- @type ModelTime --- @extends #number - ---- @type Time --- @extends #number \ No newline at end of file diff --git a/Moose Development/Moose/Dcs/DCSTypes.lua b/Moose Development/Moose/Dcs/DCSTypes.lua deleted file mode 100644 index 176725302..000000000 --- a/Moose Development/Moose/Dcs/DCSTypes.lua +++ /dev/null @@ -1,246 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCSTypes - - - ---- Time is given in seconds. --- @type Time --- @extends #number - ---- Model time is the time that drives the simulation. Model time may be stopped, accelerated and decelerated relative real time. --- @type ModelTime --- @extends #number - ---- Mission time is a model time plus time of the mission start. --- @type MissionTime --- @extends #number - - ---- Distance is given in meters. --- @type Distance --- @extends #number - ---- Angle is given in radians. --- @type Angle --- @extends #number - ---- Azimuth is an angle of rotation around world axis y counter-clockwise. --- @type Azimuth --- @extends #number - ---- Mass is given in kilograms. --- @type Mass --- @extends #number - ---- Vec3 type is a 3D-vector. --- DCS world has 3-dimensional coordinate system. DCS ground is an infinite plain. --- @type Vec3 --- @field #Distance x is directed to the north --- @field #Distance z is directed to the east --- @field #Distance y is directed up - ---- Vec2 is a 2D-vector for the ground plane as a reference plane. --- @type Vec2 --- @field #Distance x Vec2.x = Vec3.x --- @field #Distance y Vec2.y = Vec3.z - ---- Position is a composite structure. It consists of both coordinate vector and orientation matrix. Position3 (also known as "Pos3" for short) is a table that has following format: --- @type Position3 --- @field #Vec3 p --- @field #Vec3 x --- @field #Vec3 y --- @field #Vec3 z - ---- 3-dimensional box. --- @type Box3 --- @field #Vec3 min --- @field #Vec3 max - ---- Each object belongs to a type. Object type is a named couple of properties those independent of mission and common for all units of the same type. Name of unit type is a string. Samples of unit type: "Su-27", "KAMAZ" and "M2 Bradley". --- @type TypeName --- @extends #string - ---- AttributeName = string --- Each object type may have attributes. --- Attributes are enlisted in ./Scripts/Database/db_attributes.Lua. --- To know what attributes the object type has, look for the unit type script in sub-directories planes/, helicopter/s, vehicles, navy/ of ./Scripts/Database/ directory. --- @type AttributeName --- @extends #string - ---- List of @{#AttributeName} --- @type AttributeNameArray --- @list <#AttributeName> - ---- @type AI --- @field #AI.Skill Skill --- @field #AI.Task Task --- @field #AI.Option Option - ---- @type AI.Skill --- @field AVERAGE --- @field GOOD --- @field HIGH --- @field EXCELLENT --- @field PLAYER --- @field CLIENT - ---- @type AI.Task --- @field #AI.Task.WeaponExpend WeaponExpend --- @field #AI.Task.OrbitPattern OrbitPattern --- @field #AI.Task.Designation Designation --- @field #AI.Task.WaypointType WaypointType --- @field #AI.Task.TurnMethod TurnMethod --- @field #AI.Task.AltitudeType AltitudeType --- @field #AI.Task.VehicleFormation VehicleFormation - ---- @type AI.Task.WeaponExpend --- @field ONE --- @field TWO --- @field FOUR --- @field QUARTER --- @field HALF --- @field ALL - ---- @type AI.Task.OrbitPattern --- @field CIRCLE --- @field RACE_TRACK - ---- @type AI.Task.Designation --- @field NO --- @field AUTO --- @field WP --- @field IR_POINTER --- @field LASER - ---- @type AI.Task.WaypointType --- @field TAKEOFF --- @field TAKEOFF_PARKING --- @field TURNING_POINT --- @field LAND - ---- @type AI.Task.TurnMethod --- @field FLY_OVER_POINT --- @field FIN_POINT - ---- @type AI.Task.AltitudeType --- @field BARO --- @field RADIO - ---- @type AI.Task.VehicleFormation --- @field OFF_ROAD --- @field ON_ROAD --- @field RANK --- @field CONE --- @field DIAMOND --- @field VEE --- @field ECHELON_LEFT --- @field ECHELON_RIGHT - ---- @type AI.Option --- @field #AI.Option.Air Air --- @field #AI.Option.Ground Ground --- @field #AI.Option.Naval Naval - ---- @type AI.Option.Air --- @field #AI.Option.Air.id id --- @field #AI.Option.Air.val val - ---- @type AI.Option.Ground --- @field #AI.Option.Ground.id id --- @field #AI.Option.Ground.val val - ---- @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 --- @field REACTION_ON_THREAT --- @field RADAR_USING --- @field FLARE_USING --- @field FORMATION --- @field RTB_ON_BINGO --- @field SILENCE - ---- @type AI.Option.Air.val --- @field #AI.Option.Air.val.ROE ROE --- @field #AI.Option.Air.val.REACTION_ON_THREAT REACTION_ON_THREAT --- @field #AI.Option.Air.val.RADAR_USING RADAR_USING --- @field #AI.Option.Air.val.FLARE_USING FLARE_USING - ---- @type AI.Option.Air.val.ROE --- @field WEAPON_FREE --- @field OPEN_FIRE_WEAPON_FREE --- @field OPEN_FIRE --- @field RETURN_FIRE --- @field WEAPON_HOLD - ---- @type AI.Option.Air.val.REACTION_ON_THREAT --- @field NO_REACTION --- @field PASSIVE_DEFENCE --- @field EVADE_FIRE --- @field BYPASS_AND_ESCAPE --- @field ALLOW_ABORT_MISSION - ---- @type AI.Option.Air.val.RADAR_USING --- @field NEVER --- @field FOR_ATTACK_ONLY --- @field FOR_SEARCH_IF_REQUIRED --- @field FOR_CONTINUOUS_SEARCH - ---- @type AI.Option.Air.val.FLARE_USING --- @field NEVER --- @field AGAINST_FIRED_MISSILE --- @field WHEN_FLYING_IN_SAM_WEZ --- @field WHEN_FLYING_NEAR_ENEMIES - ---- @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} - ---- @type AI.Option.Ground.val --- @field #AI.Option.Ground.val.ROE ROE --- @field #AI.Option.Ground.val.ALARM_STATE ALARM_STATE - ---- @type AI.Option.Ground.val.ROE --- @field OPEN_FIRE --- @field RETURN_FIRE --- @field WEAPON_HOLD - ---- @type AI.Option.Ground.val.ALARM_STATE --- @field AUTO --- @field GREEN --- @field RED - ---- @type AI.Option.Naval.id --- @field NO_OPTION --- @field ROE - ---- @type AI.Option.Naval.val --- @field #AI.Option.Naval.val.ROE ROE - ---- @type AI.Option.Naval.val.ROE --- @field OPEN_FIRE --- @field RETURN_FIRE --- @field WEAPON_HOLD - -AI = {} --#AI - - ---- @type Desc --- @field #TypeName typeName type name --- @field #string displayName localized display name --- @field #table attributes object type attributes - ---- A distance type --- @type Distance - ---- An angle type --- @type Angle - -env.info( 'AI types created' ) - diff --git a/Moose Development/Moose/Dcs/DCSUnit.lua b/Moose Development/Moose/Dcs/DCSUnit.lua deleted file mode 100644 index e5e86aba2..000000000 --- a/Moose Development/Moose/Dcs/DCSUnit.lua +++ /dev/null @@ -1,241 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCSUnit - ---- @type Unit --- @extends Dcs.DCSCoalitionWrapper.Object#CoalitionObject --- @field ID Identifier of an unit. It assigned to an unit by the Mission Editor automatically. --- @field #Unit.Category Category --- @field #Unit.RefuelingSystem RefuelingSystem --- @field #Unit.SensorType SensorType --- @field #Unit.OpticType OpticType --- @field #Unit.RadarType RadarType --- @field #Unit.Desc Desc --- @field #Unit.DescAircraft DescAircraft --- @field #Unit.DescAirplane DescAirplane --- @field #Unit.DescHelicopter DescHelicopter --- @field #Unit.DescVehicle DescVehicle --- @field #Unit.DescShip DescShip --- @field #Unit.AmmoItem AmmoItem --- @field #list<#Unit.AmmoItem> Ammo --- @field #Unit.Sensor Sensor --- @field #Unit.Optic Optic --- @field #Unit.Radar Radar --- @field #Unit.IRST IRST - - ---- Enum that stores unit categories. --- @type Unit.Category --- @field AIRPLANE --- @field HELICOPTER --- @field GROUND_UNIT --- @field SHIP --- @field STRUCTURE - ---- Enum that stores aircraft refueling system types. --- @type Unit.RefuelingSystem --- @field BOOM_AND_RECEPTACLE --- @field PROBE_AND_DROGUE - ---- Enum that stores sensor types. --- @type Unit.SensorType --- @field OPTIC --- @field RADAR --- @field IRST --- @field RWR - ---- Enum that stores types of optic sensors. --- @type Unit.OpticType --- @field TV TV-sensor --- @field LLTV Low-level TV-sensor --- @field IR Infra-Red optic sensor - ---- Enum that stores radar types. --- @type Unit.RadarType --- @field AS air search radar --- @field SS surface/land search radar - - ---- A unit descriptor. --- @type Unit.Desc --- @extends Wrapper.Object#Object.Desc --- @field #Unit.Category category Unit Category --- @field #Mass massEmpty mass of empty unit --- @field #number speedMax istance / Time, --maximal velocity - ---- An aircraft descriptor. --- @type Unit.DescAircraft --- @extends Wrapper.Unit#Unit.Desc --- @field #Mass fuelMassMax maximal inner fuel mass --- @field #Distance range Operational range --- @field #Distance Hmax Ceiling --- @field #number VyMax #Distance / #Time, --maximal climb rate --- @field #number NyMin minimal safe acceleration --- @field #number NyMax maximal safe acceleration --- @field #Unit.RefuelingSystem tankerType refueling system type - ---- An airplane descriptor. --- @type Unit.DescAirplane --- @extends Wrapper.Unit#Unit.DescAircraft --- @field #number speedMax0 Distance / Time maximal TAS at ground level --- @field #number speedMax10K Distance / Time maximal TAS at altitude of 10 km - ---- A helicopter descriptor. --- @type Unit.DescHelicopter --- @extends Wrapper.Unit#Unit.DescAircraft --- @field #Distance HmaxStat static ceiling - ---- A vehicle descriptor. --- @type Unit.DescVehicle --- @extends Wrapper.Unit#Unit.Desc --- @field #Angle maxSlopeAngle maximal slope angle --- @field #boolean riverCrossing can the vehicle cross a rivers - ---- A ship descriptor. --- @type Unit.DescShip --- @extends #Unit.Desc - ---- ammunition item: "type-count" pair. --- @type Unit.AmmoItem --- @field #Weapon.Desc desc ammunition descriptor --- @field #number count ammunition count - ---- A unit sensor. --- @type Unit.Sensor --- @field #TypeName typeName --- @field #Unit.SensorType type - ---- An optic sensor. --- @type Unit.Optic --- @extends Wrapper.Unit#Unit.Sensor --- @field #Unit.OpticType opticType - ---- A radar. --- @type Unit.Radar --- @extends Wrapper.Unit#Unit.Sensor --- @field #Distance detectionDistanceRBM detection distance for RCS=1m^2 in real-beam mapping mode, nil if radar doesn't support surface/land search --- @field #Distance detectionDistanceHRM detection distance for RCS=1m^2 in high-resolution mapping mode, nil if radar has no HRM --- @field #Unit.Radar.detectionDistanceAir detectionDistanceAir detection distance for RCS=1m^2 airborne target, nil if radar doesn't support air search - ---- @type Unit.Radar.detectionDistanceAir --- @field #Unit.Radar.detectionDistanceAir.upperHemisphere upperHemisphere --- @field #Unit.Radar.detectionDistanceAir.lowerHemisphere lowerHemisphere - ---- @type Unit.Radar.detectionDistanceAir.upperHemisphere --- @field #Distance headOn --- @field #Distance tailOn - ---- @type Unit.Radar.detectionDistanceAir.lowerHemisphere --- @field #Distance headOn --- @field #Distance tailOn - ---- An IRST. --- @type Wrapper.Unit#Unit.IRST --- @extends Unit.Sensor --- @field #Distance detectionDistanceIdle detection of tail-on target with heat signature = 1 in upper hemisphere, engines are in idle --- @field #Distance detectionDistanceMaximal ..., engines are in maximal mode --- @field #Distance detectionDistanceAfterburner ..., engines are in afterburner mode - ---- An RWR. --- @type Unit.RWR --- @extends Wrapper.Unit#Unit.Sensor - ---- table that stores all unit sensors. --- TODO @type Sensors --- - - ---- Returns unit object by the name assigned to the unit in Mission Editor. If there is unit with such name or the unit is destroyed the function will return nil. The function provides access to non-activated units too. --- @function [parent=#Unit] getByName --- @param #string name --- @return #Unit - ---- Returns if the unit is activated. --- @function [parent=#Unit] isActive --- @param #Unit self --- @return #boolean - ---- Returns name of the player that control the unit or nil if the unit is controlled by A.I. --- @function [parent=#Unit] getPlayerName --- @param #Unit self --- @return #string - ---- returns the unit's unique identifier. --- @function [parent=#Unit] getID --- @param #Unit self --- @return #Unit.ID - - ---- Returns the unit's number in the group. The number is the same number the unit has in ME. It may not be changed during the mission. If any unit in the group is destroyed, the numbers of another units will not be changed. --- @function [parent=#Unit] getNumber --- @param #Unit self --- @return #number - ---- Returns controller of the unit if it exist and nil otherwise --- @function [parent=#Unit] getController --- @param #Unit self --- @return #Controller - ---- Returns the unit's group if it exist and nil otherwise --- @function [parent=#Unit] getGroup --- @param #Unit self --- @return Dcs.DCSWrapper.Group#Group - ---- Returns the unit's callsign - the localized string. --- @function [parent=#Unit] getCallsign --- @param #Unit self --- @return #string - ---- Returns the unit's health. Dead units has health <= 1.0 --- @function [parent=#Unit] getLife --- @param #Unit self --- @return #number - ---- returns the unit's initial health. --- @function [parent=#Unit] getLife0 --- @param #Unit self --- @return #number - ---- 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. --- @function [parent=#Unit] getFuel --- @param #Unit self --- @return #number - ---- Returns the unit ammunition. --- @function [parent=#Unit] getAmmo --- @param #Unit self --- @return #Unit.Ammo - ---- Returns the unit sensors. --- @function [parent=#Unit] getSensors --- @param #Unit self --- @return #Unit.Sensors - ---- Returns true if the unit has specified types of sensors. This function is more preferable than Unit.getSensors() if you don't want to get information about all the unit's sensors, and just want to check if the unit has specified types of sensors. --- @function [parent=#Unit] hasSensors --- @param #Unit self --- @param #Unit.SensorType sensorType (= nil) Sensor type. --- @param ... Additional parameters. --- @return #boolean --- @usage --- If sensorType is Unit.SensorType.OPTIC, additional parameters are optic sensor types. Following example checks if the unit has LLTV or IR optics: --- unit:hasSensors(Unit.SensorType.OPTIC, Unit.OpticType.LLTV, Unit.OpticType.IR) --- If sensorType is Unit.SensorType.RADAR, additional parameters are radar types. Following example checks if the unit has air search radars: --- unit:hasSensors(Unit.SensorType.RADAR, Unit.RadarType.AS) --- If no additional parameters are specified the function returns true if the unit has at least one sensor of specified type. --- If sensor type is not specified the function returns true if the unit has at least one sensor of any type. --- - ---- returns two values: --- First value indicates if at least one of the unit's radar(s) is on. --- Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. --- @function [parent=#Unit] getRadar --- @param #Unit self --- @return #boolean, Wrapper.Object#Object - ---- Returns unit descriptor. Descriptor type depends on unit category. --- @function [parent=#Unit] getDesc --- @param #Unit self --- @return #Unit.Desc - - -Unit = {} --#Unit diff --git a/Moose Development/Moose/Dcs/DCSVec3.lua b/Moose Development/Moose/Dcs/DCSVec3.lua deleted file mode 100644 index 6de2c20aa..000000000 --- a/Moose Development/Moose/Dcs/DCSVec3.lua +++ /dev/null @@ -1,11 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCSVec3 - - - ---- --- @type Vec3 --- @field #number x --- @field #number y --- @field #number z -Vec3 = {} \ No newline at end of file diff --git a/Moose Development/Moose/Dcs/DCSZone.lua b/Moose Development/Moose/Dcs/DCSZone.lua deleted file mode 100644 index 4b65bd76e..000000000 --- a/Moose Development/Moose/Dcs/DCSZone.lua +++ /dev/null @@ -1,10 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCSZone - - - ---- --- @type Zone --- @field DCSVec3#Vec3 point --- @field #number radius -Zone = {} diff --git a/Moose Development/Moose/Dcs/DCScoalition.lua b/Moose Development/Moose/Dcs/DCScoalition.lua deleted file mode 100644 index 4ec5f6f10..000000000 --- a/Moose Development/Moose/Dcs/DCScoalition.lua +++ /dev/null @@ -1,14 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCScoalition - ---- @type coalition --- @field #coalition.side side - ---- @type coalition.side --- @field NEUTRAL --- @field RED --- @field BLUE - ---- @function [parent=#coalition] getCountryCoalition --- @param #number countryId --- @return #number coalitionId diff --git a/Moose Development/Moose/Dcs/DCScountry.lua b/Moose Development/Moose/Dcs/DCScountry.lua deleted file mode 100644 index e390894cd..000000000 --- a/Moose Development/Moose/Dcs/DCScountry.lua +++ /dev/null @@ -1,27 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCScountry - ---- @type country --- @field #country.id id -country = country -- #country - ---- @type country.id --- @field RUSSIA --- @field UKRAINE --- @field USA --- @field TURKEY --- @field UK --- @field FRANCE --- @field GERMANY --- @field CANADA --- @field SPAIN --- @field THE_NETHERLANDS --- @field BELGIUM --- @field NORWAY --- @field DENMARK --- @field ISRAEL --- @field GEORGIA --- @field INSURGENTS --- @field ABKHAZIA --- @field SOUTH_OSETIA --- @field ITALY diff --git a/Moose Development/Moose/Dcs/DCSenv.lua b/Moose Development/Moose/Dcs/DCSenv.lua deleted file mode 100644 index c6fe98776..000000000 --- a/Moose Development/Moose/Dcs/DCSenv.lua +++ /dev/null @@ -1,27 +0,0 @@ -------------------------------------------------------------------------------- --- @module env - ---- @type env - ---- Add message to simulator log with caption "INFO". Message box is optional. --- @function [parent=#env] info --- @field #string message message string to add to log. --- @field #boolean showMessageBox If the parameter is true Message Box will appear. Optional. - ---- Add message to simulator log with caption "WARNING". Message box is optional. --- @function [parent=#env] warning --- @field #string message message string to add to log. --- @field #boolean showMessageBox If the parameter is true Message Box will appear. Optional. - ---- Add message to simulator log with caption "ERROR". Message box is optional. --- @function [parent=#env] error --- @field #string message message string to add to log. --- @field #boolean showMessageBox If the parameter is true Message Box will appear. Optional. - ---- Enables/disables appearance of message box each time lua error occurs. --- @function [parent=#env] setErrorMessageBoxEnabled --- @field #boolean on if true message box appearance is enabled. - - - -env = {} --#env diff --git a/Moose Development/Moose/Dcs/DCSland.lua b/Moose Development/Moose/Dcs/DCSland.lua deleted file mode 100644 index 6391142af..000000000 --- a/Moose Development/Moose/Dcs/DCSland.lua +++ /dev/null @@ -1,26 +0,0 @@ -------------------------------------------------------------------------------- --- @module land - ---- @type land --- @field #land.SurfaceType SurfaceType - - ---- @type land.SurfaceType --- @field LAND --- @field SHALLOW_WATER --- @field WATER --- @field ROAD --- @field RUNWAY - ---- Returns altitude MSL of the point. --- @function [parent=#land] getHeight --- @param #Vec2 point point on the ground. --- @return Dcs.DCSTypes#Distance - ---- returns surface type at the given point. --- @function [parent=#land] getSurfaceType --- @param #Vec2 point Point on the land. --- @return #land.SurfaceType - - -land = {} --#land \ No newline at end of file diff --git a/Moose Development/Moose/Dcs/DCStimer.lua b/Moose Development/Moose/Dcs/DCStimer.lua deleted file mode 100644 index 2d0f4a16c..000000000 --- a/Moose Development/Moose/Dcs/DCStimer.lua +++ /dev/null @@ -1,45 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCStimer - ---- @type timer - - ---- Returns model time in seconds. --- @function [parent=#timer] getTime --- @return #Time - ---- Returns mission time in seconds. --- @function [parent=#timer] getAbsTime --- @return #Time - ---- Returns mission start time in seconds. --- @function [parent=#timer] getTime0 --- @return #Time - ---- Schedules function to call at desired model time. --- Time function FunctionToCall(any argument, Time time) --- --- ... --- --- return ... --- --- end --- --- Must return model time of next call or nil. Note that the DCS scheduler calls the function in protected mode and any Lua errors in the called function will be trapped and not reported. If the function triggers a Lua error then it will be terminated and not scheduled to run again. --- @function [parent=#timer] scheduleFunction --- @param #FunctionToCall functionToCall Lua-function to call. Must have prototype of FunctionToCall. --- @param functionArgument Function argument of any type to pass to functionToCall. --- @param #Time time Model time of the function call. --- @return functionId - ---- Re-schedules function to call at another model time. --- @function [parent=#timer] setFunctionTime --- @param functionId Lua-function to call. Must have prototype of FunctionToCall. --- @param #Time time Model time of the function call. - - ---- Removes the function from schedule. --- @function [parent=#timer] removeFunction --- @param functionId Function identifier to remove from schedule - -timer = {} --#timer diff --git a/Moose Development/Moose/Dcs/DCStrigger.lua b/Moose Development/Moose/Dcs/DCStrigger.lua deleted file mode 100644 index 7bf5360be..000000000 --- a/Moose Development/Moose/Dcs/DCStrigger.lua +++ /dev/null @@ -1,8 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCStrigger - - -trigger = {} --#timer - - - \ No newline at end of file diff --git a/Moose Development/Moose/Dcs/DCSworld.lua b/Moose Development/Moose/Dcs/DCSworld.lua deleted file mode 100644 index 5bcb203d2..000000000 --- a/Moose Development/Moose/Dcs/DCSworld.lua +++ /dev/null @@ -1,35 +0,0 @@ -------------------------------------------------------------------------------- --- @module DCSWorld - ---- @type world --- @field #world.event event - - ---- @type world.event --- @field S_EVENT_INVALID --- @field S_EVENT_SHOT --- @field S_EVENT_HIT --- @field S_EVENT_TAKEOFF --- @field S_EVENT_LAND --- @field S_EVENT_CRASH --- @field S_EVENT_EJECTION --- @field S_EVENT_REFUELING --- @field S_EVENT_DEAD --- @field S_EVENT_PILOT_DEAD --- @field S_EVENT_BASE_CAPTURED --- @field S_EVENT_MISSION_START --- @field S_EVENT_MISSION_END --- @field S_EVENT_TOOK_CONTROL --- @field S_EVENT_REFUELING_STOP --- @field S_EVENT_BIRTH --- @field S_EVENT_HUMAN_FAILURE --- @field S_EVENT_ENGINE_STARTUP --- @field S_EVENT_ENGINE_SHUTDOWN --- @field S_EVENT_PLAYER_ENTER_UNIT --- @field S_EVENT_PLAYER_LEAVE_UNIT --- @field S_EVENT_PLAYER_COMMENT --- @field S_EVENT_SHOOTING_START --- @field S_EVENT_SHOOTING_END --- @field S_EVENT_MAX - -world = {} --#world \ 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 b27fa27f4..259bade99 100644 --- a/Moose Development/Moose/Functional/ATC_Ground.lua +++ b/Moose Development/Moose/Functional/ATC_Ground.lua @@ -1,18 +1,28 @@ ---- **Functional** -- The ATC\_GROUND classes monitor airbase traffic and regulate speed while taxiing. +--- **Functional** -- Monitor airbase traffic and regulate speed while taxiing. -- -- === +-- +-- ## Features: -- --- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG) +-- * Monitor speed of the airplanes of players during taxi. +-- * Communicate ATC ground operations. +-- * Kick speeding players during taxi. -- -- === +-- +-- ## Missions: +-- +-- [ABP - Airbase Police](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/ABP%20-%20Airbase%20Police) +-- +-- === -- -- ### Contributions: Dutch Baron - Concept & Testing -- ### Author: FlightControl - Framework Design & Programming -- -- === -- --- @module ATC_Ground - +-- @module Functional.ATC_Ground +-- @image Air_Traffic_Control_Ground_Operations.JPG --- @type ATC_GROUND -- @field Core.Set#SET_CLIENT SetClient @@ -417,7 +427,7 @@ end -- # Airbases monitored -- -- The following airbases are monitored at the Caucasus region. --- Use the @{Airbase#AIRBASE.Caucasus} enumeration to select the airbases to be monitored. +-- Use the @{Wrapper.Airbase#AIRBASE.Caucasus} enumeration to select the airbases to be monitored. -- -- * `AIRBASE.Caucasus.Anapa_Vityazevo` -- * `AIRBASE.Caucasus.Batumi` @@ -1021,7 +1031,7 @@ end -- # Airbases monitored -- -- The following airbases are monitored at the Nevada region. --- Use the @{Airbase#AIRBASE.Nevada} enumeration to select the airbases to be monitored. +-- Use the @{Wrapper.Airbase#AIRBASE.Nevada} enumeration to select the airbases to be monitored. -- -- * `AIRBASE.Nevada.Beatty_Airport` -- * `AIRBASE.Nevada.Boulder_City_Airport` @@ -1561,7 +1571,7 @@ end -- # Airbases monitored -- -- The following airbases are monitored at the Normandy region. --- Use the @{Airbase#AIRBASE.Normandy} enumeration to select the airbases to be monitored. +-- Use the @{Wrapper.Airbase#AIRBASE.Normandy} enumeration to select the airbases to be monitored. -- -- * `AIRBASE.Normandy.Azeville` -- * `AIRBASE.Normandy.Bazenville` diff --git a/Moose Development/Moose/Functional/Artillery.lua b/Moose Development/Moose/Functional/Artillery.lua new file mode 100644 index 000000000..637560d03 --- /dev/null +++ b/Moose Development/Moose/Functional/Artillery.lua @@ -0,0 +1,5018 @@ +--- **Functional** - Control artillery units. +-- +-- === +-- +-- The ARTY class can be used to easily assign and manage targets for artillery units using an advanced queueing system. +-- +-- ## Features: +-- +-- * Multiple targets can be assigned. No restriction on number of targets. +-- * Targets can be given a priority. Engagement of targets is executed a according to their priority. +-- * Engagements can be scheduled, i.e. will be executed at a certain time of the day. +-- * Multiple relocations of the group can be assigned and scheduled via queueing system. +-- * Special weapon types can be selected for each attack, e.g. cruise missiles for Naval units. +-- * Automatic rearming once the artillery is out of ammo (optional). +-- * Automatic relocation after each firing engagement to prevent counter strikes (optional). +-- * Automatic relocation movements to get the battery within firing range (optional). +-- * Simulation of tactical nuclear shells as well as illumination and smoke shells. +-- * New targets can be added during the mission, e.g. when they are detected by recon units. +-- * Targets and relocations can be assigned by placing markers on the F10 map. +-- * Finite state machine implementation. Mission designer can interact when certain events occur. +-- +-- ==== +-- +-- ## [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- +-- === +-- +-- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** +-- +-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) +-- +-- ==== +-- @module Functional.Arty +-- @image Artillery.JPG + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- ARTY class +-- @type ARTY +-- @field #string ClassName Name of the class. +-- @field #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 #table currentMove Holds the current commanded move, if there is one assigned. +-- @field #number Nammo0 Initial amount total ammunition (shells+rockets+missiles) of the whole group. +-- @field #number Nshells0 Initial amount of shells of the whole group. +-- @field #number Nrockets0 Initial amount of rockets of the whole group. +-- @field #number Nmissiles0 Initial amount of missiles of the whole group. +-- @field #number Nukes0 Initial amount of tactical nukes of the whole group. Default is 0. +-- @field #number Nillu0 Initial amount of illumination shells of the whole group. Default is 0. +-- @field #number Nsmoke0 Initial amount of smoke shells of the whole group. Default is 0. +-- @field #number StatusInterval Update interval in seconds between status updates. Default 10 seconds. +-- @field #number WaitForShotTime Max time in seconds to wait until fist shot event occurs after target is assigned. If time is passed without shot, the target is deleted. Default is 300 seconds. +-- @field #table DCSdesc DCS descriptors of the ARTY group. +-- @field #string Type Type of the ARTY group. +-- @field #string DisplayName Extended type name of the ARTY group. +-- @field #number IniGroupStrength Inital number of units in the ARTY group. +-- @field #boolean IsArtillery If true, ARTY group has attribute "Artillery". This is automatically derived from the DCS descriptor table. +-- @field #boolean ismobile If true, ARTY group can move. +-- @field #boolean iscargo If true, ARTY group is defined as possible cargo. If it is immobile, targets out of range are not deleted from the queue. +-- @field Cargo.CargoGroup#CARGO_GROUP cargogroup Cargo group object if ARTY group is a cargo that will be transported to another place. +-- @field #string groupname Name of the ARTY group as defined in the mission editor. +-- @field #string alias Name of the ARTY group. +-- @field #table clusters Table of names of clusters the group belongs to. Can be used to address all groups within the cluster simultaniously. +-- @field #number SpeedMax Maximum speed of ARTY group in km/h. This is determined from the DCS descriptor table. +-- @field #number Speed Default speed in km/h the ARTY group moves at. Maximum speed possible is 80% of maximum speed the group can do. +-- @field #number RearmingDistance Safe distance in meters between ARTY group and rearming group or place at which rearming is possible. Default 100 m. +-- @field Wrapper.Group#GROUP RearmingGroup Unit designated to rearm the ARTY group. +-- @field #number RearmingGroupSpeed Speed in km/h the rearming unit moves at. Default is 50% of the max speed possible of the group. +-- @field #boolean RearmingGroupOnRoad If true, rearming group will move to ARTY group or rearming place using mainly roads. Default false. +-- @field Core.Point#COORDINATE RearmingGroupCoord Initial coordinates of the rearming unit. After rearming complete, the unit will return to this position. +-- @field Core.Point#COORDINATE RearmingPlaceCoord Coordinates of the rearming place. If the place is more than 100 m away from the ARTY group, the group will go there. +-- @field #boolean RearmingArtyOnRoad If true, ARTY group will move to rearming place using mainly roads. Default false. +-- @field Core.Point#COORDINATE InitialCoord Initial coordinates of the ARTY group. +-- @field #boolean report Arty group sends messages about their current state or target to its coaliton. +-- @field #table ammoshells Table holding names of the shell types which are included when counting the ammo. Default is {"weapons.shells"} which include most shells. +-- @field #table ammorockets Table holding names of the rocket types which are included when counting the ammo. Default is {"weapons.nurs"} which includes most unguided rockets. +-- @field #table ammomissiles Table holding names of the missile types which are included when counting the ammo. Default is {"weapons.missiles"} which includes some guided missiles. +-- @field #number Nshots Number of shots fired on current target. +-- @field #number minrange Minimum firing range in kilometers. Targets closer than this distance are not engaged. Default 0.1 km. +-- @field #number maxrange Maximum firing range in kilometers. Targets further away than this distance are not engaged. Default 10000 km. +-- @field #number nukewarhead Explosion strength of tactical nuclear warhead in kg TNT. Default 75000. +-- @field #number Nukes Number of nuclear shells, the group has available. Note that if normal shells are empty, firing nukes is also not possible any more. +-- @field #number Nillu Number of illumination shells the group has available. Note that if normal shells are empty, firing illumination shells is also not possible any more. +-- @field #number illuPower Power of illumination warhead in mega candela. Default 1 mcd. +-- @field #number illuMinalt Minimum altitude in meters the illumination warhead will detonate. +-- @field #number illuMaxalt Maximum altitude in meters the illumination warhead will detonate. +-- @field #number Nsmoke Number of smoke shells the group has available. Note that if normal shells are empty, firing smoke shells is also not possible any more. +-- @field Utilities.Utils#SMOKECOLOR Smoke color of smoke shells. Default SMOKECOLOR.red. +-- @field #number nukerange Demolition range of tactical nuclear explostions. +-- @field #boolean nukefire Ignite additional fires and smoke for nuclear explosions Default true. +-- @field #number nukefires Number of nuclear fires and subexplosions. +-- @field #boolean relocateafterfire Group will relocate after each firing task. Default false. +-- @field #number relocateRmin Minimum distance in meters the group will look for places to relocate. +-- @field #number relocateRmax Maximum distance in meters the group will look for places to relocate. +-- @field #boolean markallow If true, Players are allowed to assign targets and moves for ARTY group by placing markers on the F10 map. Default is false. +-- @field #number markkey Authorization key. Only player who know this key can assign targets and moves via markers on the F10 map. Default no authorization required. +-- @field #boolean markreadonly Marks for targets are readonly and cannot be removed by players. Default is false. +-- @field #boolean autorelocate ARTY group will automatically move to within the max/min firing range. +-- @field #number autorelocatemaxdist Max distance [m] the ARTY group will travel to get within firing range. Default 50000 m = 50 km. +-- @field #boolean autorelocateonroad ARTY group will use mainly road to automatically get within firing range. Default is false. +-- @extends Core.Fsm#FSM_CONTROLLABLE + +--- Enables mission designers easily to assign targets for artillery units. Since the implementation is based on a Finite State Model (FSM), the mission designer can +-- interact with the process at certain events or states. +-- +-- A new ARTY object can be created with the @{#ARTY.New}(*group*) contructor. +-- The parameter *group* has to be a MOOSE Group object and defines ARTY group. +-- +-- The ARTY FSM process can be started by the @{#ARTY.Start}() command. +-- +-- ## The ARTY Process +-- +-- ![Process](..\Presentations\ARTY\ARTY_Process.png) +-- +-- ### Blue Branch +-- After the FMS process is started the ARTY group will be in the state **CombatReady**. Once a target is assigned the **OpenFire** event will be triggered and the group starts +-- firing. At this point the group in in the state **Firing**. +-- When the defined number of shots has been fired on the current target the event **CeaseFire** is triggered. The group will stop firing and go back to the state **CombatReady**. +-- If another target is defined (or multiple engagements of the same target), the cycle starts anew. +-- +-- ### Violet Branch +-- When the ARTY group runs out of ammunition, the event **Winchester** is triggered and the group enters the state **OutOfAmmo**. +-- In this state, the group is unable to engage further targets. +-- +-- ### Red Branch +-- With the @{#ARTY.SetRearmingGroup}(*group*) command, a special group can be defined to rearm the ARTY group. If this unit has been assigned and the group has entered the state +-- **OutOfAmmo** the event **Rearm** is triggered followed by a transition to the state **Rearming**. +-- If the rearming group is less than 100 meters away from the ARTY group, the rearming process starts. If the rearming group is more than 100 meters away from the ARTY unit, the +-- rearming group is routed to a point 20 to 100 m from the ARTY group. +-- +-- Once the rearming is complete, the **Rearmed** event is triggered and the group enters the state **CombatReady**. At this point targeted can be engaged again. +-- +-- ### Green Branch +-- The ARTY group can be ordered to change its position via the @{#ARTY.AssignMoveCoord}() function as described below. When the group receives the command to move +-- the event **Move** is triggered and the state changes to **Moving**. When the unit arrives to its destination the event **Arrived** is triggered and the group +-- becomes **CombatReady** again. +-- +-- Note, that the ARTY group will not open fire while it is in state **Moving**. This property differentiates artillery from tanks. +-- +-- ### Yellow Branch +-- When a new target is assigned via the @{#ARTY.AssignTargetCoord}() function (see below), the **NewTarget** event is triggered. +-- +-- ## Assigning Targets +-- Assigning targets is a central point of the ARTY class. Multiple targets can be assigned simultanioulsly and are put into a queue. +-- Of course, targets can be added at any time during the mission. For example, once they are detected by a reconnaissance unit. +-- +-- In order to add a target, the function @{#ARTY.AssignTargetCoord}(*coord*, *prio*, *radius*, *nshells*, *maxengage*, *time*, *weapontype*, *name*) has to be used. +-- Only the first parameter *coord* is mandatory while all remaining parameters are all optional. +-- +-- ### Parameters: +-- +-- * *coord*: Coordinates of the target, given as @{Core.Point#COORDINATE} object. +-- * *prio*: Priority of the target. This a number between 1 (high prio) and 100 (low prio). Targets with higher priority are engaged before targets with lower priority. +-- * *radius*: Radius in meters which defines the area the ARTY group will attempt to be hitting. Default is 100 meters. +-- * *nshells*: Number of shots (shells, rockets, missiles) fired by the group at each engagement of a target. Default is 5. +-- * *maxengage*: Number of times a target is engaged. +-- * *time*: Time of day the engagement is schedule in the format "hh:mm:ss" for hh=hours, mm=minutes, ss=seconds. +-- For example "10:15:35". In the case the attack will be executed at a quarter past ten in the morning at the day the mission started. +-- If the engagement should start on the following day the format can be specified as "10:15:35+1", where the +1 denots the following day. +-- This is useful for longer running missions or if the mission starts at 23:00 hours and the attack should be scheduled at 01:00 hours on the following day. +-- Of course, later days are also possible by appending "+2", "+3", etc. +-- **Note** that the time has to be given as a string. So the enclosing quotation marks "" are important. +-- * *weapontype*: Specified the weapon type that should be used for this attack if the ARTY group has multiple weapons to engage the target. +-- For example, this is useful for naval units which carry a bigger arsenal (cannons and missiles). Default is Auto, i.e. DCS logic selects the appropriate weapon type. +-- *name*: A special name can be defined for this target. Default name are the coordinates of the target in LL DMS format. If a name is already given for another target +-- or the same target should be attacked two or more times with different parameters a suffix "#01", "#02", "#03" is automatically appended to the specified name. +-- +-- ## Target Queue +-- In case multiple targets have been defined, it is important to understand how the target queue works. +-- +-- Here, the essential parameters are the priority *prio*, the number of engagements *maxengage* and the scheduled *time* as described above. +-- +-- For example, we have assigned two targets one with *prio*=10 and the other with *prio*=50 and both targets should be engaged three times (*maxengage*=3). +-- Let's first consider the case that none of the targets is scheduled to be executed at a certain time (*time*=nil). +-- The ARTY group will first engage the target with higher priority (*prio*=10). After the engagement is finished, the target with lower priority is attacked. +-- This is because the target with lower prio has been attacked one time less. After the attack on the lower priority task is finished and both targets +-- have been engaged equally often, the target with the higher priority is engaged again. This coninues until a target has engaged three times. +-- Once the maximum number of engagements is reached, the target is deleted from the queue. +-- +-- In other words, the queue is first sorted with respect to the number of engagements and targets with the same number of engagements are sorted with +-- respect to their priority. +-- +-- ### Timed Engagements +-- +-- As mentioned above, targets can be engaged at a specific time of the day via the *time* parameter. +-- +-- If the *time* parameter is specified for a target, the first engagement of that target will happen at that time of the day and not before. +-- This also applies when multiple engagements are requested via the *maxengage* parameter. The first attack will not happen before the specifed time. +-- When that timed attack is finished, the *time* parameter is deleted and the remaining engagements are carried out in the same manner as for untimed targets (described above). +-- +-- Of course, it can happen that a scheduled task should be executed at a time, when another target is already under attack. +-- If the priority of the target is higher than the priority of the current target, then the current attack is cancelled and the engagement of the target with the higher +-- priority is started. +-- +-- By contrast, if the current target has a higher priority than the target scheduled at that time, the current attack is finished before the scheduled attack is started. +-- +-- ## Determining the Amount of Ammo +-- +-- In order to determin when a unit is out of ammo and possible initiate the rearming process it is necessary to know which types of weapons have to be counted. +-- For most artillery unit types, this is simple because they only have one type of weapon and hence ammunition. +-- +-- However, there are more complex scenarios. For example, naval units carry a big arsenal of different ammunition types ranging from various cannon shell types +-- over surface-to-air missiles to cruise missiles. Obviously, not all of these ammo types can be employed for artillery tasks. +-- +-- Unfortunately, there is no easy way to count only those ammo types useable as artillery. Therefore, to keep the implementation general the user +-- can specify the names of the ammo types by the following functions: +-- +-- * @{#ARTY.SetShellTypes}(*tableofnames*): Defines the ammo types for unguided cannons, e.g. *tableofnames*={"weapons.shells"}, i.e. **all** types of shells are counted. +-- * @{#ARTY.SetRocketTypes}(*tableofnames*): Defines the ammo types of unguided rockets, e.g. *tableofnames*={"weapons.nurs"}, i.e. **all** types of rockets are counted. +-- * @{#ARTY.SetMissileTypes}(*tableofnames*): Defines the ammo types of guided missiles, e.g. is *tableofnames*={"weapons.missiles"}, i.e. **all** types of missiles are counted. +-- +-- **Note** that the default parameters "weapons.shells", "weapons.nurs", "weapons.missiles" **should in priciple** capture all the corresponding ammo types. +-- However, the logic searches for the string "weapon.missies" in the ammo type. Especially for missiles, this string is often not contained in the ammo type descriptor. +-- +-- One way to determin which types of ammo the unit carries, one can use the debug mode of the arty class via @{#ARTY.SetDebugON}(). +-- In debug mode, the all ammo types of the group are printed to the monitor as message and can be found in the DCS.log file. +-- +-- ## Employing Selected Weapons +-- +-- If an ARTY group carries multiple weapons, which can be used for artillery task, a certain weapon type can be selected to attack the target. +-- This is done via the *weapontype* parameter of the @{#ARTY.AssignTargetCoord}(..., *weapontype*, ...) function. +-- +-- The enumerator @{#ARTY.WeaponType} has been defined to select a certain weapon type. Supported values are: +-- +-- * @{#ARTY.WeaponType}.Auto: Automatic weapon selection by the DCS logic. This is the default setting. +-- * @{#ARTY.WeaponType}.Cannon: Only cannons are used during the attack. Corresponding ammo type are shells and can be defined by @{#ARTY.SetShellTypes}. +-- * @{#ARTY.WeaponType}.Rockets: Only unguided are used during the attack. Corresponding ammo type are rockets/nurs and can be defined by @{#ARTY.SetRocketTypes}. +-- * @{#ARTY.WeaponType}.CruiseMissile: Only cruise missiles are used during the attack. Corresponding ammo type are missiles and can be defined by @{#ARTY.SetMissileTypes}. +-- * @{#ARTY.WeaponType}.TacticalNukes: Use tactical nuclear shells. This works only with units that have shells and is described below. +-- * @{#ARTY.WeaponType}.IlluminationShells: Use illumination shells. This works only with units that have shells and is described below. +-- * @{#ARTY.WeaponType}.SmokeShells: Use smoke shells. This works only with units that have shells and is described below. +-- +-- ## Assigning Relocation Movements +-- The ARTY group can be commanded to move. This is done by the @{#ARTY.AssignMoveCoord}(*coord*, *time*, *speed*, *onroad*, *cancel*, *name*) function. +-- With this multiple timed moves of the group can be scheduled easily. By default, these moves will only be executed if the group is state **CombatReady**. +-- +-- ### Parameters +-- +-- * *coord*: Coordinates where the group should move to given as @{Core.Point#COORDINATE} object. +-- * *time*: The time when the move should be executed. This has to be given as a string in the format "hh:mm:ss" (hh=hours, mm=minutes, ss=seconds). +-- * *speed*: Speed of the group in km/h. +-- * *onroad*: If this parameter is set to true, the group uses mainly roads to get to the commanded coordinates. +-- * *cancel*: If set to true, any current engagement of targets is cancelled at the time the move should be executed. +-- * *name*: Can be used to set a user defined name of the move. By default the name is created from the LL DMS coordinates. +-- +-- ## Automatic Rearming +-- +-- If an ARTY group runs out of ammunition, it can be rearmed automatically. +-- +-- ### Rearming Group +-- The first way to activate the automatic rearming is to define a rearming group with the function @{#ARTY.SetRearmingGroup}(*group*). For the blue side, this +-- could be a M181 transport truck and for the red side an Ural-375 truck. +-- +-- Once the ARTY group is out of ammo and the **Rearm** event is triggered, the defined rearming truck will drive to the ARTY group. +-- So the rearming truck does not have to be placed nearby the artillery group. When the rearming is complete, the rearming truck will drive back to its original position. +-- +-- ### Rearming Place +-- The second alternative is to define a rearming place, e.g. a FRAP, airport or any other warehouse. This is done with the function @{#ARTY.SetRearmingPlace}(*coord*). +-- The parameter *coord* specifies the coordinate of the rearming place which should not be further away then 100 meters from the warehouse. +-- +-- When the **Rearm** event is triggered, the ARTY group will move to the rearming place. Of course, the group must be mobil. So for a mortar this rearming procedure would not work. +-- +-- After the rearming is complete, the ARTY group will move back to its original position and resume normal operations. +-- +-- ### Rearming Group **and** Rearming Place +-- If both a rearming group *and* a rearming place are specified like described above, both the ARTY group and the rearming truck will move to the rearming place and meet there. +-- +-- After the rearming is complete, both groups will move back to their original positions. +-- +-- ## Simulated Weapons +-- +-- In addtion to the standard weapons a group has available some special weapon types that are not possible to use in the native DCS environment are simulated. +-- +-- ### Tactical Nukes +-- +-- ARTY groups that can fire shells can also be used to fire tactical nukes. This is achieved by setting the weapon type to **ARTY.WeaponType.TacticalNukes** in the +-- @{#ARTY.AssignTargetCoord}() function. +-- +-- By default, they group does not have any nukes available. To give the group the ability the function @{#ARTY.SetTacNukeShells}(*n*) can be used. +-- This supplies the group with *n* nuclear shells, where *n* is restricted to the number of conventional shells the group can carry. +-- Note that the group must always have convenctional shells left in order to fire a nuclear shell. +-- +-- The default explostion strength is 0.075 kilo tons TNT. The can be changed with the @{#ARTY.SetTacNukeWarhead}(*strength*), where *strength* is given in kilo tons TNT. +-- +-- ### Illumination Shells +-- +-- ARTY groups that possess shells can fire shells with illumination bombs. First, the group needs to be equipped with this weapon. This is done by the +-- function @{ARTY.SetIlluminationShells}(*n*, *power*), where *n* is the number of shells the group has available and *power* the illumination power in mega candela (mcd). +-- +-- In order to execute an engagement with illumination shells one has to use the weapon type *ARTY.WeaponType.IlluminationShells* in the +-- @{#ARTY.AssignTargetCoord}() function. +-- +-- In the simulation, the explosive shell that is fired is destroyed once it gets close to the target point but before it can actually impact. +-- At this position an illumination bomb is triggered at a random altitude between 500 and 1000 meters. This interval can be set by the function +-- @{ARTY.SetIlluminationMinMaxAlt}(*minalt*, *maxalt*). +-- +-- ### Smoke Shells +-- +-- In a similar way to illumination shells, ARTY groups can also employ smoke shells. The numer of smoke shells the group has available is set by the function +-- @{#ARTY.SetSmokeShells}(*n*, *color*), where *n* is the number of shells and *color* defines the smoke color. Default is SMOKECOLOR.Red. +-- +-- The weapon type to be used in the @{#ARTY.AssignTargetCoord}() function is *ARTY.WeaponType.SmokeShells*. +-- +-- The explosive shell the group fired is destroyed shortly before its impact on the ground and smoke of the speficied color is triggered at that position. +-- +-- +-- ## Assignments via Markers on F10 Map +-- +-- Targets and relocations can be assigned by players via placing a mark on the F10 map. The marker text must contain certain keywords. +-- +-- This feature can be turned on with the @{#ARTY.SetMarkAssignmentsOn}(*key*, *readonly*). The parameter *key* is optional. When set, it can be used as PIN, i.e. only +-- players who know the correct key are able to assign and cancel targets or relocations. Default behavior is that all players belonging to the same coalition as the +-- ARTY group are able to assign targets and moves without a key. +-- +-- ### Target Assignments +-- A new target can be assigned by writing **arty engage** in the marker text. +-- This is followed by a **comma separated list** of (optional) keywords and parameters. +-- First, it is important to address the ARTY group or groups that should engage. This can be done in numrous ways. The keywords are *battery*, *alias*, *cluster*. +-- It is also possible to address all ARTY groups by the keyword *everyone* or *allbatteries*. These two can be used synonymously. +-- **Note that**, if no battery is assigned nothing will happen. +-- +-- * *everyone* or *allbatteries* The target is assigned to all batteries. +-- * *battery* Name of the ARTY group that the target is assigned to. Note that **the name is case sensitive** and has to be given in quotation marks. Default is all ARTY groups of the right coalition. +-- * *alias* Alias of the ARTY group that the target is assigned to. The alias is **case sensitive** and needs to be in quotation marks. +-- * *cluster* The cluster of ARTY groups that is addessed. Clusters can be defined by the function @{#ARTY.AddToCluster}(*clusters*). Names are **case sensitive** and need to be in quotation marks. +-- * *key* A number to authorize the target assignment. Only specifing the correct number will trigger an engagement. +-- * *time* Time for which which the engagement is schedules, e.g. 08:42. Default is as soon as possible. +-- * *prio* Priority of the engagement as number between 1 (high prio) and 100 (low prio). Default is 50, i.e. medium priority. +-- * *shots* Number of shots (shells, rockets or missiles) fired at each engagement. Default is 5. +-- * *maxengage* Number of times the target is engaged. Default is 1. +-- * *radius* Scattering radius of the fired shots in meters. Default is 100 m. +-- * *weapon* Type of weapon to be used. Valid parameters are *cannon*, *rocket*, *missile*, *nuke*. Default is automatic selection. +-- * *lldms* Specify the coordinates in Lat/Long degrees, minutes and seconds format. The actual location of the marker is unimportant here. The group will engage the coordinates given in the lldms keyword. +-- Format is DD:MM:SS[N,S] DD:MM:SS[W,E]. See example below. This can be useful when coordinates in this format are obtained from elsewhere. +-- * *readonly* The marker is readonly and cannot be deleted by users. Hence, assignment cannot be cancelled by removing the marker. +-- +-- Here are examples of valid marker texts: +-- arty engage, battery "Blue Paladin Alpha" +-- arty engage, everyone +-- arty engage, allbatteries +-- arty engage, alias "Bob", weapon missiles +-- arty engage, cluster "All Mortas" +-- arty engage, cluster "Northern Batteries" "Southern Batteries" +-- arty engage, cluster "Northern Batteries", cluster "Southern Batteries" +-- arty engage, cluster "Horwitzers", shots 20, prio 10, time 08:15, weapon cannons +-- arty engage, battery "Blue Paladin 1" "Blue MRLS 1", shots 10, time 10:15 +-- arty engage, battery "Blue MRLS 1", key 666 +-- arty engage, battery "Paladin Alpha", weapon nukes, shots 1, time 20:15 +-- arty engage, battery "Horwitzer 1", lldms 41:51:00N 41:47:58E +-- +-- Note that the keywords and parameters are *case insensitve*. Only exception are the battery, alias and cluster names. +-- These must be exactly the same as the names of the goups defined in the mission editor or the aliases and cluster names defined in the script. +-- +-- ### Relocation Assignments +-- +-- Markers can also be used to relocate the group with the keyphrase **arty move**. This is done in a similar way as assigning targets. Here, the (optional) keywords and parameters are: +-- +-- * *time* Time for which which the relocation/move is schedules, e.g. 08:42. Default is as soon as possible. +-- * *speed* The speed in km/h the group will drive at. Default is 70% of its max possible speed. +-- * *on road* Group will use mainly roads. Default is off, i.e. it will go in a straight line from its current position to the assigned coordinate. +-- * *canceltarget* Group will cancel all running firing engagements and immidiately start to move. Default is that group will wait until is current assignment is over. +-- * *battery* Name of the ARTY group that the relocation is assigned to. +-- * *alias* Alias of the ARTY group that the target is assigned to. The alias is **case sensitive** and needs to be in quotation marks. +-- * *cluster* The cluster of ARTY groups that is addessed. Clusters can be defined by the function @{#ARTY.AddToCluster}(*clusters*). Names are **case sensitive** and need to be in quotation marks. +-- * *key* A number to authorize the target assignment. Only specifing the correct number will trigger an engagement. +-- * *lldms* Specify the coordinates in Lat/Long degrees, minutes and seconds format. The actual location of the marker is unimportant. The group will move to the coordinates given in the lldms keyword. +-- Format is DD:MM:SS[N,S] DD:MM:SS[W,E]. See example below. +-- * *readonly* Marker cannot be deleted by users any more. Hence, assignment cannot be cancelled by removing the marker. +-- +-- Here are some examples: +-- arty move, battery "Blue Paladin" +-- arty move, battery "Blue MRLS", canceltarget, speed 10, on road +-- arty move, cluster "mobile", lldms 41:51:00N 41:47:58E +-- arty move, alias "Bob", weapon missiles +-- arty move, cluster "All Howitzer" +-- arty move, cluster "Northern Batteries" "Southern Batteries" +-- arty move, cluster "Northern Batteries", cluster "Southern Batteries" +-- arty move, everyone +-- +-- ### Requests +-- +-- Marks can also be to send requests to the ARTY group. This is done by the keyword **arty request**, which can have the keywords +-- +-- * *target* All assigned targets are reported. +-- * *move* All assigned relocation moves are reported. +-- * *ammo* Current ammunition status is reported. +-- +-- For example +-- arty request, everyone, ammo +-- arty request, battery "Paladin Bravo", targets +-- arty request, cluster "All Mortars", move +-- +-- The actual location of the marker is irrelevant for these requests. +-- +-- ### Cancel +-- +-- Current actions can be cancelled by the keyword **arty cancel**. Actions that can be cancelled are current engagements, relocations and rearming assignments. +-- +-- For example +-- arty cancel, target, battery "Paladin Bravo" +-- arty cancel, everyone, move +-- arty cancel, rearming, battery "MRLS Charly" +-- +-- ### Settings +-- +-- A few options can be set by marks. The corresponding keyword is **arty set**. This can be used to define the rearming place and group for a battery. +-- +-- To set the reamring place of a group at the marker position type +-- arty set, battery "Paladin Alpha", rearming place +-- +-- Setting the rearming group is independent of the position of the mark. Just create one anywhere on the map and type +-- arty set, battery "Mortar Bravo", rearming group "Ammo Truck M818" +-- Note that the name of the rearming group has to be given in quotation marks and spellt exactly as the group name defined in the mission editor. +-- +-- ## Transporting +-- +-- ARTY groups can be transported to another location as @{Cargo.Cargo} by means of classes such as @{AI.AI_Cargo_APC}, @{AI.AI_Cargo_Dispatcher_APC}, +-- @{AI.AI_Cargo_Helicopter}, @{AI.AI_Cargo_Dispatcher_Helicopter} or @{AI.AI_Cargo_Airplane}. +-- +-- In order to do this, one needs to define an ARTY object via the @{#ARTY.NewFromCargoGroup}(*cargogroup*, *alias*) function. +-- The first argument *cargogroup* has to be a @{Cargo.CargoGroup#CARGO_GROUP} object. The second argument *alias* is a string which can be freely chosen by the user. +-- +-- ## Fine Tuning +-- +-- The mission designer has a few options to tailor the ARTY object according to his needs. +-- +-- * @{#ARTY.SetAutoRelocateToFiringRange}(*maxdist*, *onroad*) lets the ARTY group automatically move to within firing range if a current target is outside the min/max firing range. The +-- optional parameter *maxdist* is the maximum distance im km the group will move. If the distance is greater no relocation is performed. Default is 50 km. +-- * @{#ARTY.SetAutoRelocateAfterEngagement}(*rmax*, *rmin*) will cause the ARTY group to change its position after each firing assignment. +-- Optional parameters *rmax*, *rmin* define the max/min distance for relocation of the group. Default distance is randomly between 300 and 800 m. +-- * @{#ARTY.AddToCluster}(*clusters*) Can be used to add the ARTY group to one or more clusters. All groups in a cluster can be addressed simultaniously with one marker command. +-- * @{#ARTY.SetSpeed}(*speed*) sets the speed in km/h the group moves at if not explicitly stated otherwise. +-- * @{#ARTY.RemoveAllTargets}() removes all targets from the target queue. +-- * @{#ARTY.RemoveTarget}(*name*) deletes the target with *name* from the target queue. +-- * @{#ARTY.SetMaxFiringRange}(*range*) defines the maximum firing range. Targets further away than this distance are not engaged. +-- * @{#ARTY.SetMinFiringRange}(*range*) defines the minimum firing range. Targets closer than this distance are not engaged. +-- * @{#ARTY.SetRearmingGroup}(*group*) sets the group responsible for rearming of the ARTY group once it is out of ammo. +-- * @{#ARTY.SetReportON}() and @{#ARTY.SetReportOFF}() can be used to enable/disable status reports of the ARTY group send to all coalition members. +-- * @{#ARTY.SetWaitForShotTime}(*waittime*) sets the time after which a target is deleted from the queue if no shooting event occured after the target engagement started. +-- Default is 300 seconds. Note that this can for example happen, when the assigned target is out of range. +-- * @{#ARTY.SetDebugON}() and @{#ARTY.SetDebugOFF}() can be used to enable/disable the debug mode. +-- +-- ## Examples +-- +-- ### Assigning Multiple Targets +-- This basic example illustrates how to assign multiple targets and defining a rearming group. +-- -- Creat a new ARTY object from a Paladin group. +-- paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) +-- +-- -- Define a rearming group. This is a Transport M818 truck. +-- paladin:SetRearmingGroup(GROUP:FindByName("Blue Ammo Truck")) +-- +-- -- Set the max firing range. A Paladin unit has a range of 20 km. +-- paladin:SetMaxFiringRange(20) +-- +-- -- Low priorty (90) target, will be engage last. Target is engaged two times. At each engagement five shots are fired. +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 3"):GetCoordinate(), 90, nil, 5, 2) +-- -- Medium priorty (nil=50) target, will be engage second. Target is engaged two times. At each engagement ten shots are fired. +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), nil, nil, 10, 2) +-- -- High priorty (10) target, will be engage first. Target is engaged three times. At each engagement twenty shots are fired. +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 2"):GetCoordinate(), 10, nil, 20, 3) +-- +-- -- Start ARTY process. +-- paladin:Start() +-- **Note** +-- +-- * If a parameter should be set to its default value, it has to be set to *nil* if other non-default parameters follow. Parameters at the end can simply be skiped. +-- * In this example, the target coordinates are taken from groups placed in the mission edit using the COORDINATE:GetCoordinate() function. +-- +-- ### Scheduled Engagements +-- -- Mission starts at 8 o'clock. +-- -- Assign two scheduled targets. +-- +-- -- Create ARTY object from Paladin group. +-- paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) +-- +-- -- Assign target coordinates. Priority=50 (medium), radius=100 m, use 5 shells per engagement, engage 1 time at two past 8 o'clock. +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 50, 100, 5, 1, "08:02:00", ARTY.WeaponType.Auto, "Target 1") +-- +-- -- Assign target coordinates. Priority=10 (high), radius=300 m, use 10 shells per engagement, engage 1 time at seven past 8 o'clock. +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 2"):GetCoordinate(), 10, 300, 10, 1, "08:07:00", ARTY.WeaponType.Auto, "Target 2") +-- +-- -- Start ARTY process. +-- paladin:Start() +-- +-- ### Specific Weapons +-- This example demonstrates how to use specific weapons during an engagement. +-- -- Define the Normandy as ARTY object. +-- normandy=ARTY:New(GROUP:FindByName("Normandy")) +-- +-- -- Add target: prio=50, radius=300 m, number of missiles=20, number of engagements=1, start time=08:05 hours, only use cruise missiles for this attack. +-- normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 20, 300, 50, 1, "08:01:00", ARTY.WeaponType.CruiseMissile) +-- +-- -- Add target: prio=50, radius=300 m, number of shells=100, number of engagements=1, start time=08:15 hours, only use cannons during this attack. +-- normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 50, 300, 100, 1, "08:15:00", ARTY.WeaponType.Cannon) +-- +-- -- Define shells that are counted to check whether the ship is out of ammo. +-- -- Note that this is necessary because the Normandy has a lot of other shell type weapons which cannot be used to engage ground targets in an artillery style manner. +-- normandy:SetShellTypes({"MK45_127"}) +-- +-- -- Define missile types that are counted. +-- normandy:SetMissileTypes({"BGM"}) +-- +-- -- Start ARTY process. +-- normandy:Start() +-- +-- ### Transportation as Cargo +-- This example demonstates how an ARTY group can be transported to another location as cargo. +-- -- Define a group as CARGO_GROUP +-- CargoGroupMortars=CARGO_GROUP:New(GROUP:FindByName("Mortars"), "Mortars", "Mortar Platoon Alpha", 100 , 10) +-- +-- -- Define the mortar CARGO GROUP as ARTY object +-- mortars=ARTY:NewFromCargoGroup(CargoGroupMortars, "Mortar Platoon Alpha") +-- +-- -- Start ARTY process +-- mortars:Start() +-- +-- -- Setup AI cargo dispatcher for e.g. helos +-- SetHeloCarriers = SET_GROUP:New():FilterPrefixes("CH-47D"):FilterStart() +-- SetCargoMortars = SET_CARGO:New():FilterTypes("Mortars"):FilterStart() +-- SetZoneDepoly = SET_ZONE:New():FilterPrefixes("Deploy"):FilterStart() +-- CargoHelo=AI_CARGO_DISPATCHER_HELICOPTER:New(SetHeloCarriers, SetCargoMortars, SetZoneDepoly) +-- CargoHelo:Start() +-- The ARTY group will be transported and resume its normal operation after it has been deployed. New targets can be assigned at any time also during the transportation process. +-- +-- @field #ARTY +ARTY={ + ClassName="ARTY", + Debug=false, + targets={}, + moves={}, + currentTarget=nil, + currentMove=nil, + Nammo0=0, + Nshells0=0, + Nrockets0=0, + Nmissiles0=0, + Nukes0=0, + Nillu0=0, + Nsmoke0=0, + StatusInterval=10, + WaitForShotTime=300, + DCSdesc=nil, + Type=nil, + DisplayName=nil, + groupname=nil, + alias=nil, + clusters={}, + ismobile=true, + iscargo=false, + cargogroup=nil, + IniGroupStrength=0, + IsArtillery=nil, + RearmingDistance=100, + RearmingGroup=nil, + RearmingGroupSpeed=nil, + RearmingGroupOnRoad=false, + RearmingGroupCoord=nil, + RearmingPlaceCoord=nil, + RearmingArtyOnRoad=false, + InitialCoord=nil, + report=true, + ammoshells={}, + ammorockets={}, + ammomissiles={}, + Nshots=0, + minrange=300, + maxrange=1000000, + nukewarhead=75000, + Nukes=nil, + nukefire=false, + nukefires=nil, + nukerange=nil, + Nillu=nil, + illuPower=1000000, + illuMinalt=500, + illuMaxalt=1000, + Nsmoke=nil, + smokeColor=SMOKECOLOR.Red, + relocateafterfire=false, + relocateRmin=300, + relocateRmax=800, + markallow=false, + markkey=nil, + markreadonly=false, + autorelocate=false, + autorelocatemaxdist=50000, + autorelocateonroad=false, +} + +--- Weapong type ID. See [here](http://wiki.hoggit.us/view/DCS_enum_weapon_flag). +-- @type ARTY.WeaponType +-- @field #number Auto Automatic selection of weapon type. +-- @field #number Cannon Cannons using conventional shells. +-- @field #number Rockets Unguided rockets. +-- @field #number CruiseMissile Cruise missiles. +-- @field #number TacticalNukes Tactical nuclear shells (simulated). +-- @field #number IlluminationShells Illumination shells (simulated). +-- @field #number SmokeShells Smoke shells (simulated). +ARTY.WeaponType={ + Auto=1073741822, + Cannon=805306368, + Rockets=30720, + CruiseMissile=2097152, + TacticalNukes=666, + IlluminationShells=667, + SmokeShells=668, +} + +--- Database of common artillery unit properties. +-- @type ARTY.db +ARTY.db={ + ["2B11 mortar"] = { -- type "2B11 mortar" + minrange = 500, -- correct? + maxrange = 7000, -- 7 km + reloadtime = 30, -- 30 sec + }, + ["SPH 2S1 Gvozdika"] = { -- type "SAU Gvozdika" + minrange = 300, -- correct? + maxrange = 15000, -- 15 km + reloadtime = nil, -- unknown + }, + ["SPH 2S19 Msta"] = { --type "SAU Msta", alias "2S19 Msta" + minrange = 300, -- correct? + maxrange = 23500, -- 23.5 km + reloadtime = nil, -- unknown + }, + ["SPH 2S3 Akatsia"] = { -- type "SAU Akatsia", alias "2S3 Akatsia" + minrange = 300, -- correct? + maxrange = 17000, -- 17 km + reloadtime = nil, -- unknown + }, + ["SPH 2S9 Nona"] = { --type "SAU 2-C9" + minrange = 500, -- correct? + maxrange = 7000, -- 7 km + reloadtime = nil, -- unknown + }, + ["SPH M109 Paladin"] = { -- type "M-109", alias "M109" + minrange = 300, -- correct? + maxrange = 22000, -- 22 km + reloadtime = nil, -- unknown + }, + ["SpGH Dana"] = { -- type "SpGH_Dana" + minrange = 300, -- correct? + maxrange = 18700, -- 18.7 km + reloadtime = nil, -- unknown + }, + ["MLRS BM-21 Grad"] = { --type "Grad-URAL", alias "MLRS BM-21 Grad" + minrange = 5000, -- 5 km + maxrange = 19000, -- 19 km + reloadtime = 420, -- 7 min + }, + ["MLRS 9K57 Uragan BM-27"] = { -- type "Uragan_BM-27" + minrange = 11500, -- 11.5 km + maxrange = 35800, -- 35.8 km + reloadtime = 840, -- 14 min + }, + ["MLRS 9A52 Smerch"] = { -- type "Smerch" + minrange = 20000, -- 20 km + maxrange = 70000, -- 70 km + reloadtime = 2160, -- 36 min + }, + ["MLRS M270"] = { --type "MRLS", alias "M270 MRLS" + minrange = 10000, -- 10 km + maxrange = 32000, -- 32 km + reloadtime = 540, -- 9 min + }, +} + +--- Some ID to identify who we are in output of the DCS.log file. +-- @field #string id +ARTY.id="ARTY | " + +--- Arty script version. +-- @field #string version +ARTY.version="1.0.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. +-- DONE: Add user defined rearm weapon types. +-- DONE: Check if target is in range. Maybe this requires a data base with the ranges of all arty units. +-- DONE: Make ARTY move to rearming position. +-- DONE: Check that right rearming vehicle is specified. Blue M818, Red Ural-375. Are there more? +-- DONE: Check if ARTY group is still alive. +-- DONE: Handle dead events. +-- DONE: Abort firing task if no shooting event occured with 5(?) minutes. Something went wrong then. Min/max range for example. +-- DONE: Improve assigned time for engagement. Next day? +-- DONE: Improve documentation. +-- DONE: Add pseudo user transitions. OnAfter... +-- DONE: Make reaming unit a group. +-- DONE: Write documenation. +-- DONE: Add command move to make arty group move. +-- DONE: remove schedulers for status event. +-- DONE: Improve handling of special weapons. When winchester if using selected weapons? +-- DONE: Make coordinate after rearming general, i.e. also work after the group has moved to anonther location. +-- DONE: Add set commands via markers. E.g. set rearming place. +-- DONE: Test stationary types like mortas ==> rearming etc. +-- DONE: Add illumination and smoke. + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Creates a new ARTY object from a MOOSE 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 + + -- 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())) + else + self:E(ARTY.id.."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())) + return nil + end + + -- Set the controllable for the FSM. + self:SetControllable(group) + + -- Set the group name + self.groupname=group:GetName() + + -- Set an alias name. + if alias~=nil then + self.alias=tostring(alias) + else + self.alias=self.groupname + end + + -- 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()) + for id,desc in pairs(self.DCSdesc) do + self:T3({id=id, desc=desc}) + end + + -- Maximum speed in km/h. + self.SpeedMax=group:GetSpeedMax() + + -- Group is mobile or not (e.g. mortars). + if self.SpeedMax>1 then + self.ismobile=true + else + self.ismobile=false + end + + -- Set speed to 0.7 of maximum. + self.Speed=self.SpeedMax * 0.7 + + -- Displayed name (similar to type name below) + self.DisplayName=self.DCSdesc.displayName + + -- Is this infantry or not. + self.IsArtillery=DCSunit:hasAttribute("Artillery") + + -- Type of group. + self.Type=group:GetTypeName() + + -- Initial group strength. + self.IniGroupStrength=#group:GetUnits() + + --------------- + -- Transitions: + --------------- + + -- Entry. + self:AddTransition("*", "Start", "CombatReady") + + -- Blue branch. + self:AddTransition("CombatReady", "OpenFire", "Firing") + self:AddTransition("Firing", "CeaseFire", "CombatReady") + + -- Violett branch. + self:AddTransition("CombatReady", "Winchester", "OutOfAmmo") + + -- Red branch. + self:AddTransition({"CombatReady", "OutOfAmmo"}, "Rearm", "Rearming") + self:AddTransition("Rearming", "Rearmed", "Rearmed") + + -- Green branch. + self:AddTransition("*", "Move", "Moving") + self:AddTransition("Moving", "Arrived", "Arrived") + + -- Yellow branch. + self:AddTransition("*", "NewTarget", "*") + + -- Not in diagram. + self:AddTransition("*", "CombatReady", "CombatReady") + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "NewMove", "*") + self:AddTransition("*", "Dead", "*") + + -- Transport as cargo (not in diagram). + self:AddTransition("*", "Loaded", "InTransit") + self:AddTransition("InTransit", "UnLoaded", "CombatReady") + + -- Unknown transitons. To be checked if adding these causes problems. + self:AddTransition("Rearming", "Arrived", "Rearming") + self:AddTransition("Rearming", "Move", "Rearming") + + + --- User function for OnAfter "NewTarget" event. + -- @function [parent=#ARTY] OnAfterNewTarget + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table target Array holding the target info. + + --- User function for OnAfter "OpenFire" event. + -- @function [parent=#ARTY] OnAfterOpenFire + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table target Array holding the target info. + + --- User function for OnAfter "CeaseFire" event. + -- @function [parent=#ARTY] OnAfterCeaseFire + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table target Array holding the target info. + + --- User function for OnAfer "NewMove" event. + -- @function [parent=#ARTY] OnAfterNewMove + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table move Array holding the move info. + + --- User function for OnAfer "Move" event. + -- @function [parent=#ARTY] OnAfterMove + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #table move Array holding the move info. + + --- User function for OnAfer "Arrived" event. + -- @function [parent=#ARTY] OnAfterArrvied + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnAfter "Winchester" event. + -- @function [parent=#ARTY] OnAfterWinchester + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnAfter "Rearm" event. + -- @function [parent=#ARTY] OnAfterRearm + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnAfter "Rearmed" event. + -- @function [parent=#ARTY] OnAfterRearmed + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnAfter "Start" event. + -- @function [parent=#ARTY] OnAfterStart + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnAfter "Status" event. + -- @function [parent=#ARTY] OnAfterStatus + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnAfter "Dead" event. + -- @function [parent=#ARTY] OnAfterDead + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- User function for OnEnter "CombatReady" state. + -- @function [parent=#ARTY] OnEnterCombatReady + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnEnter "Firing" state. + -- @function [parent=#ARTY] OnEnterFiring + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnEnter "OutOfAmmo" state. + -- @function [parent=#ARTY] OnEnterOutOfAmmo + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnEnter "Rearming" state. + -- @function [parent=#ARTY] OnEnterRearming + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnEnter "Rearmed" state. + -- @function [parent=#ARTY] OnEnterRearmed + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + --- User function for OnEnter "Moving" state. + -- @function [parent=#ARTY] OnEnterMoving + -- @param #ARTY self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Function to start the ARTY FSM process. + -- @function [parent=#ARTY] Start + -- @param #ARTY self + + --- Function to start the ARTY FSM process after a delay. + -- @function [parent=#ARTY] __Start + -- @param #ARTY self + -- @param #number Delay before start in seconds. + + --- Function to update the status of the ARTY group and tigger FSM events. Triggers the FSM event "Status". + -- @function [parent=#ARTY] Status + -- @param #ARTY self + + --- Function to update the status of the ARTY group and tigger FSM events after a delay. Triggers the FSM event "Status". + -- @function [parent=#ARTY] __Status + -- @param #ARTY self + -- @param #number Delay in seconds. + + --- Function called when a unit of the ARTY group died. Triggers the FSM event "Dead". + -- @function [parent=#ARTY] Dead + -- @param #ARTY self + + --- 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. + + --- Add a new target for the ARTY group. Triggers the FSM event "NewTarget". + -- @function [parent=#ARTY] NewTarget + -- @param #ARTY self + -- @param #table target Array holding the target data. + + --- Add a new target for the ARTY group with a delay. Triggers the FSM event "NewTarget". + -- @function [parent=#ARTY] __NewTarget + -- @param #ARTY self + -- @param #number delay Delay in seconds. + -- @param #table target Array holding the target data. + + --- Add a new relocation move for the ARTY group. Triggers the FSM event "NewMove". + -- @function [parent=#ARTY] NewMove + -- @param #ARTY self + -- @param #table move Array holding the relocation move data. + + --- Add a new relocation for the ARTY group after a delay. Triggers the FSM event "NewMove". + -- @function [parent=#ARTY] __NewMove + -- @param #ARTY self + -- @param #number delay Delay in seconds. + -- @param #table move Array holding the relocation move data. + + --- Order ARTY group to open fire on a target. Triggers the FSM event "OpenFire". + -- @function [parent=#ARTY] OpenFire + -- @param #ARTY self + -- @param #table target Array holding the target data. + + --- Order ARTY group to open fire on a target with a delay. Triggers the FSM event "Move". + -- @function [parent=#ARTY] __OpenFire + -- @param #ARTY self + -- @param #number delay Delay in seconds. + -- @param #table target Array holding the target data. + + --- Order ARTY group to cease firing on a target. Triggers the FSM event "CeaseFire". + -- @function [parent=#ARTY] CeaseFire + -- @param #ARTY self + -- @param #table target Array holding the target data. + + --- Order ARTY group to cease firing on a target after a delay. Triggers the FSM event "CeaseFire". + -- @function [parent=#ARTY] __CeaseFire + -- @param #ARTY self + -- @param #number delay Delay in seconds. + -- @param #table target Array holding the target data. + + --- Order ARTY group to move to another location. Triggers the FSM event "Move". + -- @function [parent=#ARTY] Move + -- @param #ARTY self + -- @param #table move Array holding the relocation move data. + + --- Order ARTY group to move to another location after a delay. Triggers the FSM event "Move". + -- @function [parent=#ARTY] __Move + -- @param #ARTY self + -- @param #number delay Delay in seconds. + -- @param #table move Array holding the relocation move data. + + --- Tell ARTY group it has arrived at its destination. Triggers the FSM event "Arrived". + -- @function [parent=#ARTY] Arrived + -- @param #ARTY self + + --- Tell ARTY group it has arrived at its destination after a delay. Triggers the FSM event "Arrived". + -- @function [parent=#ARTY] __Arrived + -- @param #ARTY self + -- @param #number delay Delay in seconds. + + --- Tell ARTY group it is combat ready. Triggers the FSM event "CombatReady". + -- @function [parent=#ARTY] CombatReady + -- @param #ARTY self + + --- Tell ARTY group it is combat ready after a delay. Triggers the FSM event "CombatReady". + -- @function [parent=#ARTY] __CombatReady + -- @param #ARTY self + -- @param #number delay Delay in seconds. + + --- Tell ARTY group it is out of ammo. Triggers the FSM event "Winchester". + -- @function [parent=#ARTY] Winchester + -- @param #ARTY self + + --- Tell ARTY group it is out of ammo after a delay. Triggers the FSM event "Winchester". + -- @function [parent=#ARTY] __Winchester + -- @param #ARTY self + -- @param #number delay Delay in seconds. + + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Assign target coordinates to the ARTY group. Only the first parameter, i.e. the coordinate of the target is mandatory. The remaining parameters are optional and can be used to fine tune the engagement. +-- @param #ARTY self +-- @param Core.Point#COORDINATE coord Coordinates of the target. +-- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50. +-- @param #number radius (Optional) Radius. Default is 100 m. +-- @param #number nshells (Optional) How many shells (or rockets) are fired on target per engagement. Default 5. +-- @param #number maxengage (Optional) How many times a target is engaged. Default 1. +-- @param #string time (Optional) Day time at which the target should be engaged. Passed as a string in format "08:13:45". Current task will be canceled. +-- @param #number weapontype (Optional) Type of weapon to be used to attack this target. Default ARTY.WeaponType.Auto, i.e. the DCS logic automatically determins the appropriate weapon. +-- @param #string name (Optional) Name of the target. Default is LL DMS coordinate of the target. If the name was already given, the numbering "#01", "#02",... is appended automatically. +-- @param #boolean unique (Optional) Target is unique. If the target name is already known, the target is rejected. Default false. +-- @return #string Name of the target. Can be used for further reference, e.g. deleting the target from the list. +-- @usage paladin=ARTY:New(GROUP:FindByName("Blue Paladin")) +-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 10, 300, 10, 1, "08:02:00", ARTY.WeaponType.Auto, "Target 1") +-- paladin:Start() +function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, weapontype, name, unique) + self:F({coord=coord, prio=prio, radius=radius, nshells=nshells, maxengage=maxengage, time=time, weapontype=weapontype, name=name, unique=unique}) + + -- Set default values. + nshells=nshells or 5 + radius=radius or 100 + maxengage=maxengage or 1 + prio=prio or 50 + prio=math.max( 1, prio) + prio=math.min(100, prio) + if unique==nil then + unique=false + end + weapontype=weapontype or ARTY.WeaponType.Auto + + -- Check if we have a coordinate object. + local text=nil + if coord:IsInstanceOf("GROUP") then + text="WARNING: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter - you gave a GROUP. Converting to COORDINATE..." + coord=coord:GetCoordinate() + elseif coord:IsInstanceOf("UNIT") then + text="WARNING: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter - you gave a UNIT. Converting to COORDINATE..." + coord=coord:GetCoordinate() + elseif coord:IsInstanceOf("POSITIONABLE") then + text="WARNING: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter - you gave a POSITIONABLE. Converting to COORDINATE..." + coord=coord:GetCoordinate() + elseif coord:IsInstanceOf("COORDINATE") then + -- Nothing to do here. + else + text="ERROR: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter!" + MESSAGE:New(text, 30):ToAll() + self:E(ARTY.id..text) + return nil + end + if text~=nil then + self:E(ARTY.id..text) + end + + -- Name of the target. + local _name=name or coord:ToStringLLDMS() + local _unique=true + + -- Check if the name has already been used for another target. If so, the function returns a new unique name. + _name,_unique=self:_CheckName(self.targets, _name, not unique) + + -- Target name should be unique and is not. + if unique==true and _unique==false then + self:T(ARTY.id..string.format("%s: target %s should have a unique name but name was already given. Rejecting target!", self.groupname, _name)) + return nil + end + + -- Time in seconds. + local _time + if type(time)=="string" then + _time=self:_ClockToSeconds(time) + elseif type(time)=="number" then + _time=timer.getAbsTime()+time + else + _time=timer.getAbsTime() + end + + -- Prepare target array. + local _target={name=_name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false, prio=prio, maxengage=maxengage, time=_time, weapontype=weapontype} + + -- Add to table. + table.insert(self.targets, _target) + + -- Trigger new target event. + self:__NewTarget(1, _target) + + return _name +end + +--- Assign coordinate to where the ARTY group should move. +-- @param #ARTY self +-- @param Core.Point#COORDINATE coord Coordinates of the new position. +-- @param #string time (Optional) Day time at which the group should start moving. Passed as a string in format "08:13:45". Default is now. +-- @param #number speed (Optinal) Speed in km/h the group should move at. Default 70% of max posible speed of group. +-- @param #boolean onroad (Optional) If true, group will mainly use roads. Default off, i.e. go directly towards the specified coordinate. +-- @param #boolean cancel (Optional) If true, cancel any running attack when move should begin. Default is false. +-- @param #string name (Optional) Name of the coordinate. Default is LL DMS string of the coordinate. If the name was already given, the numbering "#01", "#02",... is appended automatically. +-- @param #boolean unique (Optional) Move is unique. If the move name is already known, the move is rejected. Default false. +-- @return #string Name of the move. Can be used for further reference, e.g. deleting the move from the list. +function ARTY:AssignMoveCoord(coord, time, speed, onroad, cancel, name, unique) + self:F({coord=coord, time=time, speed=speed, onroad=onroad, cancel=cancel, name=name, unique=unique}) + + -- Reject move if the group is immobile. + if not self.ismobile then + self:T(ARTY.id..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)) + return nil + end + + -- Set speed. + if speed then + -- Make sure, given speed is less than max physiaclly possible speed of group. + speed=math.min(speed, self.SpeedMax) + elseif self.Speed then + speed=self.Speed + else + speed=self.SpeedMax*0.7 + end + + -- Default is off road. + if onroad==nil then + onroad=false + end + + -- Default is not to cancel a running attack. + if cancel==nil then + cancel=false + end + + -- Time in seconds. + local _time + if type(time)=="string" then + _time=self:_ClockToSeconds(time) + elseif type(time)=="number" then + _time=timer.getAbsTime()+time + else + _time=timer.getAbsTime() + end + + -- Prepare move array. + local _move={name=_name, coord=coord, time=_time, speed=speed, onroad=onroad, cancel=cancel} + + -- Add to table. + table.insert(self.moves, _move) + + return _name +end + +--- Set alias, i.e. the name the group will use when sending messages. +-- @param #ARTY self +-- @param #string alias The alias for the group. +function ARTY:SetAlias(alias) + self:F({alias=alias}) + self.alias=tostring(alias) +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. +function ARTY:AddToCluster(clusters) + self:F({clusters=clusters}) + + -- Convert input to table. + local names + if type(clusters)=="table" then + names=clusters + elseif type(clusters)=="string" then + names={clusters} + else + -- error message + self:E(ARTY.id.."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 + +--- 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. +function ARTY:SetMinFiringRange(range) + self:F({range=range}) + self.minrange=range*1000 or 100 +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. +function ARTY:SetMaxFiringRange(range) + self:F({range=range}) + self.maxrange=range*1000 or 1000*1000 +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. +function ARTY:SetStatusInterval(interval) + self:F({interval=interval}) + self.StatusInterval=interval or 10 +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. +function ARTY:SetWaitForShotTime(waittime) + self:F({waittime=waittime}) + self.WaitForShotTime=waittime or 300 +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. +function ARTY:SetRearmingDistance(distance) + self:F({distance=distance}) + self.RearmingDistance=distance or 100 +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. +function ARTY:SetRearmingGroup(group) + self:F({group=group}) + self.RearmingGroup=group +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. +function ARTY:SetRearmingGroupSpeed(speed) + self:F({speed=speed}) + self.RearmingGroupSpeed=speed +end + +--- Define if rearming group uses mainly roads to drive to the ARTY group or rearming place. +-- @param #ARTY self +-- @param #boolean onroad If true, rearming group uses mainly roads. If false, it drives directly to the ARTY group or rearming place. +function ARTY:SetRearmingGroupOnRoad(onroad) + self:F({onroad=onroad}) + if onroad==nil then + onroad=true + end + self.RearmingGroupOnRoad=onroad +end + +--- Define if ARTY group uses mainly roads to drive to the rearming place. +-- @param #ARTY self +-- @param #boolean onroad If true, ARTY group uses mainly roads. If false, it drives directly to the rearming place. +function ARTY:SetRearmingArtyOnRoad(onroad) + self:F({onroad=onroad}) + if onroad==nil then + onroad=true + end + self.RearmingArtyOnRoad=onroad +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. +function ARTY:SetRearmingPlace(coord) + self:F({coord=coord}) + self.RearmingPlaceCoord=coord +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. +function ARTY:SetAutoRelocateToFiringRange(maxdistance, onroad) + self:F({distance=maxdistance, onroad=onroad}) + self.autorelocate=true + self.autorelocatemaxdist=maxdistance or 50 + self.autorelocatemaxdist=self.autorelocatemaxdist*1000 + if onroad==nil then + onroad=false + end + self.autorelocateonroad=onroad +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. +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) +end + +--- Report messages of ARTY group turned on. This is the default. +-- @param #ARTY self +function ARTY:SetReportON() + self.report=true +end + +--- Report messages of ARTY group turned off. Default is on. +-- @param #ARTY self +function ARTY:SetReportOFF() + self.report=false +end + +--- Turn debug mode on. Information is printed to screen. +-- @param #ARTY self +function ARTY:SetDebugON() + self.Debug=true +end + +--- Turn debug mode off. This is the default setting. +-- @param #ARTY self +function ARTY:SetDebugOFF() + self.Debug=false +end + +--- Set default speed the group is moving at if not specified otherwise. +-- @param #ARTY self +-- @param #number speed Speed in km/h. +function ARTY:SetSpeed(speed) + self.Speed=speed +end + +--- Delete a target from target list. If the target is currently engaged, it is cancelled. +-- @param #ARTY self +-- @param #string name Name of the target. +function ARTY:RemoveTarget(name) + self:F2(name) + + -- Get target ID from namd + local id=self:_GetTargetIndexByName(name) + + if id then + + -- Remove target from table. + self:T(ARTY.id..string.format("Group %s: Removing target %s (id=%d).", self.groupname, name, id)) + table.remove(self.targets, id) + + -- Delete marker belonging to this engagement. + if self.markallow then + local batteryname,markTargetID, markMoveID=self:_GetMarkIDfromName(name) + if batteryname==self.groupname and markTargetID~=nil then + COORDINATE:RemoveMark(markTargetID) + end + end + + end + self:T(ARTY.id..string.format("Group %s: Number of targets = %d.", self.groupname, #self.targets)) +end + +--- Delete a move from move list. +-- @param #ARTY self +-- @param #string name Name of the target. +function ARTY:RemoveMove(name) + self:F2(name) + + -- Get move ID from name. + local id=self:_GetMoveIndexByName(name) + + if id then + + -- Remove move from table. + self:T(ARTY.id..string.format("Group %s: Removing move %s (id=%d).", self.groupname, name, id)) + table.remove(self.moves, id) + + -- Delete marker belonging to this relocation move. + if self.markallow then + local batteryname,markTargetID,markMoveID=self:_GetMarkIDfromName(name) + if batteryname==self.groupname and markMoveID~=nil then + COORDINATE:RemoveMark(markMoveID) + end + end + + end + self:T(ARTY.id..string.format("Group %s: Number of moves = %d.", self.groupname, #self.moves)) +end + +--- Delete ALL targets from current target list. +-- @param #ARTY self +function ARTY:RemoveAllTargets() + self:F2() + for _,target in pairs(self.targets) do + self:RemoveTarget(target.name) + end +end + +--- Define shell types that are counted to determine the ammo amount the ARTY group has. +-- @param #ARTY self +-- @param #table tableofnames Table of shell type names. +function ARTY:SetShellTypes(tableofnames) + self:F2(tableofnames) + self.ammoshells={} + for _,_type in pairs(tableofnames) do + table.insert(self.ammoshells, _type) + end +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. +function ARTY:SetRocketTypes(tableofnames) + self:F2(tableofnames) + self.ammorockets={} + for _,_type in pairs(tableofnames) do + table.insert(self.ammorockets, _type) + end +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. +function ARTY:SetMissileTypes(tableofnames) + self:F2(tableofnames) + self.ammomissiles={} + for _,_type in pairs(tableofnames) do + table.insert(self.ammomissiles, _type) + end +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. +function ARTY:SetTacNukeShells(n) + self.Nukes=n +end + +--- Set nuclear warhead explosion strength. +-- @param #ARTY self +-- @param #number strength Explosion strength in kilo tons TNT. Default is 0.075 kt. +function ARTY:SetTacNukeWarhead(strength) + self.nukewarhead=strength or 0.075 + self.nukewarhead=self.nukewarhead*1000*1000 -- convert to kg TNT. +end + +--- Set number of illumination shells available to the group. +-- Note that it can be max the number of normal shells. Also if all normal shells are empty, firing illumination shells is also not possible any more until group gets rearmed. +-- @param #ARTY self +-- @param #number n Number of illumination shells for the whole group. +-- @param #number power (Optional) Power of illumination warhead in mega candela. Default 1.0 mcd. +function ARTY:SetIlluminationShells(n, power) + self.Nillu=n + self.illuPower=power or 1.0 + self.illuPower=self.illuPower * 1000000 +end + +--- Set minimum and maximum detotation altitude for illumination shells. A value between min/max is selected randomly. +-- The illumination bomb will burn for 300 seconds (5 minutes). Assuming a descent rate of ~3 m/s the "optimal" altitude would be 900 m. +-- @param #ARTY self +-- @param #number minalt (Optional) Minium altitude in meters. Default 500 m. +-- @param #number maxalt (Optional) Maximum altitude in meters. Default 1000 m. +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 +end + +--- Set number of smoke shells available to the group. +-- Note that it can be max the number of normal shells. Also if all normal shells are empty, firing smoke shells is also not possible any more until group gets rearmed. +-- @param #ARTY self +-- @param #number n Number of smoke shells for the whole group. +-- @param Utilities.Utils#SMOKECOLOR color (Optional) Color of the smoke. Default SMOKECOLOR.Red. +function ARTY:SetSmokeShells(n, color) + self.Nsmoke=n + self.smokeColor=color or SMOKECOLOR.Red +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. +function ARTY:SetTacNukeFires(nfires, range) + self.nukefire=true + self.nukefires=nfires + self.nukerange=range +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. +function ARTY:SetMarkAssignmentsOn(key, readonly) + self.markkey=key + self.markallow=true + if readonly==nil then + self.markreadonly=false + end +end + +--- Disable assigning targets by placing markers on the F10 map. +-- @param #ARTY self +function ARTY:SetMarkTargetsOff() + self.markallow=false + self.markkey=nil +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Start Event +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Start" event. Initialized ROE and alarm state. Starts the event handler. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterStart(Controllable, From, Event, To) + self:_EventFromTo("onafterStart", Event, From, To) + + -- Debug output. + local text=string.format("Started ARTY version %s for group %s.", ARTY.version, Controllable:GetName()) + self:E(ARTY.id..text) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + -- Get Ammo. + self.Nammo0, self.Nshells0, self.Nrockets0, self.Nmissiles0=self:GetAmmo(self.Debug) + + -- Init nuclear explosion parameters if they were not set by user. + if self.nukerange==nil then + self.nukerange=1500/75000*self.nukewarhead -- linear dependence + end + if self.nukefires==nil then + self.nukefires=20/1000/1000*self.nukerange*self.nukerange + end + + -- Init nuclear shells. + if self.Nukes~=nil then + self.Nukes0=math.min(self.Nukes, self.Nshells0) + else + self.Nukes=0 + self.Nukes0=0 + end + + -- Init illumination shells. + if self.Nillu~=nil then + self.Nillu0=math.min(self.Nillu, self.Nshells0) + else + self.Nillu=0 + self.Nillu0=0 + end + + -- Init smoke shells. + if self.Nsmoke~=nil then + self.Nsmoke0=math.min(self.Nsmoke, self.Nshells0) + else + self.Nsmoke=0 + self.Nsmoke0=0 + end + + -- Check if we have and arty type that is in the DB. + local _dbproperties=self:_CheckDB(self.DisplayName) + self:T({dbproperties=_dbproperties}) + if _dbproperties~=nil then + for property,value in pairs(_dbproperties) do + self:T({property=property, value=value}) + self[property]=value + end + end + + -- Some mobility consitency checks if group cannot move. + if not self.ismobile then + self.RearmingPlaceCoord=nil + self.relocateafterfire=false + self.autorelocate=false + --self.RearmingGroupSpeed=20 + end + + -- Check that default speed is below max speed. + self.Speed=math.min(self.Speed, self.SpeedMax) + + -- Set Rearming group speed if not specified by user + if self.RearmingGroup then + + -- Get max speed of rearming group. + local speedmax=self.RearmingGroup:GetSpeedMax() + self:T(ARTY.id..string.format("%s, rearming group %s max speed = %.1f km/h.", self.groupname, self.RearmingGroup:GetName(), speedmax)) + + if self.RearmingGroupSpeed==nil then + -- Set rearming group speed to 50% of max possible speed. + self.RearmingGroupSpeed=speedmax*0.5 + else + -- Ensure that speed is <= max speed. + self.RearmingGroupSpeed=math.min(self.RearmingGroupSpeed, self.RearmingGroup:GetSpeedMax()) + end + else + -- Just to have a reasonable number for output format below. + self.RearmingGroupSpeed=23 + end + + local text=string.format("\n******************************************************\n") + text=text..string.format("Arty group = %s\n", self.groupname) + text=text..string.format("Arty alias = %s\n", self.alias) + text=text..string.format("Artillery attribute = %s\n", tostring(self.IsArtillery)) + text=text..string.format("Type = %s\n", self.Type) + text=text..string.format("Display Name = %s\n", self.DisplayName) + text=text..string.format("Number of units = %d\n", self.IniGroupStrength) + text=text..string.format("Speed max = %d km/h\n", self.SpeedMax) + text=text..string.format("Speed default = %d km/h\n", self.Speed) + text=text..string.format("Is mobile = %s\n", tostring(self.ismobile)) + text=text..string.format("Is cargo = %s\n", tostring(self.iscargo)) + text=text..string.format("Min range = %.1f km\n", self.minrange/1000) + text=text..string.format("Max range = %.1f km\n", self.maxrange/1000) + text=text..string.format("Total ammo count = %d\n", self.Nammo0) + text=text..string.format("Number of shells = %d\n", self.Nshells0) + text=text..string.format("Number of rockets = %d\n", self.Nrockets0) + text=text..string.format("Number of missiles = %d\n", self.Nmissiles0) + text=text..string.format("Number of nukes = %d\n", self.Nukes0) + text=text..string.format("Nuclear warhead = %d tons TNT\n", self.nukewarhead/1000) + text=text..string.format("Nuclear demolition = %d m\n", self.nukerange) + text=text..string.format("Nuclear fires = %d (active=%s)\n", self.nukefires, tostring(self.nukefire)) + text=text..string.format("Number of illum. = %d\n", self.Nillu0) + text=text..string.format("Illuminaton Power = %.3f mcd\n", self.illuPower/1000000) + text=text..string.format("Illuminaton Minalt = %d m\n", self.illuMinalt) + text=text..string.format("Illuminaton Maxalt = %d m\n", self.illuMaxalt) + text=text..string.format("Number of smoke = %d\n", self.Nsmoke0) + text=text..string.format("Smoke color = %d\n", self.smokeColor) + if self.RearmingGroup or self.RearmingPlaceCoord then + text=text..string.format("Rearming safe dist. = %d m\n", self.RearmingDistance) + end + if self.RearmingGroup then + text=text..string.format("Rearming group = %s\n", self.RearmingGroup:GetName()) + text=text..string.format("Rearming group speed= %d km/h\n", self.RearmingGroupSpeed) + text=text..string.format("Rearming group roads= %s\n", tostring(self.RearmingGroupOnRoad)) + end + if self.RearmingPlaceCoord then + local dist=self.InitialCoord:Get2DDistance(self.RearmingPlaceCoord) + text=text..string.format("Rearming coord dist = %d m\n", dist) + text=text..string.format("Rearming ARTY roads = %s\n", tostring(self.RearmingArtyOnRoad)) + end + text=text..string.format("Relocate after fire = %s\n", tostring(self.relocateafterfire)) + text=text..string.format("Relocate min dist. = %d m\n", self.relocateRmin) + text=text..string.format("Relocate max dist. = %d m\n", self.relocateRmax) + text=text..string.format("Auto move in range = %s\n", tostring(self.autorelocate)) + text=text..string.format("Auto move dist. max = %.1f km\n", self.autorelocatemaxdist/1000) + text=text..string.format("Auto move on road = %s\n", tostring(self.autorelocateonroad)) + text=text..string.format("Marker assignments = %s\n", tostring(self.markallow)) + text=text..string.format("Marker auth. key = %s\n", tostring(self.markkey)) + text=text..string.format("Marker readonly = %s\n", tostring(self.markreadonly)) + text=text..string.format("Clusters:\n") + for _,cluster in pairs(self.clusters) do + text=text..string.format("- %s\n", tostring(cluster)) + end + text=text..string.format("******************************************************\n") + text=text..string.format("Targets:\n") + for _, target in pairs(self.targets) do + text=text..string.format("- %s\n", self:_TargetInfo(target)) + local possible=self:_CheckWeaponTypePossible(target) + if not possible then + self:E(ARTY.id..string.format("WARNING: Selected weapon type %s is not possible", self:_WeaponTypeName(target.weapontype))) + end + if self.Debug then + local zone=ZONE_RADIUS:New(target.name, target.coord:GetVec2(), target.radius) + zone:BoundZone(180, coalition.side.NEUTRAL) + end + end + text=text..string.format("Moves:\n") + for i=1,#self.moves do + text=text..string.format("- %s\n", self:_MoveInfo(self.moves[i])) + end + text=text..string.format("******************************************************\n") + text=text..string.format("Shell types:\n") + for _,_type in pairs(self.ammoshells) do + text=text..string.format("- %s\n", _type) + end + text=text..string.format("Rocket types:\n") + for _,_type in pairs(self.ammorockets) do + text=text..string.format("- %s\n", _type) + end + text=text..string.format("Missile types:\n") + for _,_type in pairs(self.ammomissiles) do + text=text..string.format("- %s\n", _type) + end + text=text..string.format("******************************************************") + if self.Debug then + self:E(ARTY.id..text) + else + self:T(ARTY.id..text) + end + + -- Set default ROE to weapon hold. + self.Controllable:OptionROEHoldFire() + + -- Add event handler. + self:HandleEvent(EVENTS.Shot, self._OnEventShot) + self:HandleEvent(EVENTS.Dead, self._OnEventDead) + --self:HandleEvent(EVENTS.MarkAdded, self._OnEventMarkAdded) + + -- Add DCS event handler - necessary for S_EVENT_MARK_* events. So we only start it, if this was requested. + if self.markallow then + world.addEventHandler(self) + end + + -- Start checking status. + self:__Status(self.StatusInterval) +end + +--- Check the DB for properties of the specified artillery unit type. +-- @param #ARTY self +-- @return #table Properties of the requested artillery type. Returns nil if no matching DB entry could be found. +function ARTY:_CheckDB(displayname) + for _type,_properties in pairs(ARTY.db) do + self:T({type=_type, properties=_properties}) + if _type==displayname then + self:T({type=_type, properties=_properties}) + return _properties + end + end + return nil +end + +--- After "Start" event. Initialized ROE and alarm state. Starts the event handler. +-- @param #ARTY self +-- @param #boolean display (Optional) If true, send message to coalition. Default false. +function ARTY:_StatusReport(display) + + -- Set default. + if display==nil then + display=false + end + + -- Get Ammo. + local Nammo, Nshells, Nrockets, Nmissiles=self:GetAmmo() + local Nnukes=self.Nukes + local Nillu=self.Nillu + local Nsmoke=self.Nsmoke + + local Tnow=timer.getTime() + local Clock=self:_SecondsToClock(timer.getAbsTime()) + + local text=string.format("\n******************* STATUS ***************************\n") + text=text..string.format("ARTY group = %s\n", self.groupname) + text=text..string.format("Clock = %s\n", Clock) + text=text..string.format("FSM state = %s\n", self:GetState()) + text=text..string.format("Total ammo count = %d\n", Nammo) + text=text..string.format("Number of shells = %d\n", Nshells) + text=text..string.format("Number of rockets = %d\n", Nrockets) + text=text..string.format("Number of missiles = %d\n", Nmissiles) + text=text..string.format("Number of nukes = %d\n", Nnukes) + text=text..string.format("Number of illum. = %d\n", Nillu) + text=text..string.format("Number of smoke = %d\n", Nsmoke) + if self.currentTarget then + text=text..string.format("Current Target = %s\n", tostring(self.currentTarget.name)) + text=text..string.format("Curr. Tgt assigned = %d\n", Tnow-self.currentTarget.Tassigned) + else + text=text..string.format("Current Target = %s\n", "none") + end + text=text..string.format("Nshots curr. Target = %d\n", self.Nshots) + text=text..string.format("Targets:\n") + for i=1,#self.targets do + text=text..string.format("- %s\n", self:_TargetInfo(self.targets[i])) + end + if self.currentMove then + text=text..string.format("Current Move = %s\n", tostring(self.currentMove.name)) + else + text=text..string.format("Current Move = %s\n", "none") + end + text=text..string.format("Moves:\n") + for i=1,#self.moves do + text=text..string.format("- %s\n", self:_MoveInfo(self.moves[i])) + end + text=text..string.format("******************************************************") + env.info(ARTY.id..text) + MESSAGE:New(text, 20):Clear():ToCoalitionIf(self.Controllable:GetCoalition(), display) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event Handling +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Eventhandler for shot event. +-- @param #ARTY self +-- @param Core.Event#EVENTDATA EventData +function ARTY:_OnEventShot(EventData) + self:F(EventData) + + -- Weapon data. + local _weapon = EventData.Weapon:getTypeName() -- should be the same as Event.WeaponTypeName + local _weaponStrArray = self:_split(_weapon,"%.") + local _weaponName = _weaponStrArray[#_weaponStrArray] + + -- Debug info. + self:T3(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) + + 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) + 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))) + + -- Destroy weapon before impact. + local _destroyweapon=false + + if _weaponalive then + + -- Update last position. + _lastpos={x=_currpos.x, y=_currpos.y, z=_currpos.z} + + -- Coordinate and distance to target. + local _coord=COORDINATE:NewFromVec3(_lastpos) + local _dist=_coord:Get2DDistance(_data.target.coord) + + -- Debug + self:T3(ARTY.id..string.format("ARTY %s weapon to target dist = %d m", self.groupname,_dist)) + + if _data.target.weapontype==ARTY.WeaponType.IlluminationShells then + + -- Check if within distace. + if _dist<_data.target.radius then + + -- Get random coordinate within certain radius of the target. + local _cr=_data.target.coord:GetRandomCoordinateInRadius(_data.target.radius) + + -- Get random altitude over target. + local _alt=_cr:GetLandHeight()+math.random(self.illuMinalt, self.illuMaxalt) + + -- Adjust explosion height of coordinate. + local _ci=COORDINATE:New(_cr.x,_alt,_cr.z) + + -- Create illumination flare. + _ci:IlluminationBomb(self.illuPower) + + -- Destroy actual shell. + _destroyweapon=true + end + + elseif _data.target.weapontype==ARTY.WeaponType.SmokeShells then + + if _dist<_data.target.radius then + + -- Get random coordinate within a certain radius. + local _cr=_coord:GetRandomCoordinateInRadius(_data.target.radius) + + -- Fire smoke at this coordinate. + _cr:Smoke(self.smokeColor) + + -- Destroy actual shell. + _destroyweapon=true + + end + + end + + if _destroyweapon then + + self:T2(ARTY.id..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)) + + -- Check again in 0.05 seconds. + return timer.getTime() + dt + + end + + else + + -- Get impact coordinate. + local _impactcoord=COORDINATE:NewFromVec3(_lastpos) + + -- 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)) + 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)) + + local _peter={} + _peter.weapon=EventData.weapon + _peter.target=UTILS.DeepCopy(self.currentTarget) + + timer.scheduleFunction(_TrackWeapon, _peter, timer.getTime() + 2.0) + end + + -- Get current ammo. + local _nammo,_nshells,_nrockets,_nmissiles=self:GetAmmo() + + -- Decrease available nukes because we just fired one. + if self.currentTarget.weapontype==ARTY.WeaponType.TacticalNukes then + self.Nukes=self.Nukes-1 + end + + -- Decrease available illuminatin shells because we just fired one. + if self.currentTarget.weapontype==ARTY.WeaponType.IlluminationShells then + self.Nillu=self.Nillu-1 + end + + -- Decrease available illuminatin shells because we just fired one. + if self.currentTarget.weapontype==ARTY.WeaponType.SmokeShells then + self.Nsmoke=self.Nsmoke-1 + end + + -- Check if we are completely out of ammo. + local _outofammo=false + if _nammo==0 then + self:T(ARTY.id..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)) + + -- 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) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + + -- Cease fire. + _ceasefire=true + + -- Relocate if enabled. + _relocate=self.relocateafterfire + end + + -- Check if we are (partly) out of ammo. + if _outofammo or _partlyoutofammo then + _ceasefire=true + end + + -- Relocate position. + if _relocate then + self:_Relocate() + end + + -- Cease fire on current target. + if _ceasefire then + self:CeaseFire(self.currentTarget) + end + + else + self:E(ARTY.id..string.format("WARNING: No current target for group %s?!", self.groupname)) + end + end + end +end + + +--- After "Start" event. Initialized ROE and alarm state. Starts the event handler. +-- @param #ARTY self +-- @param #table Event +function ARTY:onEvent(Event) + + if Event == nil or Event.idx == nil then + self:T3("Skipping onEvent. Event or Event.idx unknown.") + return true + end + + -- Set battery and coalition. + --local batteryname=self.groupname + --local batterycoalition=self.Controllable:GetCoalition() + + self:T2(string.format("Event captured = %s", tostring(self.groupname))) + self:T2(string.format("Event id = %s", tostring(Event.id))) + self:T2(string.format("Event time = %s", tostring(Event.time))) + self:T2(string.format("Event idx = %s", tostring(Event.idx))) + self:T2(string.format("Event coalition = %s", tostring(Event.coalition))) + self:T2(string.format("Event group id = %s", tostring(Event.groupID))) + self:T2(string.format("Event text = %s", tostring(Event.text))) + if Event.initiator~=nil then + local _unitname=Event.initiator:getName() + self:T2(string.format("Event ini unit name = %s", tostring(_unitname))) + end + + if Event.id==world.event.S_EVENT_MARK_ADDED then + self:T2({event="S_EVENT_MARK_ADDED", battery=self.groupname, vec3=Event.pos}) + + elseif Event.id==world.event.S_EVENT_MARK_CHANGE then + self:T({event="S_EVENT_MARK_CHANGE", battery=self.groupname, vec3=Event.pos}) + + -- Handle event. + self:_OnEventMarkChange(Event) + + elseif Event.id==world.event.S_EVENT_MARK_REMOVED then + self:T2({event="S_EVENT_MARK_REMOVED", battery=self.groupname, vec3=Event.pos}) + + -- Hande event. + self:_OnEventMarkRemove(Event) + end + +end + +--- Function called when a F10 map mark was removed. +-- @param #ARTY self +-- @param #table Event Event data. +function ARTY:_OnEventMarkRemove(Event) + + -- Get battery coalition and name. + local batterycoalition=self.Controllable:GetCoalition() + --local batteryname=self.groupname + + if Event.text~=nil and Event.text:find("BATTERY") then + + -- Init defaults. + local _cancelmove=false + local _canceltarget=false + local _name="" + local _id=nil + + -- Check for key phrases of relocation or engagements in marker text. If not, return. + if Event.text:find("Marked Relocation") then + _cancelmove=true + _name=self:_MarkMoveName(Event.idx) + _id=self:_GetMoveIndexByName(_name) + elseif Event.text:find("Marked Target") then + _canceltarget=true + _name=self:_MarkTargetName(Event.idx) + _id=self:_GetTargetIndexByName(_name) + else + return + end + + -- Check if there is a task which matches. + if _id==nil then + return + end + + -- Check if the coalition is the same or an authorization key has been defined. + if (batterycoalition==Event.coalition and self.markkey==nil) or self.markkey~=nil then + + -- Authentify key + local _validkey=self:_MarkerKeyAuthentification(Event.text) + + -- Check if we have the right coalition. + if _validkey then + + -- This should be the unique name of the target or move. + if _cancelmove then + if self.currentMove and self.currentMove.name==_name then + -- We do clear tasks here because in Arrived() it can cause a CTD if the group did actually arrive! + self.Controllable:ClearTasks() + -- Current move is removed here. In contrast to RemoveTarget() there are is no maxengage parameter. + self:Arrived() + else + -- Remove move from queue + self:RemoveMove(_name) + end + elseif _canceltarget then + if self.currentTarget and self.currentTarget.name==_name then + -- Cease fire. + self:CeaseFire(self.currentTarget) + -- We still need to remove the target, because there might be more planned engagements (maxengage>1). + self:RemoveTarget(_name) + else + -- Remove target from queue + self:RemoveTarget(_name) + end + end + + end + end + end +end + +--- Function called when a F10 map mark was changed. This happens when a user enters text. +-- @param #ARTY self +-- @param #table Event Event data. +function ARTY:_OnEventMarkChange(Event) + + -- Check if marker has a text and the "arty" keyword. + if Event.text~=nil and Event.text:lower():find("arty") then + + -- Convert (wrong x-->z, z-->x) vec3 + -- 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} + + -- 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 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)) + return + end + + -- Check if job is assigned to this ARTY group. Default is for all ARTY groups. + local _assigned=false + + -- If any array is filled something has been assigned. + if _assign.everyone then + + -- Everyone was addressed. + _assigned=true + + else --#_assign.battery>0 or #_assign.aliases>0 or #_assign.cluster>0 then + + -- Loop over batteries. + for _,bat in pairs(_assign.battery) do + if self.groupname==bat then + _assigned=true + end + end + + -- Loop over aliases. + for _,alias in pairs(_assign.aliases) do + if self.alias==alias then + _assigned=true + end + end + + -- Loop over clusters. + for _,bat in pairs(_assign.cluster) do + for _,cluster in pairs(self.clusters) do + if cluster==bat then + _assigned=true + end + end + end + + end + + -- We were not addressed. + if not _assigned then + self:T3(ARTY.id..string.format("INFO: ARTY group %s was not addressed! Mark text:\n%s", self.groupname, Event.text)) + return + end + + -- Coordinate was given in text, e.g. as lat, long. + if _assign.coord then + _coord=_assign.coord + end + + -- Check if the authorization key is required and if it is valid. + local _validkey=self:_MarkerKeyAuthentification(Event.text) + + -- Handle requests and return. + if _assign.request and _validkey then + if _assign.requestammo then + self:_MarkRequestAmmo() + end + if _assign.requestmoves then + self:_MarkRequestMoves() + end + if _assign.requesttargets then + self:_MarkRequestTargets() + end + if _assign.requeststatus then + self:_MarkRequestStatus() + end + if _assign.requestrearming then + self:Rearm() + end + -- Requests Done ==> End of story! + return + end + + -- Cancel stuff and return. + if _assign.cancel and _validkey then + if _assign.cancelmove and self.currentMove then + self.Controllable:ClearTasks() + self:Arrived() + elseif _assign.canceltarget and self.currentTarget then + self.currentTarget.engaged=self.currentTarget.engaged+1 + self:CeaseFire(self.currentTarget) + elseif _assign.cancelrearm and self:is("Rearming") then + local nammo=self:GetAmmo() + if nammo>0 then + self:Rearmed() + else + self:Winchester() + end + end + -- Cancels Done ==> End of story! + return + end + + -- Set stuff and return. + if _assign.set and _validkey then + if _assign.setrearmingplace and self.ismobile then + self:SetRearmingPlace(_coord) + _coord:RemoveMark(Event.idx) + _coord:MarkToCoalition(string.format("Rearming place for battery %s", self.groupname), self.Controllable:GetCoalition(), false, string.format("New rearming place for battery %s defined.", self.groupname)) + if self.Debug then + _coord:SmokeOrange() + end + end + if _assign.setrearminggroup then + _coord:RemoveMark(Event.idx) + local rearminggroupcoord=_assign.setrearminggroup:GetCoordinate() + rearminggroupcoord:MarkToCoalition(string.format("Rearming group for battery %s", self.groupname), self.Controllable:GetCoalition(), false, string.format("New rearming group for battery %s defined.", self.groupname)) + self:SetRearmingGroup(_assign.setrearminggroup) + if self.Debug then + rearminggroupcoord:SmokeOrange() + end + end + -- Set stuff Done ==> End of story! + return + end + + -- Handle engagements and relocations. + if _validkey then + + -- Remove old mark because it might contain confidential data such as the key. + -- Also I don't know who can see the mark which was created. + _coord:RemoveMark(Event.idx) + + -- Anticipate marker ID. + -- WARNING: Make sure, no marks are set until the COORDINATE:MarkToCoalition() is called or the target/move name will be wrong and target cannot be removed by deleting its marker. + local _id=UTILS._MarkID+1 + + if _assign.move then + + -- Create a new name. This determins the string we search when deleting a move! + local _name=self:_MarkMoveName(_id) + + local text=string.format("%s, received new relocation assignment.", self.alias) + text=text..string.format("\nCoordinates %s",_coord:ToStringLLDMS()) + MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug) + + -- Assign a relocation of the arty group. + local _movename=self:AssignMoveCoord(_coord, _assign.time, _assign.speed, _assign.onroad, _assign.movecanceltarget,_name, true) + + if _movename~=nil then + local _mid=self:_GetMoveIndexByName(_movename) + local _move=self.moves[_mid] + + -- Create new target name. + local clock=tostring(self:_SecondsToClock(_move.time)) + local _markertext=_movename..string.format(", Time=%s, Speed=%d km/h, Use Roads=%s.", clock, _move.speed, tostring(_move.onroad)) + + -- Create a new mark. This will trigger the mark added event. + local _randomcoord=_coord:GetRandomCoordinateInRadius(100) + _randomcoord:MarkToCoalition(_markertext, batterycoalition, self.markreadonly or _assign.readonly) + else + local text=string.format("%s, relocation not possible.", self.alias) + MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug) + end + + else + + -- Create a new name. + local _name=self:_MarkTargetName(_id) + + local text=string.format("%s, received new target assignment.", self.alias) + text=text..string.format("\nCoordinates %s",_coord:ToStringLLDMS()) + if _assign.time then + text=text..string.format("\nTime %s",_assign.time) + end + if _assign.prio then + text=text..string.format("\nPrio %d",_assign.prio) + end + if _assign.radius then + text=text..string.format("\nRadius %d m",_assign.radius) + end + if _assign.nshells then + text=text..string.format("\nShots %d",_assign.nshells) + end + if _assign.maxengage then + text=text..string.format("\nEngagements %d",_assign.maxengage) + end + if _assign.weapontype then + text=text..string.format("\nWeapon %s",self:_WeaponTypeName(_assign.weapontype)) + end + MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug) + + -- Assign a new firing engagement. + -- Note, we set unique=true so this target gets only added once. + local _targetname=self:AssignTargetCoord(_coord,_assign.prio,_assign.radius,_assign.nshells,_assign.maxengage,_assign.time,_assign.weapontype, _name, true) + + if _targetname~=nil then + local _tid=self:_GetTargetIndexByName(_targetname) + local _target=self.targets[_tid] + + -- Create new target name. + local clock=tostring(self:_SecondsToClock(_target.time)) + local weapon=self:_WeaponTypeName(_target.weapontype) + local _markertext=_targetname..string.format(", Priority=%d, Radius=%d m, Shots=%d, Engagements=%d, Weapon=%s, Time=%s", _target.prio, _target.radius, _target.nshells, _target.maxengage, weapon, clock) + + -- Create a new mark. This will trigger the mark added event. + local _randomcoord=_coord:GetRandomCoordinateInRadius(250) + _randomcoord:MarkToCoalition(_markertext, batterycoalition, self.markreadonly or _assign.readonly) + end + end + end + + end + end + +end + +--- Event handler for event Dead. +-- @param #ARTY self +-- @param Core.Event#EVENTDATA EventData +function ARTY:_OnEventDead(EventData) + self:F(EventData) + + -- Name of controllable. + local _name=self.groupname + + -- Check for correct group. + if EventData.IniGroupName==_name then + + -- Dead Unit. + self:T2(string.format("%s: Captured dead event for unit %s.", _name, EventData.IniUnitName)) + + -- FSM Dead event. We give one second for update of data base. + self:__Dead(1) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM Events and States +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Status" event. Report status of group. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterStatus(Controllable, From, Event, To) + self:_EventFromTo("onafterStatus", Event, From, To) + + -- 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 + 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(ARTY.id..string.format("%s: OutOfAmmo ==> Rearm ==> Rearming", Controllable:GetName())) + self:Rearm() + end + + -- Call status again in ~10 sec. + self:__Status(self.StatusInterval) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Loaded" event. Checks if group is currently firing and removes the target by calling CeaseFire. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean If true, proceed to onafterLoaded. +function ARTY:onbeforeLoaded(Controllable, From, Event, To) + if self.currentTarget then + self:CeaseFire(self.currentTarget) + end + + return true +end + +--- After "UnLoaded" event. Group is combat ready again. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean If true, proceed to onafterLoaded. +function ARTY:onafterUnLoaded(Controllable, From, Event, To) + self:CombatReady() +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Enter "CombatReady" state. Route the group back if necessary. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onenterCombatReady(Controllable, From, Event, To) + self:_EventFromTo("onenterCombatReady", Event, From, To) + -- Debug info + self:T3(ARTY.id..string.format("onenterComabReady, from=%s, event=%s, to=%s", From, Event, To)) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "OpenFire" event. Checks if group already has a target. Checks for valid min/max range and removes the target if necessary. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table target Array holding the target info. +-- @return #boolean If true, proceed to onafterOpenfire. +function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target) + self:_EventFromTo("onbeforeOpenFire", Event, From, To) + + -- Check that group has no current target already. + if self.currentTarget then + -- This should not happen. Some earlier check failed. + self:E(ARTY.id..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)) + -- Deny transition. + return false + end + + -- Get the number of available shells, rockets or missiles requested for this target. + local nfire=self:_CheckWeaponTypeAvailable(target) + + -- Adjust if less than requested ammo is left. + target.nshells=math.min(target.nshells, nfire) + + -- No ammo left ==> deny transition. + if target.nshells<1 then + local text=string.format("%s, no ammo left to engage target %s with selected weapon type %s.") + return false + end + + return true +end + +--- After "OpenFire" event. Sets the current target and starts the fire at point task. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table target Array holding the target info. +function ARTY:onafterOpenFire(Controllable, From, Event, To, target) + self:_EventFromTo("onafterOpenFire", Event, From, To) + + -- Get target array index. + local id=self:_GetTargetIndexByName(target.name) + + -- Target is now under fire and has been engaged once more. + if id then + -- Set under fire flag. + self.targets[id].underfire=true + -- Set current target. + self.currentTarget=target + -- Set time the target was assigned. + self.currentTarget.Tassigned=timer.getTime() + end + + -- Distance to target + local range=Controllable:GetCoordinate():Get2DDistance(target.coord) + + -- Get ammo. + local Nammo, Nshells, Nrockets, Nmissiles=self:GetAmmo() + local nfire=Nammo + local _type="shots" + if target.weapontype==ARTY.WeaponType.Auto then + nfire=Nammo + _type="shots" + elseif target.weapontype==ARTY.WeaponType.Cannon then + nfire=Nshells + _type="shells" + elseif target.weapontype==ARTY.WeaponType.TacticalNukes then + nfire=self.Nukes + _type="nuclear shells" + elseif target.weapontype==ARTY.WeaponType.IlluminationShells then + nfire=self.Nillu + _type="illumination shells" + elseif target.weapontype==ARTY.WeaponType.SmokeShells then + nfire=self.Nsmoke + _type="smoke shells" + elseif target.weapontype==ARTY.WeaponType.Rockets then + nfire=Nrockets + _type="rockets" + elseif target.weapontype==ARTY.WeaponType.CruiseMissile then + nfire=Nmissiles + _type="cruise missiles" + end + + -- Adjust if less than requested ammo is left. + target.nshells=math.min(target.nshells, nfire) + + -- Send message. + local text=string.format("%s, opening fire on target %s with %d %s. Distance %.1f km.", Controllable:GetName(), target.name, target.nshells, _type, range/1000) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), 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) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "CeaseFire" event. Clears task of the group and removes the target if max engagement was reached. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table target Array holding the target info. +function ARTY:onafterCeaseFire(Controllable, From, Event, To, target) + self:_EventFromTo("onafterCeaseFire", Event, From, To) + + if target then + + -- Send message. + local text=string.format("%s, ceasing fire on target %s.", Controllable:GetName(), target.name) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report) + + -- Get target array index. + local id=self:_GetTargetIndexByName(target.name) + + -- We have a target. + if id then + -- Target was actually engaged. (Could happen that engagement was aborted while group was still aiming.) + if self.Nshots>0 then + self.targets[id].engaged=self.targets[id].engaged+1 + -- Clear the attack time. + self.targets[id].time=nil + end + -- Target is not under fire any more. + self.targets[id].underfire=false + end + + -- If number of engagements has been reached, the target is removed. + if target.engaged >= target.maxengage then + self:RemoveTarget(target.name) + end + + -- Set ROE to weapon hold. + self.Controllable:OptionROEHoldFire() + + -- Clear tasks. + self.Controllable:ClearTasks() + + else + self:E(ARTY.id..string.format("ERROR: No target in cease fire for group %s.", self.groupname)) + end + + -- Set number of shots to zero. + self.Nshots=0 + + -- ARTY group has no current target any more. + self.currentTarget=nil + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Winchester" event. Group is out of ammo. Trigger "Rearm" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterWinchester(Controllable, From, Event, To) + self:_EventFromTo("onafterWinchester", Event, From, To) + + -- Send message. + local text=string.format("%s, winchester!", Controllable:GetName()) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Rearm" event. Check if a unit to rearm the ARTY group has been defined. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean If true, proceed to onafterRearm. +function ARTY:onbeforeRearm(Controllable, From, Event, To) + self:_EventFromTo("onbeforeRearm", Event, From, To) + + local _rearmed=self:_CheckRearmed() + if _rearmed then + self:T(ARTY.id..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)) + end + + -- Check if a reaming unit or rearming place was specified. + if self.RearmingGroup and self.RearmingGroup:IsAlive() then + return true + elseif self.RearmingPlaceCoord then + return true + else + return false + end + +end + +--- After "Rearm" event. Send message if reporting is on. Route rearming unit to ARTY group. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterRearm(Controllable, From, Event, To) + self:_EventFromTo("onafterRearm", Event, From, To) + + -- Coordinate of ARTY unit. + local coordARTY=self.Controllable:GetCoordinate() + + -- Remember current coordinates so that we find our way back home. + self.InitialCoord=coordARTY + + -- Coordinate of rearming group. + local coordRARM=nil + if self.RearmingGroup then + -- Coordinate of the rearming unit. + coordRARM=self.RearmingGroup:GetCoordinate() + -- Remember the coordinates of the rearming unit. After rearming it will go back to this position. + self.RearmingGroupCoord=coordRARM + end + + if self.RearmingGroup and self.RearmingPlaceCoord and self.ismobile then + + -- CASE 1: Rearming unit and ARTY group meet at rearming place. + + -- Send message. + local text=string.format("%s, %s, request rearming at rearming place.", Controllable:GetName(), self.RearmingGroup:GetName()) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), 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) + + -- 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) + + -- Distance. + local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord) + + -- Route ARTY group to rearming place. + if dA > self.RearmingDistance then + local _tocoord=self:_VicinityCoord(self.RearmingPlaceCoord) + self:AssignMoveCoord(_tocoord, nil, nil, self.RearmingArtyOnRoad, false, "REARMING MOVE TO REARMING PLACE", true) + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Rearmed" event. Send ARTY and rearming group back to their inital positions. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterRearmed(Controllable, From, Event, To) + self:_EventFromTo("onafterRearmed", Event, From, To) + + -- Send message. + local text=string.format("%s, rearming complete.", Controllable:GetName()) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + + -- "Rearm" tactical nukes as well. + self.Nukes=self.Nukes0 + self.Nillu=self.Nillu0 + self.Nsmoke=self.Nsmoke0 + + -- Route ARTY group back to where it came from (if distance is > 100 m). + local dist=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord) + if dist > self.RearmingDistance then + self:AssignMoveCoord(self.InitialCoord, nil, nil, self.RearmingArtyOnRoad, false, "REARMING MOVE REARMING COMPLETE", true) + end + + -- Route unit back to where it came from (if distance is > 100 m). + if self.RearmingGroup and self.RearmingGroup:IsAlive() then + local d=self.RearmingGroup:GetCoordinate():Get2DDistance(self.RearmingGroupCoord) + if d > self.RearmingDistance then + self:_Move(self.RearmingGroup, self.RearmingGroupCoord, self.RearmingGroupSpeed, self.RearmingGroupOnRoad) + else + -- Clear tasks. + self.RearmingGroup:ClearTasks() + end + end + +end + +--- Check if ARTY group is rearmed, i.e. has its full amount of ammo. +-- @param #ARTY self +-- @return #boolean True if rearming is complete, false otherwise. +function ARTY:_CheckRearmed() + self:F2() + + -- Get current ammo. + local nammo,nshells,nrockets,nmissiles=self:GetAmmo() + + -- Number of units still alive. + local units=self.Controllable:GetUnits() + local nunits=0 + if units then + nunits=#units + end + + -- Full Ammo count. + local FullAmmo=self.Nammo0 * nunits / self.IniGroupStrength + + -- Rearming status in per cent. + local _rearmpc=nammo/FullAmmo*100 + + -- Send message if rearming > 1% complete + if _rearmpc>1 then + local text=string.format("%s, rearming %d %% complete.", self.alias, _rearmpc) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(self.Controllable:GetCoalition(), self.report or self.Debug) + end + + -- Return if ammo is full. + -- TODO: Strangely, I got the case that a Paladin got one more shell than it can max carry, i.e. 40 not 39 when rearming when it still had some ammo left. Need to report. + if nammo>=FullAmmo then + return true + else + return false + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Move" event. Check if a unit to rearm the ARTY group has been defined. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table move Table containing the move parameters. +-- @param Core.Point#COORDINATE ToCoord Coordinate to which the ARTY group should move. +-- @param #boolean OnRoad If true group should move on road mainly. +-- @return #boolean If true, proceed to onafterMove. +function ARTY:onbeforeMove(Controllable, From, Event, To, move) + self:_EventFromTo("onbeforeMove", Event, From, To) + + -- Check if group can actually move... + if not self.ismobile then + return false + end + + -- Check if group is engaging. + if self.currentTarget then + if move.cancel then + -- Cancel current target. + self:CeaseFire(self.currentTarget) + else + -- We should not cancel. + return false + end + end + + return true +end + +--- After "Move" event. Route group to given coordinate. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table move Table containing the move parameters. +function ARTY:onafterMove(Controllable, From, Event, To, move) + self:_EventFromTo("onafterMove", Event, From, To) + + -- Set alarm state to green and ROE to weapon hold. + self.Controllable:OptionAlarmStateGreen() + self.Controllable:OptionROEHoldFire() + + -- Take care of max speed. + local _Speed=math.min(move.speed, self.SpeedMax) + + -- Smoke coordinate + if self.Debug then + move.coord:SmokeRed() + end + + -- Set current move. + self.currentMove=move + + -- Route group to coodinate. + self:_Move(self.Controllable, move.coord, move.speed, move.onroad) + +end + +--- After "Arrived" event. Group has reached its destination. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterArrived(Controllable, From, Event, To) + self:_EventFromTo("onafterArrived", Event, From, To) + + -- Set alarm state to auto. + self.Controllable:OptionAlarmStateAuto() + + -- WARNING: calling ClearTasks() here causes CTD of DCS when move is over. Dont know why? combotask? + --self.Controllable:ClearTasks() + + -- Send message + local text=string.format("%s, arrived at destination.", Controllable:GetName()) + self:T(ARTY.id..text) + MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), self.report or self.Debug) + + -- Remove executed move from queue. + if self.currentMove then + self:RemoveMove(self.currentMove.name) + self.currentMove=nil + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "NewTarget" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table target Array holding the target parameters. +function ARTY:onafterNewTarget(Controllable, From, Event, To, target) + self:_EventFromTo("onafterNewTarget", Event, From, To) + + -- Debug message. + local text=string.format("Adding new target %s.", target.name) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:T(ARTY.id..text) +end + +--- After "NewMove" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #table move Array holding the move parameters. +function ARTY:onafterNewMove(Controllable, From, Event, To, move) + self:_EventFromTo("onafterNewTarget", Event, From, To) + + -- Debug message. + local text=string.format("Adding new move %s.", move.name) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:T(ARTY.id..text) +end + + +--- After "Dead" event, when a unit has died. When all units of a group are dead trigger "Stop" event. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterDead(Controllable, From, Event, To) + 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 + + -- Message. + local text=string.format("%s, one of our units just died! %d units left.", self.groupname, nunits) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:T(ARTY.id..text) + + -- Go to stop state. + if nunits==0 then + self:Stop() + end + +end + +--- After "Stop" event. Unhandle events and cease fire on current target. +-- @param #ARTY self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function ARTY:onafterStop(Controllable, From, Event, To) + self:_EventFromTo("onafterStop", Event, From, To) + + -- Debug info. + self:T(ARTY.id..string.format("Stopping ARTY FSM for group %s.", Controllable:GetName())) + + -- Cease Fire on current target. + if self.currentTarget then + self:CeaseFire(self.currentTarget) + end + + -- Remove all targets. + --self:RemoveAllTargets() + + -- Unhandle event. + self:UnHandleEvent(EVENTS.Shot) + self:UnHandleEvent(EVENTS.Dead) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set task for firing at a coordinate. +-- @param #ARTY self +-- @param Core.Point#COORDINATE coord Coordinates to fire upon. +-- @param #number radius Radius around coordinate. +-- @param #number nshells Number of shells to fire. +-- @param #number weapontype Type of weapon to use. +function ARTY:_FireAtCoord(coord, radius, nshells, weapontype) + self:F({coord=coord, radius=radius, nshells=nshells}) + + -- Controllable. + local group=self.Controllable --Wrapper.Group#GROUP + + -- Tactical nukes are actually cannon shells. + if weapontype==ARTY.WeaponType.TacticalNukes or weapontype==ARTY.WeaponType.IlluminationShells or weapontype==ARTY.WeaponType.SmokeShells then + weapontype=ARTY.WeaponType.Cannon + end + + -- Set ROE to weapon free. + group:OptionROEOpenFire() + + -- Get Vec2 + local vec2=coord:GetVec2() + + -- Get task. + local fire=group:TaskFireAtPoint(vec2, radius, nshells, weapontype) + + -- Execute task. + group:SetTask(fire) +end + +--- Model a nuclear blast/destruction by creating fires and destroy scenery. +-- @param #ARTY self +-- @param Core.Point#COORDINATE _coord Coordinate of the impact point (center of the blast). +function ARTY:_NuclearBlast(_coord) + + local S0=self.nukewarhead + local R0=self.nukerange + + -- Number of fires + local N0=self.nukefires + + -- Create an explosion at the last known position. + _coord:Explosion(S0) + + -- Huge fire at direct impact point. + --if self.nukefire then + _coord:BigSmokeAndFireHuge() + --end + + -- Create a table of fire coordinates within the demolition zone. + local _fires={} + for i=1,N0 do + local _fire=_coord:GetRandomCoordinateInRadius(R0) + local _dist=_fire:Get2DDistance(_coord) + table.insert(_fires, {distance=_dist, coord=_fire}) + end + + -- Sort scenery wrt to distance from impact point. + local _sort = function(a,b) return a.distance < b.distance end + table.sort(_fires,_sort) + + local function _explosion(R) + -- At R=R0 ==> explosion strength is 1% of S0 at impact point. + local alpha=math.log(100) + local strength=S0*math.exp(-alpha*R/R0) + self:T2(ARTY.id..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)) + + -- Get a random Big Smoke and fire object. + local _preset=math.random(0,7) + local _density=S/S0 --math.random()+0.1 + + _fire:BigSmokeAndFire(_preset,_density) + _fire:Explosion(S) + + end + end + + if self.nukefire==true then + ignite(_fires) + end + +--[[ + local ZoneNuke=ZONE_RADIUS:New("Nukezone", _coord:GetVec2(), 2000) + + -- Scan for Scenery objects. + ZoneNuke:Scan(Object.Category.SCENERY) + + -- Array with all possible hideouts, i.e. scenery objects in the vicinity of the group. + local scenery={} + + for SceneryTypeName, SceneryData in pairs(ZoneNuke:GetScannedScenery()) do + for SceneryName, SceneryObject in pairs(SceneryData) do + + local SceneryObject = SceneryObject -- Wrapper.Scenery#SCENERY + + -- Position of the scenery object. + local spos=SceneryObject:GetCoordinate() + + -- Distance from group to impact point. + local distance= spos:Get2DDistance(_coord) + + -- Place markers on every possible scenery object. + if self.Debug then + local MarkerID=spos:MarkToAll(string.format("%s scenery object %s", self.Controllable:GetName(), SceneryObject:GetTypeName())) + local text=string.format("%s scenery: %s, Coord %s", self.Controllable:GetName(), SceneryObject:GetTypeName(), SceneryObject:GetCoordinate():ToStringLLDMS()) + self:T2(SUPPRESSION.id..text) + end + + -- Add to table. + table.insert(scenery, {object=SceneryObject, distance=distance}) + + --SceneryObject:Destroy() + end + end + + -- Sort scenery wrt to distance from impact point. +-- local _sort = function(a,b) return a.distance < b.distance end +-- table.sort(scenery,_sort) + +-- for _,object in pairs(scenery) do +-- local sobject=object -- Wrapper.Scenery#SCENERY +-- sobject:Destroy() +-- end + +]] + +end + +--- Route group to a certain point. +-- @param #ARTY self +-- @param Wrapper.Group#GROUP group Group to route. +-- @param Core.Point#COORDINATE ToCoord Coordinate where we want to go. +-- @param #number Speed (Optional) Speed in km/h. Default is 70% of max speed the group can do. +-- @param #boolean OnRoad If true, use (mainly) roads. +function ARTY:_Move(group, ToCoord, Speed, OnRoad) + self:F2({group=group:GetName(), Speed=Speed, OnRoad=OnRoad}) + + -- Clear all tasks. + group:ClearTasks() + group:OptionAlarmStateGreen() + group:OptionROEHoldFire() + + -- Set formation. + local formation = "Off Road" + + -- Get max speed of group. + local SpeedMax=group:GetSpeedMax() + + -- Set speed. + Speed=Speed or SpeedMax*0.7 + + -- Make sure, we do not go above max speed possible. + Speed=math.min(Speed, SpeedMax) + + -- Current coordinates of group. + local cpini=group:GetCoordinate() -- Core.Point#COORDINATE + + -- Distance between current and final point. + local dist=cpini:Get2DDistance(ToCoord) + + -- Waypoint and task arrays. + local path={} + local task={} + + -- First waypoint is the current position of the group. + path[#path+1]=cpini:WaypointGround(Speed, formation) + task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) + + -- Route group on road if requested. + if OnRoad then + + -- Get path on road. + local _pathonroad=cpini:GetPathOnRoad(ToCoord) + + -- Check if we actually got a path. There are situations where nil is returned. In that case, we go directly. + if _pathonroad then + + -- Just take the first and last point. + local _first=_pathonroad[1] + local _last=_pathonroad[#_pathonroad] + + if self.Debug then + _first:SmokeGreen() + _last:SmokeGreen() + end + + -- First point on road. + path[#path+1]=_first:WaypointGround(Speed, "On Road") + task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) + + -- Last point on road. + path[#path+1]=_last:WaypointGround(Speed, "On Road") + task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false) + end + + end + + -- Last waypoint at ToCoord. + path[#path+1]=ToCoord:WaypointGround(Speed, formation) + task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, true) + + --if self.Debug then + -- cpini:SmokeBlue() + -- ToCoord:SmokeBlue() + --end + + -- Init waypoints of the group. + local Waypoints={} + + -- New points are added to the default route. + for i=1,#path do + table.insert(Waypoints, i, path[i]) + end + + -- Set task for all waypoints. + for i=1,#Waypoints do + group:SetTaskWaypoint(Waypoints[i], task[i]) + end + + -- Submit task and route group along waypoints. + group:Route(Waypoints) + +end + +--- Function called when group is passing a waypoint. +-- @param Wrapper.Group#GROUP group Group for which waypoint passing should be monitored. +-- @param #ARTY arty ARTY object. +-- @param #number i Waypoint number that has been reached. +-- @param #boolean final True if it is the final waypoint. +function ARTY._PassingWaypoint(group, arty, i, final) + + -- 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 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 + +end + +--- Relocate to another position, e.g. after an engagement to avoid couter strikes. +-- @param #ARTY self +function ARTY:_Relocate() + + -- Current position. + local _pos=self.Controllable:GetCoordinate() + + local _new=nil + local _gotit=false + local _n=0 + local _nmax=1000 + repeat + -- Get a random coordinate. + _new=_pos:GetRandomCoordinateInRadius(self.relocateRmax, self.relocateRmin) + local _surface=_new:GetSurfaceType() + + -- Check that new coordinate is not water(-ish). + if _surface~=land.SurfaceType.WATER and _surface~=land.SurfaceType.SHALLOW_WATER then + _gotit=true + end + -- Increase counter. + _n=_n+1 + until _gotit or _n>_nmax + + -- Assign relocation. + if _gotit then + self:AssignMoveCoord(_new, nil, nil, false, false, "RELOCATION MOVE AFTER FIRING") + end +end + +--- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up. +-- @param #ARTY self +-- @param #boolean display Display ammo table as message to all. Default false. +-- @return #number Total amount of ammo the whole group has left. +-- @return #number Number of shells the group has left. +-- @return #number Number of rockets the group has left. +-- @return #number Number of missiles the group has left. +function ARTY:GetAmmo(display) + self:F3({display=display}) + + -- Default is display false. + if display==nil then + display=false + end + + -- Init counter. + local nammo=0 + local nshells=0 + local nrockets=0 + local nmissiles=0 + + -- Get all units. + local units=self.Controllable:GetUnits() + if units==nil then + return nammo, nshells, nrockets, nmissiles + end + + for _,unit in pairs(units) do + + if unit and unit:IsAlive() then + + -- Output. + local text=string.format("ARTY group %s - unit %s:\n", self.groupname, unit:GetName()) + + -- Get ammo table. + local ammotable=unit:GetAmmo() + + if ammotable ~= nil then + + local weapons=#ammotable + + -- Display ammo table + if display then + self:E(ARTY.id..string.format("Number of weapons %d.", weapons)) + self:E({ammotable=ammotable}) + self:E(ARTY.id.."Ammotable:") + for id,bla in pairs(ammotable) do + self:E({id=id, ammo=bla}) + end + end + + -- Loop over all weapons. + for w=1,weapons do + + -- Number of current weapon. + local Nammo=ammotable[w]["count"] + + -- Typename of current weapon + local Tammo=ammotable[w]["desc"]["typeName"] + + local _weaponString = self:_split(Tammo,"%.") + local _weaponName = _weaponString[#_weaponString] + + -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3 + local Category=ammotable[w].desc.category + + -- Get missile category: Weapon.MissileCategory AAM=1, SAM=2, BM=3, ANTI_SHIP=4, CRUISE=5, OTHER=6 + local MissileCategory=nil + if Category==Weapon.Category.MISSILE then + MissileCategory=ammotable[w].desc.missileCategory + end + + + -- Check for correct shell type. + local _gotshell=false + if #self.ammoshells>0 then + -- User explicitly specified the valid type(s) of shells. + for _,_type in pairs(self.ammoshells) do + if string.match(Tammo, _type) and Category==Weapon.Category.SHELL then + _gotshell=true + end + end + else + if Category==Weapon.Category.SHELL then + _gotshell=true + end + end + + -- Check for correct rocket type. + local _gotrocket=false + if #self.ammorockets>0 then + for _,_type in pairs(self.ammorockets) do + if string.match(Tammo, _type) and Category==Weapon.Category.ROCKET then + _gotrocket=true + end + end + else + if Category==Weapon.Category.ROCKET then + _gotrocket=true + end + end + + -- Check for correct missile type. + local _gotmissile=false + if #self.ammomissiles>0 then + for _,_type in pairs(self.ammomissiles) do + if string.match(Tammo,_type) and Category==Weapon.Category.MISSILE then + _gotmissile=true + end + end + else + if Category==Weapon.Category.MISSILE then + _gotmissile=true + end + end + + -- We are specifically looking for shells or rockets here. + if _gotshell then + + -- Add up all shells. + nshells=nshells+Nammo + + -- Debug info. + text=text..string.format("- %d shells of type %s\n", Nammo, _weaponName) + + elseif _gotrocket then + + -- Add up all rockets. + nrockets=nrockets+Nammo + + -- Debug info. + text=text..string.format("- %d rockets of type %s\n", Nammo, _weaponName) + + elseif _gotmissile then + + -- Add up all cruise missiles (category 5) + if MissileCategory==Weapon.MissileCategory.CRUISE then + nmissiles=nmissiles+Nammo + end + + -- Debug info. + text=text..string.format("- %d %s missiles of type %s\n", Nammo, self:_MissileCategoryName(MissileCategory), _weaponName) + + else + + -- Debug info. + text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory)) + + end + + end + end + + -- Debug text and send message. + if display then + self:E(ARTY.id..text) + else + self:T3(ARTY.id..text) + end + MESSAGE:New(text, 10):ToAllIf(display) + + end + end + + -- Total amount of ammunition. + nammo=nshells+nrockets+nmissiles + + return nammo, nshells, nrockets, nmissiles +end + +--- Returns a name of a missile category. +-- @param #ARTY self +-- @param #number categorynumber Number of missile category from weapon missile category enumerator. See https://wiki.hoggitworld.com/view/DCS_Class_Weapon +-- @return #string Missile category name. +function ARTY:_MissileCategoryName(categorynumber) + local cat="unknown" + if categorynumber==Weapon.MissileCategory.AAM then + cat="air-to-air" + elseif categorynumber==Weapon.MissileCategory.SAM then + cat="surface-to-air" + elseif categorynumber==Weapon.MissileCategory.BM then + cat="ballistic" + elseif categorynumber==Weapon.MissileCategory.ANTI_SHIP then + cat="anti-ship" + elseif categorynumber==Weapon.MissileCategory.CRUISE then + cat="cruise" + elseif categorynumber==Weapon.MissileCategory.OTHER then + cat="other" + end + return cat +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Mark Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Extract engagement assignments and parameters from mark text. +-- @param #ARTY self +-- @param #string text Marker text. +-- @return #boolean If true, authentification successful. +function ARTY:_MarkerKeyAuthentification(text) + + -- Set battery and coalition. + --local batteryname=self.groupname + local batterycoalition=self.Controllable:GetCoalition() + + -- Get assignment. + local mykey=nil + if self.markkey~=nil then + + -- keywords are split by "," + local keywords=self:_split(text, ",") + for _,key in pairs(keywords) do + local s=self:_split(key, " ") + local val=s[2] + if key:lower():find("key") then + mykey=tonumber(val) + self:T(ARTY.id..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))) + + -- Send message + local text="" + if mykey==nil then + text=string.format("%s, authorization required but did not receive a key!", self.alias) + elseif _validkey==false then + text=string.format("%s, authorization required but did receive an incorrect key (key=%s)!", self.alias, tostring(mykey)) + elseif _validkey==true then + text=string.format("%s, authentification successful!", self.alias) + end + MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug) + end + + return _validkey +end + +--- Extract engagement assignments and parameters from mark text. +-- @param #ARTY self +-- @param #string text Marker text to be analyzed. +-- @return #table Table with assignment parameters, e.g. number of shots, radius, time etc. +function ARTY:_Markertext(text) + self:F(text) + + -- Assignment parameters. + local assignment={} + assignment.battery={} + assignment.aliases={} + assignment.cluster={} + assignment.everyone=false + assignment.move=false + assignment.engage=false + assignment.request=false + assignment.cancel=false + assignment.set=false + assignment.readonly=false + assignment.movecanceltarget=false + assignment.cancelmove=false + assignment.canceltarget=false + assignment.cancelrearm=false + assignment.setrearmingplace=false + assignment.setrearminggroup=false + + -- Check for correct keywords. + if text:lower():find("arty engage") or text:lower():find("arty attack") then + assignment.engage=true + elseif text:lower():find("arty move") or text:lower():find("arty relocate") then + assignment.move=true + elseif text:lower():find("arty request") then + assignment.request=true + elseif text:lower():find("arty cancel") then + assignment.cancel=true + elseif text:lower():find("arty set") then + assignment.set=true + else + self:E(ARTY.id..'ERROR: Neither "ARTY ENGAGE" nor "ARTY MOVE" nor "ARTY RELOCATE" nor "ARTY REQUEST" nor "ARTY CANCEL" nor "ARTY SET" keyword specified!') + return nil + end + + -- keywords are split by "," + local keywords=self:_split(text, ",") + self:T({keywords=keywords}) + + for _,keyphrase in pairs(keywords) do + + -- Split keyphrase by space. First one is the key and second, ... the parameter(s) until the next comma. + local str=self:_split(keyphrase, " ") + local key=str[1] + local val=str[2] + + -- Debug output. + self:T3(ARTY.id..string.format("%s, keyphrase = %s, key = %s, val = %s", self.groupname, tostring(keyphrase), tostring(key), tostring(val))) + + -- Battery name, i.e. which ARTY group should fire. + if key:lower():find("battery") then + + local v=self:_split(keyphrase, '"') + + for i=2,#v,2 do + table.insert(assignment.battery, v[i]) + self:T2(ARTY.id..string.format("Key Battery=%s.", v[i])) + end + + elseif key:lower():find("alias") then + + local v=self:_split(keyphrase, '"') + + for i=2,#v,2 do + table.insert(assignment.aliases, v[i]) + self:T2(ARTY.id..string.format("Key Aliases=%s.", v[i])) + end + + elseif key:lower():find("cluster") then + + local v=self:_split(keyphrase, '"') + + for i=2,#v,2 do + table.insert(assignment.cluster, v[i]) + self:T2(ARTY.id..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.")) + + elseif keyphrase:lower():find("irrevocable") or keyphrase:lower():find("readonly") then + + assignment.readonly=true + self:T2(ARTY.id..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)) + + elseif assignment.engage and key:lower():find("shot") then + + assignment.nshells=tonumber(val) + self:T(ARTY.id..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)) + + elseif assignment.engage and key:lower():find("radius") then + + assignment.radius=tonumber(val) + self:T2(ARTY.id..string.format("Key Radius=%s.", val)) + + elseif assignment.engage and key:lower():find("weapon") then + + if val:lower():find("cannon") then + assignment.weapontype=ARTY.WeaponType.Cannon + elseif val:lower():find("rocket") then + assignment.weapontype=ARTY.WeaponType.Rockets + elseif val:lower():find("missile") then + assignment.weapontype=ARTY.WeaponType.CruiseMissile + elseif val:lower():find("nuke") then + assignment.weapontype=ARTY.WeaponType.TacticalNukes + elseif val:lower():find("illu") then + assignment.weapontype=ARTY.WeaponType.IlluminationShells + elseif val:lower():find("smoke") then + assignment.weapontype=ARTY.WeaponType.SmokeShells + else + assignment.weapontype=ARTY.WeaponType.Auto + end + self:T2(ARTY.id..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)) + + 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.")) + + 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.")) + + elseif assignment.request and keyphrase:lower():find("rearm") then + + assignment.requestrearming=true + self:T2(ARTY.id..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.")) + + elseif assignment.request and keyphrase:lower():find("target") then + + assignment.requesttargets=true + self:T2(ARTY.id..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.")) + + 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.")) + + 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.")) + + 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.")) + + elseif assignment.cancel and keyphrase:lower():find("rearm") then + + assignment.cancelrearm=true + self:T2(ARTY.id..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.")) + + 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))) + + 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)) + + 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)) + + -- Convert LL to coordinate object. + if _latitude and _longitude then + assignment.coord=COORDINATE:NewFromLLDD(_latitude,_longitude) + end + + end + end + end + + return assignment +end + +--- Request ammo via mark. +-- @param #ARTY self +function ARTY:_MarkRequestAmmo() + self:GetAmmo(true) +end + +--- Request status via mark. +-- @param #ARTY self +function ARTY:_MarkRequestStatus() + self:_StatusReport(true) +end + +--- Request Moves. +-- @param #ARTY self +function ARTY:_MarkRequestMoves() + local text=string.format("%s, relocations:", self.groupname) + if #self.moves>0 then + for _,move in pairs(self.moves) do + if self.currentMove and move.name == self.currentMove.name then + text=text..string.format("\n- %s (current)", self:_MoveInfo(move)) + else + text=text..string.format("\n- %s", self:_MoveInfo(move)) + end + end + else + text=text..string.format("\n- no queued relocations") + end + MESSAGE:New(text, 20):Clear():ToCoalition(self.Controllable:GetCoalition()) +end + +--- Request Targets. +-- @param #ARTY self +function ARTY:_MarkRequestTargets() + local text=string.format("%s, targets:", self.groupname) + if #self.targets>0 then + for _,target in pairs(self.targets) do + if self.currentTarget and target.name == self.currentTarget.name then + text=text..string.format("\n- %s (current)", self:_TargetInfo(target)) + else + text=text..string.format("\n- %s", self:_TargetInfo(target)) + end + end + else + text=text..string.format("\n- no queued targets") + end + MESSAGE:New(text, 20):Clear():ToCoalition(self.Controllable:GetCoalition()) +end + +--- Create a name for an engagement initiated by placing a marker. +-- @param #ARTY self +-- @param #number markerid ID of the placed marker. +-- @return #string Name of target engagement. +function ARTY:_MarkTargetName(markerid) + return string.format("BATTERY=%s, Marked Target ID=%d", self.groupname, markerid) +end + +--- Create a name for a relocation move initiated by placing a marker. +-- @param #ARTY self +-- @param #number markerid ID of the placed marker. +-- @return #string Name of relocation move. +function ARTY:_MarkMoveName(markerid) + return string.format("BATTERY=%s, Marked Relocation ID=%d", self.groupname, markerid) +end + +--- Get the marker ID from the assigned task name. +-- @param #ARTY self +-- @param #string name Name of the assignment. +-- @return #string Name of the ARTY group or nil +-- @return #number ID of the marked target or nil. +-- @return #number ID of the marked relocation move or nil +function ARTY:_GetMarkIDfromName(name) + + -- keywords are split by "," + local keywords=self:_split(name, ",") + + local battery=nil + local markTID=nil + local markMID=nil + + for _,key in pairs(keywords) do + + local str=self:_split(key, "=") + local par=str[1] + local val=str[2] + + if par:find("BATTERY") then + battery=val + end + if par:find("Marked Target ID") then + markTID=tonumber(val) + end + if par:find("Marked Relocation ID") then + markMID=tonumber(val) + end + + end + + return battery, markTID, markMID +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Helper Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Sort targets with respect to priority and number of times it was already engaged. +-- @param #ARTY self +function ARTY:_SortTargetQueuePrio() + self:F2() + + -- Sort results table wrt times they have already been engaged. + local function _sort(a, b) + return (a.engaged < b.engaged) or (a.engaged==b.engaged and a.prio < b.prio) + end + table.sort(self.targets, _sort) + + -- Debug output. + self:T3(ARTY.id.."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))) + end +end + +--- Sort array with respect to time. Array elements must have a .time entry. +-- @param #ARTY self +-- @param #table queue Array to sort. Should have elemnt .time. +function ARTY:_SortQueueTime(queue) + self:F3({queue=queue}) + + -- Sort targets w.r.t attack time. + local function _sort(a, b) + if a.time == nil and b.time == nil then + return false + end + if a.time == nil then + return false + end + if b.time == nil then + return true + end + return a.time < b.time + end + table.sort(queue, _sort) + + -- Debug output. + self:T3(ARTY.id.."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)) + end + +end + +--- Heading from point a to point b in degrees. +--@param #ARTY self +--@param Core.Point#COORDINATE a Coordinate. +--@param Core.Point#COORDINATE b Coordinate. +--@return #number angle Angle from a to b in degrees. +function ARTY:_GetHeading(a, b) + local dx = b.x-a.x + local dy = b.z-a.z + local angle = math.deg(math.atan2(dy,dx)) + if angle < 0 then + angle = 360 + angle + end + return angle +end + +--- Check all targets whether they are in range. +-- @param #ARTY self +function ARTY:_CheckTargetsInRange() + + local targets2delete={} + + for i=1,#self.targets do + local _target=self.targets[i] + + self:T3(ARTY.id..string.format("Before: Target %s - in range = %s", _target.name, tostring(_target.inrange))) + + -- Check if target is in range. + local _inrange,_toofar,_tooclose,_remove=self:_TargetInRange(_target) + self:T3(ARTY.id..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(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) + + 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 + + end + + -- Update value. + _target.inrange=_inrange + + self:T3(ARTY.id..string.format("After: Target %s - in range = %s", _target.name, tostring(_target.inrange))) + end + end + + -- Remove targets not in range. + for _,targetname in pairs(targets2delete) do + self:RemoveTarget(targetname) + end + +end + +--- Check all normal (untimed) targets and return the target with the highest priority which has been engaged the fewest times. +-- @param #ARTY self +-- @return #table Target which is due to be attacked now or nil if no target could be found. +function ARTY:_CheckNormalTargets() + self:F3() + + -- Sort targets w.r.t. prio and number times engaged already. + self:_SortTargetQueuePrio() + + -- No target engagements if rearming! + if self:is("Rearming") then + return nil + end + + -- Loop over all sorted targets. + for i=1,#self.targets do + local _target=self.targets[i] + + -- Debug info. + self:T3(ARTY.id..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))) + + return _target + end + end + + return nil +end + +--- Check all timed targets and return the target which should be attacked next. +-- @param #ARTY self +-- @return #table Target which is due to be attacked now. +function ARTY:_CheckTimedTargets() + self:F3() + + -- Current time. + local Tnow=timer.getAbsTime() + + -- Sort Targets wrt time. + self:_SortQueueTime(self.targets) + + -- No target engagements if rearming! + if self:is("Rearming") then + return nil + end + + for i=1,#self.targets do + local _target=self.targets[i] + + -- Debug info. + self:T3(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. + 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))) + return _target + end + else + -- No current target. + self:T2(ARTY.id..string.format("Found TIMED target %s.", self:_TargetInfo(_target))) + return _target + end + end + + end + + return nil +end + +--- Check all moves and return the one which should be executed next. +-- @param #ARTY self +-- @return #table Move which is due. +function ARTY:_CheckMoves() + self:F3() + + -- Current time. + local Tnow=timer.getAbsTime() + + -- Sort Targets wrt time. + self:_SortQueueTime(self.moves) + + -- Check if we are currently firing. + local firing=false + if self.currentTarget then + firing=true + end + + -- Loop over all moves in queue. + for i=1,#self.moves do + + -- Shortcut. + local _move=self.moves[i] + + if string.find(_move.name, "REARMING MOVE") and ((self.currentMove and self.currentMove.name~=_move.name) or self.currentMove==nil) then + -- We got an rearming assignment which has priority. + return _move + elseif (Tnow >= _move.time) and (firing==false or _move.cancel) and (not self.currentMove) and (not self:is("Rearming")) then + -- Time for move is reached and maybe current target should be cancelled. + return _move + end + end + + return nil +end + +--- Check whether shooting started within a certain time (~5 min). If not, the current target is considered invalid and removed from the target list. +-- @param #ARTY self +function ARTY:_CheckShootingStarted() + self:F2() + + if self.currentTarget then + + -- Current time. + local Tnow=timer.getTime() + + -- Get name and id of target. + local name=self.currentTarget.name + + -- Time that passed after current target has been assigned. + local dt=Tnow-self.currentTarget.Tassigned + + -- Debug info + if self.Nshots==0 then + self:T(ARTY.id..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)) + + -- CeaseFire. + self:CeaseFire(self.currentTarget) + + -- Remove target from list. + self:RemoveTarget(name) + + end + end +end + +--- Get the index of a target by its name. +-- @param #ARTY self +-- @param #string name Name of target. +-- @return #number Arrayindex of target. +function ARTY:_GetTargetIndexByName(name) + self:F2(name) + + for i=1,#self.targets do + local targetname=self.targets[i].name + self:T3(ARTY.id..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)) + return i + end + end + + self:T2(ARTY.id..string.format("WARNING: Target with name %s could not be found. (This can happen.)", name)) + return nil +end + +--- Get the index of a move by its name. +-- @param #ARTY self +-- @param #string name Name of move. +-- @return #number Arrayindex of move. +function ARTY:_GetMoveIndexByName(name) + self:F2(name) + + for i=1,#self.moves do + local movename=self.moves[i].name + self:T3(ARTY.id..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)) + return i + end + end + + self:T2(ARTY.id..string.format("WARNING: Move with name %s could not be found. (This can happen.)", name)) + return nil +end + +--- Check if group is (partly) out of ammo of a special weapon type. +-- @param #ARTY self +-- @param #table targets Table of targets. +-- @return @boolean True if any target requests a weapon type that is empty. +function ARTY:_CheckOutOfAmmo(targets) + + -- Get current ammo. + local _nammo,_nshells,_nrockets,_nmissiles=self:GetAmmo() + + -- Special weapon type requested ==> Check if corresponding ammo is empty. + local _partlyoutofammo=false + + for _,Target in pairs(targets) do + + if Target.weapontype==ARTY.WeaponType.Auto and _nammo==0 then + + self:T(ARTY.id..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)) + _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)) + _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)) + _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)) + _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)) + _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)) + _partlyoutofammo=true + + end + + end + + return _partlyoutofammo +end + +--- Check if a selected weapon type is available for this target, i.e. if the current amount of ammo of this weapon type is currently available. +-- @param #ARTY self +-- @param #boolean target Target array data structure. +-- @return #number Amount of shells, rockets or missiles available of the weapon type selected for the target. +function ARTY:_CheckWeaponTypeAvailable(target) + + -- Get current ammo of group. + local Nammo, Nshells, Nrockets, Nmissiles=self:GetAmmo() + + -- Check if enough ammo is there for the selected weapon type. + local nfire=Nammo + if target.weapontype==ARTY.WeaponType.Auto then + nfire=Nammo + elseif target.weapontype==ARTY.WeaponType.Cannon then + nfire=Nshells + elseif target.weapontype==ARTY.WeaponType.TacticalNukes then + nfire=self.Nukes + elseif target.weapontype==ARTY.WeaponType.IlluminationShells then + nfire=self.Nillu + elseif target.weapontype==ARTY.WeaponType.SmokeShells then + nfire=self.Nsmoke + elseif target.weapontype==ARTY.WeaponType.Rockets then + nfire=Nrockets + elseif target.weapontype==ARTY.WeaponType.CruiseMissile then + nfire=Nmissiles + end + + return nfire +end +--- Check if a selected weapon type is in principle possible for this group. The current amount of ammo might be zero but the group still can be rearmed at a later point in time. +-- @param #ARTY self +-- @param #boolean target Target array data structure. +-- @return #boolean True if the group can carry this weapon type, false otherwise. +function ARTY:_CheckWeaponTypePossible(target) + + -- Check if enough ammo is there for the selected weapon type. + local possible=false + if target.weapontype==ARTY.WeaponType.Auto then + possible=self.Nammo0>0 + elseif target.weapontype==ARTY.WeaponType.Cannon then + possible=self.Nshells0>0 + elseif target.weapontype==ARTY.WeaponType.TacticalNukes then + possible=self.Nukes0>0 + elseif target.weapontype==ARTY.WeaponType.IlluminationShells then + possible=self.Nillu0>0 + elseif target.weapontype==ARTY.WeaponType.SmokeShells then + possible=self.Nsmoke0>0 + elseif target.weapontype==ARTY.WeaponType.Rockets then + possible=self.Nrockets0>0 + elseif target.weapontype==ARTY.WeaponType.CruiseMissile then + possible=self.Nmissiles0>0 + end + + return possible +end + +--- Check if a name is unique. If not, a new unique name can be created by adding a running index #01, #02, ... +-- @param #ARTY self +-- @param #table givennames Table with entries of already given names. Must contain a .name item. +-- @param #string name Name to check if it already exists in givennames table. +-- @param #boolean makeunique If true, a new unique name is returned by appending the running index. +-- @return #string Unique name, which is not already given for another target. +function ARTY:_CheckName(givennames, name, makeunique) + self:F2({givennames=givennames, name=name}) + + local newname=name + local counter=1 + local n=1 + local nmax=100 + if makeunique==nil then + makeunique=true + end + + repeat -- until a unique name is found. + + -- We assume the name is unique. + local _unique=true + + -- Loop over all targets already defined. + for _,_target in pairs(givennames) do + + -- Target name. + local _givenname=_target.name + + -- Name is already used by another target. + if _givenname==newname then + + -- Name is already used for another target ==> try again with new name. + _unique=false + + end + + -- Debug info. + self:T3(ARTY.id..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))) + + -- 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)) + return newname, true +end + +--- Check if target is in range. +-- @param #ARTY self +-- @param #table target Target table. +-- @param #boolean message (Optional) If true, send a message to the coalition if the target is not in range. Default is no message is send. +-- @return #boolean True if target is in range, false otherwise. +-- @return #boolean True if ARTY group is too far away from the target, i.e. distance > max firing range. +-- @return #boolean True if ARTY group is too close to the target, i.e. distance < min finring range. +-- @return #boolean True if target should be removed since ARTY group is immobile and not cargo. +function ARTY:_TargetInRange(target, message) + self:F3(target) + + -- Default is no message. + if message==nil then + message=false + end + + -- Distance between ARTY group and target. + self:E({controllable=self.Controllable, targetcoord=target.coord}) + local _dist=self.Controllable:GetCoordinate():Get2DDistance(target.coord) + + -- Assume we are in range. + local _inrange=true + local _tooclose=false + local _toofar=false + local text="" + + if _dist < self.minrange then + _inrange=false + _tooclose=true + text=string.format("%s, target is out of range. Distance of %.1f km is below min range of %.1f km.", self.alias, _dist/1000, self.minrange/1000) + elseif _dist > self.maxrange then + _inrange=false + _toofar=true + text=string.format("%s, target is out of range. Distance of %.1f km is greater than max range of %.1f km.", self.alias, _dist/1000, self.maxrange/1000) + end + + -- Debug output. + if not _inrange then + self:T(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. + local _remove=false + if not (self.ismobile or self.iscargo) and _inrange==false then + --self:RemoveTarget(target.name) + _remove=true + end + + return _inrange,_toofar,_tooclose,_remove +end + +--- Get the weapon type name, which should be used to attack the target. +-- @param #ARTY self +-- @param #number tnumber Number of weapon type ARTY.WeaponType.XXX +-- @return #number tnumber of weapon type. +function ARTY:_WeaponTypeName(tnumber) + self:F2(tnumber) + local name="unknown" + if tnumber==ARTY.WeaponType.Auto then + name="Auto" -- (Cannon, Rockets, Missiles) + elseif tnumber==ARTY.WeaponType.Cannon then + name="Cannons" + elseif tnumber==ARTY.WeaponType.Rockets then + name="Rockets" + elseif tnumber==ARTY.WeaponType.CruiseMissile then + name="Cruise Missiles" + elseif tnumber==ARTY.WeaponType.TacticalNukes then + name="Tactical Nukes" + elseif tnumber==ARTY.WeaponType.IlluminationShells then + name="Illumination Shells" + elseif tnumber==ARTY.WeaponType.SmokeShells then + name="Smoke Shells" + end + return name +end + +--- Find a random coordinate in the vicinity of another coordinate. +-- @param #ARTY self +-- @param Core.Point#COORDINATE coord Center coordinate. +-- @param #number rmin (Optional) Minimum distance in meters from center coordinate. Default 20 m. +-- @param #number rmax (Optional) Maximum distance in meters from center coordinate. Default 80 m. +-- @return Core.Point#COORDINATE Random coordinate in a certain distance from center coordinate. +function ARTY:_VicinityCoord(coord, rmin, rmax) + self:F2({coord=coord, rmin=rmin, rmax=rmax}) + -- Set default if necessary. + rmin=rmin or 20 + rmax=rmax or 80 + -- Random point withing range. + local vec2=coord:GetRandomVec2InRadius(rmax, rmin) + local pops=COORDINATE:NewFromVec2(vec2) + -- Debug info. + self:T3(ARTY.id..string.format("Vicinity distance = %d (rmin=%d, rmax=%d)", pops:Get2DDistance(coord), rmin, rmax)) + return pops +end + +--- Print event-from-to string to DCS log file. +-- @param #ARTY self +-- @param #string BA Before/after info. +-- @param #string Event Event. +-- @param #string From From state. +-- @param #string To To state. +function ARTY:_EventFromTo(BA, Event, From, To) + local text=string.format("%s: %s EVENT %s: %s --> %s", BA, self.groupname, Event, From, To) + self:T3(ARTY.id..text) +end + +--- Split string. C.f. http://stackoverflow.com/questions/1426954/split-string-in-lua +-- @param #ARTY self +-- @param #string str Sting to split. +-- @param #string sep Speparator for split. +-- @return #table Split text. +function ARTY:_split(str, sep) + self:F3({str=str, sep=sep}) + local result = {} + local regex = ("([^%s]+)"):format(sep) + for each in str:gmatch(regex) do + table.insert(result, each) + end + return result +end + +--- Returns the target parameters as formatted string. +-- @param #ARTY self +-- @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) +end + +--- Returns a formatted string with information about all move parameters. +-- @param #ARTY self +-- @param #table move Move table item. +-- @return #string Info string. +function ARTY:_MoveInfo(move) + self:F3(move) + local _clock=self:_SecondsToClock(move.time) + return string.format("%s: time=%s, speed=%d, onroad=%s, cancel=%s", move.name, _clock, move.speed, tostring(move.onroad), tostring(move.cancel)) +end + +--- Convert Latitude and Lontigude from DMS to DD. +-- @param #ARTY self +-- @param #string l1 Latitude or longitude as string in the format DD:MM:SS N/S/W/E +-- @param #string l2 Latitude or longitude as string in the format DD:MM:SS N/S/W/E +-- @return #number Latitude in decimal degree format. +-- @return #number Longitude in decimal degree format. +function ARTY:_LLDMS2DD(l1,l2) + self:F2(l1,l2) + + -- Make an array of lat and long. + local _latlong={l1,l2} + + local _latitude=nil + local _longitude=nil + + for _,ll in pairs(_latlong) do + + -- Format is expected as "DD:MM:SS" or "D:M:S". + local _format = "%d+:%d+:%d+" + local _ldms=ll:match(_format) + + if _ldms then + + -- Split DMS to degrees, minutes and seconds. + local _dms=self:_split(_ldms, ":") + local _deg=tonumber(_dms[1]) + local _min=tonumber(_dms[2]) + local _sec=tonumber(_dms[3]) + + -- Convert DMS to DD. + local function DMS2DD(d,m,s) + return d+m/60+s/3600 + end + + -- Detect with hemisphere is meant. + if ll:match("N") then + _latitude=DMS2DD(_deg,_min,_sec) + elseif ll:match("S") then + _latitude=-DMS2DD(_deg,_min,_sec) + elseif ll:match("W") then + _longitude=-DMS2DD(_deg,_min,_sec) + elseif ll:match("E") then + _longitude=DMS2DD(_deg,_min,_sec) + end + + -- Debug text. + local text=string.format("DMS %02d Deg %02d min %02d sec",_deg,_min,_sec) + self:T2(ARTY.id..text) + + 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) + + return _latitude,_longitude +end + +--- Convert time in seconds to hours, minutes and seconds. +-- @param #ARTY self +-- @param #number seconds Time in seconds. +-- @return #string Time in format Hours:minutes:seconds. +function ARTY:_SecondsToClock(seconds) + self:F3({seconds=seconds}) + + if seconds==nil then + return nil + end + + -- Seconds + local seconds = tonumber(seconds) + + -- Seconds of this day. + local _seconds=seconds%(60*60*24) + + if seconds <= 0 then + return nil + else + local hours = string.format("%02.f", math.floor(_seconds/3600)) + local mins = string.format("%02.f", math.floor(_seconds/60 - (hours*60))) + local secs = string.format("%02.f", math.floor(_seconds - hours*3600 - mins *60)) + local days = string.format("%d", seconds/(60*60*24)) + return hours..":"..mins..":"..secs.."+"..days + end +end + +--- Convert clock time from hours, minutes and seconds to seconds. +-- @param #ARTY self +-- @param #string clock String of clock time. E.g., "06:12:35". +function ARTY:_ClockToSeconds(clock) + self:F3({clock=clock}) + + if clock==nil then + return nil + end + + -- Seconds init. + local seconds=0 + + -- Split additional days. + local dsplit=self:_split(clock, "+") + + -- Convert days to seconds. + if #dsplit>1 then + seconds=seconds+tonumber(dsplit[2])*60*60*24 + end + + -- Split hours, minutes, seconds + local tsplit=self:_split(dsplit[1], ":") + + -- Get time in seconds + local i=1 + for _,time in ipairs(tsplit) do + if i==1 then + -- Hours + seconds=seconds+tonumber(time)*60*60 + elseif i==2 then + -- Minutes + seconds=seconds+tonumber(time)*60 + elseif i==3 then + -- Seconds + seconds=seconds+tonumber(time) + end + i=i+1 + end + + self:T3(ARTY.id..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 016bb10ef..daed34b02 100644 --- a/Moose Development/Moose/Functional/CleanUp.lua +++ b/Moose Development/Moose/Functional/CleanUp.lua @@ -1,26 +1,23 @@ ---- **Functional** -- The CLEANUP_AIRBASE class keeps an area clean of crashing or colliding airplanes. It also prevents airplanes from firing within this area. +--- **Functional** -- Keep airbases clean of crashing or colliding airplanes, and kill missiles when being fired at airbases. -- -- === -- --- ### Author: **FlightControl** --- ### Contributions: +-- ## 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. -- -- === -- --- @module CleanUp - ---- @type CLEANUP_AIRBASE.__ Methods which are not intended for mission designers, but which are used interally by the moose designer :-) --- @field #map<#string,Wrapper.Airbase#AIRBASE> Airbases Map of Airbases. --- @extends Core.Base#BASE - ---- @type CLEANUP_AIRBASE --- @extends #CLEANUP_AIRBASE.__ - ---- # CLEANUP_AIRBASE, extends @{Base#BASE} +-- ## Missions: -- --- ![Banner Image](..\Presentations\CLEANUP_AIRBASE\Dia1.JPG) +-- [CLA - CleanUp Airbase](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/CLA%20-%20CleanUp%20Airbase) +-- +-- === -- --- The CLEANUP_AIRBASE class keeps airbases clean, and tries to guarantee continuous airbase operations, even under combat. -- 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. @@ -45,7 +42,26 @@ -- -- By following the above guidelines, you can add airbase cleanup with acceptable CPU overhead. -- --- ## 1. CLEANUP_AIRBASE Constructor +-- === +-- +-- ### Author: **FlightControl** +-- ### Contributions: +-- +-- === +-- +-- @module Functional.CleanUp +-- @image CleanUp_Airbases.JPG + +--- @type CLEANUP_AIRBASE.__ Methods which are not intended for mission designers, but which are used interally by the moose designer :-) +-- @field #map<#string,Wrapper.Airbase#AIRBASE> Airbases Map of Airbases. +-- @extends Core.Base#BASE + +--- @type CLEANUP_AIRBASE +-- @extends #CLEANUP_AIRBASE.__ + +--- Keeps airbases clean, and tries to guarantee continuous airbase operations, even under combat. +-- +-- # 1. CLEANUP_AIRBASE Constructor -- -- Creates the main object which is preventing the airbase to get polluted with debris on the runway, which halts the airbase. -- @@ -56,12 +72,12 @@ -- CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi ) -- CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi ) -- --- ## 2. Add or Remove airbases +-- # 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. +-- # 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. @@ -114,6 +130,20 @@ 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 + if self:IsInAirbase( Unit:GetVec2() ) then + self:F( { UnitName = UnitName } ) + self.CleanUpList[UnitName] = {} + self.CleanUpList[UnitName].CleanUpUnit = Unit + self.CleanUpList[UnitName].CleanUpGroup = Unit:GetGroup() + self.CleanUpList[UnitName].CleanUpGroupName = Unit:GetGroup():GetName() + self.CleanUpList[UnitName].CleanUpUnitName = Unit:GetName() + end + end + end return self end @@ -173,7 +203,7 @@ end ---- Destroys a @{Unit} from the simulator, but checks first if it is still existing! +--- Destroys a @{Wrapper.Unit} from the simulator, but checks first if it is still existing! -- @param #CLEANUP_AIRBASE self -- @param Wrapper.Unit#UNIT CleanUpUnit The object to be destroyed. function CLEANUP_AIRBASE.__:DestroyUnit( CleanUpUnit ) @@ -182,7 +212,7 @@ function CLEANUP_AIRBASE.__:DestroyUnit( CleanUpUnit ) if CleanUpUnit then local CleanUpUnitName = CleanUpUnit:GetName() local CleanUpGroup = CleanUpUnit:GetGroup() - -- TODO Client bug in 1.5.3 + -- TODO DCS BUG - Client bug in 1.5.3 if CleanUpGroup:IsAlive() then local CleanUpGroupUnits = CleanUpGroup:GetUnits() if #CleanUpGroupUnits == 1 then @@ -200,7 +230,7 @@ end --- Destroys a missile from the simulator, but checks first if it is still existing! -- @param #CLEANUP_AIRBASE self --- @param Dcs.DCSTypes#Weapon MissileObject +-- @param DCS#Weapon MissileObject function CLEANUP_AIRBASE.__:DestroyMissile( MissileObject ) self:F( { MissileObject } ) @@ -215,11 +245,15 @@ end function CLEANUP_AIRBASE.__:OnEventBirth( EventData ) self:F( { EventData } ) - self.CleanUpList[EventData.IniDCSUnitName] = {} - self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnit = EventData.IniUnit - self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroup = EventData.IniGroup - self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroupName = EventData.IniDCSGroupName - self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnitName = EventData.IniDCSUnitName + if EventData.IniUnit:IsAlive() ~= nil then + if self:IsInAirbase( EventData.IniUnit:GetVec2() ) then + self.CleanUpList[EventData.IniDCSUnitName] = {} + self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnit = EventData.IniUnit + self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroup = EventData.IniGroup + self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroupName = EventData.IniDCSGroupName + self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnitName = EventData.IniDCSUnitName + end + end end @@ -231,7 +265,7 @@ end function CLEANUP_AIRBASE.__:OnEventCrash( Event ) self:F( { Event } ) - --TODO: This stuff is not working due to a DCS bug. Burning units cannot be destroyed. + --TODO: DCS BUG - This stuff is not working due to a DCS bug. Burning units cannot be destroyed. -- self:T("before getGroup") -- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired -- self:T("after getGroup") @@ -290,9 +324,9 @@ function CLEANUP_AIRBASE.__:OnEventHit( Event ) end end ---- Add the @{DCSWrapper.Unit#Unit} to the CleanUpList for CleanUp. +--- Add the @{DCS#Unit} to the CleanUpList for CleanUp. -- @param #CLEANUP_AIRBASE self --- @param Wrapper.Unit#UNIT CleanUpUnit +-- @param DCS#UNIT CleanUpUnit -- @oaram #string CleanUpUnitName function CLEANUP_AIRBASE.__:AddForCleanUp( CleanUpUnit, CleanUpUnitName ) self:F( { CleanUpUnit, CleanUpUnitName } ) @@ -351,45 +385,50 @@ function CLEANUP_AIRBASE.__:CleanUpSchedule() local CleanUpGroupName = CleanUpListData.CleanUpGroupName if CleanUpUnit:IsAlive() ~= nil then + + if self:IsInAirbase( CleanUpUnit:GetVec2() ) then - if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then - - local CleanUpCoordinate = CleanUpUnit:GetCoordinate() - - self:T( { "CleanUp Scheduler", CleanUpUnitName } ) - if CleanUpUnit:GetLife() <= CleanUpUnit:GetLife0() * 0.95 then - if CleanUpUnit:IsAboveRunway() then - if CleanUpUnit:InAir() then - - local CleanUpLandHeight = CleanUpCoordinate:GetLandHeight() - local CleanUpUnitHeight = CleanUpCoordinate.y - CleanUpLandHeight - - if CleanUpUnitHeight < 100 then - self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because below safe height and damaged." } ) - self:DestroyUnit( CleanUpUnit ) - end - else - self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because on runway and damaged." } ) - self:DestroyUnit( CleanUpUnit ) - end - end - end - -- Clean Units which are waiting for a very long time in the CleanUpZone. - if CleanUpUnit then - local CleanUpUnitVelocity = CleanUpUnit:GetVelocityKMH() - if CleanUpUnitVelocity < 1 then - if CleanUpListData.CleanUpMoved then - if CleanUpListData.CleanUpTime + 180 <= timer.getTime() then - self:T( { "CleanUp Scheduler", "Destroy due to not moving anymore " .. CleanUpUnitName } ) - self:DestroyUnit( CleanUpUnit ) - end - end - else - CleanUpListData.CleanUpTime = timer.getTime() - CleanUpListData.CleanUpMoved = true - end - end - + if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then + + local CleanUpCoordinate = CleanUpUnit:GetCoordinate() + + self:T( { "CleanUp Scheduler", CleanUpUnitName } ) + if CleanUpUnit:GetLife() <= CleanUpUnit:GetLife0() * 0.95 then + if CleanUpUnit:IsAboveRunway() then + if CleanUpUnit:InAir() then + + local CleanUpLandHeight = CleanUpCoordinate:GetLandHeight() + local CleanUpUnitHeight = CleanUpCoordinate.y - CleanUpLandHeight + + if CleanUpUnitHeight < 100 then + self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because below safe height and damaged." } ) + self:DestroyUnit( CleanUpUnit ) + end + else + self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because on runway and damaged." } ) + self:DestroyUnit( CleanUpUnit ) + end + end + end + -- Clean Units which are waiting for a very long time in the CleanUpZone. + if CleanUpUnit and not CleanUpUnit:GetPlayerName() then + local CleanUpUnitVelocity = CleanUpUnit:GetVelocityKMH() + if CleanUpUnitVelocity < 1 then + if CleanUpListData.CleanUpMoved then + if CleanUpListData.CleanUpTime + 180 <= timer.getTime() then + self:T( { "CleanUp Scheduler", "Destroy due to not moving anymore " .. CleanUpUnitName } ) + self:DestroyUnit( CleanUpUnit ) + end + end + else + CleanUpListData.CleanUpTime = timer.getTime() + CleanUpListData.CleanUpMoved = true + end + end + else + -- not anymore in an airbase zone, remove from cleanup list. + self.CleanUpList[CleanUpUnitName] = nil + end else -- Do nothing ... self.CleanUpList[CleanUpUnitName] = nil diff --git a/Moose Development/Moose/Functional/Designate.lua b/Moose Development/Moose/Functional/Designate.lua index b2e75c915..b8b1c24bb 100644 --- a/Moose Development/Moose/Functional/Designate.lua +++ b/Moose Development/Moose/Functional/Designate.lua @@ -2,198 +2,205 @@ -- -- === -- --- DESIGNATE is orchestrating the designation of potential targets executed by a Recce group, --- and communicates these to a dedicated attacking group of players, --- so that following a dynamically generated menu system, --- each detected set of potential targets can be lased or smoked... +-- ## Features: -- --- Targets can be: +-- * Faciliate the communication of detected targets to players. +-- * Designate targets using lasers, through a menu system. +-- * Designate targets using smoking, through a menu system. +-- * Designate targets using illumination, through a menu system. +-- * Auto lase targets. +-- * Refresh detection upon specified time intervals. +-- * Prioritization on threat levels. +-- * Reporting system of threats. +-- +-- === +-- +-- ## Missions: +-- +-- [DES - Designation](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/DES%20-%20Designation) +-- +-- === +-- +-- Targets detected by recce will be communicated to a group of attacking players. +-- A menu system is made available that allows to: -- -- * **Lased** for a period of time. -- * **Smoked**. Artillery or airplanes with Illuminatino ordonance need to be present. (WIP, but early demo ready.) -- * **Illuminated** through an illumination bomb. Artillery or airplanes with Illuminatino ordonance need to be present. (WIP, but early demo ready. -- --- === +-- The following terminology is being used throughout this document: -- --- # **AUTHORS and CONTRIBUTIONS** +-- * The **DesignateObject** is the object of the DESIGNATE class, which is this class explained in the document. +-- * The **DetectionObject** is the object of a DETECTION_ class (DETECTION_TYPES, DETECTION_AREAS, DETECTION_UNITS), which is executing the detection and grouping of Targets into _DetectionItems_. +-- * **TargetGroups** is the list of detected target groupings by the _DetectionObject_. Each _TargetGroup_ contains a _TargetSet_. +-- * **TargetGroup** is one element of the __TargetGroups__ list, and contains a _TargetSet_. +-- * The **TargetSet** is a SET_UNITS collection of _Targets_, that have been detected by the _DetectionObject_. +-- * A **Target** is a detected UNIT object by the _DetectionObject_. +-- * A **Threat Level** is a number from 0 to 10 that is calculated based on the threat of the Target in an Air to Ground battle scenario. +-- * The **RecceSet** is a SET_GROUP collection that contains the **RecceGroups**. +-- * A **RecceGroup** is a GROUP object containing the **Recces**. +-- * A **Recce** is a UNIT object executing the reconnaissance as part the _DetectionObject_. A Recce can be of any UNIT type. +-- * An **AttackGroup** is a GROUP object that contain _Players_. +-- * A **Player** is an active CLIENT object containing a human player. +-- * A **Designate Menu** is the menu that is dynamically created during the designation process for each _AttackGroup_. +-- +-- # Player Manual +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia3.JPG) +-- +-- A typical mission setup would require Recce (a @{Set} of Recce) to be detecting potential targets. +-- The DetectionObject will group the detected targets based on the detection method being used. +-- Possible detection methods could be by Area, by Type or by Unit. +-- Each grouping will result in a **TargetGroup**, for terminology and clarity we will use this term throughout the document. +-- +-- **Recce** require to have Line of Sight (LOS) towards the targets. +-- The **Recce** will report any detected targets to the Players (on the picture Observers). +-- When targets are detected, a menu will be made available that allows those **TargetGroups** to be designated. +-- Designation can be done by Lasing, Smoking and Illumination. +-- Smoking is useful during the day, while illumination is recommended to be used during the night. +-- Smoking can designate specific targets, but not very precise, while lasing is very accurate and allows to +-- players to attack the targets using laser guided bombs or rockets. +-- Illumination will lighten up the Target Area. +-- +-- **Recce** can be ground based or airborne. Airborne **Recce** (AFAC) can be really useful to designate a large amount of targets +-- in a wide open area, as airborne **Recce** has a large LOS. +-- However, ground based **Recce** are very useful to smoke or illuminate targets, as they can be much closer +-- to the Target Area. +-- +-- It is recommended to make the **Recce** invisible and immortal using the Mission Editor in DCS World. +-- This will ensure that the detection process won't be interrupted and that targets can be designated. +-- However, you don't have to, so to simulate a more real-word situation or simulation, **Recce can also be destroyed**! +-- +-- ## 1. Player View (Observer) +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia4.JPG) +-- +-- The RecceSet is continuously detecting for potential Targets, +-- executing its task as part of the DetectionObject. +-- Once Targets have been detected, the DesignateObject will trigger the **Detect Event**. +-- +-- In order to prevent an overflow in the DesignateObject of detected targets, +-- there is a maximum amount of TargetGroups +-- that can be put in **scope** of the DesignateObject. +-- We call this the **MaximumDesignations** term. +-- +-- ## 2. Designate Menu +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia5.JPG) +-- +-- For each detected TargetGroup, there is: +-- +-- * A **Designate Menu** are created and continuously refreshed, containing the **DesignationID** and the **Designation Status**. +-- * The RecceGroups are reporting to each AttackGroup, sending **Messages** containing the Threat Level and the TargetSet composition. +-- +-- A Player can then select an action from the **Designate Menu**. +-- The Designation Status is shown between the ( ). +-- +-- It indicates for each TargetGroup the current active designation action applied: +-- +-- * An "I" for Illumnation designation. +-- * An "S" for Smoking designation. +-- * An "L" for Lasing designation. +-- +-- Note that multiple designation methods can be active at the same time! +-- Note the **Auto Lase** option. When switched on, the available **Recce** will lase +-- Targets when detected. +-- +-- Targets are designated per **Threat Level**. +-- The most threatening targets from an Air to Ground perspective, are designated first! +-- This is for all designation methods. +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia6.JPG) +-- +-- Each Designate Menu has a sub menu structure, which allows specific actions to be triggered: +-- +-- * Lase Targets using a specific laser code. +-- * Smoke Targets using a specific smoke color. +-- * Illuminate areas. +-- +-- ## 3. Lasing Targets +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia7.JPG) +-- +-- Lasing targets is done as expected. Each available Recce can lase only ONE target through! +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia8.JPG) +-- +-- Lasing can be done for specific laser codes. The Su-25T requires laser code 1113, while the A-10A requires laser code 1680. +-- For those, specific menu options can be made available for players to lase with these codes. +-- Auto Lase (as explained above), will ensure continuous lasing of available targets. +-- The status report shows which targets are being designated. +-- +-- The following logic is executed when a TargetGroup is selected to be *lased* from the Designation Menu: +-- +-- * The RecceSet is searched for any Recce that is within *designation distance* from a Target in the TargetGroup that is currently not being designated. +-- * If there is a Recce found that is currently no designating a target, and is within designation distance from the Target, then that Target will be designated. +-- * During designation, any Recce that does not have Line of Sight (LOS) and is not within disignation distance from the Target, will stop designating the Target, and a report is given. +-- * When a Recce is designating a Target, and that Target is destroyed, then the Recce will stop designating the Target, and will report the event. +-- * When a Recce is designating a Target, and that Recce is destroyed, then the Recce will be removed from the RecceSet and designation will stop without reporting. +-- * When all RecceGroups are destroyed from the RecceSet, then the DesignationObject will stop functioning, and nothing will be reported. +-- +-- In this way, DESIGNATE assists players to designate ground targets for a coordinated attack! +-- +-- ## 4. Illuminating Targets +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia9.JPG) +-- +-- Illumination bombs are fired between 500 and 700 meters altitude and will burn about 2 minutes, while slowly decending. +-- Each available recce within range will fire an illumination bomb. +-- Illumination bombs can be fired in while lasing targets. +-- When illumination bombs are fired, it will take about 2 minutes until a sequent bomb run can be requested using the menus. +-- +-- ## 5. Smoking Targets +-- +-- ![Banner Image](..\Presentations\DESIGNATE\Dia10.JPG) +-- +-- Smoke will fire for 5 minutes. +-- Each available recce within range will smoke a target. +-- Smoking can be requested while lasing targets. +-- Smoke will appear "around" the targets, because of accuracy limitations. +-- +-- +-- Have FUN! +-- +-- === -- -- ### Contributions: -- -- * [**Ciribob**](https://forums.eagle.ru/member.php?u=112175): Showing the way how to lase targets + how laser codes work!!! Explained the autolase script. -- * [**EasyEB**](https://forums.eagle.ru/member.php?u=112055): Ideas and Beta Testing -- * [**Wingthor**](https://forums.eagle.ru/member.php?u=123698): Beta Testing --- -- -- ### Authors: -- -- * **FlightControl**: Design & Programming -- --- @module Designate - +-- === +-- +-- @module Functional.Designate +-- @image Designation.JPG do -- DESIGNATE --- @type DESIGNATE -- @extends Core.Fsm#FSM_PROCESS - --- # DESIGNATE class, extends @{Fsm#FSM} - -- - -- DESIGNATE is managing the designation of detected targets. - -- Targets detected by recce will be communicated to a group of attacking players. - -- A menu system is made available that allows to: - -- - -- * **Lased** for a period of time. - -- * **Smoked**. Artillery or airplanes with Illuminatino ordonance need to be present. (WIP, but early demo ready.) - -- * **Illuminated** through an illumination bomb. Artillery or airplanes with Illuminatino ordonance need to be present. (WIP, but early demo ready. - -- - -- The following terminology is being used throughout this document: - -- - -- * The **DesignateObject** is the object of the DESIGNATE class, which is this class explained in the document. - -- * The **DetectionObject** is the object of a DETECTION_ class (DETECTION_TYPES, DETECTION_AREAS, DETECTION_UNITS), which is executing the detection and grouping of Targets into _DetectionItems_. - -- * **TargetGroups** is the list of detected target groupings by the _DetectionObject_. Each _TargetGroup_ contains a _TargetSet_. - -- * **TargetGroup** is one element of the __TargetGroups__ list, and contains a _TargetSet_. - -- * The **TargetSet** is a SET_UNITS collection of _Targets_, that have been detected by the _DetectionObject_. - -- * A **Target** is a detected UNIT object by the _DetectionObject_. - -- * A **Threat Level** is a number from 0 to 10 that is calculated based on the threat of the Target in an Air to Ground battle scenario. - -- * The **RecceSet** is a SET_GROUP collection that contains the **RecceGroups**. - -- * A **RecceGroup** is a GROUP object containing the **Recces**. - -- * A **Recce** is a UNIT object executing the reconnaissance as part the _DetectionObject_. A Recce can be of any UNIT type. - -- * An **AttackGroup** is a GROUP object that contain _Players_. - -- * A **Player** is an active CLIENT object containing a human player. - -- * A **Designate Menu** is the menu that is dynamically created during the designation process for each _AttackGroup_. - -- - -- ## 0. Player Manual - -- - -- ![Banner Image](..\Presentations\DESIGNATE\Dia3.JPG) - -- - -- A typical mission setup would require Recce (a @{Set} of Recce) to be detecting potential targets. - -- The DetectionObject will group the detected targets based on the detection method being used. - -- Possible detection methods could be by Area, by Type or by Unit. - -- Each grouping will result in a **TargetGroup**, for terminology and clarity we will use this term throughout the document. - -- - -- **Recce** require to have Line of Sight (LOS) towards the targets. - -- The **Recce** will report any detected targets to the Players (on the picture Observers). - -- When targets are detected, a menu will be made available that allows those **TargetGroups** to be designated. - -- Designation can be done by Lasing, Smoking and Illumination. - -- Smoking is useful during the day, while illumination is recommended to be used during the night. - -- Smoking can designate specific targets, but not very precise, while lasing is very accurate and allows to - -- players to attack the targets using laser guided bombs or rockets. - -- Illumination will lighten up the Target Area. - -- - -- **Recce** can be ground based or airborne. Airborne **Recce** (AFAC) can be really useful to designate a large amount of targets - -- in a wide open area, as airborne **Recce** has a large LOS. - -- However, ground based **Recce** are very useful to smoke or illuminate targets, as they can be much closer - -- to the Target Area. - -- - -- It is recommended to make the **Recce** invisible and immortal using the Mission Editor in DCS World. - -- This will ensure that the detection process won't be interrupted and that targets can be designated. - -- However, you don't have to, so to simulate a more real-word situation or simulation, **Recce can also be destroyed**! - -- - -- ### 0.1. Player View (Observer) - -- - -- ![Banner Image](..\Presentations\DESIGNATE\Dia4.JPG) - -- - -- The RecceSet is continuously detecting for potential Targets, - -- executing its task as part of the DetectionObject. - -- Once Targets have been detected, the DesignateObject will trigger the **Detect Event**. - -- - -- In order to prevent an overflow in the DesignateObject of detected targets, - -- there is a maximum amount of TargetGroups - -- that can be put in **scope** of the DesignateObject. - -- We call this the **MaximumDesignations** term. - -- - -- ### 0.2. Designate Menu - -- - -- ![Banner Image](..\Presentations\DESIGNATE\Dia5.JPG) - -- - -- For each detected TargetGroup, there is: - -- - -- * A **Designate Menu** are created and continuously refreshed, containing the **DesignationID** and the **Designation Status**. - -- * The RecceGroups are reporting to each AttackGroup, sending **Messages** containing the Threat Level and the TargetSet composition. - -- - -- A Player can then select an action from the **Designate Menu**. - -- The Designation Status is shown between the ( ). - -- - -- It indicates for each TargetGroup the current active designation action applied: - -- - -- * An "I" for Illumnation designation. - -- * An "S" for Smoking designation. - -- * An "L" for Lasing designation. - -- - -- Note that multiple designation methods can be active at the same time! - -- Note the **Auto Lase** option. When switched on, the available **Recce** will lase - -- Targets when detected. - -- - -- Targets are designated per **Threat Level**. - -- The most threatening targets from an Air to Ground perspective, are designated first! - -- This is for all designation methods. - -- - -- ![Banner Image](..\Presentations\DESIGNATE\Dia6.JPG) - -- - -- Each Designate Menu has a sub menu structure, which allows specific actions to be triggered: - -- - -- * Lase Targets using a specific laser code. - -- * Smoke Targets using a specific smoke color. - -- * Illuminate areas. - -- - -- ### 0.3. Lasing Targets - -- - -- ![Banner Image](..\Presentations\DESIGNATE\Dia7.JPG) - -- - -- Lasing targets is done as expected. Each available Recce can lase only ONE target through! - -- - -- ![Banner Image](..\Presentations\DESIGNATE\Dia8.JPG) - -- - -- Lasing can be done for specific laser codes. The Su-25T requires laser code 1113, while the A-10A requires laser code 1680. - -- For those, specific menu options can be made available for players to lase with these codes. - -- Auto Lase (as explained above), will ensure continuous lasing of available targets. - -- The status report shows which targets are being designated. - -- - -- The following logic is executed when a TargetGroup is selected to be *lased* from the Designation Menu: - -- - -- * The RecceSet is searched for any Recce that is within *designation distance* from a Target in the TargetGroup that is currently not being designated. - -- * If there is a Recce found that is currently no designating a target, and is within designation distance from the Target, then that Target will be designated. - -- * During designation, any Recce that does not have Line of Sight (LOS) and is not within disignation distance from the Target, will stop designating the Target, and a report is given. - -- * When a Recce is designating a Target, and that Target is destroyed, then the Recce will stop designating the Target, and will report the event. - -- * When a Recce is designating a Target, and that Recce is destroyed, then the Recce will be removed from the RecceSet and designation will stop without reporting. - -- * When all RecceGroups are destroyed from the RecceSet, then the DesignationObject will stop functioning, and nothing will be reported. - -- - -- In this way, DESIGNATE assists players to designate ground targets for a coordinated attack! - -- - -- ### 0.4. Illuminating Targets - -- - -- ![Banner Image](..\Presentations\DESIGNATE\Dia9.JPG) - -- - -- Illumination bombs are fired between 500 and 700 meters altitude and will burn about 2 minutes, while slowly decending. - -- Each available recce within range will fire an illumination bomb. - -- Illumination bombs can be fired in while lasing targets. - -- When illumination bombs are fired, it will take about 2 minutes until a sequent bomb run can be requested using the menus. - -- - -- ### 0.5. Smoking Targets - -- - -- ![Banner Image](..\Presentations\DESIGNATE\Dia10.JPG) - -- - -- Smoke will fire for 5 minutes. - -- Each available recce within range will smoke a target. - -- Smoking can be requested while lasing targets. - -- Smoke will appear “around” the targets, because of accuracy limitations. + --- Manage the designation of detected targets. -- -- - -- Have FUN! - -- - -- ## 1. DESIGNATE constructor + -- # 1. DESIGNATE constructor -- -- * @{#DESIGNATE.New}(): Creates a new DESIGNATE object. -- - -- ## 2. DESIGNATE is a FSM + -- # 2. DESIGNATE is a FSM -- -- Designate is a finite state machine, which allows for controlled transitions of states. -- - -- ### 2.1 DESIGNATE States + -- ## 2.1 DESIGNATE States -- -- * **Designating** ( Group ): The designation process. -- - -- ### 2.2 DESIGNATE Events + -- ## 2.2 DESIGNATE Events -- -- * **@{#DESIGNATE.Detect}**: Detect targets. -- * **@{#DESIGNATE.LaseOn}**: Lase the targets with the specified Index. @@ -201,7 +208,7 @@ do -- DESIGNATE -- * **@{#DESIGNATE.Smoke}**: Smoke the targets with the specified Index. -- * **@{#DESIGNATE.Status}**: Report designation status. -- - -- ## 3. Maximum Designations + -- # 3. Maximum Designations -- -- In order to prevent an overflow of designations due to many Detected Targets, there is a -- Maximum Designations scope that is set in the DesignationObject. @@ -209,9 +216,9 @@ do -- DESIGNATE -- The method @{#DESIGNATE.SetMaximumDesignations}() will put a limit on the amount of designations put in scope of the DesignationObject. -- Using the menu system, the player can "forget" a designation, so that gradually a new designation can be put in scope when detected. -- - -- ## 4. Laser codes + -- # 4. Laser codes -- - -- ### 4.1. Set possible laser codes + -- ## 4.1. Set possible laser codes -- -- An array of laser codes can be provided, that will be used by the DESIGNATE when lasing. -- The laser code is communicated by the Recce when it is lasing a larget. @@ -229,11 +236,11 @@ do -- DESIGNATE -- -- The above sets a collection of possible laser codes that can be assigned. **Note the { } notation!** -- - -- ### 4.2. Auto generate laser codes + -- ## 4.2. Auto generate laser codes -- -- Use the method @{#DESIGNATE.GenerateLaserCodes}() to generate all possible laser codes. Logic implemented and advised by Ciribob! -- - -- ### 4.3. Add specific lase codes to the lase menu + -- ## 4.3. Add specific lase codes to the lase menu -- -- Certain plane types can only drop laser guided ordonnance when targets are lased with specific laser codes. -- The SU-25T needs targets to be lased using laser code 1113. @@ -242,7 +249,7 @@ do -- DESIGNATE -- The method @{#DESIGNATE.AddMenuLaserCode}() to allow a player to lase a target using a specific laser code. -- Remove such a lase menu option using @{#DESIGNATE.RemoveMenuLaserCode}(). -- - -- ## 5. Autolase to automatically lase detected targets. + -- # 5. Autolase to automatically lase detected targets. -- -- DetectionItems can be auto lased once detected by Recces. As such, there is almost no action required from the Players using the Designate Menu. -- The **auto lase** function can be activated through the Designation Menu. @@ -253,7 +260,7 @@ do -- DESIGNATE -- -- Activate the auto lasing. -- - -- ## 6. Target prioritization on threat level + -- # 6. Target prioritization on threat level -- -- Targets can be detected of different types in one DetectionItem. Depending on the type of the Target, a different threat level applies in an Air to Ground combat context. -- SAMs are of a higher threat than normal tanks. So, if the Target type was recognized, the Recces will select those targets that form the biggest threat first, @@ -266,12 +273,12 @@ do -- DESIGNATE -- -- The example will activate the threat level prioritization for this the Designate object. Threats will be marked based on the threat level of the Target. -- - -- ## 6. Designate Menu Location for a Mission + -- # 7. Designate Menu Location for a Mission -- - -- You can make DESIGNATE work for a @{Mission#MISSION} object. In this way, the designate menu will not appear in the root of the radio menu, but in the menu of the Mission. + -- You can make DESIGNATE work for a @{Tasking.Mission#MISSION} object. In this way, the designate menu will not appear in the root of the radio menu, but in the menu of the Mission. -- Use the method @{#DESIGNATE.SetMission}() to set the @{Mission} object for the designate function. -- - -- ## 7. Status Report + -- # 8. Status Report -- -- A status report is available that displays the current Targets detected, grouped per DetectionItem, and a list of which Targets are currently being marked. -- @@ -279,9 +286,9 @@ do -- DESIGNATE -- * The status report can be automatically flashed by selecting "Status" -> "Flash Status On". -- * The automatic flashing of the status report can be deactivated by selecting "Status" -> "Flash Status Off". -- * The flashing of the status menu is disabled by default. - -- * The method @{#DESIGNATE.FlashStatusMenu}() can be used to enable or disable to flashing of the status menu. + -- * The method @{#DESIGNATE.SetFlashStatusMenu}() can be used to enable or disable to flashing of the status menu. -- - -- Designate:FlashStatusMenu( true ) + -- Designate:SetFlashStatusMenu( true ) -- -- The example will activate the flashing of the status menu for this Designate object. -- @@ -467,7 +474,7 @@ do -- DESIGNATE self.Designating = {} self:SetDesignateName() - self.LaseDuration = 60 + self:SetLaseDuration() -- Default is 120 seconds. self:SetFlashStatusMenu( false ) self:SetFlashDetectionMessages( true ) @@ -670,6 +677,14 @@ do -- DESIGNATE return self end + --- Set the lase duration for designations. + -- @param #DESIGNATE self + -- @param #number LaseDuration The time in seconds a lase will continue to hold on target. The default is 120 seconds. + -- @return #DESIGNATE + function DESIGNATE:SetLaseDuration( LaseDuration ) + self.LaseDuration = LaseDuration or 120 + return self + end --- Generate an array of possible laser codes. -- Each new lase will select a code from this table. @@ -793,16 +808,16 @@ do -- DESIGNATE -- @return #DESIGNATE function DESIGNATE:DesignationScope() - local DetectedItems = self.Detection:GetDetectedItems() + local DetectedItems = self.Detection:GetDetectedItemsByIndex() local DetectedItemCount = 0 for DesignateIndex, Designating in pairs( self.Designating ) do - local DetectedItem = DetectedItems[DesignateIndex] + local DetectedItem = self.Detection:GetDetectedItemByIndex( DesignateIndex ) if DetectedItem then -- Check LOS... local IsDetected = self.Detection:IsDetectedItemDetected( DetectedItem ) - self:F({IsDetected = IsDetected, DetectedItem }) + self:F({IsDetected = IsDetected }) if IsDetected == false then self:F("Removing") -- This Detection is obsolete, remove from the designate scope @@ -861,7 +876,7 @@ do -- DESIGNATE -- @return #DESIGNATE function DESIGNATE:CoordinateLase() - local DetectedItems = self.Detection:GetDetectedItems() + local DetectedItems = self.Detection:GetDetectedItemsByIndex() for DesignateIndex, Designating in pairs( self.Designating ) do local DetectedItem = DetectedItems[DesignateIndex] @@ -891,7 +906,7 @@ do -- DESIGNATE if self.FlashStatusMenu[AttackGroup] or ( MenuAttackGroup and ( AttackGroup:GetName() == MenuAttackGroup:GetName() ) ) then local DetectedReport = REPORT:New( "Targets ready for Designation:" ) - local DetectedItems = self.Detection:GetDetectedItems() + local DetectedItems = self.Detection:GetDetectedItemsByIndex() for DesignateIndex, Designating in pairs( self.Designating ) do local DetectedItem = DetectedItems[DesignateIndex] @@ -937,89 +952,112 @@ do -- DESIGNATE return self end - --- Sets the Designate Menu. + + --- Sets the Designate Menu for one attack groups. + -- @param #DESIGNATE self + -- @return #DESIGNATE + function DESIGNATE:SetMenu( AttackGroup ) + + self.MenuDesignate = self.MenuDesignate or {} + + local MissionMenu = nil + + if self.Mission then + --MissionMenu = self.Mission:GetRootMenu( AttackGroup ) + MissionMenu = self.Mission:GetMenu( AttackGroup ) + end + + local MenuTime = timer.getTime() + + self.MenuDesignate[AttackGroup] = MENU_GROUP_DELAYED:New( AttackGroup, self.DesignateName, MissionMenu ):SetTime( MenuTime ):SetTag( self.DesignateName ) + local MenuDesignate = self.MenuDesignate[AttackGroup] -- Core.Menu#MENU_GROUP_DELAYED + + -- Set Menu option for auto lase + + if self.AutoLase then + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Auto Lase Off", MenuDesignate, self.MenuAutoLase, self, false ):SetTime( MenuTime ):SetTag( self.DesignateName ) + else + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Auto Lase On", MenuDesignate, self.MenuAutoLase, self, true ):SetTime( MenuTime ):SetTag( self.DesignateName ) + end + + local StatusMenu = MENU_GROUP_DELAYED:New( AttackGroup, "Status", MenuDesignate ):SetTime( MenuTime ):SetTag( self.DesignateName ) + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Report Status", StatusMenu, self.MenuStatus, self, AttackGroup ):SetTime( MenuTime ):SetTag( self.DesignateName ) + + if self.FlashStatusMenu[AttackGroup] then + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Flash Status Report Off", StatusMenu, self.MenuFlashStatus, self, AttackGroup, false ):SetTime( MenuTime ):SetTag( self.DesignateName ) + else + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Flash Status Report On", StatusMenu, self.MenuFlashStatus, self, AttackGroup, true ):SetTime( MenuTime ):SetTag( self.DesignateName ) + end + + local DesignateCount = 0 + + for DesignateIndex, Designating in pairs( self.Designating ) do + + local DetectedItem = self.Detection:GetDetectedItemByIndex( DesignateIndex ) + + if DetectedItem then + + local Coord = self.Detection:GetDetectedItemCoordinate( DetectedItem ) + local ID = self.Detection:GetDetectedItemID( DetectedItem ) + local MenuText = ID --.. ", " .. Coord:ToStringA2G( AttackGroup ) + + MenuText = string.format( "(%3s) %s", Designating, MenuText ) + local DetectedMenu = MENU_GROUP_DELAYED:New( AttackGroup, MenuText, MenuDesignate ):SetTime( MenuTime ):SetTag( self.DesignateName ) + + -- Build the Lasing menu. + if string.find( Designating, "L", 1, true ) == nil then + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Search other target", DetectedMenu, self.MenuForget, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) + for LaserCode, MenuText in pairs( self.MenuLaserCodes ) do + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, string.format( MenuText, LaserCode ), DetectedMenu, self.MenuLaseCode, self, DesignateIndex, self.LaseDuration, LaserCode ):SetTime( MenuTime ):SetTag( self.DesignateName ) + end + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Lase with random laser code(s)", DetectedMenu, self.MenuLaseOn, self, DesignateIndex, self.LaseDuration ):SetTime( MenuTime ):SetTag( self.DesignateName ) + else + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Stop lasing", DetectedMenu, self.MenuLaseOff, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) + end + + -- Build the Smoking menu. + if string.find( Designating, "S", 1, true ) == nil then + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke red", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Red ):SetTime( MenuTime ):SetTag( self.DesignateName ) + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke blue", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Blue ):SetTime( MenuTime ):SetTag( self.DesignateName ) + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke green", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Green ):SetTime( MenuTime ):SetTag( self.DesignateName ) + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke white", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.White ):SetTime( MenuTime ):SetTag( self.DesignateName ) + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke orange", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Orange ):SetTime( MenuTime ):SetTag( self.DesignateName ) + end + + -- Build the Illuminate menu. + if string.find( Designating, "I", 1, true ) == nil then + MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Illuminate", DetectedMenu, self.MenuIlluminate, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) + end + end + + DesignateCount = DesignateCount + 1 + if DesignateCount > 10 then + break + end + end + MenuDesignate:Remove( MenuTime, self.DesignateName ) + MenuDesignate:Set() + end + + + --- Sets the Designate Menu for all the attack groups. -- @param #DESIGNATE self -- @return #DESIGNATE function DESIGNATE:SetDesignateMenu() self.AttackSet:Flush( self ) + + local Delay = 1 self.AttackSet:ForEachGroupAlive( --- @param Wrapper.Group#GROUP GroupReport function( AttackGroup ) - self.MenuDesignate = self.MenuDesignate or {} - - local MissionMenu = nil - - if self.Mission then - MissionMenu = self.Mission:GetRootMenu( AttackGroup ) - end - - local MenuTime = timer.getTime() - - self.MenuDesignate[AttackGroup] = MENU_GROUP_DELAYED:New( AttackGroup, self.DesignateName, MissionMenu ):SetTime( MenuTime ):SetTag( self.DesignateName ) - local MenuDesignate = self.MenuDesignate[AttackGroup] -- Core.Menu#MENU_GROUP_DELAYED - - -- Set Menu option for auto lase - - if self.AutoLase then - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Auto Lase Off", MenuDesignate, self.MenuAutoLase, self, false ):SetTime( MenuTime ):SetTag( self.DesignateName ) - else - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Auto Lase On", MenuDesignate, self.MenuAutoLase, self, true ):SetTime( MenuTime ):SetTag( self.DesignateName ) - end - - local StatusMenu = MENU_GROUP_DELAYED:New( AttackGroup, "Status", MenuDesignate ):SetTime( MenuTime ):SetTag( self.DesignateName ) - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Report Status", StatusMenu, self.MenuStatus, self, AttackGroup ):SetTime( MenuTime ):SetTag( self.DesignateName ) - - if self.FlashStatusMenu[AttackGroup] then - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Flash Status Report Off", StatusMenu, self.MenuFlashStatus, self, AttackGroup, false ):SetTime( MenuTime ):SetTag( self.DesignateName ) - else - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Flash Status Report On", StatusMenu, self.MenuFlashStatus, self, AttackGroup, true ):SetTime( MenuTime ):SetTag( self.DesignateName ) - end - for DesignateIndex, Designating in pairs( self.Designating ) do - - local DetectedItem = self.Detection:GetDetectedItemByIndex( DesignateIndex ) - - if DetectedItem then - - local Coord = self.Detection:GetDetectedItemCoordinate( DetectedItem ) - local ID = self.Detection:GetDetectedItemID( DetectedItem ) - local MenuText = ID --.. ", " .. Coord:ToStringA2G( AttackGroup ) - - MenuText = string.format( "(%3s) %s", Designating, MenuText ) - local DetectedMenu = MENU_GROUP_DELAYED:New( AttackGroup, MenuText, MenuDesignate ):SetTime( MenuTime ):SetTag( self.DesignateName ) - - -- Build the Lasing menu. - if string.find( Designating, "L", 1, true ) == nil then - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Search other target", DetectedMenu, self.MenuForget, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) - for LaserCode, MenuText in pairs( self.MenuLaserCodes ) do - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, string.format( MenuText, LaserCode ), DetectedMenu, self.MenuLaseCode, self, DesignateIndex, 60, LaserCode ):SetTime( MenuTime ):SetTag( self.DesignateName ) - end - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Lase with random laser code(s)", DetectedMenu, self.MenuLaseOn, self, DesignateIndex, 60 ):SetTime( MenuTime ):SetTag( self.DesignateName ) - else - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Stop lasing", DetectedMenu, self.MenuLaseOff, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) - end - - -- Build the Smoking menu. - if string.find( Designating, "S", 1, true ) == nil then - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke red", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Red ):SetTime( MenuTime ):SetTag( self.DesignateName ) - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke blue", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Blue ):SetTime( MenuTime ):SetTag( self.DesignateName ) - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke green", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Green ):SetTime( MenuTime ):SetTag( self.DesignateName ) - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke white", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.White ):SetTime( MenuTime ):SetTag( self.DesignateName ) - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke orange", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Orange ):SetTime( MenuTime ):SetTag( self.DesignateName ) - end - - -- Build the Illuminate menu. - if string.find( Designating, "I", 1, true ) == nil then - MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Illuminate", DetectedMenu, self.MenuIlluminate, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName ) - end - end - end - MenuDesignate:Remove( MenuTime, self.DesignateName ) - MenuDesignate:Set() - end + self:ScheduleOnce( Delay, self.SetMenu, self, AttackGroup ) + Delay = Delay + 1 + end + ) return self @@ -1130,10 +1168,10 @@ do -- DESIGNATE if string.find( self.Designating[Index], "L", 1, true ) == nil then self.Designating[Index] = self.Designating[Index] .. "L" + self.LaseStart = timer.getTime() + self.LaseDuration = Duration + self:Lasing( Index, Duration, LaserCode ) end - self.LaseStart = timer.getTime() - self.LaseDuration = Duration - self:Lasing( Index, Duration, LaserCode ) end @@ -1292,7 +1330,7 @@ do -- DESIGNATE local MarkedLaserCodesText = ReportLaserCodes:Text(', ') self.CC:GetPositionable():MessageToSetGroup( "Marking " .. MarkingCount .. " x " .. MarkedTypesText .. ", code " .. MarkedLaserCodesText .. ".", 5, self.AttackSet, self.DesignateName ) - self:__Lasing( -30, Index, Duration, LaserCodeRequested ) + self:__Lasing( -self.LaseDuration, Index, Duration, LaserCodeRequested ) self:SetDesignateMenu() diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index 99d98c863..fd3029b5a 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -2,22 +2,25 @@ -- -- === -- --- ![Banner Image](..\Presentations\DETECTION\Dia1.JPG) +-- ## Features: +-- +-- * Detection of targets by recce units. +-- * Group detected targets per unit, type or area (zone). +-- * Keep persistency of detected targets, if when detection is lost. +-- * Provide an indication of detected targets. +-- * Report detected targets. +-- * Refresh detection upon specified time intervals. -- -- === -- --- DETECTION classes facilitate the detection of enemy units within the battle zone executed by FACs (Forward Air Controllers) or RECCEs (Reconnassance Units). --- DETECTION uses the in-built detection capabilities of DCS World, but adds new functionalities. +-- ## Missions: -- --- Find the DETECTION classes documentation further in this document in the globals section. +-- [DET - Detection](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/DET%20-%20Detection) -- -- === -- --- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/DET%20-%20Detection) --- --- === --- --- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl3Cf5jpI6BS0sBOVWK__tji) +-- Facilitate the detection of enemy units within the battle zone executed by FACs (Forward Air Controllers) or RECCEs (Reconnassance Units). +-- It uses the in-built detection capabilities of DCS World, but adds new functionalities. -- -- === -- @@ -29,27 +32,24 @@ -- -- * FlightControl : Analysis, Design, Programming, Testing -- --- @module Detection +-- === +-- +-- @module Functional.Detection +-- @image Detection.JPG -----BASE:TraceClass("DETECTION_BASE") -----BASE:TraceClass("DETECTION_AREAS") -----BASE:TraceClass("DETECTION_UNITS") -----BASE:TraceClass("DETECTION_TYPES") do -- DETECTION_BASE --- @type DETECTION_BASE -- @field Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. - -- @field Dcs.DCSTypes#Distance DetectionRange The range till which targets are accepted to be detected. + -- @field DCS#Distance DetectionRange The range till which targets are accepted to be detected. -- @field #DETECTION_BASE.DetectedObjects DetectedObjects The list of detected objects. -- @field #table DetectedObjectsIdentified Map of the DetectedObjects identified. -- @field #number DetectionRun -- @extends Core.Fsm#FSM - --- DETECTION_BASE class, extends @{Fsm#FSM} - -- - -- The DETECTION_BASE class defines the core functions to administer detected objects. - -- The DETECTION_BASE class will detect objects within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s). + --- Defines the core functions to administer detected objects. + -- The DETECTION_BASE class will detect objects within the battle zone for a list of @{Wrapper.Group}s detecting targets following (a) detection method(s). -- -- ## DETECTION_BASE constructor -- @@ -105,11 +105,11 @@ do -- DETECTION_BASE -- -- Various methods exist how to retrieve the grouped items from a DETECTION_BASE derived class: -- - -- * The method @{Detection#DETECTION_BASE.GetDetectedItems}() retrieves the DetectedItems[] list. - -- * A DetectedItem from the DetectedItems[] list can be retrieved using the method @{Detection#DETECTION_BASE.GetDetectedItem}( DetectedItemIndex ). + -- * The method @{Functional.Detection#DETECTION_BASE.GetDetectedItems}() retrieves the DetectedItems[] list. + -- * A DetectedItem from the DetectedItems[] list can be retrieved using the method @{Functional.Detection#DETECTION_BASE.GetDetectedItem}( DetectedItemIndex ). -- Note that this method returns a DetectedItem element from the list, that contains a Set variable and further information -- about the DetectedItem that is set by the DETECTION_BASE derived classes, used to group the DetectedItem. - -- * A DetectedSet from the DetectedItems[] list can be retrieved using the method @{Detection#DETECTION_BASE.GetDetectedSet}( DetectedItemIndex ). + -- * A DetectedSet from the DetectedItems[] list can be retrieved using the method @{Functional.Detection#DETECTION_BASE.GetDetectedSet}( DetectedItemIndex ). -- This method retrieves the Set from a DetectedItem element from the DetectedItem list (DetectedItems[ DetectedItemIndex ].Set ). -- -- ## **Visual filters** to fine-tune the probability of the detected objects @@ -146,7 +146,7 @@ do -- DETECTION_BASE -- -- Note that based on this probability factor, not only the detection but also the **type** of the unit will be applied! -- - -- Use the method @{Detection#DETECTION_BASE.SetDistanceProbability}() to set the probability factor upon a 10 km distance. + -- Use the method @{Functional.Detection#DETECTION_BASE.SetDistanceProbability}() to set the probability factor upon a 10 km distance. -- -- ### Alpha Angle visual detection probability -- @@ -158,7 +158,7 @@ do -- DETECTION_BASE -- For example, if a alpha angle probability factor of 0.7 is given, the extrapolated probabilities of the different angles would look like: -- 0°: 70%, 10°: 75,21%, 20°: 80,26%, 30°: 85%, 40°: 89,28%, 50°: 92,98%, 60°: 95,98%, 70°: 98,19%, 80°: 99,54%, 90°: 100% -- - -- Use the method @{Detection#DETECTION_BASE.SetAlphaAngleProbability}() to set the probability factor if 0°. + -- Use the method @{Functional.Detection#DETECTION_BASE.SetAlphaAngleProbability}() to set the probability factor if 0°. -- -- ### Cloudy Zones detection probability -- @@ -166,7 +166,7 @@ do -- DETECTION_BASE -- The Cloudy Zones work with the ZONE_BASE derived classes. The mission designer can define within the mission -- zones that reflect cloudy areas where detected units may not be so easily visually detected. -- - -- Use the method @{Detection#DETECTION_BASE.SetZoneProbability}() to set for a defined number of zones, the probability factors. + -- Use the method @{Functional.Detection#DETECTION_BASE.SetZoneProbability}() to set for a defined number of zones, the probability factors. -- -- Note however, that the more zones are defined to be "cloudy" within a detection, the more performance it will take -- from the DETECTION_BASE to calculate the presence of the detected unit within each zone. @@ -183,7 +183,7 @@ do -- DETECTION_BASE -- ### Detection acceptance of within range limit -- -- A range can be set that will limit a successful detection for a unit. - -- Use the method @{Detection#DETECTION_BASE.SetAcceptRange}() to apply a range in meters till where detected units will be accepted. + -- Use the method @{Functional.Detection#DETECTION_BASE.SetAcceptRange}() to apply a range in meters till where detected units will be accepted. -- -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. -- @@ -200,7 +200,7 @@ do -- DETECTION_BASE -- ### Detection acceptance if within zone(s). -- -- Specific ZONE_BASE object(s) can be given as a parameter, which will only accept a detection if the unit is within the specified ZONE_BASE object(s). - -- Use the method @{Detection#DETECTION_BASE.SetAcceptZones}() will accept detected units if they are within the specified zones. + -- Use the method @{Functional.Detection#DETECTION_BASE.SetAcceptZones}() will accept detected units if they are within the specified zones. -- -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. -- @@ -220,7 +220,7 @@ do -- DETECTION_BASE -- ### Detection rejectance if within zone(s). -- -- Specific ZONE_BASE object(s) can be given as a parameter, which will reject detection if the unit is within the specified ZONE_BASE object(s). - -- Use the method @{Detection#DETECTION_BASE.SetRejectZones}() will reject detected units if they are within the specified zones. + -- Use the method @{Functional.Detection#DETECTION_BASE.SetRejectZones}() will reject detected units if they are within the specified zones. -- An example of how to use the method is shown below. -- -- local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers. @@ -240,7 +240,7 @@ do -- DETECTION_BASE -- -- ## Detection of Friendlies Nearby -- - -- Use the method @{Detection#DETECTION_BASE.SetFriendliesRange}() to set the range what will indicate when friendlies are nearby + -- Use the method @{Functional.Detection#DETECTION_BASE.SetFriendliesRange}() to set the range what will indicate when friendlies are nearby -- a DetectedItem. The default range is 6000 meters. For air detections, it is advisory to use about 30.000 meters. -- -- ## DETECTION_BASE is a Finite State Machine @@ -292,6 +292,7 @@ do -- DETECTION_BASE -- @list <#DETECTION_BASE.DetectedItem> --- @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. @@ -450,7 +451,16 @@ do -- DETECTION_BASE -- @param #DETECTION_BASE self -- @param #number Delay The delay in seconds. + self:AddTransition( "Detecting", "DetectedItem", "Detecting" ) + --- OnAfter Transition Handler for Event DetectedItem. + -- @function [parent=#DETECTION_BASE] OnAfterDetectedItem + -- @param #DETECTION_BASE self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param #table DetectedItem The DetectedItem. + self:AddTransition( "*", "Stop", "Stopped" ) --- OnBefore Transition Handler for Event Stop. @@ -510,7 +520,6 @@ do -- DETECTION_BASE -- @param #string Event The Event string. -- @param #string To The To State string. function DETECTION_BASE:onafterDetect(From,Event,To) - self:F( { From, Event, To } ) local DetectDelay = 0.1 self.DetectionCount = 0 @@ -519,8 +528,21 @@ do -- DETECTION_BASE local DetectionTimeStamp = timer.getTime() + -- Reset detection cache for the next detection run. + for DetectionObjectName, DetectedObjectData in pairs( self.DetectedObjects ) do + + self.DetectedObjects[DetectionObjectName].IsDetected = false + self.DetectedObjects[DetectionObjectName].IsVisible = false + self.DetectedObjects[DetectionObjectName].KnowDistance = nil + self.DetectedObjects[DetectionObjectName].LastTime = nil + self.DetectedObjects[DetectionObjectName].LastPos = nil + self.DetectedObjects[DetectionObjectName].LastVelocity = nil + self.DetectedObjects[DetectionObjectName].Distance = 10000000 + + end 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 @@ -532,8 +554,10 @@ do -- DETECTION_BASE -- @param #string Event The Event string. -- @param #string To The To State string. -- @param Wrapper.Group#GROUP DetectionGroup The Group detecting. + -- @param #number DetectionTimeStamp Time stamp of detection event. function DETECTION_BASE:onafterDetectionGroup( From, Event, To, DetectionGroup, DetectionTimeStamp ) - self:F( { From, Event, To } ) + + --self:F( { DetectedObjects = self.DetectedObjects } ) self.DetectionRun = self.DetectionRun + 1 @@ -541,7 +565,7 @@ do -- DETECTION_BASE if DetectionGroup:IsAlive() then - self:T( { "DetectionGroup is Alive", DetectionGroup:GetName() } ) + --self:T( { "DetectionGroup is Alive", DetectionGroup:GetName() } ) local DetectionGroupName = DetectionGroup:GetName() local DetectionUnit = DetectionGroup:GetUnit(1) @@ -557,13 +581,27 @@ do -- DETECTION_BASE self.DetectDLINK ) - self:F( DetectedTargets ) + self:F( { DetectedTargets = DetectedTargets } ) for DetectionObjectID, Detection in pairs( DetectedTargets ) do - local DetectedObject = Detection.object -- Dcs.DCSWrapper.Object#Object + local DetectedObject = Detection.object -- DCS#Object if DetectedObject and DetectedObject:isExist() and DetectedObject.id_ < 50000000 then -- and ( DetectedObject:getCategory() == Object.Category.UNIT or DetectedObject:getCategory() == Object.Category.STATIC ) then - + local DetectedObjectName = DetectedObject:getName() + if not self.DetectedObjects[DetectedObjectName] then + self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {} + self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName + self.DetectedObjects[DetectedObjectName].Object = DetectedObject + end + end + end + + for DetectionObjectName, DetectedObjectData in pairs( self.DetectedObjects ) do + + local DetectedObject = DetectedObjectData.Object + + if DetectedObject:isExist() then + local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity = DetectionUnit:IsTargetDetected( DetectedObject, self.DetectVisual, @@ -574,8 +612,8 @@ do -- DETECTION_BASE self.DetectDLINK ) - self:T2( { TargetIsDetected = TargetIsDetected, TargetIsVisible = TargetIsVisible, TargetLastTime = TargetLastTime, TargetKnowType = TargetKnowType, TargetKnowDistance = TargetKnowDistance, TargetLastPos = TargetLastPos, TargetLastVelocity = TargetLastVelocity } ) - + --self:T2( { TargetIsDetected = TargetIsDetected, TargetIsVisible = TargetIsVisible, TargetLastTime = TargetLastTime, TargetKnowType = TargetKnowType, TargetKnowDistance = TargetKnowDistance, TargetLastPos = TargetLastPos, TargetLastVelocity = TargetLastVelocity } ) + -- Only process if the target is visible. Detection also returns invisible units. --if Detection.visible == true then @@ -596,7 +634,7 @@ do -- DETECTION_BASE local DetectedUnitCategory = DetectedObject:getDesc().category - self:F( { "Detected Target:", DetectionGroupName, DetectedObjectName, DetectedObjectType, Distance, DetectedUnitCategory } ) + --self:F( { "Detected Target:", DetectionGroupName, DetectedObjectName, DetectedObjectType, Distance, DetectedUnitCategory } ) -- Calculate Acceptance @@ -638,19 +676,19 @@ do -- DETECTION_BASE -- Calculate additional probabilities - if not self.DetectedObjects[DetectedObjectName] and Detection.visible and self.DistanceProbability then + if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.DistanceProbability then local DistanceFactor = Distance / 4 local DistanceProbabilityReversed = ( 1 - self.DistanceProbability ) * DistanceFactor local DistanceProbability = 1 - DistanceProbabilityReversed DistanceProbability = DistanceProbability * 30 / 300 local Probability = math.random() -- Selects a number between 0 and 1 - self:T( { Probability, DistanceProbability } ) + --self:T( { Probability, DistanceProbability } ) if Probability > DistanceProbability then DetectionAccepted = false end end - if not self.DetectedObjects[DetectedObjectName] and Detection.visible and self.AlphaAngleProbability then + if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.AlphaAngleProbability then local NormalVec2 = { x = DetectedObjectVec2.x - DetectionGroupVec2.x, y = DetectedObjectVec2.y - DetectionGroupVec2.y } local AlphaAngle = math.atan2( NormalVec2.y, NormalVec2.x ) local Sinus = math.sin( AlphaAngle ) @@ -660,14 +698,14 @@ do -- DETECTION_BASE AlphaAngleProbability = AlphaAngleProbability * 30 / 300 local Probability = math.random() -- Selects a number between 0 and 1 - self:T( { Probability, AlphaAngleProbability } ) + --self:T( { Probability, AlphaAngleProbability } ) if Probability > AlphaAngleProbability then DetectionAccepted = false end end - if not self.DetectedObjects[DetectedObjectName] and Detection.visible and self.ZoneProbability then + if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.ZoneProbability then for ZoneDataID, ZoneData in pairs( self.ZoneProbability ) do self:F({ZoneData}) @@ -677,7 +715,7 @@ do -- DETECTION_BASE if ZoneObject:IsPointVec2InZone( DetectedObjectVec2 ) == true then local Probability = math.random() -- Selects a number between 0 and 1 - self:T( { Probability, ZoneProbability } ) + --self:T( { Probability, ZoneProbability } ) if Probability > ZoneProbability then DetectionAccepted = false break @@ -690,16 +728,29 @@ do -- DETECTION_BASE HasDetectedObjects = true - self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {} + self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {} self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName - self.DetectedObjects[DetectedObjectName].IsDetected = TargetIsDetected - self.DetectedObjects[DetectedObjectName].IsVisible = TargetIsVisible - self.DetectedObjects[DetectedObjectName].LastTime = TargetLastTime - self.DetectedObjects[DetectedObjectName].LastPos = TargetLastPos - self.DetectedObjects[DetectedObjectName].LastVelocity = TargetLastVelocity - self.DetectedObjects[DetectedObjectName].KnowType = TargetKnowType - self.DetectedObjects[DetectedObjectName].KnowDistance = Detection.distance -- TargetKnowDistance - self.DetectedObjects[DetectedObjectName].Distance = Distance + + if TargetIsDetected and TargetIsDetected == true then + self.DetectedObjects[DetectedObjectName].IsDetected = TargetIsDetected + end + + if TargetIsDetected and TargetIsVisible and TargetIsVisible == true then + self.DetectedObjects[DetectedObjectName].IsVisible = TargetIsDetected and TargetIsVisible + end + + if TargetIsDetected and not self.DetectedObjects[DetectedObjectName].KnowType then + self.DetectedObjects[DetectedObjectName].KnowType = TargetIsDetected and TargetKnowType + end + self.DetectedObjects[DetectedObjectName].KnowDistance = TargetKnowDistance -- Detection.distance -- TargetKnowDistance + self.DetectedObjects[DetectedObjectName].LastTime = ( TargetIsDetected and TargetIsVisible == false ) and TargetLastTime + self.DetectedObjects[DetectedObjectName].LastPos = ( TargetIsDetected and TargetIsVisible == false ) and TargetLastPos + self.DetectedObjects[DetectedObjectName].LastVelocity = ( TargetIsDetected and TargetIsVisible == false ) and TargetLastVelocity + + if not self.DetectedObjects[DetectedObjectName].Distance or ( Distance and self.DetectedObjects[DetectedObjectName].Distance > Distance ) then + self.DetectedObjects[DetectedObjectName].Distance = Distance + end + self.DetectedObjects[DetectedObjectName].DetectionTimeStamp = DetectionTimeStamp self:F( { DetectedObject = self.DetectedObjects[DetectedObjectName] } ) @@ -709,14 +760,18 @@ do -- DETECTION_BASE DetectedUnits[DetectedObjectName] = DetectedUnit else -- if beyond the DetectionRange then nullify... + self:F( { DetectedObject = "No more detection for " .. DetectedObjectName } ) if self.DetectedObjects[DetectedObjectName] then self.DetectedObjects[DetectedObjectName] = nil end end - --end + + --self:T2( self.DetectedObjects ) + else + -- The previously detected object does not exist anymore, delete from the cache. + self:F( "Removing from DetectedObjects: " .. DetectionObjectName ) + self.DetectedObjects[DetectionObjectName] = nil end - - self:T2( self.DetectedObjects ) end if HasDetectedObjects then @@ -726,7 +781,6 @@ do -- DETECTION_BASE end if self.DetectionCount > 0 and self.DetectionRun == self.DetectionCount then - self:T( "--> Create Detection Sets" ) -- First check if all DetectedObjects were detected. -- This is important. When there are DetectedObjects in the list, but were not detected, @@ -743,6 +797,9 @@ do -- DETECTION_BASE 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 ) @@ -766,7 +823,6 @@ do -- DETECTION_BASE local DetectedSet = DetectedItem.Set if DetectedSet:Count() == 0 then - self:F3( { DetectedItemID = DetectedItemID } ) self:RemoveDetectedItem( DetectedItemID ) end @@ -778,8 +834,7 @@ do -- DETECTION_BASE -- @param #string UnitName The UnitName that needs to be forgotten from the DetectionItem Sets. -- @return #DETECTION_BASE function DETECTION_BASE:ForgetDetectedUnit( UnitName ) - self:F2() - + local DetectedItems = self:GetDetectedItems() for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do @@ -796,7 +851,6 @@ do -- DETECTION_BASE -- @param #DETECTION_BASE self -- @return #DETECTION_BASE function DETECTION_BASE:CreateDetectionItems() - self:F2() self:F( "Error, in DETECTION_BASE class..." ) return self @@ -902,7 +956,7 @@ do -- DETECTION_BASE -- DetectionObject:FilterCategories( { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) -- -- @param #DETECTION_BASE self - -- @param #list FilterCategories The Categories entries + -- @param #list FilterCategories The Categories entries -- @return #DETECTION_BASE self function DETECTION_BASE:FilterCategories( FilterCategories ) self:F2() @@ -958,7 +1012,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() @@ -1176,18 +1230,17 @@ do -- DETECTION_BASE --- Returns if there are friendlies nearby the FAC units ... -- @param #DETECTION_BASE self -- @param DetectedItem - -- @param Dcs.DCSUnit#Unit.Category Category The category of the unit. + -- @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 Dcs.DCSUnit#Unit.Category Category The category of the unit. + -- @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 ) @@ -1237,7 +1290,7 @@ do -- DETECTION_BASE --- Background worker function to determine if there are friendlies nearby ... -- @param #DETECTION_BASE self function DETECTION_BASE:ReportFriendliesNearBy( TargetData ) - self:F( { "Search Friendlies", DetectedItem = TargetData.DetectedItem } ) + --self:F( { "Search Friendlies", DetectedItem = TargetData.DetectedItem } ) local DetectedItem = TargetData.DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem local DetectedSet = TargetData.DetectedItem.Set @@ -1260,9 +1313,9 @@ do -- DETECTION_BASE } - --- @param Dcs.DCSWrapper.Unit#Unit FoundDCSUnit + --- @param DCS#Unit FoundDCSUnit -- @param Wrapper.Group#GROUP ReportGroup - -- @param Set#SET_GROUP ReportSetGroup + -- @param Core.Set#SET_GROUP ReportSetGroup local FindNearByFriendlies = function( FoundDCSUnit, ReportGroupData ) local DetectedItem = ReportGroupData.DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem @@ -1286,7 +1339,7 @@ do -- DETECTION_BASE if FoundUnitInReportSetGroup == true then -- If the recce was part of the friendlies found, then check if the recce is part of the allowed friendly unit prefixes. for PrefixID, Prefix in pairs( self.FriendlyPrefixes or {} ) do - self:F( { "Friendly Prefix:", Prefix = Prefix } ) + --self:F( { "Friendly Prefix:", Prefix = Prefix } ) -- In case a match is found (so a recce unit name is part of the friendly prefixes), then report that recce to be part of the friendlies. -- This is important if CAP planes (so planes using their own radar) to be scanning for targets as part of the EWR network. -- But CAP planes are also attackers, so they need to be considered friendlies too! @@ -1298,7 +1351,7 @@ do -- DETECTION_BASE end end - self:F( { "Friendlies near Target:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) + --self:F( { "Friendlies near Target:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } ) if FoundUnitCoalition ~= EnemyCoalition and FoundUnitInReportSetGroup == false then local FriendlyUnit = UNIT:Find( FoundDCSUnit ) @@ -1313,7 +1366,7 @@ do -- DETECTION_BASE local Distance = DetectedUnitCoord:Get2DDistance( FriendlyUnit:GetCoordinate() ) DetectedItem.FriendliesDistance = DetectedItem.FriendliesDistance or {} DetectedItem.FriendliesDistance[Distance] = FriendlyUnit - self:T( { "Friendlies Found:", FriendlyUnitName = FriendlyUnitName, Distance = Distance, FriendlyUnitCategory = FriendlyUnitCategory, FriendliesCategory = self.FriendliesCategory } ) + --self:F( { "Friendlies Found:", FriendlyUnitName = FriendlyUnitName, Distance = Distance, FriendlyUnitCategory = FriendlyUnitCategory, FriendliesCategory = self.FriendliesCategory } ) return true end @@ -1355,6 +1408,9 @@ do -- DETECTION_BASE end ) end + + self:F( { Friendlies = DetectedItem.FriendliesNearBy, Players = DetectedItem.PlayersNearBy } ) + end end @@ -1405,16 +1461,18 @@ do -- DETECTION_BASE -- @param #string ObjectName -- @return #DETECTION_BASE.DetectedObject function DETECTION_BASE:GetDetectedObject( ObjectName ) - --self:F2( ObjectName ) + self:F2( { ObjectName = ObjectName } ) if ObjectName then local DetectedObject = self.DetectedObjects[ObjectName] if DetectedObject then + --self:F( { DetectedObjects = self.DetectedObjects } ) -- Only return detected objects that are alive! local DetectedUnit = UNIT:FindByName( ObjectName ) if DetectedUnit and DetectedUnit:IsAlive() then if self:IsDetectedObjectIdentified( DetectedObject ) == false then + --self:F( { DetectedObject = DetectedObject } ) return DetectedObject end end @@ -1517,7 +1575,8 @@ do -- DETECTION_BASE end - --- Get the detected @{Set#SET_BASE}s. + --- Get the DetectedItems by Key. + -- This will return the DetectedItems collection, indexed by the Key, which can be any object that acts as the key of the detection. -- @param #DETECTION_BASE self -- @return #DETECTION_BASE.DetectedItems function DETECTION_BASE:GetDetectedItems() @@ -1525,6 +1584,15 @@ do -- DETECTION_BASE return self.DetectedItems end + --- Get the DetectedItems by Index. + -- This will return the DetectedItems collection, indexed by an internal numerical Index. + -- @param #DETECTION_BASE self + -- @return #DETECTION_BASE.DetectedItems + function DETECTION_BASE:GetDetectedItemsByIndex() + + return self.DetectedItemsByIndex + end + --- Get the amount of SETs with detected objects. -- @param #DETECTION_BASE self -- @return #number The amount of detected items. Note that the amount of detected items can differ with the reality, because detections are not real-time but doen in intervals! @@ -1589,7 +1657,7 @@ do -- DETECTION_BASE return "" end - --- Get the @{Set#SET_UNIT} of a detecttion area using a given numeric index. + --- Get the @{Core.Set#SET_UNIT} of a detecttion area using a given numeric index. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return Core.Set#SET_UNIT DetectedSet @@ -1603,7 +1671,7 @@ do -- DETECTION_BASE return nil end - --- Set IsDetected flag for all DetectedItems. + --- Set IsDetected flag for the DetectedItem, which can have more units. -- @param #DETECTION_BASE self -- @return #DETECTION_BASE.DetectedItem DetectedItem -- @return #boolean true if at least one UNIT is detected from the DetectedSet, false if no UNIT was detected from the DetectedSet. @@ -1638,7 +1706,7 @@ do -- DETECTION_BASE do -- Zones - --- Get the @{Zone#ZONE_UNIT} of a detection area using a given numeric index. + --- Get the @{Core.Zone#ZONE_UNIT} of a detection area using a given numeric index. -- @param #DETECTION_BASE self -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem. -- @return Core.Zone#ZONE_UNIT DetectedZone @@ -1801,15 +1869,16 @@ end do -- DETECTION_UNITS - --- # DETECTION_UNITS class, extends @{Detection#DETECTION_BASE} + --- @type DETECTION_UNITS + -- @field DCS#Distance DetectionRange The range till which targets are detected. + -- @extends Functional.Detection#DETECTION_BASE + + --- Will detect units within the battle zone. -- - -- The DETECTION_UNITS class will detect units within the battle zone. - -- It will build a DetectedItems list filled with DetectedItems. Each DetectedItem will contain a field Set, which contains a @{Set#SET_UNIT} containing ONE @{UNIT} object reference. + -- It will build a DetectedItems list filled with DetectedItems. Each DetectedItem will contain a field Set, which contains a @{Core.Set#SET_UNIT} containing ONE @{UNIT} object reference. -- Beware that when the amount of units detected is large, the DetectedItems list will be large also. -- - -- @type DETECTION_UNITS - -- @field Dcs.DCSTypes#Distance DetectionRange The range till which targets are detected. - -- @extends #DETECTION_BASE + -- @field #DETECTION_UNITS DETECTION_UNITS = { ClassName = "DETECTION_UNITS", DetectionRange = nil, @@ -1835,7 +1904,7 @@ do -- DETECTION_UNITS --- Make text documenting the changes of the detected zone. -- @param #DETECTION_UNITS self - -- @param #DETECTION_UNITS.DetectedItem DetectedItem + -- @param #DETECTION_BASE.DetectedItem DetectedItem -- @return #string The Changes text function DETECTION_UNITS:GetChangeText( DetectedItem ) self:F( DetectedItem ) @@ -1876,11 +1945,9 @@ do -- DETECTION_UNITS -- @param #DETECTION_UNITS self -- @return #DETECTION_UNITS self function DETECTION_UNITS:CreateDetectionItems() - self:F2( #self.DetectedObjects ) - -- Loop the current detected items, and check if each object still exists and is detected. - for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + for DetectedItemKey, DetectedItem in pairs( self.DetectedItems ) do local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT @@ -1898,6 +1965,7 @@ do -- DETECTION_UNITS -- Yes, the DetectedUnit is still detected or exists. Flag as identified. self:IdentifyDetectedObject( DetectedObject ) + self:F( { "**DETECTED**", IsVisible = DetectedObject.IsVisible } ) -- Update the detection with the new data provided. DetectedItem.TypeName = DetectedUnit:GetTypeName() DetectedItem.CategoryName = DetectedUnit:GetCategoryName() @@ -1915,6 +1983,11 @@ do -- DETECTION_UNITS DetectedItemSet:Remove( DetectedUnitName ) end end + if DetectedItemSet:Count() == 0 then + -- Now the Set is empty, meaning that a detected item has no units anymore. + -- Delete the DetectedItem from the detections + self:RemoveDetectedItem( DetectedItemKey ) + end end @@ -2017,6 +2090,9 @@ do -- DETECTION_UNITS Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText) Report:Add( string.format( "Threat: [%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10-ThreatLevelA2G ) ) ) Report:Add( string.format("Type: %s%s", UnitCategoryText, UnitDistanceText ) ) + Report:Add( string.format("Visible: %s", DetectedItem.IsVisible and "yes" or "no" ) ) + Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) ) + Report:Add( string.format("Distance: %s", DetectedItem.KnowDistance and "yes" or "no" ) ) return Report end return nil @@ -2047,15 +2123,15 @@ end do -- DETECTION_TYPES - --- # 3) DETECTION_TYPES class, extends @{Detection#DETECTION_BASE} - -- - -- The DETECTION_TYPES class will detect units within the battle zone. + --- @type DETECTION_TYPES + -- @extends Functional.Detection#DETECTION_BASE + + --- Will detect units within the battle zone. -- It will build a DetectedItems[] list filled with DetectedItems, grouped by the type of units detected. - -- Each DetectedItem will contain a field Set, which contains a @{Set#SET_UNIT} containing ONE @{UNIT} object reference. + -- Each DetectedItem will contain a field Set, which contains a @{Core.Set#SET_UNIT} containing ONE @{UNIT} object reference. -- Beware that when the amount of different types detected is large, the DetectedItems[] list will be large also. -- - -- @type DETECTION_TYPES - -- @extends #DETECTION_BASE + -- @field #DETECTION_TYPES DETECTION_TYPES = { ClassName = "DETECTION_TYPES", DetectionRange = nil, @@ -2081,7 +2157,7 @@ do -- DETECTION_TYPES --- Make text documenting the changes of the detected zone. -- @param #DETECTION_TYPES self - -- @param #DETECTION_TYPES.DetectedItem DetectedItem + -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem -- @return #string The Changes text function DETECTION_TYPES:GetChangeText( DetectedItem ) self:F( DetectedItem ) @@ -2122,11 +2198,10 @@ do -- DETECTION_TYPES -- @param #DETECTION_TYPES self -- @return #DETECTION_TYPES self function DETECTION_TYPES:CreateDetectionItems() - self:F2( #self.DetectedObjects ) -- Loop the current detected items, and check if each object still exists and is detected. - for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do + for DetectedItemKey, DetectedItem in pairs( self.DetectedItems ) do local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT local DetectedTypeName = DetectedItem.TypeName @@ -2149,6 +2224,11 @@ do -- DETECTION_TYPES DetectedItemSet:Remove( DetectedUnitName ) end end + if DetectedItemSet:Count() == 0 then + -- Now the Set is empty, meaning that a detected item has no units anymore. + -- Delete the DetectedItem from the detections + self:RemoveDetectedItem( DetectedItemKey ) + end end @@ -2252,42 +2332,42 @@ end do -- DETECTION_AREAS - --- # 4) DETECTION_AREAS class, extends @{Detection#DETECTION_BASE} - -- - -- The DETECTION_AREAS class will detect units within the battle zone for a list of @{Group}s detecting targets following (a) detection method(s), - -- and will build a list (table) of @{Set#SET_UNIT}s containing the @{Unit#UNIT}s detected. + --- @type DETECTION_AREAS + -- @field DCS#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. + -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Wrapper.Unit}s, @{Zone}s, the center @{Wrapper.Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. + -- @extends Functional.Detection#DETECTION_BASE + + --- Detect units within the battle zone for a list of @{Wrapper.Group}s detecting targets following (a) detection method(s), + -- and will build a list (table) of @{Core.Set#SET_UNIT}s containing the @{Wrapper.Unit#UNIT}s detected. -- The class is group the detected units within zones given a DetectedZoneRange parameter. -- A set with multiple detected zones will be created as there are groups of units detected. -- -- ## 4.1) Retrieve the Detected Unit Sets and Detected Zones -- - -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Detection#DECTECTION_BASE} and - -- the methods to manage the DetectedItems[].Zone(s) is implemented in @{Detection#DETECTION_AREAS}. + -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Functional.Detection#DECTECTION_BASE} and + -- the methods to manage the DetectedItems[].Zone(s) is implemented in @{Functional.Detection#DETECTION_AREAS}. -- - -- Retrieve the DetectedItems[].Set with the method @{Detection#DETECTION_BASE.GetDetectedSet}(). A @{Set#SET_UNIT} object will be returned. + -- 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 @{Detection#DETECTION_BASE.GetDetectionZones}(). - -- To understand the amount of zones created, use the method @{Detection#DETECTION_BASE.GetDetectionZoneCount}(). - -- If you want to obtain a specific zone from the DetectedZones, use the method @{Detection#DETECTION_BASE.GetDetectionZone}() with a given index. + -- 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 @{Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. + -- Use the methods @{Functional.Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Functional.Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place. -- -- ## 4.5) Flare or Smoke or Bound detected zones -- -- Use the methods: -- - -- * @{Detection#DETECTION_AREAS.FlareDetectedZones}() to flare in a color - -- * @{Detection#DETECTION_AREAS.SmokeDetectedZones}() to smoke in a color - -- * @{Detection#DETECTION_AREAS.SmokeDetectedZones}() to bound with a tire with a white flag + -- * @{Functional.Detection#DETECTION_AREAS.FlareDetectedZones}() to flare in a color + -- * @{Functional.Detection#DETECTION_AREAS.SmokeDetectedZones}() to smoke in a color + -- * @{Functional.Detection#DETECTION_AREAS.SmokeDetectedZones}() to bound with a tire with a white flag -- -- the detected zones when a new detection has taken place. -- - -- @type DETECTION_AREAS - -- @field Dcs.DCSTypes#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 @{Unit}s, @{Zone}s, the center @{Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange. - -- @extends #DETECTION_BASE + -- @field #DETECTION_AREAS DETECTION_AREAS = { ClassName = "DETECTION_AREAS", DetectionZoneRange = nil, @@ -2297,7 +2377,7 @@ do -- DETECTION_AREAS --- DETECTION_AREAS constructor. -- @param #DETECTION_AREAS self -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role. - -- @param Dcs.DCSTypes#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. + -- @param DCS#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target. -- @return #DETECTION_AREAS function DETECTION_AREAS:New( DetectionSetGroup, DetectionZoneRange ) @@ -2343,6 +2423,7 @@ do -- DETECTION_AREAS 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 @@ -2505,10 +2586,11 @@ do -- DETECTION_AREAS -- @param #DETECTION_AREAS self -- @return #DETECTION_AREAS self function DETECTION_AREAS:CreateDetectionItems() - self:F2() - self:T( "Checking Detected Items for new Detected Units ..." ) + self:F( "Checking Detected Items for new Detected Units ..." ) + --self:F( { DetectedObjects = self.DetectedObjects } ) + -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units. -- Regroup when needed, split groups when needed. for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do @@ -2517,8 +2599,7 @@ do -- DETECTION_AREAS if DetectedItem then - self:T( { "Detected Item ID:", DetectedItemID } ) - + self:T2( { "Detected Item ID: ", DetectedItemID } ) local DetectedSet = DetectedItem.Set @@ -2647,7 +2728,6 @@ do -- DETECTION_AREAS local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem if DetectedItem then - self:T( "Detection Area #" .. DetectedItem.ID ) local DetectedSet = DetectedItem.Set if not self:IsDetectedObjectIdentified( DetectedObject ) and DetectedUnit:IsInZone( DetectedItem.Zone ) then self:IdentifyDetectedObject( DetectedObject ) diff --git a/Moose Development/Moose/Functional/Escort.lua b/Moose Development/Moose/Functional/Escort.lua index 6fcddbe7c..89d77425c 100644 --- a/Moose Development/Moose/Functional/Escort.lua +++ b/Moose Development/Moose/Functional/Escort.lua @@ -2,61 +2,81 @@ -- -- === -- --- @{#ESCORT} class +-- ## 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. +-- -- === --- The @{#ESCORT} class allows you to interact with escorting AI on your flight and take the lead. +-- +-- ## Missions: +-- +-- [ESC - Escorting](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/ESC%20-%20Escorting) +-- +-- === +-- +-- Allows you to interact with escorting AI on your flight and take the lead. +-- -- Each escorting group can be commanded with a whole set of radio commands (radio menu in your flight, and then F10). -- -- The radio commands will vary according the category of the group. The richest set of commands are with Helicopters and AirPlanes. -- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts. -- --- RADIO MENUs that can be created: --- === +-- # RADIO MENUs that can be created: +-- -- Find a summary below of the current available commands: -- --- Navigation ...: --- --------------- +-- ## Navigation ...: +-- -- Escort group navigation functions: -- -- * **"Join-Up and Follow at x meters":** The escort group fill follow you at about x meters, and they will follow you. -- * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color. -- * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops. -- --- Hold position ...: --- ------------------ +-- ## Hold position ...: +-- -- Escort group navigation functions: -- -- * **"At current location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. -- * **"At client location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped. -- --- Report targets ...: --- ------------------- +-- ## Report targets ...: +-- -- Report targets will make the escort group to report any target that it identifies within a 8km range. Any detected target can be attacked using the 4. Attack nearby targets function. (see below). -- -- * **"Report now":** Will report the current detected targets. -- * **"Report targets on":** Will make the escort group to report detected targets and will fill the "Attack nearby targets" menu list. -- * **"Report targets off":** Will stop detecting targets. -- --- Scan targets ...: --- ----------------- +-- ## Scan targets ...: +-- -- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or defined task. -- -- * **"Scan targets 30 seconds":** Scan 30 seconds for targets. -- * **"Scan targets 60 seconds":** Scan 60 seconds for targets. -- --- Attack targets ...: --- ------------------- +-- ## Attack targets ...: +-- -- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed. -- --- Request assistance from ...: --- ---------------------------- +-- ## Request assistance from ...: +-- -- This menu item will list all detected targets within a 15km range, as with the menu item **Attack Targets**. -- This menu item allows to request attack support from other escorts supporting the current client group. -- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles. -- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area. -- --- ROE ...: --- -------- +-- ## ROE ...: +-- -- Sets the Rules of Engagement (ROE) of the escort group when in flight. -- -- * **"Hold Fire":** The escort group will hold fire. @@ -64,8 +84,8 @@ -- * **"Open Fire":** The escort group will open fire on designated targets. -- * **"Weapon Free":** The escort group will engage with any target. -- --- Evasion ...: --- ------------ +-- ## 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. @@ -73,35 +93,43 @@ -- * **"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 ...: --- ------------------- +-- ## 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. -- --- ESCORT construction methods. -- === +-- +-- ### Authors: **FlightControl** +-- +-- === +-- +-- @module Functional.Escort +-- @image Escorting.JPG + + + +--- @type ESCORT +-- @extends Core.Base#BASE +-- @field Wrapper.Client#CLIENT EscortClient +-- @field Wrapper.Group#GROUP EscortGroup +-- @field #string EscortName +-- @field #ESCORT.MODE EscortMode The mode the escort is in. +-- @field Core.Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. +-- @field #number FollowDistance The current follow distance. +-- @field #boolean ReportTargets If true, nearby targets are reported. +-- @Field DCS#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. +-- @field DCS#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. +-- @field FunctionalMENU_GROUPDETECTION_BASE Detection + +--- ESCORT class +-- +-- # ESCORT construction methods. +-- -- Create a new SPAWN object with the @{#ESCORT.New} method: -- --- * @{#ESCORT.New}: Creates a new ESCORT object from a @{Group#GROUP} for a @{Client#CLIENT}, with an optional briefing text. +-- * @{#ESCORT.New}: Creates a new ESCORT object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT}, with an optional briefing text. -- --- ESCORT initialization methods. --- === --- The following menus are created within the RADIO MENU (F10) of an active unit hosted by a player: --- --- * @{#ESCORT.MenuFollowAt}: Creates a menu to make the escort follow the client. --- * @{#ESCORT.MenuHoldAtEscortPosition}: Creates a menu to hold the escort at its current position. --- * @{#ESCORT.MenuHoldAtLeaderPosition}: Creates a menu to hold the escort at the client position. --- * @{#ESCORT.MenuScanForTargets}: Creates a menu so that the escort scans targets. --- * @{#ESCORT.MenuFlare}: Creates a menu to disperse flares. --- * @{#ESCORT.MenuSmoke}: Creates a menu to disparse smoke. --- * @{#ESCORT.MenuReportTargets}: Creates a menu so that the escort reports targets. --- * @{#ESCORT.MenuReportPosition}: Creates a menu so that the escort reports its current position from bullseye. --- * @{#ESCORT.MenuAssistedAttack: Creates a menu so that the escort supportes assisted attack from other escorts with the client. --- * @{#ESCORT.MenuROE: Creates a menu structure to set the rules of engagement of the escort. --- * @{#ESCORT.MenuEvasion: Creates a menu structure to set the evasion techniques when the escort is under threat. --- * @{#ESCORT.MenuResumeMission}: Creates a menu structure so that the escort can resume from a waypoint. --- --- -- @usage -- -- Declare a new EscortPlanes object as follows: -- @@ -111,25 +139,8 @@ -- -- -- Now use these 2 objects to construct the new EscortPlanes object. -- EscortPlanes = ESCORT:New( EscortClient, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." ) --- -- --- --- @module Escort --- @author FlightControl - ---- ESCORT class --- @type ESCORT --- @extends Core.Base#BASE --- @field Wrapper.Client#CLIENT EscortClient --- @field Wrapper.Group#GROUP EscortGroup --- @field #string EscortName --- @field #ESCORT.MODE EscortMode The mode the escort is in. --- @field Core.Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class. --- @field #number FollowDistance The current follow distance. --- @field #boolean ReportTargets If true, nearby targets are reported. --- @Field Dcs.DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup. --- @field Dcs.DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup. --- @field FunctionalMENU_GROUPDETECTION_BASE Detection +-- @field #ESCORT ESCORT = { ClassName = "ESCORT", EscortName = nil, -- The Escort Name @@ -295,7 +306,7 @@ end --- Defines a menu slot to let the escort Join and Follow you at a certain distance. -- This menu will appear under **Navigation**. -- @param #ESCORT self --- @param Dcs.DCSTypes#Distance Distance The distance in meters that the escort needs to follow the client. +-- @param DCS#Distance Distance The distance in meters that the escort needs to follow the client. -- @return #ESCORT function ESCORT:MenuFollowAt( Distance ) self:F(Distance) @@ -320,8 +331,8 @@ end --- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds. -- This menu will appear under **Hold position**. -- @param #ESCORT self --- @param Dcs.DCSTypes#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.DCSTypes#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 DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCS#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. -- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. -- @return #ESCORT -- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. @@ -381,8 +392,8 @@ end --- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds. -- This menu will appear under **Navigation**. -- @param #ESCORT self --- @param Dcs.DCSTypes#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.DCSTypes#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 DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCS#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. -- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. -- @return #ESCORT -- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function. @@ -442,8 +453,8 @@ end --- Defines a menu slot to let the escort scan for targets at a certain height for a certain time in seconds. -- This menu will appear under **Scan targets**. -- @param #ESCORT self --- @param Dcs.DCSTypes#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.DCSTypes#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 DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters. +-- @param DCS#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given. -- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed. -- @return #ESCORT function ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat ) @@ -567,7 +578,7 @@ end -- This menu will appear under **Report targets**. -- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed. -- @param #ESCORT self --- @param Dcs.DCSTypes#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. +-- @param DCS#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds. -- @return #ESCORT function ESCORT:MenuReportTargets( Seconds ) self:F( { Seconds } ) @@ -737,7 +748,7 @@ end -- @param Functional.Escort#ESCORT self -- @param Wrapper.Group#GROUP EscortGroup -- @param Wrapper.Client#CLIENT EscortClient --- @param Dcs.DCSTypes#Distance Distance +-- @param DCS#Distance Distance function ESCORT:JoinUpAndFollow( EscortGroup, EscortClient, Distance ) self:F( { EscortGroup, EscortClient, Distance } ) diff --git a/Moose Development/Moose/Functional/MissileTrainer.lua b/Moose Development/Moose/Functional/MissileTrainer.lua index 7659b3c8a..eb9c50e86 100644 --- a/Moose Development/Moose/Functional/MissileTrainer.lua +++ b/Moose Development/Moose/Functional/MissileTrainer.lua @@ -1,20 +1,27 @@ ---- **Functional** -- MISSILETRAINER helps you to train missile avoidance. +--- **Functional** -- Train missile defence and deflection. -- -- === -- --- 1) @{MissileTrainer#MISSILETRAINER} class, extends @{Base#BASE} +-- ## Features: +-- +-- * Track the missiles fired at you and other players, providing bearing and range information of the missiles towards the airplanes. +-- * Provide alerts of missile launches, including detailed information of the units launching, including bearing, range � +-- * Provide alerts when a missile would have killed your aircraft. +-- * Provide alerts when the missile self destructs. +-- * Enable / Disable and Configure the Missile Trainer using the various menu options. +-- -- === --- The @{#MISSILETRAINER} class uses the DCS world messaging system to be alerted of any missiles fired, and when a missile would hit your aircraft, +-- +-- ## Missions: +-- +-- [MIT - Missile Trainer](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/MIT%20-%20Missile%20Trainer) +-- +-- === +-- +-- Uses the MOOSE messaging system to be alerted of any missiles fired, and when a missile would hit your aircraft, -- the class will destroy the missile within a certain range, to avoid damage to your aircraft. --- It suports the following functionality: --- --- * Track the missiles fired at you and other players, providing bearing and range information of the missiles towards the airplanes. --- * Provide alerts of missile launches, including detailed information of the units launching, including bearing, range � --- * Provide alerts when a missile would have killed your aircraft. --- * Provide alerts when the missile self destructs. --- * Enable / Disable and Configure the Missile Trainer using the various menu options. -- --- When running a mission where MISSILETRAINER is used, the following radio menu structure ( 'Radio Menu' -> 'Other (F10)' -> 'MissileTrainer' ) options are available for the players: +-- When running a mission where the missile trainer is used, the following radio menu structure ( 'Radio Menu' -> 'Other (F10)' -> 'MissileTrainer' ) options are available for the players: -- -- * **Messages**: Menu to configure all messages. -- * **Messages On**: Show all messages. @@ -44,17 +51,40 @@ -- * **150 meter**: Destroys the missile when the distance to the aircraft is below or equal to 150 meter. -- * **200 meter**: Destroys the missile when the distance to the aircraft is below or equal to 200 meter. -- +-- === +-- +-- ### Authors: **FlightControl** +-- +-- ### Contributions: +-- +-- * **Stuka (Danny)**: Who you can search on the Eagle Dynamics Forums. Working together with Danny has resulted in the MISSILETRAINER class. +-- Danny has shared his ideas and together we made a design. +-- Together with the **476 virtual team**, we tested the MISSILETRAINER class, and got much positive feedback! +-- * **132nd Squadron**: Testing and optimizing the logic. +-- +-- === -- --- 1.1) MISSILETRAINER construction methods: --- ----------------------------------------- +-- @module Functional.MissileTrainer +-- @image Missile_Trainer.JPG + + +--- @type MISSILETRAINER +-- @field Core.Set#SET_CLIENT DBClients +-- @extends Core.Base#BASE + + +--- +-- +-- # Constructor: +-- -- Create a new MISSILETRAINER object with the @{#MISSILETRAINER.New} method: -- -- * @{#MISSILETRAINER.New}: Creates a new MISSILETRAINER object taking the maximum distance to your aircraft to evaluate when a missile needs to be destroyed. -- -- MISSILETRAINER will collect each unit declared in the mission with a skill level "Client" and "Player", and will monitor the missiles shot at those. -- --- 1.2) MISSILETRAINER initialization methods: --- ------------------------------------------- +-- # Initialization: +-- -- A MISSILETRAINER object will behave differently based on the usage of initialization methods: -- -- * @{#MISSILETRAINER.InitMessagesOnOff}: Sets by default the display of any message to be ON or OFF. @@ -67,24 +97,8 @@ -- * @{#MISSILETRAINER.InitRangeOnOff}: Sets by default the display of range information of missiles ON of OFF. -- * @{#MISSILETRAINER.InitBearingOnOff}: Sets by default the display of bearing information of missiles ON of OFF. -- * @{#MISSILETRAINER.InitMenusOnOff}: Allows to configure the options through the radio menu. --- --- === -- --- CREDITS --- === --- **Stuka (Danny)** Who you can search on the Eagle Dynamics Forums. --- Working together with Danny has resulted in the MISSILETRAINER class. --- Danny has shared his ideas and together we made a design. --- Together with the **476 virtual team**, we tested the MISSILETRAINER class, and got much positive feedback! --- --- @module MissileTrainer --- @author FlightControl - - ---- The MISSILETRAINER class --- @type MISSILETRAINER --- @field Core.Set#SET_CLIENT DBClients --- @extends Core.Base#BASE +-- @field #MISSILETRAINER MISSILETRAINER = { ClassName = "MISSILETRAINER", TrackingMissiles = {}, diff --git a/Moose Development/Moose/Functional/Movement.lua b/Moose Development/Moose/Functional/Movement.lua index a5d4d7058..7c004d4e2 100644 --- a/Moose Development/Moose/Functional/Movement.lua +++ b/Moose Development/Moose/Functional/Movement.lua @@ -1,4 +1,4 @@ ---- **Functional** -- Limit the MOVEMENT of simulaneous moving ground vehicles. +--- **Functional** -- Limit the movement of simulaneous moving ground vehicles. -- -- === -- @@ -7,11 +7,14 @@ -- Performance: If in a DCSRTE there are a lot of moving GROUND units, then in a multi player mission, this WILL create lag if -- the main DCS execution core of your CPU is fully utilized. So, this class will limit the amount of simultaneous moving GROUND units -- on defined intervals (currently every minute). --- @module Movement +-- @module Functional.Movement +-- @image MOOSE.JPG ---- the MOVEMENT class --- @type MOVEMENT +--- @type MOVEMENT -- @extends Core.Base#BASE + +--- +--@field #MOVEMENT MOVEMENT = { ClassName = "MOVEMENT", } diff --git a/Moose Development/Moose/Functional/Protect.lua b/Moose Development/Moose/Functional/Protect.lua deleted file mode 100644 index f8375b867..000000000 --- a/Moose Development/Moose/Functional/Protect.lua +++ /dev/null @@ -1,305 +0,0 @@ ---- **Functional** -- The PROTECT class handles the protection of objects, which can be zones, units, scenery. --- --- === --- --- ### Author: **FlightControl** --- ### Contributions: **MillerTime** --- --- === --- --- @module Protect - ---- @type PROTECT.__ Methods which are not intended for mission designers, but which are used interally by the moose designer :-) --- @extends Core.Fsm#FSM - ---- @type PROTECT --- @extends #PROTECT.__ - ---- # PROTECT, extends @{Base#BASE} --- --- @field #PROTECT -PROTECT = { - ClassName = "PROTECT", -} - ---- Get the ProtectZone --- @param #PROTECT self --- @return Core.Zone#ZONE_BASE -function PROTECT:GetProtectZone() - return self.ProtectZone -end - - ---- Get the name of the ProtectZone --- @param #PROTECT self --- @return #string -function PROTECT:GetProtectZoneName() - return self.ProtectZone:GetName() -end - - ---- Set the owning coalition of the zone. --- @param #PROTECT self --- @param DCSCoalition.DCSCoalition#coalition Coalition -function PROTECT:SetCoalition( Coalition ) - self.Coalition = Coalition -end - - ---- Get the owning coalition of the zone. --- @param #PROTECT self --- @return DCSCoalition.DCSCoalition#coalition Coalition. -function PROTECT:GetCoalition() - return self.Coalition -end - - ---- Get the owning coalition name of the zone. --- @param #PROTECT self --- @return #string Coalition name. -function PROTECT: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 "" -end - - -function PROTECT:IsGuarded() - - local IsGuarded = self.ProtectZone:IsAllInZoneOfCoalition( self.Coalition ) - self:F( { IsGuarded = IsGuarded } ) - return IsGuarded -end - -function PROTECT:IsCaptured() - - local IsCaptured = self.ProtectZone:IsAllInZoneOfOtherCoalition( self.Coalition ) - self:F( { IsCaptured = IsCaptured } ) - return IsCaptured -end - - -function PROTECT:IsAttacked() - - local IsAttacked = self.ProtectZone:IsSomeInZoneOfCoalition( self.Coalition ) - self:F( { IsAttacked = IsAttacked } ) - return IsAttacked -end - - -function PROTECT:IsEmpty() - - local IsEmpty = self.ProtectZone:IsNoneInZone() - self:F( { IsEmpty = IsEmpty } ) - return IsEmpty -end - - ---- Check if the units are still alive. --- @param #PROTECT self -function PROTECT:AreProtectUnitsAlive() - - local IsAlive = false - - local UnitSet = self.ProtectUnitSet - UnitSet:Flush( self ) - local UnitList = UnitSet:GetSet() - - for UnitID, ProtectUnit in pairs( UnitList ) do - local IsUnitAlive = ProtectUnit:IsAlive() - if IsUnitAlive == true then - IsAlive = true - break - end - end - - return IsAlive -end - ---- Check if the statics are still alive. --- @param #PROTECT self -function PROTECT:AreProtectStaticsAlive() - - local IsAlive = false - - local StaticSet = self.ProtectStaticSet - StaticSet:Flush( self ) - local StaticList = StaticSet:GetSet() - - for UnitID, ProtectStatic in pairs( StaticList ) do - local IsStaticAlive = ProtectStatic:IsAlive() - if IsStaticAlive == true then - IsAlive = true - break - end - end - - return IsAlive -end - - ---- Check if there is a capture unit in the zone. --- @param #PROTECT self -function PROTECT:IsCaptureUnitInZone() - - local CaptureUnitSet = self.CaptureUnitSet - CaptureUnitSet:Flush( self ) - - local IsInZone = self.CaptureUnitSet:IsPartiallyInZone( self.ProtectZone ) - - self:F({IsInZone = IsInZone}) - - return IsInZone -end - ---- Smoke. --- @param #PROTECT self --- @param #SMOKECOLOR.Color SmokeColor -function PROTECT:Smoke( SmokeColor ) - - self.SmokeColor = SmokeColor -end - - ---- Flare. --- @param #PROTECT self --- @param #SMOKECOLOR.Color FlareColor -function PROTECT:Flare( FlareColor ) - self.ProtectZone:FlareZone( FlareColor, math.random( 1, 360 ) ) -end - - ---- Mark. --- @param #PROTECT self -function PROTECT:Mark() - - local Coord = self.ProtectZone:GetCoordinate() - local ZoneName = self:GetProtectZoneName() - 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( "Guard Zone: " .. ZoneName .. "\nStatus: " .. State ) - self.MarkRed = Coord:MarkToCoalitionRed( "Capture Zone: " .. ZoneName .. "\nStatus: " .. State ) - else - self.MarkRed = Coord:MarkToCoalitionRed( "Guard Zone: " .. ZoneName .. "\nStatus: " .. State ) - self.MarkBlue = Coord:MarkToCoalitionBlue( "Capture Zone: " .. ZoneName .. "\nStatus: " .. State ) - end -end - - ---- Bound. --- @param #PROTECT self -function PROTECT:onafterStart() - - self:ScheduleRepeat( 5, 15, 0.1, nil, self.StatusCoalition, self ) - self:ScheduleRepeat( 5, 15, 0.1, nil, self.StatusZone, self ) - self:ScheduleRepeat( 10, 15, 0, nil, self.StatusSmoke, self ) -end - ---- Bound. --- @param #PROTECT self -function PROTECT: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 PROTECT:onenterCaptured() - - local NewCoalition = self.ProtectZone:GetCoalition() - self:F( { NewCoalition = NewCoalition } ) - self:SetCoalition( NewCoalition ) - - self:Mark() -end - - -function PROTECT:onenterEmpty() - - self:Mark() -end - - -function PROTECT:onenterAttacked() - - self:Mark() -end - - ---- Check status Coalition ownership. --- @param #PROTECT self -function PROTECT:StatusCoalition() - - self:F( { State = self:GetState() } ) - - self.ProtectZone:Scan() - - if self:IsGuarded() then - self:Guard() - else - if self:IsCaptured() then - self:Capture() - end - end -end - ---- Check status Zone. --- @param #PROTECT self -function PROTECT:StatusZone() - - self:F( { State = self:GetState() } ) - - self.ProtectZone:Scan() - - if self:IsAttacked() then - self:Attack() - else - if self:IsEmpty() then - self:Empty() - end - end -end - ---- Check status Smoke. --- @param #PROTECT self -function PROTECT:StatusSmoke() - - local CurrentTime = timer.getTime() - - if self.SmokeTime == nil or self.SmokeTime + 300 <= CurrentTime then - if self.SmokeColor then - self.ProtectZone:GetCoordinate():Smoke( self.SmokeColor ) - --self.SmokeColor = nil - self.SmokeTime = CurrentTime - end - end -end - - - - - diff --git a/Moose Development/Moose/Functional/PseudoATC.lua b/Moose Development/Moose/Functional/PseudoATC.lua new file mode 100644 index 000000000..bfb458078 --- /dev/null +++ b/Moose Development/Moose/Functional/PseudoATC.lua @@ -0,0 +1,994 @@ +--- **Functional** - Rudimentary ATC. +-- +-- ![Banner Image](..\Presentations\PSEUDOATC\PSEUDOATC_Main.jpg) +-- +-- ==== +-- +-- The pseudo ATC enhances the standard DCS ATC functions. +-- +-- In particular, a menu entry "Pseudo ATC" is created in the "F10 Other..." radiomenu. +-- +-- ## Features: +-- +-- * Weather report at nearby airbases and mission waypoints. +-- * Report absolute bearing and range to nearest airports and mission waypoints. +-- * Report current altitude AGL of own aircraft. +-- * Upon request, ATC reports altitude until touchdown. +-- * Works with static and dynamic weather. +-- * Player can select the unit system (metric or imperial) in which information is reported. +-- * All maps supported (Caucasus, NTTR, Normandy, Persian Gulf and all future maps). +-- +-- ==== +-- +-- # YouTube Channel +-- +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- +-- === +-- +-- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** +-- +-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) +-- +-- ==== +-- @module Functional.PseudoATC +-- @image Pseudo_ATC.JPG + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- PSEUDOATC class +-- @type PSEUDOATC +-- @field #string ClassName Name of the Class. +-- @field #table player Table comprising each player info. +-- @field #boolean Debug If true, print debug info to dcs.log file. +-- @field #number mdur Duration in seconds how low messages to the player are displayed. +-- @field #number mrefresh Interval in seconds after which the F10 menu is refreshed. E.g. by the closest airports. Default is 120 sec. +-- @field #number talt Interval in seconds between reporting altitude until touchdown. Default 3 sec. +-- @field #boolean chatty Display some messages on events like take-off and touchdown. +-- @field #boolean eventsmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. +-- @extends Core.Base#BASE + +--- Adds some rudimentary ATC functionality via the radio menu. +-- +-- Local weather reports can be requested for nearby airports and player's mission waypoints. +-- The weather report includes +-- +-- * QFE and QNH pressures, +-- * Temperature, +-- * Wind direction and strength. +-- +-- The list of airports is updated every 60 seconds. This interval can be adjusted by the function @{#PSEUDOATC.SetMenuRefresh}(*interval*). +-- +-- Likewise, absolute bearing and range to the close by airports and mission waypoints can be requested. +-- +-- The player can switch the unit system in which all information is displayed during the mission with the MOOSE settings radio menu. +-- The unit system can be set to either imperial or metric. Altitudes are reported in feet or meter, distances in kilometers or nautical miles, +-- temperatures in degrees Fahrenheit or Celsius and QFE/QNH pressues in inHg or mmHg. +-- Note that the pressures are also reported in hPa independent of the unit system setting. +-- +-- In bad weather conditions, the ATC can "talk you down", i.e. will continuously report your altitude on the final approach. +-- Default reporting time interval is 3 seconds. This can be adjusted via the @{#PSEUDOATC.SetReportAltInterval}(*interval*) function. +-- The reporting stops automatically when the player lands or can be stopped manually by clicking on the radio menu item again. +-- So the radio menu item acts as a toggle to switch the reporting on and off. +-- +-- ## Scripting +-- +-- Scripting is almost trivial. Just add the following two lines to your script: +-- +-- pseudoATC=PSEUDOATC:New() +-- pseudoATC:Start() +-- +-- +-- @field #PSEUDOATC +PSEUDOATC={ + ClassName = "PSEUDOATC", + player={}, + Debug=false, + mdur=30, + mrefresh=120, + talt=3, + chatty=true, + eventsmoose=true, +} + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Some ID to identify who we are in output of the DCS.log file. +-- @field #string id +PSEUDOATC.id="PseudoATC | " + +--- PSEUDOATC version. +-- @field #number version +PSEUDOATC.version="0.9.1" + +----------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO list +-- DONE: Add takeoff event. +-- DONE: Add user functions. + +----------------------------------------------------------------------------------------------------------------------------------------- + +--- PSEUDOATC contructor. +-- @param #PSEUDOATC self +-- @return #PSEUDOATC Returns a PSEUDOATC object. +function PSEUDOATC:New() + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) -- #PSEUDOATC + + -- Debug info + self:E(PSEUDOATC.id..string.format("PseudoATC version %s", PSEUDOATC.version)) + + -- Return object. + return self +end + +--- Starts the PseudoATC event handlers. +-- @param #PSEUDOATC self +function PSEUDOATC:Start() + self:F() + + -- Debug info + self:E(PSEUDOATC.id.."Starting PseudoATC") + + -- Handle events. + if self.eventsmoose then + self:T(PSEUDOATC.id.."Events are handled by MOOSE.") + self:HandleEvent(EVENTS.Birth, self._OnBirth) + self:HandleEvent(EVENTS.Land, self._PlayerLanded) + self:HandleEvent(EVENTS.Takeoff, self._PlayerTakeOff) + self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft) + self:HandleEvent(EVENTS.Crash, self._PlayerLeft) + --self:HandleEvent(EVENTS.Ejection, self._PlayerLeft) + --self:HandleEvent(EVENTS.PilotDead, self._PlayerLeft) + else + self:T(PSEUDOATC.id.."Events are handled by DCS.") + -- Events are handled directly by DCS. + world.addEventHandler(self) + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions + +--- Debug mode on. Send messages to everone. +-- @param #PSEUDOATC self +function PSEUDOATC:DebugOn() + self.Debug=true +end + +--- Debug mode off. This is the default setting. +-- @param #PSEUDOATC self +function PSEUDOATC:DebugOff() + self.Debug=false +end + +--- Chatty mode on. Display some messages on take-off and touchdown. +-- @param #PSEUDOATC self +function PSEUDOATC:ChattyOn() + self.chatty=true +end + +--- Chatty mode off. Don't display some messages on take-off and touchdown. +-- @param #PSEUDOATC self +function PSEUDOATC:ChattyOff() + self.chatty=false +end + +--- Set duration how long messages are displayed. +-- @param #PSEUDOATC self +-- @param #number duration Time in seconds. Default is 30 sec. +function PSEUDOATC:SetMessageDuration(duration) + self.mdur=duration or 30 +end + +--- Set time interval after which the F10 radio menu is refreshed. +-- @param #PSEUDOATC self +-- @param #number interval Interval in seconds. Default is every 120 sec. +function PSEUDOATC:SetMenuRefresh(interval) + self.mrefresh=interval or 120 +end + +--- Enable/disable event handling by MOOSE or DCS. +-- @param #PSEUDOATC self +-- @param #boolean switch If true, events are handled by MOOSE (default). If false, events are handled directly by DCS. +function PSEUDOATC:SetEventsMoose(switch) + self.eventsmoose=switch +end + +--- Set time interval for reporting altitude until touchdown. +-- @param #PSEUDOATC self +-- @param #number interval Interval in seconds. Default is every 3 sec. +function PSEUDOATC:SetReportAltInterval(interval) + self.talt=interval or 3 +end + +----------------------------------------------------------------------------------------------------------------------------------------- +-- Event Handling + +--- Event handler for suppressed groups. +--@param #PSEUDOATC self +--@param #table Event Event data table. Holds event.id, event.initiator and event.target etc. +function PSEUDOATC:onEvent(Event) + if Event == nil or Event.initiator == nil or Unit.getByName(Event.initiator:getName()) == nil then + return true + end + + local DCSiniunit = Event.initiator + local DCSplace = Event.place + local DCSsubplace = Event.subplace + + local EventData={} + local _playerunit=nil + local _playername=nil + + if Event.initiator then + EventData.IniUnitName = Event.initiator:getName() + EventData.IniDCSGroup = Event.initiator:getGroup() + EventData.IniGroupName = Event.initiator:getGroup():getName() + -- Get player unit and name. This returns nil,nil if the event was not fired by a player unit. And these are the only events we are interested in. + _playerunit, _playername = self:_GetPlayerUnitAndName(EventData.IniUnitName) + end + + if Event.place then + EventData.Place=Event.place + EventData.PlaceName=Event.place:getName() + end + if Event.subplace then + EventData.SubPlace=Event.subplace + EventData.SubPlaceName=Event.subplace:getName() + end + + -- Event info. + self:T3(PSEUDOATC.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id))) + self:T3(PSEUDOATC.id..string.format("EVENT: Ini unit = %s" , tostring(EventData.IniUnitName))) + self:T3(PSEUDOATC.id..string.format("EVENT: Ini group = %s" , tostring(EventData.IniGroupName))) + self:T3(PSEUDOATC.id..string.format("EVENT: Ini player = %s" , tostring(_playername))) + self:T3(PSEUDOATC.id..string.format("EVENT: Place = %s" , tostring(EventData.PlaceName))) + self:T3(PSEUDOATC.id..string.format("EVENT: SubPlace = %s" , tostring(EventData.SubPlaceName))) + + -- Event birth. + if Event.id == world.event.S_EVENT_BIRTH and _playername then + self:_OnBirth(EventData) + end + + -- Event takeoff. + if Event.id == world.event.S_EVENT_TAKEOFF and _playername and EventData.Place then + self:_PlayerTakeOff(EventData) + end + + -- Event land. + if Event.id == world.event.S_EVENT_LAND and _playername and EventData.Place then + self:_PlayerLanded(EventData) + end + + -- Event player left unit + if Event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT and _playername then + self:_PlayerLeft(EventData) + end + + -- Event crash ==> player left unit + if Event.id == world.event.S_EVENT_CRASH and _playername then + self:_PlayerLeft(EventData) + end + +--[[ + -- Event eject ==> player left unit + if Event.id == world.event.S_EVENT_EJECTION and _playername then + self:_PlayerLeft(EventData) + end + + -- Event pilot dead ==> player left unit + if Event.id == world.event.S_EVENT_PILOT_DEAD and _playername then + self:_PlayerLeft(EventData) + end +]] +end + +--- Function called my MOOSE event handler when a player enters a unit. +-- @param #PSEUDOATC self +-- @param Core.Event#EVENTDATA EventData +function PSEUDOATC:_OnBirth(EventData) + self:F({EventData=EventData}) + + -- Get unit and player. + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + -- Check if a player entered. + if _unit and _playername then + self:PlayerEntered(_unit) + end + +end + +--- Function called by MOOSE event handler when a player leaves a unit or dies. +-- @param #PSEUDOATC self +-- @param Core.Event#EVENTDATA EventData +function PSEUDOATC:_PlayerLeft(EventData) + self:F({EventData=EventData}) + + -- Get unit and player. + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + -- Check if a player left. + if _unit and _playername then + self:PlayerLeft(_unit) + end +end + +--- Function called by MOOSE event handler when a player landed. +-- @param #PSEUDOATC self +-- @param Core.Event#EVENTDATA EventData +function PSEUDOATC:_PlayerLanded(EventData) + self:F({EventData=EventData}) + + -- Get unit, player and place. + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + local _base=nil + local _baseName=nil + if EventData.place then + _base=EventData.place + _baseName=EventData.place:getName() + end +-- if EventData.subplace then +-- local _subPlace=EventData.subplace +-- local _subPlaceName=EventData.subplace:getName() +-- end + + -- Call landed function. + if _unit and _playername and _base then + self:PlayerLanded(_unit, _baseName) + end +end + +--- Function called by MOOSE/DCS event handler when a player took off. +-- @param #PSEUDOATC self +-- @param Core.Event#EVENTDATA EventData +function PSEUDOATC:_PlayerTakeOff(EventData) + self:F({EventData=EventData}) + + -- Get unit, player and place. + local _unitName=EventData.IniUnitName + local _unit,_playername=self:_GetPlayerUnitAndName(_unitName) + local _base=nil + local _baseName=nil + if EventData.place then + _base=EventData.place + _baseName=EventData.place:getName() + end + + -- Call take-off function. + if _unit and _playername and _base then + self:PlayerTakeOff(_unit, _baseName) + end +end + +----------------------------------------------------------------------------------------------------------------------------------------- +-- Event Functions + +--- Function called when a player enters a unit. +-- @param #PSEUDOATC self +-- @param Wrapper.Unit#UNIT unit Unit the player entered. +function PSEUDOATC:PlayerEntered(unit) + self:F2({unit=unit}) + + -- Get player info. + local group=unit:GetGroup() --Wrapper.Group#GROUP + local GID=group:GetID() + local GroupName=group:GetName() + local PlayerName=unit:GetPlayerName() + local UnitName=unit:GetName() + local CallSign=unit:GetCallsign() + + -- 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() + + -- Info message. + local text=string.format("Player %s entered unit %s of group %s (id=%d).", PlayerName, UnitName, GroupName, GID) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + + -- Create main F10 menu, i.e. "F10/Pseudo ATC" + self.player[GID].menu_main=missionCommands.addSubMenuForGroup(GID, "Pseudo ATC") + + -- Create/update list of nearby airports. + self:LocalAirports(GID) + + -- Create submenu of local airports. + self:MenuAirports(GID) + + -- Create submenu Waypoints. + self:MenuWaypoints(GID) + + -- 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) + +end + +--- Function called when a player has landed. +-- @param #PSEUDOATC self +-- @param Wrapper.Unit#UNIT unit Unit of player which has landed. +-- @param #string place Name of the place the player landed at. +function PSEUDOATC:PlayerLanded(unit, place) + self:F2({unit=unit, place=place}) + + -- Gather some information. + local group=unit:GetGroup() + local 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 + + -- Debug message. + local text=string.format("Player %s in unit %s of group %s (id=%d) landed at %s.", PlayerName, UnitName, GroupName, id, place) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + + -- Stop altitude reporting timer if its activated. + self:AltitudeTimerStop(id) + + -- Welcome message. + if place and self.chatty then + local text=string.format("Touchdown! Welcome to %s. Have a nice day!", place) + MESSAGE:New(text, self.mdur):ToGroup(group) + end + +end + +--- Function called when a player took off. +-- @param #PSEUDOATC self +-- @param Wrapper.Unit#UNIT unit Unit of player which has landed. +-- @param #string place Name of the place the player landed at. +function PSEUDOATC:PlayerTakeOff(unit, place) + self:F2({unit=unit, place=place}) + + -- Gather some information. + local group=unit:GetGroup() + local 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 + + -- 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) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text, 30):ToAllIf(self.Debug) + + -- Bye-Bye message. + if place and self.chatty then + local text=string.format("%s, %s, you are airborne. Have a safe trip!", place, CallSign) + MESSAGE:New(text, self.mdur):ToGroup(group) + end + +end + +--- Function called when a player leaves a unit or dies. +-- @param #PSEUDOATC self +-- @param Wrapper.Unit#UNIT unit Player unit which was left. +function PSEUDOATC:PlayerLeft(unit) + self:F({unit=unit}) + + -- Get id. + local group=unit:GetGroup() + local id=group:GetID() + + if self.player[id] then + + -- 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) + 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) + end + + -- Stop scheduler for reporting alt if it runs. + self:AltitudeTimerStop(id) + + -- Remove main menu. + if self.player[id].menu_main then + missionCommands.removeItem(self.player[id].menu_main) + end + + -- Remove player array. + self.player[id]=nil + + end +end + +----------------------------------------------------------------------------------------------------------------------------------------- +-- Menu Functions + +--- Refreshes all player menues. +-- @param #PSEUDOATC self. +-- @param #number id Group id of player unit. +function PSEUDOATC:MenuRefresh(id) + self:F({id=id}) + + -- Debug message. + local text=string.format("Refreshing menues for player %s in group %s.", self.player[id].playername, self.player[id].groupname) + self:T(PSEUDOATC.id..text) + MESSAGE:New(text,30):ToAllIf(self.Debug) + + -- Clear menu. + self:MenuClear(id) + + -- Create list of nearby airports. + self:LocalAirports(id) + + -- Create submenu Local Airports. + self:MenuAirports(id) + + -- Create submenu Waypoints etc. + self:MenuWaypoints(id) + +end + + +--- Clear player menus. +-- @param #PSEUDOATC self. +-- @param #number id Group id of player unit. +function PSEUDOATC:MenuClear(id) + self:F(id) + + -- Debug message. + local text=string.format("Clearing menus for player %s in group %s.", self.player[id].playername, self.player[id].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 + 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 + 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 + 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 + 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) + + -- Table for menu entries. + self.player[id].menu_airports=missionCommands.addSubMenuForGroup(id, "Local Airports", self.player[id].menu_main) + + local i=0 + for _,airport in pairs(self.player[id].airports) do + + i=i+1 + if i > 10 then + break -- Max 10 airports due to 10 menu items restriction. + end + + local name=airport.name + local d=airport.distance + local pos=AIRBASE:FindByName(name):GetCoordinate() + + --F10menu_ATC_airports[ID][name] = missionCommands.addSubMenuForGroup(ID, name, F10menu_ATC) + local submenu=missionCommands.addSubMenuForGroup(id, name, self.player[id].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) + + -- Debug message. + self:T(string.format(PSEUDOATC.id.."Creating airport menu item %s for ID %d", name, id)) + 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) + + -- Player unit and callsign. + local unit=self.player[id].unit --Wrapper.Unit#UNIT + local callsign=self.player[id].callsign + + -- Debug info. + self:T(PSEUDOATC.id..string.format("Creating waypoint menu for %s (ID %d).", callsign, id)) + + if #self.player[id].waypoints>0 then + + -- F10/PseudoATC/Waypoints + self.player[id].menu_waypoints=missionCommands.addSubMenuForGroup(id, "Waypoints", self.player[id].menu_main) + + local j=0 + for i, wp in pairs(self.player[id].waypoints) do + + -- Increase counter + j=j+1 + + if j>10 then + break -- max ten menu entries + end + + -- Position of Waypoint + local pos=COORDINATE:New(wp.x, wp.alt, wp.y) + local name=string.format("Waypoint %d", i-1) + + -- "F10/PseudoATC/Waypoints/Waypoint X" + local submenu=missionCommands.addSubMenuForGroup(id, name, self.player[id].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) + 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) +end + +----------------------------------------------------------------------------------------------------------------------------------------- +-- Reporting Functions + +--- 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 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}) + + -- Player unit system settings. + local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + + local text=string.format("Local weather at %s:\n", location) + + -- Get pressure in hPa. + local Pqnh=position:GetPressure(0) -- Get pressure at sea level. + local Pqfe=position:GetPressure() -- Get pressure at (land) height of position. + + -- Pressure conversion + local hPa2inHg=0.0295299830714 + local hPa2mmHg=0.7500615613030 + + -- Unit conversion. + local _Pqnh=string.format("%.2f inHg", Pqnh * hPa2inHg) + local _Pqfe=string.format("%.2f inHg", Pqfe * hPa2inHg) + if settings:IsMetric() then + _Pqnh=string.format("%.1f mmHg", Pqnh * hPa2mmHg) + _Pqfe=string.format("%.1f mmHg", Pqfe * hPa2mmHg) + end + + -- Message text. + text=text..string.format("QFE %.1f hPa = %s.\n", Pqfe, _Pqfe) + text=text..string.format("QNH %.1f hPa = %s.\n", Pqnh, _Pqnh) + + -- Get temperature at position in degrees Celsius. + local T=position:GetTemperature() + + -- Correct unit system. + local _T=string.format('%d°F', UTILS.CelciusToFarenheit(T)) + if settings:IsMetric() then + _T=string.format('%d°C', T) + end + + -- Message text. + local text=text..string.format("Temperature %s\n", _T) + + -- Get wind direction and speed. + local Dir,Vel=position:GetWind() + + -- Get Beaufort wind scale. + local Bn,Bd=UTILS.BeaufortScale(Vel) + + -- Formatted wind direction. + local Ds = string.format('%03d°', Dir) + + -- Velocity in player units. + local Vs=string.format("%.1f knots", UTILS.MpsToKnots(Vel)) + if settings:IsMetric() then + Vs=string.format('%.1f m/s', Vel) + end + + -- Message text. + local text=text..string.format("Wind from %s at %s (%s).", Ds, Vs, Bd) + + -- Send message + self:_DisplayMessageToGroup(self.player[id].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 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}) + + -- Current coordinates. + local unit=self.player[id].unit --Wrapper.Unit#UNIT + local coord=unit:GetCoordinate() + + -- Direction vector from current position (coord) to target (position). + local angle=coord:HeadingTo(position) + + -- Range from current to + local range=coord:Get2DDistance(position) + + -- Bearing string. + local Bs=string.format('%03d°', angle) + + -- Settings. + local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + + + local Rs=string.format("%.1f NM", UTILS.MetersToNM(range)) + if settings:IsMetric() then + Rs=string.format("%.1f km", range/1000) + end + + -- Message text. + local text=string.format("%s: Bearing %s, Range %s.", location, Bs, Rs) + + -- Send message to player group. + MESSAGE:New(text, self.mdur):ToGroup(self.player[id].group) +end + +--- Report altitude above ground level of player unit. +-- @param #PSEUDOATC self +-- @param #number id Group id to the report is delivered. +-- @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}) + + local dt = dt or self.mdur + if _clear==nil then + _clear=false + end + + -- Return height [m] above ground level. + local function get_AGL(p) + local agl=0 + local vec2={x=p.x,y=p.z} + local ground=land.getHeight(vec2) + local agl=p.y-ground + return agl + end + + -- Get height AGL. + local unit=self.player[id].unit --Wrapper.Unit#UNIT + + if unit and unit:IsAlive() then + + local position=unit:GetCoordinate() + local height=get_AGL(position) + local callsign=unit:GetCallsign() + + -- Settings. + local settings=_DATABASE:GetPlayerSettings(self.player[id].playername) or _SETTINGS --Core.Settings#SETTINGS + + -- Height string. + local Hs=string.format("%d ft", UTILS.MetersToFeet(height)) + if settings:IsMetric() then + Hs=string.format("%d m", height) + end + + -- Message text. + local _text=string.format("%s, your altitude is %s AGL.", callsign, Hs) + + -- Append flight level. + if _clear==false then + _text=_text..string.format(" FL%03d.", position.y/30.48) + end + + -- Send message to player group. + self:_DisplayMessageToGroup(self.player[id].unit,_text, dt,_clear) + + -- Return height + return height + end + + return 0 +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) + + if self.player[id].altimerid then + -- If the timer is on, we turn it off. + self:AltitudeTimerStop(id) + else + -- If the timer is off, we turn it on. + self:AltitudeTimeStart(id) + end +end + +--- Start altitude reporting scheduler. +-- @param #PSEUDOATC self. +-- @param #number id Group id of player unit. +function PSEUDOATC:AltitudeTimeStart(id) + self:F(id) + + -- Debug info. + self:T(PSEUDOATC.id..string.format("Starting altitude report timer for player ID %d.", id)) + + -- 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) +end + +--- Stop/destroy DCS scheduler function for reporting altitude. +-- @param #PSEUDOATC self. +-- @param #number id Group id of player unit. +function PSEUDOATC:AltitudeTimerStop(id) + + -- Debug info. + self:T(PSEUDOATC.id..string.format("Stopping altitude report timer for player ID %d.", id)) + + -- Stop timer. + if self.player[id].altimerid then + self.player[id].altimer:Stop(self.player[id].altimerid) + end + + self.player[id].altimer=nil + self.player[id].altimerid=nil +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc + +--- 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) + + -- Airports table. + self.player[id].airports=nil + self.player[id].airports={} + + -- Current player position. + local pos=self.player[id].unit:GetCoordinate() + + -- Loop over coalitions. + for i=0,2 do + + -- Get all airbases of coalition. + local airports=coalition.getAirbases(i) + + -- Loop over airbases + for _,airbase in pairs(airports) do + + local name=airbase:getName() + local q=AIRBASE:FindByName(name):GetCoordinate() + local d=q:Get2DDistance(pos) + + -- Add to table. + table.insert(self.player[id].airports, {distance=d, name=name}) + + end + end + + --- compare distance (for sorting airports) + local function compare(a,b) + return a.distance < b.distance + end + + -- Sort airports table w.r.t. distance to player. + table.sort(self.player[id].airports, compare) + +end + +--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. +-- @param #PSEUDOATC self +-- @param #string _unitName Name of the player unit. +-- @return Wrapper.Unit#UNIT Unit of player. +-- @return #string Name of the player. +-- @return nil If player does not exist. +function PSEUDOATC:_GetPlayerUnitAndName(_unitName) + self:F(_unitName) + + if _unitName ~= nil then + + -- Get DCS unit from its name. + local DCSunit=Unit.getByName(_unitName) + if DCSunit then + + -- Get the player name to make sure a player entered. + local playername=DCSunit:getPlayerName() + local unit=UNIT:Find(DCSunit) + + -- Debug output. + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) + + if unit and playername then + -- Return MOOSE unit and player name + return unit, playername + end + + end + end + + return nil,nil +end + + +--- Display message to group. +-- @param #PSEUDOATC self +-- @param Wrapper.Unit#UNIT _unit Player unit. +-- @param #string _text Message text. +-- @param #number _time Duration how long the message is displayed. +-- @param #boolean _clear Clear up old messages. +function PSEUDOATC:_DisplayMessageToGroup(_unit, _text, _time, _clear) + self:F({unit=_unit, text=_text, time=_time, clear=_clear}) + + _time=_time or self.Tmsg + if _clear==nil then + _clear=false + end + + -- Group ID. + local _gid=_unit:GetGroup():GetID() + + if _gid then + if _clear == true then + trigger.action.outTextForGroup(_gid, _text, _time, _clear) + else + trigger.action.outTextForGroup(_gid, _text, _time) + end + end + +end + +--- Returns a string which consits of this callsign and the player name. +-- @param #PSEUDOATC self +-- @param #string unitname Name of the player unit. +function PSEUDOATC:_myname(unitname) + self:F2(unitname) + + local unit=UNIT:FindByName(unitname) + local pname=unit:GetPlayerName() + local csign=unit:GetCallsign() + + return string.format("%s (%s)", csign, pname) +end + diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 04da94f98..9a1c9306b 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -1,38 +1,31 @@ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- **Functional** - (R2.2) - Create random airtraffic in your missions. +--- **Functional** - Create random airtraffic in your missions. -- -- === -- --- ![Banner Image](..\Presentations\RAT\RAT.png) --- --- === --- --- The aim of the RAT class is to fill the empty DCS world with randomized air traffic and bring more life to your airports. --- --- In particular, it is designed to spawn AI air units at random airports. These units will be assigned a random flight path to another random airport on the map. --- +-- The aim of the RAT class is to fill the empty DCS world with randomized air traffic and bring more life to your airports. +-- In particular, it is designed to spawn AI air units at random airports. These units will be assigned a random flight path to another random airport on the map. -- Even the mission designer will not know where aircraft will be spawned and which route they follow. -- --- ## Features +-- ## Features: -- --- * Very simple interface. Just one unit and two lines of Lua code needed to fill your map. --- * High degree of randomization. Aircraft will spawn at random airports, have random routes and random destinations. --- * Specific departure and/or destination airports can be chosen. --- * Departure and destination airports can be restricted by coalition. --- * Planes and helicopters supported. Helicopters can also be send to FARPs and ships. --- * Units can also be spawned in air within pre-defined zones of the map. --- * Aircraft will be removed when they arrive at their destination (or get stuck on the ground). --- * When a unit is removed a new unit with a different flight plan is respawned. --- * Aircraft can report their status during the route. --- * All of the above can be customized by the user if necessary. --- * All current (Caucasus, Nevada, Normandy) and future maps are supported. +-- * Very simple interface. Just one unit and two lines of Lua code needed to fill your map. +-- * High degree of randomization. Aircraft will spawn at random airports, have random routes and random destinations. +-- * Specific departure and/or destination airports can be chosen. +-- * Departure and destination airports can be restricted by coalition. +-- * Planes and helicopters supported. Helicopters can also be send to FARPs and ships. +-- * Units can also be spawned in air within pre-defined zones of the map. +-- * Aircraft will be removed when they arrive at their destination (or get stuck on the ground). +-- * When a unit is removed a new unit with a different flight plan is respawned. +-- * Aircraft can report their status during the route. +-- * All of the above can be customized by the user if necessary. +-- * All current (Caucasus, Nevada, Normandy, Persian Gulf) and future maps are supported. -- --- The RAT class creates an entry in the F10 menu which allows to +-- The RAT class creates an entry in the F10 radio menu which allows to: -- --- * Create new groups on-the-fly, i.e. at run time within the mission, --- * Destroy specific groups (e.g. if they get stuck or damaged and block a runway), --- * Request the status of all RAT aircraft or individual groups, --- * Place markers at waypoints on the F10 map for each group. +-- * Create new groups on-the-fly, i.e. at run time within the mission, +-- * Destroy specific groups (e.g. if they get stuck or damaged and block a runway), +-- * Request the status of all RAT aircraft or individual groups, +-- * Place markers at waypoints on the F10 map for each group. -- -- Note that by its very nature, this class is suited best for civil or transport aircraft. However, it also works perfectly fine for military aircraft of any kind. -- @@ -40,33 +33,33 @@ -- -- === -- --- # Demo Missions +-- ## Missions: -- --- ### [RAT Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/Release/RAT%20-%20Random%20Air%20Traffic) --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) +-- ### [RAT - Random Air Traffic](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/RAT%20-%20Random%20Air%20Traffic) -- -- === -- -- # YouTube Channel -- --- ### RAT videos are work in progress. --- ### [MOOSE YouTube Channel](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1jirWIo4t4YxqN-HxjqRkL) +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- ### [MOOSE - RAT - Random Air Traffic](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0u4Zxywtg-mx_ov4vi68CO) -- -- === -- -- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** -- --- ### Contributions: **Sven van de Velde ([FlightControl](https://forums.eagle.ru/member.php?u=89536))** +-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) -- -- === --- @module Rat +-- @module Functional.Rat +-- @image RAT.JPG ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- RAT class -- @type RAT -- @field #string ClassName Name of the Class. -- @field #boolean Debug Turn debug messages on or off. --- @field Core.Group#GROUP templategroup Group serving as template for the RAT aircraft. +-- @field Wrapper.Group#GROUP templategroup Group serving as template for the RAT aircraft. -- @field #string alias Alias for spawned group. -- @field #boolean spawninitialized If RAT:Spawn() was already called this RAT object is set to true to prevent users to call it again. -- @field #number spawndelay Delay time in seconds before first spawning happens. @@ -74,6 +67,7 @@ -- @field #number coalition Coalition of spawn group template. -- @field #number country Country of spawn group template. -- @field #string category Category of aircarft: "plane" or "heli". +-- @field #number groupsize Number of aircraft in group. -- @field #string friendly Possible departure/destination airport: all=blue+red+neutral, same=spawn+neutral, spawnonly=spawn, blue=blue+neutral, blueonly=blue, red=red+neutral, redonly=red. -- @field #table ctable Table with the valid coalitons from choice self.friendly. -- @field #table aircraft Table which holds the basic aircraft properties (speed, range, ...). @@ -114,23 +108,26 @@ -- @field #number FLminuser Minimum flight level set by user. -- @field #number FLmaxuser Maximum flight level set by user. -- @field #boolean commute Aircraft commute between departure and destination, i.e. when respawned the departure airport becomes the new destiation. +-- @field #boolean starshape If true, aircraft travel A-->B-->A-->C-->A-->D... for commute. +-- @field #string homebase Home base for commute and return zone. Aircraft will always return to this base but otherwise travel in a star shaped way. -- @field #boolean continuejourney Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination. -- @field #number ngroups Number of groups to be spawned in total. -- @field #number alive Number of groups which are alive. --- @field #boolean f10menu Add an F10 menu for RAT. +-- @field #boolean f10menu If true, add an F10 radiomenu for RAT. Default is false. -- @field #table Menu F10 menu items for this RAT object. -- @field #string SubMenuName Submenu name for RAT object. -- @field #boolean respawn_at_landing Respawn aircraft the moment they land rather than at engine shutdown. -- @field #boolean norespawn Aircraft will not be respawned after they have finished their route. -- @field #boolean respawn_after_takeoff Aircraft will be respawned directly after take-off. --- @field #number respawn_delay Delay in seconds until repawn happens after landing. +-- @field #boolean respawn_after_crash Aircraft will be respawned after a crash, e.g. when they get shot down. +-- @field #boolean respawn_inair Aircraft are allowed to spawned in air if they cannot be respawned on ground because there is not free parking spot. Default is true. +-- @field #number respawn_delay Delay in seconds until a repawn happens. -- @field #table markerids Array with marker IDs. -- @field #table waypointdescriptions Table with strings for waypoint descriptions of markers. -- @field #table waypointstatus Table with strings of waypoint status. -- @field #string livery Livery of the aircraft set by user. -- @field #string skill Skill of AI. -- @field #boolean ATCswitch Enable/disable ATC if set to true/false. --- @field #string parking_id String with a special parking ID for the aircraft. -- @field #boolean radio If true/false disables radio messages from the RAT groups. -- @field #number frequency Radio frequency used by the RAT groups. -- @field #string modulation Ratio modulation. Either "FM" or "AM". @@ -141,15 +138,21 @@ -- @field #number activate_delay Delay in seconds before first uncontrolled group is activated. Default is 5 seconds. -- @field #number activate_delta Time interval in seconds between activation of uncontrolled groups. Default is 5 seconds. -- @field #number activate_frand Randomization factor of time interval (activate_delta) between activating uncontrolled groups. Default is 0. --- @field #number activate_max=0 Maximal number of uncontrolle aircraft, which will be activated at a time. Default is 0 +-- @field #number activate_max Maximum number of uncontrolled aircraft, which will be activated at the same time. Default is 1. -- @field #string onboardnum Sets the onboard number prefix. Same as setting "TAIL #" in the mission editor. --- @field #number onboardnum0 (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is one. --- @field #number rbug_maxretry Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway. Default is 3. +-- @field #number onboardnum0 (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is 1. +-- @field #boolean checkonrunway Aircraft are checked if they were accidentally spawned on the runway. Default is true. +-- @field #number onrunwayradius Distance (in meters) from a runway spawn point until a unit is considered to have accidentally been spawned on a runway. Default is 75 m. +-- @field #number onrunwaymaxretry Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway. Default is 3. +-- @field #boolean checkontop Aircraft are checked if they were accidentally spawned on top of another unit. Default is true. +-- @field #number ontopradius Radius in meters until which a unit is considered to be on top of another. Default is 2 m. +-- @field Wrapper.Airbase#AIRBASE.TerminalType termtype Type of terminal to be used when spawning at an airbase. +-- @field #number parkingscanradius Radius in meters until which parking spots are scanned for obstacles like other units, statics or scenery. +-- @field #boolean parkingscanscenery If true, area around parking spots is scanned for scenery objects. Default is false. +-- @field #boolean parkingverysafe If true, parking spots are considered as non-free until a possible aircraft has left and taken off. Default false. -- @extends Core.Spawn#SPAWN ----# RAT class, extends @{Spawn#SPAWN} --- The RAT class implements an easy to use way to randomly fill your map with AI aircraft. --- +--- Implements an easy to use way to randomly fill your map with AI aircraft. -- -- ## Airport Selection -- @@ -213,6 +216,44 @@ -- Hence, the default flight plan for a RAT aircraft will be: Fly from airport A to B, get respawned at C and fly to D, get respawned at E and fly to F, ... -- This ensures that you always have a constant number of AI aircraft on your map. -- +-- ## Parking Problems +-- +-- One big issue in DCS is that not all aircraft can be spawned on every airport or airbase. In particular, bigger aircraft might not have a valid parking spot at smaller airports and +-- airstripes. This can lead to multiple problems in DCS. +-- +-- * Landing: When an aircraft tries to land at an airport where it does not have a valid parking spot, it is immidiately despawned the moment its wheels touch the runway, i.e. +-- when a landing event is triggered. This leads to the loss of the RAT aircraft. On possible way to circumvent the this problem is to let another RAT aircraft spawn at landing +-- and not when it shuts down its engines. See the @{RAT.RespawnAfterLanding}() function. +-- * Spawning: When a big aircraft is dynamically spawned on a small airbase a few things can go wrong. For example, it could be spawned at a parking spot with a shelter. +-- Or it could be damaged by a scenery object when it is taxiing out to the runway, or it could overlap with other aircraft on parking spots near by. +-- +-- You can check yourself if an aircraft has a valid parking spot at an airbase by dragging its group on the airport in the mission editor and set it to start from ramp. +-- If it stays at the airport, it has a valid parking spot, if it jumps to another airport, it does not have a valid parking spot on that airbase. +-- +-- ### Setting the Terminal Type +-- Each parking spot has a specific type depending on its size or if a helicopter spot or a shelter etc. The classification is not perfect but it is the best we have. +-- If you encounter problems described above, you can request a specific terminal type for the RAT aircraft. This can be done by the @{#RAT.SetTerminalType}(*terminaltype*) +-- function. The parameter *terminaltype* can be set as follows +-- +-- * AIRBASE.TerminalType.HelicopterOnly: Special spots for Helicopers. +-- * AIRBASE.TerminalType.Shelter: Hardened Air Shelter. Currently only on Caucaus map. +-- * AIRBASE.TerminalType.OpenMed: Open/Shelter air airplane only. +-- * AIRBASE.TerminalType.OpenBig: Open air spawn points. Generally larger but does not guarantee large aircraft are capable of spawning there. +-- * AIRBASE.TerminalType.OpenMedOrBig: Combines OpenMed and OpenBig spots. +-- * AIRBASE.TerminalType.HelicopterUnsable: Combines HelicopterOnly, OpenMed and OpenBig. +-- * AIRBASE.TerminalType.FighterAircraft: Combines Shelter, OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. +-- +-- So for example +-- c17=RAT:New("C-17") +-- c17:SetTerminalType(AIRBASE.TerminalType.OpenBig) +-- c17:Spawn(5) +-- +-- This would randomly spawn five C-17s but only on airports which have big open air parking spots. Note that also only destination airports are allowed +-- which do have this type of parking spot. This should ensure that the aircraft is able to land at the destination without beeing despawned immidiately. +-- +-- Also, the aircraft are spawned only on the requested parking spot types and not on any other type. If no parking spot of this type is availabe at the +-- moment of spawning, the group is automatically spawned in air above the selected airport. +-- -- ## Examples -- -- Here are a few examples, how you can modify the default settings of RAT class objects. @@ -303,6 +344,7 @@ RAT={ coalition = nil, -- Coalition of spawn group template. country = nil, -- Country of the group template. category = nil, -- Category of aircarft: "plane" or "heli". + groupsize=nil, -- Number of aircraft in the group. friendly = "same", -- Possible departure/destination airport: same=spawn+neutral, spawnonly=spawn, blue=blue+neutral, blueonly=blue, red=red+neutral, redonly=red, neutral. ctable = {}, -- Table with the valid coalitons from choice self.friendly. aircraft = {}, -- Table which holds the basic aircraft properties (speed, range, ...). @@ -343,23 +385,26 @@ RAT={ FLmaxuser=nil, -- Maximum flight level set by user. FLuser=nil, -- Flight level set by users explicitly. commute=false, -- Aircraft commute between departure and destination, i.e. when respawned the departure airport becomes the new destiation. + starshape=false, -- If true, aircraft travel A-->B-->A-->C-->A-->D... for commute. + homebase=nil, -- Home base for commute. continuejourney=false, -- Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination. alive=0, -- Number of groups which are alive. ngroups=nil, -- Number of groups to be spawned in total. - f10menu=true, -- Add an F10 menu for RAT. + f10menu=false, -- Add an F10 menu for RAT. Menu={}, -- F10 menu items for this RAT object. SubMenuName=nil, -- Submenu name for RAT object. respawn_at_landing=false, -- Respawn aircraft the moment they land rather than at engine shutdown. norespawn=false, -- Aircraft will not get respawned. respawn_after_takeoff=false, -- Aircraft will be respawned directly after takeoff. - respawn_delay=nil, -- Delay in seconds until repawn happens after landing. + respawn_after_crash=true, -- Aircraft will be respawned after a crash. + respawn_inair=true, -- Aircraft are spawned in air if there is no free parking spot on the ground. + respawn_delay=0, -- Delay in seconds until repawn happens after landing. markerids={}, -- Array with marker IDs. waypointdescriptions={}, -- Array with descriptions for waypoint markers. waypointstatus={}, -- Array with status info on waypoints. livery=nil, -- Livery of the aircraft. skill="High", -- Skill of AI. ATCswitch=true, -- Enable ATC. - parking_id=nil, -- Specific parking ID when aircraft are spawned at airports. radio=nil, -- If true/false disables radio messages from the RAT groups. frequency=nil, -- Radio frequency used by the RAT groups. modulation=nil, -- Ratio modulation. Either "FM" or "AM". @@ -371,10 +416,18 @@ RAT={ activate_delay=5, -- Delay in seconds before first uncontrolled group is activated. activate_delta=5, -- Time interval in seconds between activation of uncontrolled groups. activate_frand=0, -- Randomization factor of time interval (activate_delta) between activating uncontrolled groups. - activate_max=0, -- Max number of uncontrolle aircraft, which will be activated at a time. + activate_max=1, -- Max number of uncontrolle aircraft, which will be activated at a time. onboardnum=nil, -- Tail number. onboardnum0=1, -- (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is one. - rbug_maxretry=3, -- Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway. + checkonrunway=true, -- Check whether aircraft have been spawned on the runway. + onrunwayradius=75, -- Distance from a runway spawn point until a unit is considered to have accidentally been spawned on a runway. + onrunwaymaxretry=3, -- Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway. + checkontop=false, -- Check whether aircraft have been spawned on top of another unit. + ontopradius=2, -- Radius in meters until which a unit is considered to be on top of another. + termtype=nil, -- Terminal type. + parkingscanradius=40, -- Scan radius. + parkingscanscenery=false, -- Scan parking spots for scenery obstacles. + parkingverysafe=false, -- Very safe option. } ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -493,7 +546,7 @@ RAT.id="RAT | " --- RAT version. -- @list version RAT.version={ - version = "2.2.0", + version = "2.3.4", print = true, } @@ -533,14 +586,15 @@ RAT.version={ --DONE: Find way to respawn aircraft at same position where the last was despawned for commute and journey. --TODO: Check that same alias is not given twice. Need to store previous ones and compare. +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor New ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Create a new RAT object. -- @param #RAT self -- @param #string groupname Name of the group as defined in the mission editor. This group is serving as a template for all spawned units. -- @param #string alias (Optional) Alias of the group. This is and optional parameter but must(!) be used if the same template group is used for more than one RAT object. --- @return #RAT Object of RAT class. --- @return #nil If the group does not exist in the mission editor. +-- @return #RAT Object of RAT class or nil if the group does not exist in the mission editor. -- @usage yak1:RAT("RAT_YAK") will create a RAT object called "yak1". The template group in the mission editor must have the name "RAT_YAK". -- @usage yak2:RAT("RAT_YAK", "Yak2") will create a RAT object "yak2". The template group in the mission editor must have the name "RAT_YAK" but the group will be called "Yak2" in e.g. the F10 menu. function RAT:New(groupname, alias) @@ -556,7 +610,7 @@ function RAT:New(groupname, alias) end -- Welcome message. - self:F(RAT.id.."Creating new RAT object from template: "..groupname) + self:F(RAT.id..string.format("Creating new RAT object from template: %s.", groupname)) -- Set alias. alias=alias or groupname @@ -569,13 +623,16 @@ function RAT:New(groupname, alias) -- Check the group actually exists. if DCSgroup==nil then - self:E(RAT.id.."ERROR: Group with name "..groupname.." does not exist in the mission editor!") + self:E(RAT.id..string.format("ERROR: Group with name %s does not exist in the mission editor!", groupname)) return nil end -- Store template group. self.templategroup=GROUP:FindByName(groupname) + -- Get number of aircraft in group. + self.groupsize=self.templategroup:GetSize() + -- Set own coalition. self.coalition=DCSgroup:getCoalition() @@ -588,11 +645,14 @@ function RAT:New(groupname, alias) return self end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Spawn function ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Triggers the spawning of AI aircraft. Note that all additional options should be set before giving the spawn command. -- @param #RAT self -- @param #number naircraft (Optional) Number of aircraft to spawn. Default is one aircraft. +-- @return #boolean True if spawning was successful or nil if nothing was spawned. -- @usage yak:Spawn(5) will spawn five aircraft. By default aircraft will spawn at neutral and red airports if the template group is part of the red coaliton. function RAT:Spawn(naircraft) @@ -681,6 +741,7 @@ function RAT:Spawn(naircraft) end text=text..string.format("Min dist to destination: %4.1f\n", self.mindist) text=text..string.format("Max dist to destination: %4.1f\n", self.maxdist) + text=text..string.format("Terminal type: %s\n", tostring(self.termtype)) text=text..string.format("Takeoff type: %i\n", self.takeoff) text=text..string.format("Landing type: %i\n", self.landing) text=text..string.format("Commute: %s\n", tostring(self.commute)) @@ -689,12 +750,16 @@ function RAT:Spawn(naircraft) text=text..string.format("Return Zone: %s\n", tostring(self.returnzone)) text=text..string.format("Spawn delay: %4.1f\n", self.spawndelay) text=text..string.format("Spawn interval: %4.1f\n", self.spawninterval) + text=text..string.format("Respawn delay: %s\n", tostring(self.respawn_delay)) + text=text..string.format("Respawn off: %s\n", tostring(self.norespawn)) text=text..string.format("Respawn after landing: %s\n", tostring(self.respawn_at_landing)) - text=text..string.format("Respawning off: %s\n", tostring(self.norespawn)) text=text..string.format("Respawn after take-off: %s\n", tostring(self.respawn_after_takeoff)) - text=text..string.format("Respawn delay: %s\n", tostring(self.respawn_delay)) + text=text..string.format("Respawn after crash: %s\n", tostring(self.respawn_after_crash)) + text=text..string.format("Respawn in air: %s\n", tostring(self.respawn_inair)) text=text..string.format("ROE: %s\n", tostring(self.roe)) text=text..string.format("ROT: %s\n", tostring(self.rot)) + text=text..string.format("Immortal: %s\n", tostring(self.immortal)) + text=text..string.format("Invisible: %s\n", tostring(self.invisible)) text=text..string.format("Vclimb: %4.1f\n", self.Vclimb) text=text..string.format("AlphaDescent: %4.2f\n", self.AlphaDescent) text=text..string.format("Vcruisemax: %s\n", tostring(self.Vcruisemax)) @@ -713,12 +778,15 @@ function RAT:Spawn(naircraft) text=text..string.format("Radio frequency : %s\n", tostring(self.frequency)) text=text..string.format("Radio modulation : %s\n", tostring(self.frequency)) text=text..string.format("Tail # prefix : %s\n", tostring(self.onboardnum)) + text=text..string.format("Check on runway: %s\n", tostring(self.checkonrunway)) + text=text..string.format("Max respawn attempts: %s\n", tostring(self.onrunwaymaxretry)) + text=text..string.format("Check on top: %s\n", tostring(self.checkontop)) text=text..string.format("Uncontrolled: %s\n", tostring(self.uncontrolled)) if self.uncontrolled and self.activate_uncontrolled then + text=text..string.format("Uncontrolled max : %4.1f\n", self.activate_max) text=text..string.format("Uncontrolled delay: %4.1f\n", self.activate_delay) text=text..string.format("Uncontrolled delta: %4.1f\n", self.activate_delta) text=text..string.format("Uncontrolled frand: %4.1f\n", self.activate_frand) - text=text..string.format("Uncontrolled max : %4.1f\n", self.activate_max) end if self.livery then text=text..string.format("Available liveries:\n") @@ -752,142 +820,38 @@ function RAT:Spawn(naircraft) -- Handle events. self:HandleEvent(EVENTS.Birth, self._OnBirth) - self:HandleEvent(EVENTS.EngineStartup, self._EngineStartup) + self:HandleEvent(EVENTS.EngineStartup, self._OnEngineStartup) self:HandleEvent(EVENTS.Takeoff, self._OnTakeoff) self:HandleEvent(EVENTS.Land, self._OnLand) self:HandleEvent(EVENTS.EngineShutdown, self._OnEngineShutdown) - self:HandleEvent(EVENTS.Dead, self._OnDead) - self:HandleEvent(EVENTS.Crash, self._OnCrash) - -- TODO: add hit event? + self:HandleEvent(EVENTS.Dead, self._OnDeadOrCrash) + self:HandleEvent(EVENTS.Crash, self._OnDeadOrCrash) + self:HandleEvent(EVENTS.Hit, self._OnHit) + -- No groups should be spawned. if self.ngroups==0 then return nil - elseif self.uncontrolled then - for i=1,self.ngroups do - self:_SpawnWithRoute() - end - if self.activate_uncontrolled then - SCHEDULER:New(nil, self._ActivateUncontrolled, {self}, self.activate_delay, self.activate_delta, self.activate_frand) - end - else - SCHEDULER:New(nil, self._SpawnWithRoute, {self}, Tstart, dt, 0.0, Tstop) end + -- Start scheduled spawning. + SCHEDULER:New(nil, self._SpawnWithRoute, {self}, Tstart, dt, 0.0, Tstop) + + -- Start scheduled activation of uncontrolled groups. + if self.uncontrolled and self.activate_uncontrolled then + SCHEDULER:New(nil, self._ActivateUncontrolled, {self}, self.activate_delay, self.activate_delta, self.activate_frand) + end + + return true end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- Randomly activates an uncontrolled aircraft. --- @param #RAT self -function RAT:_ActivateUncontrolled() - if self.Debug then - env.info(RAT.id.."_ActivateUncontrolled") - end - - -- Spawn indices of uncontrolled inactive aircraft. - local idx={} - local rat={} - - -- Number of active aircraft. - local nactive=0 - - -- Loop over RAT groups and count the active ones. - for spawnindex,ratcraft in pairs(self.ratcraft) do - - local group=ratcraft.group --Wrapper.Group#GROUP - - if group and group:IsAlive() then - - if self.Debug then - local text=string.format("Spawnindex = %d, group name = %s, active = %s", spawnindex, ratcraft.group:GetName(), tostring(ratcraft.active)) - env.info(RAT.id..text) - end - - if ratcraft.active then - nactive=nactive+1 - else - table.insert(idx, spawnindex) - end - - end - end - - if self.Debug then - local text=string.format("Nactive = %d, Ninactive = %d, max active=%d", nactive, #idx, self.activate_max) - env.info(RAT.id..text) - end - - if #idx>0 and nactive Enable takeoff air. if self.Ndeparture_Zones>0 and self.takeoff~=RAT.wp.air then self.takeoff=RAT.wp.air - self:E(RAT.id.."ERROR: At least one zone defined as departure and takeoff is NOT set to air. Enabling air start!") + self:E(RAT.id..string.format("ERROR: At least one zone defined as departure and takeoff is NOT set to air. Enabling air start for RAT group %s!", self.alias)) end -- No airport and no zone specified. if self.Ndeparture_Airports==0 and self.Ndeparture_Zone==0 then self.random_departure=true - local text="No airports or zones found given in SetDeparture(). Enabling random departure airports!" + local text=string.format("No airports or zones found given in SetDeparture(). Enabling random departure airports for RAT group %s!", self.alias) self:E(RAT.id.."ERROR: "..text) MESSAGE:New(text, 30):ToAll() end @@ -997,13 +961,19 @@ function RAT:_CheckConsistency() end end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + --- Set the friendly coalitions from which the airports can be used as departure and destination. -- @param #RAT self -- @param #string friendly "same"=own coalition+neutral (default), "sameonly"=own coalition only, "neutral"=all neutral airports. -- Default is "same", so aircraft will use airports of the coalition their spawn template has plus all neutral airports. +-- @return #RAT RAT self object. -- @usage yak:SetCoalition("neutral") will spawn aircraft randomly on all neutral airports. -- @usage yak:SetCoalition("sameonly") will spawn aircraft randomly on airports belonging to the same coalition only as the template. function RAT:SetCoalition(friendly) + self:F2(friendly) if friendly:lower()=="sameonly" then self.friendly=RAT.coal.sameonly elseif friendly:lower()=="neutral" then @@ -1011,12 +981,23 @@ function RAT:SetCoalition(friendly) else self.friendly=RAT.coal.same end + return self end --- Set coalition of RAT group. You can make red templates blue and vice versa. +-- Note that a country is also set automatically if it has not done before via RAT:SetCountry. +-- +-- * For blue, the country is set to USA. +-- * For red, the country is set to RUSSIA. +-- * For neutral, the country is set to SWITZERLAND. +-- +-- This is important, since it is ultimately the COUNTRY that determines the coalition of the aircraft. +-- You can set the country explicitly via the RAT:SetCountry() function if necessary. -- @param #RAT self --- @param #string color Color of coalition, i.e. "red" or blue". +-- @param #string color Color of coalition, i.e. "red" or blue" or "neutral". +-- @return #RAT RAT self object. function RAT:SetCoalitionAircraft(color) + self:F2(color) if color:lower()=="blue" then self.coalition=coalition.side.BLUE if not self.country then @@ -1029,24 +1010,99 @@ function RAT:SetCoalitionAircraft(color) end elseif color:lower()=="neutral" then self.coalition=coalition.side.NEUTRAL + if not self.country then + self.country=country.id.SWITZERLAND + end end + return self end ---- Set country of RAT group. This overrules the coalition settings. +--- Set country of RAT group. +-- See [DCS_enum_country](https://wiki.hoggitworld.com/view/DCS_enum_country). +-- +-- This overrules the coalition settings. So if you want your group to be of a specific coalition, you have to set a country that is part of that coalition. -- @param #RAT self --- @param #number id DCS country enumerator ID. For example country.id.USA or country.id.RUSSIA. +-- @param DCS#country.id id DCS country enumerator ID. For example country.id.USA or country.id.RUSSIA. +-- @return #RAT RAT self object. function RAT:SetCountry(id) + self:F2(id) self.country=id + return self +end + +--- Set the terminal type the aircraft use when spawning at an airbase. See [DCS_func_getParking](https://wiki.hoggitworld.com/view/DCS_func_getParking). +-- Note that some additional terminal types have been introduced. Check @{Wrapper.Airbase#AIRBASE} class for details. +-- Also note that only airports which have this kind of terminal are possible departures and/or destinations. +-- @param #RAT self +-- @param Wrapper.Airbase#AIRBASE.TerminalType termtype Type of terminal. Use enumerator AIRBASE.TerminalType.XXX. +-- @return #RAT RAT self object. +-- +-- @usage +-- c17=RAT:New("C-17 BIG Plane") +-- c17:SetTerminalType(AIRBASE.TerminalType.OpenBig) -- Only very big parking spots are used. +-- c17:Spawn(5) +function RAT:SetTerminalType(termtype) + self:F2(termtype) + self.termtype=termtype + return self +end + +--- Set the scan radius around parking spots. Parking spot is considered to be occupied if any obstacle is found with the radius. +-- @param #RAT self +-- @param #number radius Radius in meters. Default 50 m. +-- @return #RAT RAT self object. +function RAT:SetParkingScanRadius(radius) + self:F2(radius) + self.parkingscanradius=radius or 50 + return self +end + +--- Enables scanning for scenery objects around parking spots which might block the spot. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetParkingScanSceneryON() + self:F2() + self.parkingscanscenery=true + return self +end + +--- Disables scanning for scenery objects around parking spots which might block the spot. This is also the default setting. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetParkingScanSceneryOFF() + self:F2() + self.parkingscanscenery=false + return self +end + +--- A parking spot is not free until a possible aircraft has left and taken off. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetParkingSpotSafeON() + self:F2() + self.parkingverysafe=true + return self +end + +--- A parking spot is free as soon as possible aircraft has left the place. This is the default. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetParkingSpotSafeOFF() + self:F2() + self.parkingverysafe=false + return self end --- Set takeoff type. Starting cold at airport, starting hot at airport, starting at runway, starting in the air. -- Default is "takeoff-coldorhot". So there is a 50% chance that the aircraft starts with cold engines and 50% that it starts with hot engines. -- @param #RAT self -- @param #string type Type can be "takeoff-cold" or "cold", "takeoff-hot" or "hot", "takeoff-runway" or "runway", "air". +-- @return #RAT RAT self object. -- @usage RAT:Takeoff("hot") will spawn RAT objects at airports with engines started. -- @usage RAT:Takeoff("cold") will spawn RAT objects at airports with engines off. -- @usage RAT:Takeoff("air") will spawn RAT objects in air over random airports or within pre-defined zones. function RAT:SetTakeoff(type) + self:F2(type) local _Type if type:lower()=="takeoff-cold" or type:lower()=="cold" then @@ -1062,15 +1118,59 @@ function RAT:SetTakeoff(type) end self.takeoff=_Type + + return self +end + +--- Set takeoff type cold. Aircraft will spawn at a parking spot with engines off. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetTakeoffCold() + self.takeoff=RAT.wp.cold + return self +end + +--- Set takeoff type to hot. Aircraft will spawn at a parking spot with engines on. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetTakeoffHot() + self.takeoff=RAT.wp.hot + return self +end + +--- Set takeoff type to runway. Aircraft will spawn directly on the runway. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetTakeoffRunway() + self.takeoff=RAT.wp.runway + return self +end + +--- Set takeoff type to cold or hot. Aircraft will spawn at a parking spot with 50:50 change of engines on or off. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetTakeoffColdOrHot() + self.takeoff=RAT.wp.coldorhot + return self +end + +--- Set takeoff type to air. Aircraft will spawn in the air. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:SetTakeoffAir() + self.takeoff=RAT.wp.air + return self end --- Set possible departure ports. This can be an airport or a zone defined in the mission editor. -- @param #RAT self -- @param #string departurenames Name or table of names of departure airports or zones. +-- @return #RAT RAT self object. -- @usage RAT:SetDeparture("Sochi-Adler") will spawn RAT objects at Sochi-Adler airport. -- @usage RAT:SetDeparture({"Sochi-Adler", "Gudauta"}) will spawn RAT aircraft radomly at Sochi-Adler or Gudauta airport. -- @usage RAT:SetDeparture({"Zone A", "Gudauta"}) will spawn RAT aircraft in air randomly within Zone A, which has to be defined in the mission editor, or within a zone around Gudauta airport. Note that this also requires RAT:takeoff("air") to be set. function RAT:SetDeparture(departurenames) + self:F2(departurenames) -- Random departure is deactivated now that user specified departure ports. self.random_departure=false @@ -1101,13 +1201,16 @@ function RAT:SetDeparture(departurenames) end + return self end --- Set name of destination airports or zones for the AI aircraft. -- @param #RAT self -- @param #string destinationnames Name of the destination airport or table of destination airports. +-- @return #RAT RAT self object. -- @usage RAT:SetDestination("Krymsk") makes all aircraft of this RAT oject fly to Krymsk airport. function RAT:SetDestination(destinationnames) + self:F2(destinationnames) -- Random departure is deactivated now that user specified departure ports. self.random_destination=false @@ -1138,76 +1241,105 @@ function RAT:SetDestination(destinationnames) end + return self end --- Destinations are treated as zones. Aircraft will not land but rather be despawned when they reach a random point in the zone. -- @param #RAT self +-- @return #RAT RAT self object. function RAT:DestinationZone() + self:F2() + -- Destination is a zone. Needs special care. self.destinationzone=true -- Landing type is "air" because we don't actually land at the airport. self.landing=RAT.wp.air + + return self end --- Aircraft will fly to a random point within a zone and then return to its departure airport or zone. -- @param #RAT self -function RAT:ReturnZone() +-- @return #RAT RAT self object. +function RAT:ReturnZone() + self:F2() -- Destination is a zone. Needs special care. self.returnzone=true + return self end --- Include all airports which lie in a zone as possible destinations. -- @param #RAT self -- @param Core.Zone#ZONE zone Zone in which the departure airports lie. Has to be a MOOSE zone. +-- @return #RAT RAT self object. function RAT:SetDestinationsFromZone(zone) + self:F2(zone) -- Random departure is deactivated now that user specified departure ports. self.random_destination=false -- Set zone. self.destination_Azone=zone + + return self end --- Include all airports which lie in a zone as possible destinations. -- @param #RAT self -- @param Core.Zone#ZONE zone Zone in which the destination airports lie. Has to be a MOOSE zone. +-- @return #RAT RAT self object. function RAT:SetDeparturesFromZone(zone) + self:F2(zone) + -- Random departure is deactivated now that user specified departure ports. self.random_departure=false -- Set zone. self.departure_Azone=zone + + return self end --- Add all friendly airports to the list of possible departures. -- @param #RAT self +-- @return #RAT RAT self object. function RAT:AddFriendlyAirportsToDepartures() + self:F2() self.addfriendlydepartures=true + return self end --- Add all friendly airports to the list of possible destinations -- @param #RAT self +-- @return #RAT RAT self object. function RAT:AddFriendlyAirportsToDestinations() + self:F2() self.addfriendlydestinations=true + return self end --- Airports, FARPs and ships explicitly excluded as departures and destinations. -- @param #RAT self -- @param #string ports Name or table of names of excluded airports. +-- @return #RAT RAT self object. function RAT:ExcludedAirports(ports) + self:F2(ports) if type(ports)=="string" then self.excluded_ports={ports} else self.excluded_ports=ports end + return self end --- Set skill of AI aircraft. Default is "High". -- @param #RAT self -- @param #string skill Skill, options are "Average", "Good", "High", "Excellent" and "Random". Parameter is case insensitive. +-- @return #RAT RAT self object. function RAT:SetAISkill(skill) + self:F2(skill) if skill:lower()=="average" then self.skill="Average" elseif skill:lower()=="good" then @@ -1219,141 +1351,312 @@ function RAT:SetAISkill(skill) else self.skill="High" end + return self end --- Set livery of aircraft. If more than one livery is specified in a table, the actually used one is chosen randomly from the selection. -- @param #RAT self -- @param #table skins Name of livery or table of names of liveries. +-- @return #RAT RAT self object. function RAT:Livery(skins) + self:F2(skins) if type(skins)=="string" then self.livery={skins} else self.livery=skins end + return self end --- Change aircraft type. This is a dirty hack which allows to change the aircraft type of the template group. -- Note that all parameters like cruise speed, climb rate, range etc are still taken from the template group which likely leads to strange behaviour. -- @param #RAT self -- @param #string actype Type of aircraft which is spawned independent of the template group. Use with care and expect problems! +-- @return #RAT RAT self object. function RAT:ChangeAircraft(actype) + self:F2(actype) self.actype=actype + return self end --- Aircraft will continue their journey from their destination. This means they are respawned at their destination and get a new random destination. -- @param #RAT self +-- @return #RAT RAT self object. function RAT:ContinueJourney() + self:F2() self.continuejourney=true self.commute=false + return self end --- Aircraft will commute between their departure and destination airports or zones. -- @param #RAT self -function RAT:Commute() +-- @param #boolean starshape If true, keep homebase, i.e. travel A-->B-->A-->C-->A-->D... instead of A-->B-->A-->B-->A... +-- @return #RAT RAT self object. +function RAT:Commute(starshape) + self:F2() self.commute=true self.continuejourney=false + if starshape then + self.starshape=starshape + else + self.starshape=false + end + return self end --- Set the delay before first group is spawned. -- @param #RAT self -- @param #number delay Delay in seconds. Default is 5 seconds. Minimum delay is 0.5 seconds. +-- @return #RAT RAT self object. function RAT:SetSpawnDelay(delay) + self:F2(delay) delay=delay or 5 self.spawndelay=math.max(0.5, delay) + return self end --- Set the interval between spawnings of the template group. -- @param #RAT self -- @param #number interval Interval in seconds. Default is 5 seconds. Minimum is 0.5 seconds. +-- @return #RAT RAT self object. function RAT:SetSpawnInterval(interval) + self:F2(interval) interval=interval or 5 self.spawninterval=math.max(0.5, interval) + return self end --- Make aircraft respawn the moment they land rather than at engine shut down. -- @param #RAT self --- @param #number delay (Optional) Delay in seconds until respawn happens after landing. Default is 180 seconds. Minimum is 0.5 seconds. +-- @param #number delay (Optional) Delay in seconds until respawn happens after landing. Default is 180 seconds. Minimum is 1.0 seconds. +-- @return #RAT RAT self object. function RAT:RespawnAfterLanding(delay) + self:F2(delay) delay = delay or 180 self.respawn_at_landing=true - delay=math.max(0.5, delay) + delay=math.max(1.0, delay) self.respawn_delay=delay + return self +end + +--- Sets the delay between despawning and respawning aircraft. +-- @param #RAT self +-- @param #number delay Delay in seconds until respawn happens. Default is 1 second. Minimum is 1 second. +-- @return #RAT RAT self object. +function RAT:SetRespawnDelay(delay) + self:F2(delay) + delay = delay or 1.0 + delay=math.max(1.0, delay) + self.respawn_delay=delay + return self end --- Aircraft will not get respawned when they finished their route. -- @param #RAT self +-- @return #RAT RAT self object. function RAT:NoRespawn() + self:F2() self.norespawn=true + return self end --- Number of tries to respawn an aircraft in case it has accitentally been spawned on runway. -- @param #RAT self -- @param #number n Number of retries. Default is 3. +-- @return #RAT RAT self object. function RAT:SetMaxRespawnTriedWhenSpawnedOnRunway(n) + self:F2(n) n=n or 3 - self.rbug_maxretry=n + self.onrunwaymaxretry=n + return self end --- Aircraft will be respawned directly after take-off. -- @param #RAT self +-- @return #RAT RAT self object. function RAT:RespawnAfterTakeoff() + self:F2() self.respawn_after_takeoff=true + return self end ---- Set parking id of aircraft. +--- Aircraft will be respawned after they crashed or get shot down. This is the default behavior. -- @param #RAT self --- @param #string id Parking ID of the aircraft. -function RAT:SetParkingID(id) - self.parking_id=id - self:T(RAT.id.."Setting parking ID to "..self.parking_id) +-- @return #RAT RAT self object. +function RAT:RespawnAfterCrashON() + self:F2() + self.respawn_after_crash=true + return self +end + +--- Aircraft will not be respawned after they crashed or get shot down. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:RespawnAfterCrashOFF() + self:F2() + self.respawn_after_crash=false + return self +end + +--- If aircraft cannot be spawned on parking spots, it is allowed to spawn them in air above the same airport. Note that this is also the default behavior. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:RespawnInAirAllowed() + self:F2() + self.respawn_inair=true + return self +end + +--- If aircraft cannot be spawned on parking spots, it is NOT allowed to spawn them in air. This has only impact if aircraft are supposed to be spawned on the ground (and not in a zone). +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:RespawnInAirNotAllowed() + self:F2() + self.respawn_inair=false + return self +end + +--- Check if aircraft have accidentally been spawned on the runway. If so they will be removed immediatly. +-- @param #RAT self +-- @param #boolean switch If true, check is performed. If false, this check is omitted. +-- @param #number radius Distance in meters until a unit is considered to have spawned accidentally on the runway. Default is 75 m. +-- @return #RAT RAT self object. +function RAT:CheckOnRunway(switch, distance) + self:F2(switch) + if switch==nil then + switch=true + end + self.checkonrunway=switch + self.onrunwayradius=distance or 75 + return self +end + +--- Check if aircraft have accidentally been spawned on top of each other. If yes, they will be removed immediately. +-- @param #RAT self +-- @param #boolean switch If true, check is performed. If false, this check is omitted. +-- @param #number radius Radius in meters until which a unit is considered to be on top of each other. Default is 2 m. +-- @return #RAT RAT self object. +function RAT:CheckOnTop(switch, radius) + self:F2(switch) + if switch==nil then + switch=true + end + self.checkontop=switch + self.ontopradius=radius or 2 + return self +end + +--- Put parking spot coordinates in a data base for future use of aircraft. (Obsolete! API function will be removed soon.) +-- @param #RAT self +-- @param #boolean switch If true, parking spots are memorized. This is also the default setting. +-- @return #RAT RAT self object. +function RAT:ParkingSpotDB(switch) + self:E("RAT ParkingSpotDB function is obsolete and will be removed soon!") + return self end --- Enable Radio. Overrules the ME setting. -- @param #RAT self +-- @return #RAT RAT self object. function RAT:RadioON() + self:F2() self.radio=true + return self end --- Disable Radio. Overrules the ME setting. -- @param #RAT self +-- @return #RAT RAT self object. function RAT:RadioOFF() + self:F2() self.radio=false + return self end --- Set radio frequency. -- @param #RAT self -- @param #number frequency Radio frequency. +-- @return #RAT RAT self object. function RAT:RadioFrequency(frequency) + self:F2(frequency) self.frequency=frequency + return self end ---- Spawn aircraft in uncontrolled state. Aircraft will only sit at their parking spots. They can be activated randomly by the RAT:ActivateUncontrolled() function. +--- Set radio modulation. Default is AM. -- @param #RAT self -function RAT:Uncontrolled() - self.uncontrolled=true +-- @param #string modulation Either "FM" or "AM". If no value is given, modulation is set to AM. +-- @return #RAT RAT self object. +function RAT:RadioModulation(modulation) + self:F2(modulation) + if modulation=="AM" then + self.modulation=radio.modulation.AM + elseif modulation=="FM" then + self.modulation=radio.modulation.FM + else + self.modulation=radio.modulation.AM + end + return self +end + +--- Radio menu On. Default is off. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:RadioMenuON() + self:F2() + self.f10menu=true + return self +end + +--- Radio menu Off. This is the default setting. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:RadioMenuOFF() + self:F2() + self.f10menu=false + return self end --- Aircraft are invisible. -- @param #RAT self +-- @return #RAT RAT self object. function RAT:Invisible() + self:F2() self.invisible=true + return self end --- Aircraft are immortal. -- @param #RAT self +-- @return #RAT RAT self object. function RAT:Immortal() + self:F2() self.immortal=true + return self +end + +--- Spawn aircraft in uncontrolled state. Aircraft will only sit at their parking spots. They can be activated randomly by the RAT:ActivateUncontrolled() function. +-- @param #RAT self +-- @return #RAT RAT self object. +function RAT:Uncontrolled() + self:F2() + self.uncontrolled=true + return self end --- Activate uncontrolled aircraft. -- @param #RAT self +-- @param #number maxactivated Maximal numnber of activated aircraft. Absolute maximum will be the number of spawned groups. Default is 1. -- @param #number delay Time delay in seconds before (first) aircraft is activated. Default is 1 second. -- @param #number delta Time difference in seconds before next aircraft is activated. Default is 1 second. -- @param #number frand Factor [0,...,1] for randomization of time difference between aircraft activations. Default is 0, i.e. no randomization. --- @param #number maxactivated Maximal numnber of activated aircraft. Absolute maximum will be the number of spawned groups. -function RAT:ActivateUncontrolled(delay,delta,frand,maxactivated) +-- @return #RAT RAT self object. +function RAT:ActivateUncontrolled(maxactivated, delay, delta, frand) + self:F2({max=maxactivated, delay=delay, delta=delta, rand=frand}) self.activate_uncontrolled=true + self.activate_max=maxactivated or 1 self.activate_delay=delay or 1 self.activate_delta=delta or 1 self.activate_frand=frand or 0 @@ -1368,63 +1671,64 @@ function RAT:ActivateUncontrolled(delay,delta,frand,maxactivated) self.activate_frand=math.max(self.activate_frand,0) self.activate_frand=math.min(self.activate_frand,1) - self.activate_max=maxactivated -end - ---- Set radio modulation. Default is AM. --- @param #RAT self --- @param #string modulation Either "FM" or "AM". If no value is given, modulation is set to AM. -function RAT:RadioModulation(modulation) - if modulation=="AM" then - self.modulation=radio.modulation.AM - elseif modulation=="FM" then - self.modulation=radio.modulation.FM - else - self.modulation=radio.modulation.AM - end + return self end --- Set the time after which inactive groups will be destroyed. -- @param #RAT self -- @param #number time Time in seconds. Default is 600 seconds = 10 minutes. Minimum is 60 seconds. +-- @return #RAT RAT self object. function RAT:TimeDestroyInactive(time) + self:F2(time) time=time or self.Tinactive time=math.max(time, 60) self.Tinactive=time + return self end --- Set the maximum cruise speed of the aircraft. -- @param #RAT self -- @param #number speed Speed in km/h. +-- @return #RAT RAT self object. function RAT:SetMaxCruiseSpeed(speed) + self:F2(speed) -- Convert to m/s. self.Vcruisemax=speed/3.6 + return self end --- Set the climb rate. This automatically sets the climb angle. -- @param #RAT self -- @param #number rate Climb rate in ft/min. Default is 1500 ft/min. Minimum is 100 ft/min. Maximum is 15,000 ft/min. +-- @return #RAT RAT self object. function RAT:SetClimbRate(rate) + self:F2(rate) rate=rate or self.Vclimb rate=math.max(rate, 100) rate=math.min(rate, 15000) self.Vclimb=rate + return self end --- Set the angle of descent. Default is 3.6 degrees, which corresponds to 3000 ft descent after one mile of travel. -- @param #RAT self -- @param #number angle Angle of descent in degrees. Minimum is 0.5 deg. Maximum 50 deg. +-- @return #RAT RAT self object. function RAT:SetDescentAngle(angle) + self:F2(angle) angle=angle or self.AlphaDescent angle=math.max(angle, 0.5) angle=math.min(angle, 50) self.AlphaDescent=angle + return self end --- Set rules of engagement (ROE). Default is weapon hold. This is a peaceful class. -- @param #RAT self -- @param #string roe "hold" = weapon hold, "return" = return fire, "free" = weapons free. +-- @return #RAT RAT self object. function RAT:SetROE(roe) + self:F2(roe) if roe=="return" then self.roe=RAT.ROE.returnfire elseif roe=="free" then @@ -1432,12 +1736,15 @@ function RAT:SetROE(roe) else self.roe=RAT.ROE.weaponhold end + return self end --- Set reaction to threat (ROT). Default is no reaction, i.e. aircraft will simply ignore all enemies. -- @param #RAT self -- @param #string rot "noreaction" = no reaction to threats, "passive" = passive defence, "evade" = evade enemy attacks. +-- @return #RAT RAT self object. function RAT:SetROT(rot) + self:F2(rot) if rot=="passive" then self.rot=RAT.ROT.passive elseif rot=="evade" then @@ -1445,171 +1752,232 @@ function RAT:SetROT(rot) else self.rot=RAT.ROT.noreaction end + return self end --- Set the name of the F10 submenu. Default is the name of the template group. -- @param #RAT self -- @param #string name Submenu name. +-- @return #RAT RAT self object. function RAT:MenuName(name) + self:F2(name) self.SubMenuName=tostring(name) + return self end --- Enable ATC, which manages the landing queue for RAT aircraft if they arrive simultaniously at the same airport. -- @param #RAT self --- @param #boolean switch Enable ATC (true) or Disable ATC (false). No argument means ATC enabled. +-- @param #boolean switch Enable ATC (true) or Disable ATC (false). No argument means ATC enabled. +-- @return #RAT RAT self object. function RAT:EnableATC(switch) + self:F2(switch) if switch==nil then switch=true end self.ATCswitch=switch + return self end --- Turn messages from ATC on or off. Default is on. This setting effects all RAT objects and groups! -- @param #RAT self --- @param #boolean switch Enable (true) or disable (false) messages from ATC. +-- @param #boolean switch Enable (true) or disable (false) messages from ATC. +-- @return #RAT RAT self object. function RAT:ATC_Messages(switch) + self:F2(switch) if switch==nil then switch=true end RAT.ATC.messages=switch + return self end --- Max number of planes that get landing clearance of the RAT ATC. This setting effects all RAT objects and groups! -- @param #RAT self -- @param #number n Number of aircraft that are allowed to land simultaniously. Default is 2. +-- @return #RAT RAT self object. function RAT:ATC_Clearance(n) + self:F2(n) RAT.ATC.Nclearance=n or 2 + return self end --- Delay between granting landing clearance for simultanious landings. This setting effects all RAT objects and groups! -- @param #RAT self -- @param #number time Delay time when the next aircraft will get landing clearance event if the previous one did not land yet. Default is 240 sec. +-- @return #RAT RAT self object. function RAT:ATC_Delay(time) + self:F2(time) RAT.ATC.delay=time or 240 + return self end --- Set minimum distance between departure and destination. Default is 5 km. -- Minimum distance should not be smaller than maybe ~500 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) + return self end --- Set maximum distance between departure and destination. Default is 5000 km but aircarft range is also taken into account automatically. -- @param #RAT self -- @param #number dist Distance in km. +-- @return #RAT RAT self object. function RAT:SetMaxDistance(dist) + self:F2(dist) -- Distance in meters. self.maxdist=dist*1000 + return self end --- Turn debug messages on or off. Default is off. -- @param #RAT self -- @param #boolean switch Turn debug on=true or off=false. No argument means on. +-- @return #RAT RAT self object. function RAT:_Debug(switch) + self:F2(switch) if switch==nil then switch=true end self.Debug=switch + return self end --- Enable debug mode. More output in dcs.log file and onscreen messages to all. -- @param #RAT self +-- @return #RAT RAT self object. function RAT:Debugmode() + self:F2() self.Debug=true + return self end --- Aircraft report status update messages along the route. -- @param #RAT self -- @param #boolean switch Swtich reports on (true) or off (false). No argument is on. +-- @return #RAT RAT self object. function RAT:StatusReports(switch) + self:F2(switch) if switch==nil then switch=true end self.reportstatus=switch + return self end --- Place markers of waypoints on the F10 map. Default is off. -- @param #RAT self -- @param #boolean switch true=yes, false=no. +-- @return #RAT RAT self object. function RAT:PlaceMarkers(switch) + self:F2(switch) if switch==nil then switch=true end self.placemarkers=switch + return self end --- Set flight level. Setting this value will overrule all other logic. Aircraft will try to fly at this height regardless. -- @param #RAT self -- @param #number FL Fight Level in hundrets of feet. E.g. FL200 = 20000 ft ASL. +-- @return #RAT RAT self object. function RAT:SetFL(FL) + self:F2(FL) FL=FL or self.FLcruise FL=math.max(FL,0) self.FLuser=FL*RAT.unit.FL2m + return self end --- Set max flight level. Setting this value will overrule all other logic. Aircraft will try to fly at less than this FL regardless. -- @param #RAT self -- @param #number FL Maximum Fight Level in hundrets of feet. +-- @return #RAT RAT self object. function RAT:SetFLmax(FL) + self:F2(FL) self.FLmaxuser=FL*RAT.unit.FL2m + return self end --- Set max cruising altitude above sea level. -- @param #RAT self -- @param #number alt Altitude ASL in meters. +-- @return #RAT RAT self object. function RAT:SetMaxCruiseAltitude(alt) + self:F2(alt) self.FLmaxuser=alt + return self end --- Set min flight level. Setting this value will overrule all other logic. Aircraft will try to fly at higher than this FL regardless. -- @param #RAT self -- @param #number FL Maximum Fight Level in hundrets of feet. +-- @return #RAT RAT self object. function RAT:SetFLmin(FL) + self:F2(FL) self.FLminuser=FL*RAT.unit.FL2m + return self end --- Set min cruising altitude above sea level. -- @param #RAT self -- @param #number alt Altitude ASL in meters. +-- @return #RAT RAT self object. function RAT:SetMinCruiseAltitude(alt) + self:F2(alt) self.FLminuser=alt + return self end --- Set flight level of cruising part. This is still be checked for consitancy with selected route and prone to radomization. -- Default is FL200 for planes and FL005 for helicopters. -- @param #RAT self -- @param #number FL Flight level in hundrets of feet. E.g. FL200 = 20000 ft ASL. +-- @return #RAT RAT self object. function RAT:SetFLcruise(FL) + self:F2(FL) self.FLcruise=FL*RAT.unit.FL2m + return self end --- Set cruising altitude. This is still be checked for consitancy with selected route and prone to radomization. -- @param #RAT self -- @param #number alt Cruising altitude ASL in meters. +-- @return #RAT RAT self object. function RAT:SetCruiseAltitude(alt) + self:F2(alt) self.FLcruise=alt + return self end --- Set onboard number prefix. Same as setting "TAIL #" in the mission editor. Note that if you dont use this function, the values defined in the template group of the ME are taken. -- @param #RAT self -- @param #string tailnumprefix String of the tail number prefix. If flight consists of more than one aircraft, two digits are appended automatically, i.e. 001, 002, ... -- @param #number zero (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is 0. +-- @return #RAT RAT self object. function RAT:SetOnboardNum(tailnumprefix, zero) + self:F2({tailnumprefix=tailnumprefix, zero=zero}) self.onboardnum=tailnumprefix if zero ~= nil then self.onboardnum0=zero end + return self end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Private functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Initialize basic parameters of the aircraft based on its (template) group in the mission editor. -- @param #RAT self --- @param Dcs.DCSWrapper.Group#Group DCSgroup Group of the aircraft in the mission editor. +-- @param DCS#Group DCSgroup Group of the aircraft in the mission editor. function RAT:_InitAircraft(DCSgroup) + self:F2(DCSgroup) local DCSunit=DCSgroup:getUnit(1) local DCSdesc=DCSunit:getDesc() @@ -1647,6 +2015,15 @@ function RAT:_InitAircraft(DCSgroup) -- service ceiling in meters self.aircraft.ceiling=DCSdesc.Hmax + -- Store all descriptors. + --self.aircraft.descriptors=DCSdesc + + -- aircraft dimensions + self.aircraft.length=DCSdesc.box.max.x + self.aircraft.height=DCSdesc.box.max.y + self.aircraft.width=DCSdesc.box.max.z + self.aircraft.box=math.max(self.aircraft.length,self.aircraft.width) + -- info message local text=string.format("\n******************************************************\n") text=text..string.format("Aircraft parameters:\n") @@ -1654,6 +2031,9 @@ function RAT:_InitAircraft(DCSgroup) text=text..string.format("Alias = %s\n", self.alias) text=text..string.format("Category = %s\n", self.category) text=text..string.format("Type = %s\n", self.aircraft.type) + text=text..string.format("Length (x) = %6.1f m\n", self.aircraft.length) + text=text..string.format("Width (z) = %6.1f m\n", self.aircraft.width) + text=text..string.format("Height (y) = %6.1f m\n", self.aircraft.height) text=text..string.format("Max air speed = %6.1f m/s\n", self.aircraft.Vmax) text=text..string.format("Max climb speed = %6.1f m/s\n", self.aircraft.Vymax) text=text..string.format("Initial Fuel = %6.1f\n", self.aircraft.fuel*100) @@ -1681,8 +2061,9 @@ end -- @param #table _waypoint First waypoint to be used (for continue journey, commute, etc). -- @param Core.Point#COORDINATE _lastpos (Optional) Position where the aircraft will be spawned. -- @param #number _nrespawn Number of already performed respawn attempts (e.g. spawning on runway bug). +-- @param #table parkingdata Explicitly specify the parking spots when spawning at an airport. -- @return #number Spawn index. -function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _livery, _waypoint, _lastpos, _nrespawn) +function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _livery, _waypoint, _lastpos, _nrespawn, parkingdata) self:F({rat=RAT.id, departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, waypoint=_waypoint, lastpos=_lastpos, nrespawn=_nrespawn}) -- Set takeoff type. @@ -1702,19 +2083,13 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live local temp={RAT.wp.cold, RAT.wp.hot} takeoff=temp[math.random(2)] end - + -- Number of respawn attempts after spawning on runway. local nrespawn=0 if _nrespawn then nrespawn=_nrespawn end - -- Spawn position. - local lastpos=nil - if _lastpos then - lastpos=_lastpos - end - -- Set flight plan. local departure, destination, waypoints, WPholding, WPfinal = self:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) @@ -1738,11 +2113,18 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live end -- Modify the spawn template to follow the flight plan. - self:_ModifySpawnTemplate(waypoints, livery, lastpos) + local successful=self:_ModifySpawnTemplate(waypoints, livery, _lastpos, departure, takeoff, parkingdata) + if not successful then + return nil + end -- Actually spawn the group. local group=self:SpawnWithIndex(self.SpawnIndex) -- Wrapper.Group#GROUP + -- Increase counter of alive groups (also uncontrolled ones). + self.alive=self.alive+1 + self:T(RAT.id..string.format("Alive groups counter now = %d.",self.alive)) + -- ATC is monitoring this flight (if it is supposed to land). if self.ATCswitch and landing==RAT.wp.landing then if self.returnzone then @@ -1780,14 +2162,21 @@ function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _live self.ratcraft[self.SpawnIndex]["departure"]=departure self.ratcraft[self.SpawnIndex]["waypoints"]=waypoints self.ratcraft[self.SpawnIndex]["airborne"]=group:InAir() + self.ratcraft[self.SpawnIndex]["nunits"]=group:GetInitialSize() -- Time and position on ground. For check if aircraft is stuck somewhere. if group:InAir() then self.ratcraft[self.SpawnIndex]["Tground"]=nil self.ratcraft[self.SpawnIndex]["Pground"]=nil + self.ratcraft[self.SpawnIndex]["Uground"]=nil self.ratcraft[self.SpawnIndex]["Tlastcheck"]=nil else self.ratcraft[self.SpawnIndex]["Tground"]=timer.getTime() self.ratcraft[self.SpawnIndex]["Pground"]=group:GetCoordinate() + self.ratcraft[self.SpawnIndex]["Uground"]={} + for _,_unit in pairs(group:GetUnits()) do + local _unitname=_unit:GetName() + self.ratcraft[self.SpawnIndex]["Uground"][_unitname]=_unit:GetCoordinate() + end self.ratcraft[self.SpawnIndex]["Tlastcheck"]=timer.getTime() end -- Initial and current position. For calculating the travelled distance. @@ -1852,11 +2241,13 @@ end --- Respawn a group. -- @param #RAT self --- @param Wrapper.Group#GROUP group Group to be repawned. -function RAT:_Respawn(group) +-- @param #number index Spawn index. +-- @param Core.Point#COORDINATE lastpos Last known position of the group. +-- @param #number delay Delay before respawn +function RAT:_Respawn(index, lastpos, delay) -- Get the spawn index from group - local index=self:GetSpawnIndexFromGroup(group) + --local index=self:GetSpawnIndexFromGroup(group) -- Get departure and destination from previous journey. local departure=self.ratcraft[index].departure @@ -1865,7 +2256,7 @@ function RAT:_Respawn(group) local landing=self.ratcraft[index].landing local livery=self.ratcraft[index].livery local lastwp=self.ratcraft[index].waypoints[#self.ratcraft[index].waypoints] - local lastpos=group:GetCoordinate() + --local lastpos=group:GetCoordinate() local _departure=nil local _destination=nil @@ -1887,7 +2278,10 @@ function RAT:_Respawn(group) -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. -- TODO: Need to think if continuejourney with respawn_after_takeoff actually makes sense. if landing==RAT.wp.landing and lastpos and not (self.respawn_at_landing or self.respawn_after_takeoff) then - _lastpos=lastpos + -- Check that we have an airport or FARP but not a ship (which would be categroy 1). + if destination:GetCategory()==4 then + _lastpos=lastpos + end end if self.destinationzone then @@ -1923,8 +2317,22 @@ function RAT:_Respawn(group) elseif self.commute then -- We commute between departure and destination. - _departure=destination:GetName() - _destination=departure:GetName() + + if self.starshape==true then + if destination:GetName()==self.homebase then + -- We are at our home base ==> destination is again randomly selected. + _departure=self.homebase + _destination=nil -- destination will be set anew + else + -- We are not a our home base ==> we fly back to our home base. + _departure=destination:GetName() + _destination=self.homebase + end + else + -- Simply switch departure and destination. + _departure=destination:GetName() + _destination=departure:GetName() + end -- Use the same livery for next aircraft. _livery=livery @@ -1933,7 +2341,10 @@ function RAT:_Respawn(group) -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff. -- TODO: Need to think if commute with respawn_after_takeoff actually makes sense. if landing==RAT.wp.landing and lastpos and not (self.respawn_at_landing or self.respawn_after_takeoff) then - _lastpos=lastpos + -- Check that we have landed on an airport or FARP but not a ship (which would be categroy 1). + if destination:GetCategory()==4 then + _lastpos=lastpos + end end -- Handle takeoff type. @@ -1982,7 +2393,17 @@ function RAT:_Respawn(group) end -- Debug - self:F({departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, lastwp=_lastwp}) + self:T2({departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, lastwp=_lastwp}) + + -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test). + local respawndelay + if delay then + respawndelay=delay + elseif self.respawn_delay then + respawndelay=self.respawn_delay+3 -- despawn happens after self.respawndelay. We add another 3 sec for free parking. + else + respawndelay=3 + end -- Spawn new group. local arg={} @@ -1994,7 +2415,8 @@ function RAT:_Respawn(group) arg.livery=_livery arg.lastwp=_lastwp arg.lastpos=_lastpos - SCHEDULER:New(nil, self._SpawnWithRouteTimer, {arg}, self.respawn_delay or 1) + self:T(RAT.id..string.format("%s delayed respawn in %.1f seconds.", self.alias, respawndelay)) + SCHEDULER:New(nil, self._SpawnWithRouteTimer, {arg}, respawndelay) end @@ -2009,10 +2431,11 @@ end --- Set the route of the AI plane. Due to DCS landing bug, this has to be done before the unit is spawned. -- @param #RAT self --- @param takeoff #RAT.wp Takeoff type. Could also be air start. --- @param landing #RAT.wp Landing type. Could also be a destination in air. +-- @param #number takeoff Takeoff type. Could also be air start. +-- @param #number landing Landing type. Could also be a destination in air. -- @param Wrapper.Airport#AIRBASE _departure (Optional) Departure airbase. -- @param Wrapper.Airport#AIRBASE _destination (Optional) Destination airbase. +-- @param #table _waypoint Initial waypoint. -- @return Wrapper.Airport#AIRBASE Departure airbase. -- @return Wrapper.Airport#AIRBASE Destination airbase. -- @return #table Table of flight plan waypoints. @@ -2033,7 +2456,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) local VxCruiseMin = math.min(VxCruiseMax*0.70, 166) -- Cruise speed (randomized). Expectation value at midpoint between min and max. - local VxCruise = self:_Random_Gaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin, (VxCruiseMax-VxCruiseMax)/4, VxCruiseMin, VxCruiseMax) + local VxCruise = UTILS.RandomGaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin, (VxCruiseMax-VxCruiseMax)/4, VxCruiseMin, VxCruiseMax) -- Climb speed 90% ov Vmax but max 720 km/h. local VxClimb = math.min(self.aircraft.Vmax*0.90, 200) @@ -2075,19 +2498,21 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) -- If it's not an airport, check whether it's a zone. departure=ZONE:New(_departure) else - local text=string.format("ERROR: Specified departure airport %s does not exist for %s!", _departure, self.alias) - self:E(RAT.id.."ERROR: "..text) + local text=string.format("ERROR! Specified departure airport %s does not exist for %s.", _departure, self.alias) + self:E(RAT.id..text) end else departure=self:_PickDeparture(takeoff) + if self.commute and self.starshape==true and self.homebase==nil then + self.homebase=departure:GetName() + end end -- Return nil if no departure could be found. if not departure then - local text=string.format("No valid departure airport could be found for %s.", self.alias) - MESSAGE:New(text, 60):ToAll() - self:E(RAT.id.."ERROR: "..text) + local text=string.format("ERROR! No valid departure airport could be found for %s.", self.alias) + self:E(RAT.id..text) return nil end @@ -2389,7 +2814,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) end -- Set cruise altitude. Selected from Gaussian distribution but limited to FLmin and FLmax. - local FLcruise=self:_Random_Gaussian(FLcruise_expect, math.abs(FLmax-FLmin)/4, FLmin, FLmax) + local FLcruise=UTILS.RandomGaussian(FLcruise_expect, math.abs(FLmax-FLmin)/4, FLmin, FLmax) -- Overrule setting if user specified a flight level explicitly. if self.FLuser then @@ -2471,7 +2896,7 @@ function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint) text=text..string.format("h_descent_max = %6.1f m\n", h_descent_max) end text=text..string.format("******************************************************\n") - self:T(RAT.id..text) + self:T2(RAT.id..text) -- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back. if d_cruise<0 then @@ -2628,14 +3053,27 @@ function RAT:_PickDeparture(takeoff) if self.random_departure then -- Airports of friendly coalitions. - for _,airport in pairs(self.airports) do + for _,_airport in pairs(self.airports) do + local airport=_airport --Wrapper.Airbase#AIRBASE + local name=airport:GetName() if not self:_Excluded(name) then if takeoff==RAT.wp.air then + table.insert(departures, airport:GetZone()) -- insert zone object. + else - table.insert(departures, airport) -- insert airport object. + + -- Check if airbase has the right terminals. + local nspots=1 + if self.termtype~=nil then + nspots=airport:GetParkingSpotsNumber(self.termtype) + end + + if nspots>0 then + table.insert(departures, airport) -- insert airport object. + end end end @@ -2652,15 +3090,23 @@ function RAT:_PickDeparture(takeoff) dep=AIRBASE:FindByName(name):GetZone() else dep=AIRBASE:FindByName(name) + -- Check if the airport has a valid parking spot + if self.termtype~=nil and dep~=nil then + local _dep=dep --Wrapper.Airbase#AIRBASE + local nspots=_dep:GetParkingSpotsNumber(self.termtype) + if nspots==0 then + dep=nil + end + end end elseif self:_ZoneExists(name) then if takeoff==RAT.wp.air then dep=ZONE:New(name) else - self:E(RAT.id.."ERROR: Takeoff is not in air. Cannot use "..name.." as departure!") + self:E(RAT.id..string.format("ERROR! Takeoff is not in air. Cannot use %s as departure.", name)) end else - self:E(RAT.id.."ERROR: No airport or zone found with name "..name) + self:E(RAT.id..string.format("ERROR: No airport or zone found with name %s.", name)) end -- Add to departures table. @@ -2685,10 +3131,10 @@ function RAT:_PickDeparture(takeoff) else text=string.format("%s: Chosen departure airport: %s (ID %d)", self.alias, departure:GetName(), departure:GetID()) end - MESSAGE:New(text, 30):ToAllIf(self.Debug) + --MESSAGE:New(text, 30):ToAllIf(self.Debug) self:T(RAT.id..text) else - self:E(RAT.id..string.format("ERROR: No departure airport or zone found for %s!", self.alias)) + self:E(RAT.id..string.format("ERROR! No departure airport or zone found for %s.", self.alias)) departure=nil end @@ -2716,7 +3162,8 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) if random then -- Airports of friendly coalitions. - for _,airport in pairs(self.airports) do + for _,_airport in pairs(self.airports) do + local airport=_airport --Wrapper.Airbase#AIRBASE local name=airport:GetName() if self:_IsFriendly(name) and not self:_Excluded(name) and name~=departure:GetName() then @@ -2728,7 +3175,14 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) if landing==RAT.wp.air then table.insert(destinations, airport:GetZone()) -- insert zone object. else - table.insert(destinations, airport) -- insert airport object. + -- Check if the requested terminal type is available. + local nspot=1 + if self.termtype then + nspot=airport:GetParkingSpotsNumber(self.termtype) + end + if nspot>0 then + table.insert(destinations, airport) -- insert airport object. + end end end end @@ -2748,15 +3202,23 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) dest=AIRBASE:FindByName(name):GetZone() else dest=AIRBASE:FindByName(name) + -- Check if the requested terminal type is available. + local nspot=1 + if self.termtype then + nspot=dest:GetParkingSpotsNumber(self.termtype) + end + if nspot==0 then + dest=nil + end end elseif self:_ZoneExists(name) then if landing==RAT.wp.air then dest=ZONE:New(name) else - self:E(RAT.id.."ERROR: Landing is not in air. Cannot use zone "..name.." as destination!") + self:E(RAT.id..string.format("ERROR! Landing is not in air. Cannot use zone %s as destination!", name)) end else - self:E(RAT.id.."ERROR: No airport or zone found with name "..name) + self:E(RAT.id..string.format("ERROR! No airport or zone found with name %s", name)) end if dest then @@ -2777,7 +3239,7 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) end -- Info message. - self:T(RAT.id.."Number of possible destinations = "..#destinations) + self:T(RAT.id..string.format("Number of possible destinations = %s.", #destinations)) if #destinations > 0 then --- Compare distance of destination airports. @@ -2810,10 +3272,10 @@ function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing) text=string.format("%s Chosen destination airport: %s (ID %d).", self.alias, destination:GetName(), destination:GetID()) end self:T(RAT.id..text) - MESSAGE:New(text, 30):ToAllIf(self.Debug) + --MESSAGE:New(text, 30):ToAllIf(self.Debug) else - self:E(RAT.id.."ERROR: No destination airport or zone found.") + self:E(RAT.id.."ERROR! No destination airport or zone found.") destination=nil end @@ -2899,22 +3361,37 @@ function RAT:_GetAirportsOfMap() table.insert(self.airports_map, _myab) local text="MOOSE: Airport ID = ".._myab:GetID().." and Name = ".._myab:GetName()..", Category = ".._myab:GetCategory()..", TypeName = ".._myab:GetTypeName() - self:T2(RAT.id..text) + self:T(RAT.id..text) end end end ---- Get all "friendly" airports of the current map. +--- Get all "friendly" airports of the current map. Fills the self.airports{} table. -- @param #RAT self function RAT:_GetAirportsOfCoalition() for _,coalition in pairs(self.ctable) do - for _,airport in pairs(self.airports_map) do + for _,_airport in pairs(self.airports_map) do + local airport=_airport --Wrapper.Airbase#AIRBASE + local category=airport:GetDesc().category if airport:GetCoalition()==coalition then -- Planes cannot land on FARPs. - local condition1=self.category==RAT.cat.plane and airport:GetTypeName()=="FARP" + --local condition1=self.category==RAT.cat.plane and airport:GetTypeName()=="FARP" + local condition1=self.category==RAT.cat.plane and category==Airbase.Category.HELIPAD -- Planes cannot land on ships. - local condition2=self.category==RAT.cat.plane and airport:GetCategory()==1 + --local condition2=self.category==RAT.cat.plane and airport:GetCategory()==1 + local condition2=self.category==RAT.cat.plane and category==Airbase.Category.SHIP + + -- Check that airport has the requested terminal types. + -- NOT good here because we would also not allow any airport zones! + --[[ + local nspots=1 + if self.termtype then + nspots=airport:GetParkingSpotsNumber(self.termtype) + end + local condition3 = nspots==0 + ]] + if not (condition1 or condition2) then table.insert(self.airports, airport) end @@ -2923,9 +3400,9 @@ function RAT:_GetAirportsOfCoalition() end if #self.airports==0 then - local text="No possible departure/destination airports found!" - MESSAGE:New(text, 60):ToAll() - self:E(RAT.id.."ERROR: "..text) + local text=string.format("No possible departure/destination airports found for RAT %s.", tostring(self.alias)) + MESSAGE:New(text, 10):ToAll() + self:E(RAT.id..text) end end @@ -2937,9 +3414,7 @@ end -- @param #number forID (Optional) Send message only for this ID. function RAT:Status(message, forID) - --message=message or false - --forID=forID or false - + -- Optional arguments. if message==nil then message=false end @@ -2949,14 +3424,18 @@ function RAT:Status(message, forID) -- Current time. local Tnow=timer.getTime() + + -- Alive counter. + local nalive=0 - -- Loop over all ratcraft. + -- Loop over all ratcraft. for spawnindex,ratcraft in ipairs(self.ratcraft) do -- Get group. local group=ratcraft.group --Wrapper.Group#GROUP if group and group:IsAlive() then + nalive=nalive+1 -- Gather some information. local prefix=self:_GetPrefixFromGroup(group) @@ -2971,6 +3450,8 @@ function RAT:Status(message, forID) local type=self.aircraft.type local status=ratcraft.status local active=ratcraft.active + local Nunits=ratcraft.nunits -- group:GetSize() + local N0units=group:GetInitialSize() -- Monitor time and distance on ground. local Tg=0 @@ -2981,6 +3462,7 @@ function RAT:Status(message, forID) -- Aircraft is airborne. ratcraft["Tground"]=nil ratcraft["Pground"]=nil + ratcraft["Uground"]=nil ratcraft["Tlastcheck"]=nil else --Aircraft is on ground. @@ -2997,15 +3479,39 @@ function RAT:Status(message, forID) -- If more than Tinactive seconds passed since last check ==> check how much we moved meanwhile. if dTlast > self.Tinactive then - -- If aircraft did not move more than 50 m since last check, we call it stationary and despawn it. - -- Aircraft which are spawned uncontrolled or starting their engines are not counted. - if Dg<50 and active and not status==RAT.status.EventBirth then + --[[ + if Dg<50 and active and status~=RAT.status.EventBirth then stationary=true end + ]] - -- Set the current time to know when the next check is necessary. + -- Loop over all units. + for _,_unit in pairs(group:GetUnits()) do + + if _unit and _unit:IsAlive() then + + -- Unit name, coord and distance since last check. + local unitname=_unit:GetName() + local unitcoord=_unit:GetCoordinate() + local Ug=unitcoord:Get2DDistance(ratcraft.Uground[unitname]) + + -- Debug info + self:T2(RAT.id..string.format("Unit %s travelled distance on ground %.1f m since %d seconds.", unitname, Ug, dTlast)) + + -- If aircraft did not move more than 50 m since last check, we call it stationary and despawn it. + -- Aircraft which are spawned uncontrolled or starting their engines are not counted. + if Ug<50 and active and status~=RAT.status.EventBirth then + stationary=true + end + + -- Update coords. + ratcraft["Uground"][unitname]=unitcoord + end + end + + -- Set the current time to know when the next check is necessary. ratcraft["Tlastcheck"]=Tnow - ratcraft["Pground"]=coords + ratcraft["Pground"]=coords end else @@ -3013,6 +3519,11 @@ function RAT:Status(message, forID) ratcraft["Tground"]=Tnow ratcraft["Tlastcheck"]=Tnow ratcraft["Pground"]=coords + ratcraft["Uground"]={} + for _,_unit in pairs(group:GetUnits()) do + local unitname=_unit:GetName() + ratcraft.Uground[unitname]=_unit:GetCoordinate() + end end end @@ -3029,7 +3540,12 @@ function RAT:Status(message, forID) -- Status report. if (forID and spawnindex==forID) or (not forID) then - local text=string.format("ID %i of group %s\n", spawnindex, prefix) + local text=string.format("ID %i of flight %s", spawnindex, prefix) + if N0units>1 then + text=text..string.format(" (%d/%d)\n", Nunits, N0units) + else + text=text.."\n" + end if self.commute then text=text..string.format("%s commuting between %s and %s\n", type, departure, destination) elseif self.continuejourney then @@ -3053,7 +3569,7 @@ function RAT:Status(message, forID) text=text..string.format("\nTime on ground = %6.0f seconds\n", Tg) text=text..string.format("Position change = %8.1f m since %3.0f seconds.", Dg, dTlast) end - self:T2(RAT.id..text) + self:T(RAT.id..text) if message then MESSAGE:New(text, 20):ToAll() end @@ -3064,25 +3580,33 @@ function RAT:Status(message, forID) -- Despawn unit if it did not move more then 50 m in the last 180 seconds. if stationary then - local text=string.format("Group %s is despawned after being %4.0f seconds inaktive on ground.", self.alias, dTlast) + local text=string.format("Group %s is despawned after being %d seconds inaktive on ground.", self.alias, dTlast) self:T(RAT.id..text) self:_Despawn(group) end + -- Despawn group if life is < 10% and distance travelled < 100 m. if life<10 and Dtravel<100 then local text=string.format("Damaged group %s is despawned. Life = %3.0f", self.alias, life) + self:T(RAT.id..text) self:_Despawn(group) end + end + -- Despawn groups after they have reached their destination zones. if ratcraft.despawnme then + local text=string.format("Flight %s will be despawned NOW!", self.alias) self:T(RAT.id..text) -- Despawn old group. - if not self.norespawn then - self:_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) + self:_Despawn(group, 0) + end else @@ -3093,11 +3617,10 @@ function RAT:Status(message, forID) end - if (message and not forID) then - local text=string.format("Alive groups of %s: %d", self.alias, self.alive) - self:T(RAT.id..text) - MESSAGE:New(text, 20):ToAll() - end + -- Alive groups. + local text=string.format("Alive groups of %s: %d, nalive=%d/%d", self.alias, self.alive, nalive, self.ngroups) + self:T(RAT.id..text) + MESSAGE:New(text, 20):ToAllIf(message and not forID) end @@ -3112,10 +3635,10 @@ function RAT:_GetLife(group) if unit then life=unit:GetLife()/unit:GetLife0()*100 else - self:T2(RAT.id.."ERROR: Unit does not exist in RAT_Getlife(). Returning zero.") + self:T2(RAT.id.."ERROR! Unit does not exist in RAT_Getlife(). Returning zero.") end else - self:T2(RAT.id.."ERROR: Group does not exist in RAT_Getlife(). Returning zero.") + self:T2(RAT.id.."ERROR! Group does not exist in RAT_Getlife(). Returning zero.") end return life end @@ -3131,10 +3654,6 @@ function RAT:_SetStatus(group, status) -- Get index from groupname. local index=self:GetSpawnIndexFromGroup(group) - if self.Debug or self.reportstatus then - env.info(RAT.id..string.format("Group %s has status %s, spawnindex = %d", group:GetName(), status, index)) - end - if self.ratcraft[index] then -- Set new status. @@ -3148,7 +3667,7 @@ function RAT:_SetStatus(group, status) local text=string.format("Flight %s: %s.", group:GetName(), status) self:T(RAT.id..text) - if (not (no1 or no2 or no3)) then + if not (no1 or no2 or no3) then MESSAGE:New(text, 10):ToAllIf(self.reportstatus) end @@ -3157,11 +3676,38 @@ function RAT:_SetStatus(group, status) end end +--- Get status of group. +-- @param #RAT self +-- @param Wrapper.Group#GROUP group Group. +-- @return #string status Status of group. +function RAT:GetStatus(group) + + if group and group:IsAlive() then + + -- Get index from groupname. + local index=self:GetSpawnIndexFromGroup(group) + + if self.ratcraft[index] then + + -- Set new status. + return self.ratcraft[index].status + + end + + end + + return "nonexistant" +end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Function is executed when a unit is spawned. -- @param #RAT self +-- @param Core.Event#EVENTDATA EventData function RAT:_OnBirth(EventData) + self:F3(EventData) + self:T3(RAT.id.."Captured event birth!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP @@ -3178,9 +3724,6 @@ function RAT:_OnBirth(EventData) local text="Event: Group "..SpawnGroup:GetName().." was born." self:T(RAT.id..text) - -- Increase counter of alive groups (also uncontrolled ones). - self.alive=self.alive+1 - -- Set status. local status="unknown in birth" if SpawnGroup:InAir() then @@ -3192,7 +3735,7 @@ function RAT:_OnBirth(EventData) end self:_SetStatus(SpawnGroup, status) - -- Get some info ablout this flight. + -- Get some info ablout this flight. local i=self:GetSpawnIndexFromGroup(SpawnGroup) local _departure=self.ratcraft[i].departure:GetName() local _destination=self.ratcraft[i].destination:GetName() @@ -3201,20 +3744,26 @@ function RAT:_OnBirth(EventData) local _landing=self.ratcraft[i].landing local _livery=self.ratcraft[i].livery + -- Some is only useful for an actual airbase (not a zone). + local _airbase=AIRBASE:FindByName(_departure) + -- Check if aircraft group was accidentally spawned on the runway. -- This can happen due to no parking slots available and other DCS bugs. local onrunway=false - if _takeoff ~= RAT.wp.runway then - onrunway=self:_CheckOnRunway(SpawnGroup, _departure) - end + if _airbase then + -- Check that we did not want to spawn at a runway or in air. + if self.checkonrunway and _takeoff ~= RAT.wp.runway and _takeoff ~= RAT.wp.air then + onrunway=_airbase:CheckOnRunWay(SpawnGroup, self.onrunwayradius, false) + end + end -- Workaround if group was spawned on runway. if onrunway then -- Error message. - local text=string.format("ERROR: RAT group of %s was spawned on runway (DCS bug). Group #%d will be despawned immediately!", self.alias, i) + local text=string.format("ERROR: RAT group of %s was spawned on runway. Group #%d will be despawned immediately!", self.alias, i) MESSAGE:New(text,30):ToAllIf(self.Debug) - env.info(RAT.id..text) + self:E(RAT.id..text) if self.Debug then SpawnGroup:FlareRed() end @@ -3223,25 +3772,24 @@ function RAT:_OnBirth(EventData) self:_Despawn(SpawnGroup) -- Try to respawn the group if there is at least another airport or random airport selection is used. - if (self.Ndeparture_Airports>=2 or self.random_departure) and _nrespawn=2 or self.random_departure) and _nrespawn new state %s.", SpawnGroup:GetName(), currentstate, status) + self:T(RAT.id..text) + + -- Respawn group. + local idx=self:GetSpawnIndexFromGroup(SpawnGroup) + local coord=SpawnGroup:GetCoordinate() + self:_Respawn(idx, coord) + end + + -- Despawn group. + text="Event: Group "..SpawnGroup:GetName().." will be destroyed now." self:T(RAT.id..text) - - -- Respawn group. - self:_Respawn(SpawnGroup) + self:_Despawn(SpawnGroup) + end - - - -- Despawn group. - text="Event: Group "..SpawnGroup:GetName().." will be destroyed now." - self:T(RAT.id..text) - self:_Despawn(SpawnGroup) end end @@ -3483,15 +4005,84 @@ function RAT:_OnEngineShutdown(EventData) end end ---- Function is executed when a unit is dead. +--- Function is executed when a unit is hit. -- @param #RAT self -function RAT:_OnDead(EventData) +-- @param Core.Event#EVENTDATA EventData +function RAT:_OnHit(EventData) + self:F3(EventData) + self:T(RAT.id..string.format("Captured event Hit by %s! Initiator %s. Target %s", self.alias, tostring(EventData.IniUnitName), tostring(EventData.TgtUnitName))) + + local SpawnGroup = EventData.TgtGroup --Wrapper.Group#GROUP + + if SpawnGroup then + + -- Get the template name of the group. This can be nil if this was not a spawned group. + local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) + + -- Check that the template name actually belongs to this object. + if EventPrefix and EventPrefix == self.alias then + -- Debug info. + self:T(RAT.id..string.format("Event: Group %s was hit. Unit %s.", SpawnGroup:GetName(), tostring(EventData.TgtUnitName))) + + local text=string.format("%s, unit %s was hit!", self.alias, EventData.TgtUnitName) + MESSAGE:New(text, 10):ToAllIf(self.reportstatus or self.Debug) + end + end +end +--- Function is executed when a unit is dead or crashes. +-- @param #RAT self +-- @param Core.Event#EVENTDATA EventData +function RAT:_OnDeadOrCrash(EventData) + self:F3(EventData) + self:T3(RAT.id.."Captured event DeadOrCrash!") + local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP if SpawnGroup then - env.info(string.format("%sGroup %s died!", RAT.id, SpawnGroup:GetName())) + -- Get the template name of the group. This can be nil if this was not a spawned group. + local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) + + if EventPrefix then + + -- Check that the template name actually belongs to this object. + if EventPrefix == self.alias then + + -- Decrease group alive counter. + self.alive=self.alive-1 + + -- Debug info. + local text=string.format("Event: Group %s crashed or died. Alive counter = %d.", SpawnGroup:GetName(), self.alive) + self:T(RAT.id..text) + + -- Split crash and dead events. + if EventData.id == world.event.S_EVENT_CRASH then + + -- Call crash event. This handles when a group crashed or + self:_OnCrash(EventData) + + elseif EventData.id == world.event.S_EVENT_DEAD then + + -- Call dead event. + self:_OnDead(EventData) + + end + end + end + end +end + +--- Function is executed when a unit is dead. +-- @param #RAT self +-- @param Core.Event#EVENTDATA EventData +function RAT:_OnDead(EventData) + self:F3(EventData) + self:T3(RAT.id.."Captured event Dead!") + + local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP + + if SpawnGroup then -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) @@ -3501,9 +4092,8 @@ function RAT:_OnDead(EventData) -- Check that the template name actually belongs to this object. if EventPrefix == self.alias then - local text="Event: Group "..SpawnGroup:GetName().." died." + local text=string.format("Event: Group %s died. Unit %s.", SpawnGroup:GetName(), EventData.IniUnitName) self:T(RAT.id..text) - env.info(RAT.id..text) -- Set status. local status=RAT.status.EventDead @@ -3513,43 +4103,51 @@ function RAT:_OnDead(EventData) end else - self:E(RAT.id.."ERROR: Group does not exist in RAT:_OnDead().") + self:T2(RAT.id.."ERROR: Group does not exist in RAT:_OnDead().") end end --- Function is executed when a unit crashes. -- @param #RAT self +-- @param Core.Event#EVENTDATA EventData function RAT:_OnCrash(EventData) + self:F3(EventData) + self:T3(RAT.id.."Captured event Crash!") local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP if SpawnGroup then - - self:T(string.format("%sGroup %s crashed!", RAT.id, SpawnGroup:GetName())) - env.info(string.format("%sGroup %s crashed!", RAT.id, SpawnGroup:GetName())) -- Get the template name of the group. This can be nil if this was not a spawned group. local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup) - if EventPrefix then + -- Check that the template name actually belongs to this object. + if EventPrefix and EventPrefix == self.alias then - -- Check that the template name actually belongs to this object. - if EventPrefix == self.alias then - - local text="Event: Group "..SpawnGroup:GetName().." crashed." + -- Update number of alive units in the group. + local _i=self:GetSpawnIndexFromGroup(SpawnGroup) + self.ratcraft[_i].nunits=self.ratcraft[_i].nunits-1 + local _n=self.ratcraft[_i].nunits + local _n0=SpawnGroup:GetInitialSize() + + -- Debug info. + local text=string.format("Event: Group %s crashed. Unit %s. Units still alive %d of %d.", SpawnGroup:GetName(), EventData.IniUnitName, _n, _n0) + self:T(RAT.id..text) + + -- Set status. + local status=RAT.status.EventCrash + self:_SetStatus(SpawnGroup, status) + + -- Respawn group if all units are dead. + if _n==0 and self.respawn_after_crash and not self.norespawn then + local text=string.format("No units left of group %s. Group will be respawned now.", SpawnGroup:GetName()) self:T(RAT.id..text) - env.info(RAT.id..text) - - -- Set status. - --self:_SetStatus(SpawnGroup, "Crashed") - local status=RAT.status.EventCrash - self:_SetStatus(SpawnGroup, status) - - --TODO: Aircraft are not respawned if they crash. Should they? - - --TODO: Maybe spawn some people at the crash site and send a distress call. - -- And define them as cargo which can be rescued. + -- Respawn group. + local idx=self:GetSpawnIndexFromGroup(SpawnGroup) + local coord=SpawnGroup:GetCoordinate() + self:_Respawn(idx, coord) end + end else @@ -3563,7 +4161,8 @@ end -- Index of ratcraft array is taken from spawned group name. -- @param #RAT self -- @param Wrapper.Group#GROUP group Group to be despawned. -function RAT:_Despawn(group) +-- @param #number delay Delay in seconds before the despawn happens. +function RAT:_Despawn(group, delay) if group ~= nil then @@ -3575,6 +4174,8 @@ function RAT:_Despawn(group) self.ratcraft[index].group=nil self.ratcraft[index]["status"]="Dead" + --TODO: Maybe here could be some more arrays deleted? + --TODO: Somehow this causes issues. --[[ --self.ratcraft[index]["group"]=group self.ratcraft[index]["destination"]=nil @@ -3597,26 +4198,87 @@ function RAT:_Despawn(group) self.ratcraft[index].despawnme=nil self.ratcraft[index].nrespawn=nil ]] - -- Remove ratcraft table entry. - --TODO: Somehow this causes issues. --table.remove(self.ratcraft, index) - --TODO: What events are actually fired when doing this? Crash and Dead or just Dead or...? - -- Destroy should create a crash event but for each unit. - group:Destroy() - - -- Decrease group alive counter. - self.alive=self.alive-1 + + -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test). + local despawndelay=0 + if delay then + -- Explicitly requested delay time. + despawndelay=delay + elseif self.respawn_delay then + -- Despawn afer respawn_delay. Actual respawn happens in +3 seconds to allow for free parking. + despawndelay=self.respawn_delay + end + + -- This will destroy the DCS group and create a single DEAD event. + --if despawndelay>0.5 then + self:T(RAT.id..string.format("%s delayed despawn in %.1f seconds.", self.alias, despawndelay)) + SCHEDULER:New(nil, self._Destroy, {self, group}, despawndelay) + --else + --self:_Destroy(group) + --end -- Remove submenu for this group. if self.f10menu and self.SubMenuName ~= nil then self.Menu[self.SubMenuName]["groups"][index]:Remove() end + end end +end - --TODO: Maybe here could be some more arrays deleted? +--- Destroys the RAT DCS group and all of its DCS units. +-- Note that this raises a DEAD event at run-time. +-- So all event listeners will catch the DEAD event of this DCS group. +-- @param #RAT self +-- @param Wrapper.Group#GROUP group The RAT group to be destroyed. +function RAT:_Destroy(group) + self:F2(group) + + local DCSGroup = group:GetDCSObject() -- DCS#Group + + if DCSGroup and DCSGroup:isExist() then + + -- Cread one single Dead event and delete units from database. + local triggerdead=true + for _,DCSUnit in pairs(DCSGroup:getUnits()) do + + -- Dead event. + if DCSUnit then + if triggerdead then + self:_CreateEventDead(timer.getTime(), DCSUnit) + triggerdead=false + end + + -- Delete from data base. + _DATABASE:DeleteUnit(DCSUnit:getName()) + end + end + + -- Destroy DCS group. + DCSGroup:destroy() + DCSGroup = nil + end + + return nil +end + +--- Create a Dead event. +-- @param #RAT self +-- @param DCS#Time EventTime The time stamp of the event. +-- @param DCS#Object Initiator The initiating object of the event. +function RAT:_CreateEventDead(EventTime, Initiator) + self:F( { EventTime, Initiator } ) + + local Event = { + id = world.event.S_EVENT_DEAD, + time = EventTime, + initiator = Initiator, + } + + world.onEvent( Event ) end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -3723,9 +4385,7 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport text=text..string.format("No airport/zone specified\n") end text=text.."******************************************************\n" - if self.Debug then - self:T2(RAT.id..text) - end + self:T2(RAT.id..text) -- define waypoint local RoutePoint = {} @@ -3753,16 +4413,13 @@ function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport if AirbaseCategory == Airbase.Category.SHIP then RoutePoint.linkUnit = AirbaseID RoutePoint.helipadId = AirbaseID - --self:T(RAT.id.."WP: Ship id = "..AirbaseID) elseif AirbaseCategory == Airbase.Category.HELIPAD then RoutePoint.linkUnit = AirbaseID RoutePoint.helipadId = AirbaseID - --self:T(RAT.id.."WP: Helipad id = "..AirbaseID) elseif AirbaseCategory == Airbase.Category.AIRDROME then RoutePoint.airdromeId = AirbaseID - --self:T(RAT.id.."WP: Airdrome id = "..AirbaseID) else - --self:E(RAT.id.."Unknown Airport categoryin _Waypoint()!") + self:T(RAT.id.."Unknown Airport category in _Waypoint()!") end end -- properties @@ -3829,10 +4486,8 @@ function RAT:_Routeinfo(waypoints, comment) text=text..string.format("Total distance = %6.1f km\n", total/1000) text=text..string.format("******************************************************\n") - -- send message - if self.Debug then - env.info(RAT.id..text) - end + -- Debug info. + self:T2(RAT.id..text) -- return total route length in meters return total @@ -3842,11 +4497,11 @@ end --- Orbit at a specified position at a specified alititude with a specified speed. -- @param #RAT self --- @param Dcs.DCSTypes#Vec2 P1 The point to hold the position. +-- @param DCS#Vec2 P1 The point to hold the position. -- @param #number Altitude The altitude ASL at which to hold the position. -- @param #number Speed The speed flying when holding the position in m/s. -- @param #number Duration Duration of holding pattern in seconds. --- @return Dcs.DCSTasking.Task#Task DCSTask +-- @return DCS#Task DCSTask function RAT:_TaskHolding(P1, Altitude, Speed, Duration) --local LandHeight = land.getHeight(P1) @@ -3919,8 +4574,6 @@ function RAT._WaypointFunction(group, rat, wp) -- New status. local status=rat.waypointstatus[wp] - - --rat.ratcraft[sdx].status=status rat:_SetStatus(group, status) if wp==WPholding then @@ -3945,7 +4598,7 @@ function RAT._WaypointFunction(group, rat, wp) if landing==RAT.wp.air then text=string.format("Activating despawn switch for flight %s! Group will be detroyed soon.", group:GetName()) - MESSAGE:New(text, 30):ToAllIf(rat.Debug) + MESSAGE:New(text, 10):ToAllIf(rat.Debug) BASE.T(rat, RAT.id..text) -- Enable despawn switch. Next time the status function is called, the aircraft will be despawned. rat.ratcraft[sdx].despawnme=true @@ -3995,36 +4648,177 @@ end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Calculate the max flight level for a given distance and fixed climb and descent rates. This function is obsolete now. --- In other words we have a distance between two airports and want to know how high we --- can climb before we must descent again to arrive at the destination without any level/cruising part. +--- Randomly activates an uncontrolled aircraft. -- @param #RAT self --- @param #number alpha Angle of climb [rad]. --- @param #number beta Angle of descent [rad]. --- @param #number d Distance between the two airports [m]. --- @param #number phi Angle between departure and destination [rad]. --- @param #number h0 Height [m] of departure airport. Note we implicitly assume that the height difference between departure and destination is negligible. --- @return #number Maximal flight level in meters. -function RAT:_FLmax(alpha, beta, d, phi, h0) --- Solve ASA triangle for one side (d) and two adjacent angles (alpha, beta) given. - local gamma=math.rad(180)-alpha-beta - local a=d*math.sin(alpha)/math.sin(gamma) - local b=d*math.sin(beta)/math.sin(gamma) - -- h1 and h2 should be equal. - local h1=b*math.sin(alpha) - local h2=a*math.sin(beta) - -- We also take the slope between departure and destination into account. - local h3=b*math.cos(math.pi/2-(alpha+phi)) - -- Debug message. - local text=string.format("\nFLmax = FL%3.0f = %6.1f m.\n", h1/RAT.unit.FL2m, h1) - text=text..string.format( "FLmax = FL%3.0f = %6.1f m.\n", h2/RAT.unit.FL2m, h2) - text=text..string.format( "FLmax = FL%3.0f = %6.1f m.", h3/RAT.unit.FL2m, h3) - if self.Debug then - self:T3(RAT.id..text) +function RAT:_ActivateUncontrolled() + self:F() + + -- Spawn indices of uncontrolled inactive aircraft. + local idx={} + local rat={} + + -- Number of active aircraft. + local nactive=0 + + -- Loop over RAT groups and count the active ones. + for spawnindex,ratcraft in pairs(self.ratcraft) do + + local group=ratcraft.group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + local text=string.format("Uncontrolled: Group = %s (spawnindex = %d), active = %s.", ratcraft.group:GetName(), spawnindex, tostring(ratcraft.active)) + self:T2(RAT.id..text) + + if ratcraft.active then + nactive=nactive+1 + else + table.insert(idx, spawnindex) + end + + end end - return h3+h0 + + -- Debug message. + local text=string.format("Uncontrolled: Ninactive = %d, Nactive = %d (of max %d).", #idx, nactive, self.activate_max) + self:T(RAT.id..text) + + if #idx>0 and nactive=xmin and r<=xmax) or i>100 then - gotit=true - end - end - - return r - -end --- Place markers of the waypoints. Note we assume a very specific number and type of waypoints here. -- @param #RAT self @@ -4305,21 +5067,55 @@ end -- @param #table waypoints The waypoints of the AI flight plan. -- @param #string livery (Optional) Livery of the aircraft. All members of a flight will get the same livery. -- @param Core.Point#COORDINATE spawnplace (Optional) Place where spawning should happen. If not present, first waypoint is taken. -function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace) +-- @param Wrapper.Airbase#AIRBASE departure Departure airbase or zone. +-- @param #number takeoff Takeoff type. +-- @param #table parkingdata Parking data, i.e. parking spot coordinates and terminal ids for all units of the group. +-- @return #boolean True if modification was successful or nil if not, e.g. when no parking space was found and spawn in air is disabled. +function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, takeoff, parkingdata) + self:F2({waypoints=waypoints, livery=livery, spawnplace=spawnplace, departure=departure, takeoff=takeoff, parking=parkingdata}) -- The 3D vector of the first waypoint, i.e. where we actually spawn the template group. - local PointVec3 = {x=waypoints[1].x, y=waypoints[1].alt, z=waypoints[1].y} + local PointVec3 = COORDINATE:New(waypoints[1].x, waypoints[1].alt, waypoints[1].y) if spawnplace then - PointVec3 = spawnplace:GetVec3() - self:T({spawnplace=PointVec3}) + PointVec3 = COORDINATE:NewFromCoordinate(spawnplace) end + + -- Template group and unit. + local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix) + local TemplateUnit=TemplateGroup:GetUnit(1) + -- Check if we spawn on ground. + local spawnonground=takeoff==RAT.wp.cold or takeoff==RAT.wp.hot or takeoff==RAT.wp.runway + + -- 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 + local AirbaseCategory = departure:GetDesc().category + 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==RAT.wp.runway + end + + local automatic=false + if automatic and spawnonground then + PointVec3=PointVec3:GetClosestParkingSpot(true, departure) + end + -- Heading from first to seconds waypoints to align units in case of air start. local course = self:_Course(waypoints[1], waypoints[2]) local heading = self:_Heading(course) if self:_GetSpawnIndex(self.SpawnIndex+1) then + -- Get template from group. local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate if SpawnTemplate then @@ -4330,22 +5126,247 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace) -- This is used in the SPAWN:SpawnWithIndex() function. Some values are overwritten there! self.SpawnUnControlled=true end + + -- Number of units in the group. With grouping this can actually differ from the template group size! + local nunits=#SpawnTemplate.units + + -- 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 then + -- Number of free parking spots. + local nfree=0 + + -- Set terminal type. Can also be nil. + local termtype=self.termtype + if spawnonrunway then + termtype=AIRBASE.TerminalType.Runway + end + + -- Scan options. Might make that input somehow. + local scanradius=self.parkingscanradius + local scanunits=true + local scanstatics=true + local scanscenery=self.parkingscanscenery + local verysafe=self.parkingverysafe + + -- Get free parking spots depending on where we spawn. + if spawnonship or spawnonfarp or spawnonrunway then + -- These places work procedural and have some kind of build in queue ==> Less effort. + self:T(RAT.id..string.format("Group %s is spawned on farp/ship/runway %s.", self.alias, departure:GetName())) + nfree=departure:GetFreeParkingSpotsNumber(termtype, true) + spots=departure:GetFreeParkingSpotsTable(termtype, true) + elseif parkingdata~=nil then + -- Parking data explicitly set by user as input parameter. + nfree=#parkingdata + spots=parkingdata + else + -- Helo is spawned. + if self.category==RAT.cat.heli then + if termtype==nil then + -- Try exclusive helo spots first. + self:T(RAT.id..string.format("Helo group %s is spawned at %s using terminal type %d.", self.alias, departure:GetName(), AIRBASE.TerminalType.HelicopterOnly)) + spots=departure:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits) + nfree=#spots + if nfree=1 then + + -- All units get the same spot. DCS takes care of the rest. + for i=1,nunits do + table.insert(parkingspots, spots[1].Coordinate) + table.insert(parkingindex, spots[1].TerminalID) + end + -- This is actually used... + PointVec3=spots[1].Coordinate + + else + -- If there is absolutely not spot ==> air start! + _notenough=true + end + + elseif spawnonairport then + + if nfree>=nunits then + + for i=1,nunits do + table.insert(parkingspots, spots[i].Coordinate) + table.insert(parkingindex, spots[i].TerminalID) + end + + else + -- Not enough spots for the whole group ==> air start! + _notenough=true + end + end + + -- Not enough spots ==> Prepare airstart. + if _notenough then + + if self.respawn_inair and not self.SpawnUnControlled then + self:E(RAT.id..string.format("WARNING: Group %s has no parking spots at %s ==> air start!", self.SpawnTemplatePrefix, departure:GetName())) + + -- Not enough parking spots at the airport ==> Spawn in air. + spawnonground=false + spawnonship=false + spawnonfarp=false + spawnonrunway=false + + -- Set waypoint type/action to turning point. + waypoints[1].type = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][1] -- type = Turning Point + waypoints[1].action = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][2] -- action = Turning Point + + -- Adjust altitude to be 500-1000 m above the airbase. + PointVec3.x=PointVec3.x+math.random(-1500,1500) + PointVec3.z=PointVec3.z+math.random(-1500,1500) + if self.category==RAT.cat.heli then + PointVec3.y=PointVec3:GetLandHeight()+math.random(100,1000) + else + -- Randomize position so that multiple AC wont be spawned on top even in air. + PointVec3.y=PointVec3:GetLandHeight()+math.random(500,3000) + end + else + self:E(RAT.id..string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, departure:GetName())) + return nil + end + end + + else + + -- Air start requested initially! + + --PointVec3.y is already set from first waypoint here! + + end + + +--- new + -- Translate the position of the Group Template to the Vec3. - for UnitID = 1, #SpawnTemplate.units do - self:T('Before Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) + for UnitID = 1, nunits do - -- Tranlate position. + -- 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) - SpawnTemplate.units[UnitID].x = TX - SpawnTemplate.units[UnitID].y = TY - SpawnTemplate.units[UnitID].alt = PointVec3.y + + if spawnonground then + + -- Sh�ps and FARPS seem to have a build in queue. + if spawnonship or spawnonfarp or spawnonrunway or automatic then + self:T(RAT.id..string.format("RAT group %s spawning at farp, ship or runway %s.", self.alias, departure:GetName())) + + -- Spawn on ship. We take only the position of the ship. + SpawnTemplate.units[UnitID].x = PointVec3.x --TX + SpawnTemplate.units[UnitID].y = PointVec3.z --TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + else + self:T(RAT.id..string.format("RAT group %s spawning at airbase %s on parking spot id %d", self.alias, departure:GetName(), parkingindex[UnitID])) + + -- Get coordinates of parking spot. + SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x + SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z + SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y + end + + else + self:T(RAT.id..string.format("RAT group %s spawning in air at %s.", self.alias, departure:GetName())) + + -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set. + SpawnTemplate.units[UnitID].x = TX + SpawnTemplate.units[UnitID].y = TY + SpawnTemplate.units[UnitID].alt = PointVec3.y + end + + -- Place marker at spawn position. + if self.Debug then + local unitspawn=COORDINATE:New(SpawnTemplate.units[UnitID].x, SpawnTemplate.units[UnitID].alt, SpawnTemplate.units[UnitID].y) + unitspawn:MarkToAll(string.format("RAT %s Spawnplace unit #%d", self.alias, UnitID)) + end + + -- Parking spot id. + UnitTemplate.parking = nil + UnitTemplate.parking_id = nil + if parkingindex[UnitID] and not automatic then + UnitTemplate.parking = parkingindex[UnitID] + end + + -- Debug info. + self:T2(RAT.id..string.format("RAT group %s unit number %d: Parking = %s",self.alias, UnitID, tostring(UnitTemplate.parking))) + self:T2(RAT.id..string.format("RAT group %s unit number %d: Parking ID = %s",self.alias, UnitID, tostring(UnitTemplate.parking_id))) + + + -- Set initial heading. SpawnTemplate.units[UnitID].heading = heading SpawnTemplate.units[UnitID].psi = -heading @@ -4373,16 +5394,6 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace) SpawnTemplate.CountryID=self.country end - -- Parking spot. - UnitTemplate.parking = nil - UnitTemplate.parking_id = self.parking_id - --self:T(RAT.id.."Parking ID "..tostring(self.parking_id)) - - -- Initial altitude - UnitTemplate.alt=PointVec3.y - - self:T('After Translation SpawnTemplate.units['..UnitID..'].x = '..SpawnTemplate.units[UnitID].x..', SpawnTemplate.units['..UnitID..'].y = '..SpawnTemplate.units[UnitID].y) - end -- Copy waypoints into spawntemplate. By this we avoid the nasty DCS "landing bug" :) @@ -4411,6 +5422,8 @@ function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace) self:T(SpawnTemplate) end end + + return true end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -4422,7 +5435,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() @@ -4445,7 +5458,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 @@ -4470,7 +5483,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 @@ -4501,7 +5514,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 @@ -4509,7 +5522,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 @@ -4517,7 +5530,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 @@ -4559,12 +5572,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) @@ -4692,12 +5705,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 @@ -4712,12 +5720,13 @@ end -- @field #table min Minimum number of RAT groups alive. -- @field #number nrat Number of RAT objects. -- @field #number ntot Total number of active RAT groups. --- @field #number Tcheck Time interval between checking of alive groups. +-- @field #number Tcheck Time interval in seconds between checking of alive groups. +-- @field #number dTspawn Time interval in seconds between spawns of groups. -- @field Core.Scheduler#SCHEDULER manager Scheduler managing the RAT objects. -- @field #number managerid Managing scheduler id. -- @extends Core.Base#BASE ----# RATMANAGER class, extends @{Base#BASE} +---# RATMANAGER class, extends @{Core.Base#BASE} -- The RATMANAGER class manages spawning of multiple RAT objects in a very simple way. It is created by the @{#RATMANAGER.New}() contructor. -- RAT objects with different "tasks" can be defined as usual. However, they **must not** be spawned via the @{#RAT.Spawn}() function. -- @@ -4732,26 +5741,26 @@ end -- In this example, three different @{#RAT} objects are created (but not spawned manually). The @{#RATMANAGER} takes care that at least five aircraft of each type are alive and that the total number of aircraft -- spawned is 25. The @{#RATMANAGER} is started after 30 seconds and stopped after two hours. -- --- local a10c=RAT:New("RAT_A10C", "A-10C managed") --- a10c:SetDeparture({"Batumi"}) --- --- local f15c=RAT:New("RAT_F15C", "F15C managed") --- f15c:SetDeparture({"Sochi-Adler"}) --- f15c:DestinationZone() --- f15c:SetDestination({"Zone C"}) --- --- local av8b=RAT:New("RAT_AV8B", "AV8B managed") --- av8b:SetDeparture({"Zone C"}) --- av8b:SetTakeoff("air") --- av8b:DestinationZone() --- av8b:SetDestination({"Zone A"}) --- --- local manager=RATMANAGER:New(25) --- manager:Add(a10c, 5) --- manager:Add(f15c, 5) --- manager:Add(av8b, 5) --- manager:Start(30) --- manager:Stop(7200) +-- local a10c=RAT:New("RAT_A10C", "A-10C managed") +-- a10c:SetDeparture({"Batumi"}) +-- +-- local f15c=RAT:New("RAT_F15C", "F15C managed") +-- f15c:SetDeparture({"Sochi-Adler"}) +-- f15c:DestinationZone() +-- f15c:SetDestination({"Zone C"}) +-- +-- local av8b=RAT:New("RAT_AV8B", "AV8B managed") +-- av8b:SetDeparture({"Zone C"}) +-- av8b:SetTakeoff("air") +-- av8b:DestinationZone() +-- av8b:SetDestination({"Zone A"}) +-- +-- local manager=RATMANAGER:New(25) +-- manager:Add(a10c, 5) +-- manager:Add(f15c, 5) +-- manager:Add(av8b, 5) +-- manager:Start(30) +-- manager:Stop(7200) -- -- @field #RATMANAGER RATMANAGER={ @@ -4763,7 +5772,8 @@ RATMANAGER={ min={}, nrat=0, ntot=nil, - Tcheck=30, + Tcheck=60, + dTspawn=1.0, manager=nil, managerid=nil, } @@ -4795,6 +5805,7 @@ end -- @param #RATMANAGER self -- @param #RAT ratobject RAT object to be managed. -- @param #number min Minimum number of groups for this RAT object. Default is 1. +-- @return #RATMANAGER RATMANAGER self object. function RATMANAGER:Add(ratobject,min) --Automatic respawning is disabled. @@ -4814,11 +5825,14 @@ function RATMANAGER:Add(ratobject,min) -- Call spawn to initialize RAT parameters. ratobject:Spawn(0) + + return self end --- Starts the RAT manager and spawns the initial random number RAT groups for each RAT object. -- @param #RATMANAGER self -- @param #number delay Time delay in seconds after which the RAT manager is started. Default is 5 seconds. +-- @return #RATMANAGER RATMANAGER self object. function RATMANAGER:Start(delay) -- Time delay. @@ -4835,10 +5849,13 @@ function RATMANAGER:Start(delay) -- Start scheduler. SCHEDULER:New(nil, self._Start, {self}, delay) + + return self end --- Instantly starts the RAT manager and spawns the initial random number RAT groups for each RAT object. -- @param #RATMANAGER self +-- @return #RATMANAGER RATMANAGER self object. function RATMANAGER:_Start() -- Ensure that ntot is at least sum of min RAT groups. @@ -4852,48 +5869,75 @@ function RATMANAGER:_Start() local N=self:_RollDice(self.nrat, self.ntot, self.min, self.alive) -- Loop over all RAT objects and spawn groups. + local time=0.0 for i=1,self.nrat do for j=1,N[i] do - self.rat[i]:_SpawnWithRoute() - end - -- Start activation scheduler for uncontrolled aircraft. - if self.rat[i].uncontrolled and self.rat[i].activate_uncontrolled then - SCHEDULER:New(nil, self.rat[i]._ActivateUncontrolled, {self.rat[i]}, self.rat[i].activate_delay, self.rat[i].activate_delta, self.rat[i].activate_frand) + time=time+self.dTspawn + SCHEDULER:New(nil, RAT._SpawnWithRoute, {self.rat[i]}, time) end end + -- Start activation scheduler for uncontrolled aircraft. + for i=1,self.nrat do + if self.rat[i].uncontrolled and self.rat[i].activate_uncontrolled then + -- Start activating stuff but not before the latest spawn has happend. + local Tactivate=math.max(time+1, self.rat[i].activate_delay) + SCHEDULER:New(self.rat[i], self.rat[i]._ActivateUncontrolled, {self.rat[i]}, Tactivate, self.rat[i].activate_delta, self.rat[i].activate_frand) + end + end + + -- Start the manager. But not earlier than the latest spawn has happened! + local TstartManager=math.max(time+1, self.Tcheck) + -- Start manager scheduler. - self.manager, self.managerid = SCHEDULER:New(nil, self._Manage, {self}, 5, self.Tcheck) --Core.Scheduler#SCHEDULER + self.manager, self.managerid = SCHEDULER:New(self, self._Manage, {self}, TstartManager, self.Tcheck) --Core.Scheduler#SCHEDULER -- Info - local text=string.format(RATMANAGER.id.."Starting RAT manager with scheduler ID %s.", self.managerid) + local text=string.format(RATMANAGER.id.."Starting RAT manager with scheduler ID %s in %d seconds. Repeat interval %d seconds.", self.managerid, TstartManager, self.Tcheck) self:E(text) + return self end --- Stops the RAT manager. -- @param #RATMANAGER self -- @param #number delay Delay in seconds before the manager is stopped. Default is 1 second. +-- @return #RATMANAGER RATMANAGER self object. function RATMANAGER:Stop(delay) delay=delay or 1 self:E(string.format(RATMANAGER.id.."Manager will be stopped in %d seconds.", delay)) SCHEDULER:New(nil, self._Stop, {self}, delay) + return self end --- Instantly stops the RAT manager by terminating its scheduler. -- @param #RATMANAGER self +-- @return #RATMANAGER RATMANAGER self object. function RATMANAGER:_Stop() self:E(string.format(RATMANAGER.id.."Stopping manager with scheduler ID %s.", self.managerid)) self.manager:Stop(self.managerid) + return self end ---- Sets the time interval between checks of alive RAT groups. Default is 30 seconds. +--- Sets the time interval between checks of alive RAT groups. Default is 60 seconds. -- @param #RATMANAGER self -- @param #number dt Time interval in seconds. +-- @return #RATMANAGER RATMANAGER self object. function RATMANAGER:SetTcheck(dt) - self.Tcheck=dt or 30 + self.Tcheck=dt or 60 + return self end +--- Sets the time interval between spawning of groups. +-- @param #RATMANAGER self +-- @param #number dt Time interval in seconds. Default is 1 second. +-- @return #RATMANAGER RATMANAGER self object. +function RATMANAGER:SetTspawn(dt) + self.dTspawn=dt or 1.0 + return self +end + + --- Manager function. Calculating the number of current groups and respawning new groups if necessary. -- @param #RATMANAGER self function RATMANAGER:_Manage() @@ -4902,18 +5946,18 @@ function RATMANAGER:_Manage() local ntot=self:_Count() -- Debug info. - if self.Debug then - local text=string.format("Number of alive groups %d. New groups to be spawned %d.", ntot, self.ntot-ntot) - self:T(RATMANAGER.id..text) - end + local text=string.format("Number of alive groups %d. New groups to be spawned %d.", ntot, self.ntot-ntot) + self:T(RATMANAGER.id..text) -- Get number of necessary spawns. local N=self:_RollDice(self.nrat, self.ntot, self.min, self.alive) -- Loop over all RAT objects and spawn new groups if necessary. + local time=0.0 for i=1,self.nrat do for j=1,N[i] do - self.rat[i]:_SpawnWithRoute() + time=time+self.dTspawn + SCHEDULER:New(nil, RAT._SpawnWithRoute, {self.rat[i]}, time) end end end @@ -4946,10 +5990,8 @@ function RATMANAGER:_Count() ntotal=ntotal+n -- Debug output. - if self.Debug then - local text=string.format("Number of alive groups of %s = %d", self.name[i], n) - self:T(RATMANAGER.id..text) - end + local text=string.format("Number of alive groups of %s = %d", self.name[i], n) + self:T(RATMANAGER.id..text) end -- Return grand total. @@ -5029,7 +6071,7 @@ function RATMANAGER:_RollDice(nrat,ntot,min,alive) end -- Debug info - --env.info(string.format("RATMANAGER: i=%d, alive=%d, min=%d, mini=%d, maxi=%d, add=%d, sumN=%d, sumP=%d", j, alive[j], min[j], mini[j], maxi[j], N[j],sN, sP)) + self:T3(string.format("RATMANAGER: i=%d, alive=%d, min=%d, mini=%d, maxi=%d, add=%d, sumN=%d, sumP=%d", j, alive[j], min[j], mini[j], maxi[j], N[j],sN, sP)) end @@ -5042,14 +6084,12 @@ function RATMANAGER:_RollDice(nrat,ntot,min,alive) table.insert(done,j) -- Debug info - if self.Debug then - local text=RATMANAGER.id.."\n" - for i=1,nrat do - text=text..string.format("%s: i=%d, alive=%d, min=%d, mini=%d, maxi=%d, add=%d\n", self.name[i], i, alive[i], min[i], mini[i], maxi[i], N[i]) - end - text=text..string.format("Total # of groups to add = %d", sum(N, done)) - self:T2(text) + local text=RATMANAGER.id.."\n" + for i=1,nrat do + text=text..string.format("%s: i=%d, alive=%d, min=%d, mini=%d, maxi=%d, add=%d\n", self.name[i], i, alive[i], min[i], mini[i], maxi[i], N[i]) end + text=text..string.format("Total # of groups to add = %d", sum(N, done)) + self:T(text) -- Return number of groups to be spawned. return N diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index 3c946ea79..394f134f2 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -1,12 +1,7 @@ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- **Functional** - (R2.3) - Range Practice. +--- **Functional** - Range Practice. -- -- === -- --- ![Banner Image](..\Presentations\RANGE\RANGE_Main.png) --- --- === --- -- 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 @@ -14,30 +9,23 @@ -- -- [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is highly recommended for this class. -- --- ## Features +-- ## Features: -- --- * Bomb and rocket impact point from closest range target is measured and distance reported to the player. --- * Number of hits on strafing passes are counted. --- * 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. --- * Rocket or bomb 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. +-- * 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. +-- * Range targets can be marked by smoke. +-- * Range can be illuminated by illumination bombs for night practices. +-- * 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. -- -- === -- --- # Demo Missions --- --- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases) --- --- === --- --- # YouTube Channel --- +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) -- ### [MOOSE - On the Range - Demonstration Video](https://www.youtube.com/watch?v=kIXcxNB9_3M) -- -- === @@ -47,7 +35,8 @@ -- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536), [Ciribob](https://forums.eagle.ru/member.php?u=112175) -- -- === --- @module Range +-- @module Functional.Range +-- @image Range.JPG ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- RANGE class @@ -55,8 +44,9 @@ -- @field #string ClassName Name of the Class. -- @field #boolean Debug If true, debug info is send as messages on the screen. -- @field #string rangename Name of the range. --- @field Core.Point#COORDINATE location Coordinate of the range. --- @field #number rangeradius Radius of range defining its total size for e.g. smoking bomb impact points and sending radio messages. Default 10 km. +-- @field Core.Point#COORDINATE location Coordinate of the range location. +-- @field #number rangeradius Radius of range defining its total size for e.g. smoking bomb impact points and sending radio messages. Default 5 km. +-- @field Core.Zone#ZONE rangezone MOOSE zone object of the range. For example, no bomb impacts are smoked if bombs fall outside of the range zone. -- @field #table strafeTargets Table of strafing targets. -- @field #table bombingTargets Table of targets to bomb. -- @field #number nbombtargets Number of bombing targets. @@ -68,7 +58,10 @@ -- @field #table bombPlayerResults Table containing the bombing results of each player. -- @field #table PlayerSettings Indiviual 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 Tmsg Time [sec] messages to players are displayed. Default 30 sec. +-- @field #string examinergroupname Name of the examiner group which should get all messages. +-- @field #boolean examinerexclusive If true, only the examiner gets messages. If false, clients and examiner get messages. -- @field #number strafemaxalt Maximum altitude above ground for registering for a strafe run. Default is 914 m = 3000 ft. -- @field #number ndisplayresult Number of (player) results that a displayed. Default is 10. -- @field Utilities.Utils#SMOKECOLOR BombSmokeColor Color id used for smoking bomb targets. @@ -79,14 +72,16 @@ -- @field #number scorebombdistance Distance from closest target up to which bomb hits are counted. Default 1000 m. -- @field #number TdelaySmoke Time delay in seconds between impact of bomb and starting the smoke. Default 3 seconds. -- @field #boolean eventmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. Default true. +-- @field #boolean trackbombs If true (default), all bomb types are tracked and impact point to closest bombing target is evaluated. +-- @field #boolean trackrockets If true (default), all rocket types are tracked and impact point to closest bombing target is evaluated. +-- @field #boolean trackmissiles If true (default), all missile types are tracked and impact point to closest bombing target is evaluated. -- @extends Core.Base#BASE ----# RANGE class, extends @{Base#BASE} --- 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. +--- 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. -- --- Generally, a range consits 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 or rocket to the closest range target is measured and tabulated. +-- 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. @@ -101,31 +96,34 @@ -- ## Strafe Pits -- Each strafe pit can consist of multiple targets. Often one findes two or three strafe targets next to each other. -- --- A strafe pit can be added to the range by the @{#RANGE.AddStrafepit}(unitnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) function. +-- A strafe pit can be added to the range by the @{#RANGE.AddStrafePit}(*targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*) function. -- --- The first parameter defines the target. This has to be given as a lua table which contains the unit names of the targets as 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 last parameter *foulline* sets the distance from the pit targets to the foul line. Hit from closer than this line are not counted! -- --- 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 judges as a good pass. --- --- The last parameter "foulline" sets the distance from the pit targets to the foul line. Hit from closer than this line are not counted. +-- Another function to add a strafe pit is @{#RANGE.AddStrafePitGroup}(*group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*). Here, +-- the first parameter *group* is a MOOSE @{Wrapper.Group} object and **all** units in this group define **one** strafe pit. -- -- Finally, a valid approach has to be performed below a certain maximum altitude. The default is 914 meters (3000 ft) AGL. This is a parameter valid for all -- strafing pits of the range and can be adjusted by the @{#RANGE.SetMaxStrafeAlt}(maxalt) function. -- -- ## Bombing targets --- One ore multiple bombing targets can be added to the range by the @{#RANGE.AddBombingTargets}(unitnames goodhitrange,static) function. +-- One ore multiple bombing targets can be added to the range by the @{#RANGE.AddBombingTargets}(targetnames, goodhitrange, randommove) function. -- --- The first parameter "unitnames" has to be a lua table, which contains the names of the units as defined in the mission editor. --- --- The 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". --- --- The final (optional) parameter "static" can be enabled (set to true) if static bomb targets are used rather than alive units. +-- * 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. +-- +-- 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: @@ -138,6 +136,9 @@ -- * @{#RANGE.SetStrafeTargetSmokeColor}() sets the color used to smoke strafe targets. -- * @{#RANGE.SetStrafePitSmokeColor}() sets the color used to smoke strafe pit approach boxes. -- * @{#RANGE.SetSmokeTimeDelay}() sets the time delay between smoking bomb/rocket impact points after impact. +-- * @{#RANGE.TrackBombsON}() or @{#RANGE.TrackBombsOFF}() can be used to enable/disable tracking and evaluating of all bomb types a player fires. +-- * @{#RANGE.TrackRocketsON}() or @{#RANGE.TrackRocketsOFF}() can be used to enable/disable tracking and evaluating of all rocket types a player fires. +-- * @{#RANGE.TrackMissilesON}() or @{#RANGE.TrackMissilesOFF}() can be used to enable/disable tracking and evaluating of all missile types a player fires. -- -- ## Radio Menu -- Each range gets a radio menu with various submenus where each player can adjust his individual settings or request information about the range or his scores. @@ -155,11 +156,11 @@ -- ## Examples -- -- ### Goldwater Range --- This example shows hot to set up the Barry M. Goldwater range. It consists of two strafe pits each has two targets plus three bombing targets. +-- 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. -- --- The [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is used in this example. --- --- -- 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. +-- -- 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"} -- @@ -167,16 +168,15 @@ -- local bombtargets={"GWR Bomb Target Circle Left", "GWR Bomb Target Circle Right", "GWR Bomb Target Hard"} -- -- -- Create a range object. --- local GoldwaterRange=RANGE:New("Goldwater Range") +-- GoldwaterRange=RANGE:New("Goldwater Range") -- --- -- Distance between foul line and strafe target. Note that this could also be done manually by simply measuring the distance between the target and the foul line in the ME. --- local strafe=UNIT:FindByName("GWR Strafe Pit Left 1") --- local foul=UNIT:FindByName("GWR Foul Line Left") --- local fouldist=strafe:GetCoordinate():Get2DDistance(foul:GetCoordinate()) +-- -- 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, 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) @@ -184,6 +184,27 @@ -- -- Start range. -- GoldwaterRange:Start() -- +-- The [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is (implicitly) used in this example. +-- +-- ## Debugging +-- +-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in +-- C:\Users\\Saved Games\DCS\Logs\dcs.log +-- All output concerning the RANGE class should have the string "RANGE" in the corresponding line. +-- +-- The verbosity of the output can be increased by adding the following lines to your script: +-- +-- BASE:TraceOnOff(true) +-- BASE:TraceLevel(1) +-- BASE:TraceClass("RANGE") +-- +-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{BASE} for more details. +-- +-- The function @{#RANGE.DebugON}() can be used to send messages on screen. It also smokes all defined strafe and bombing targets, the strafe pit approach boxes and the range zone. +-- +-- Note that it can happen that the RANGE radio menu is not shown. Check that the range object is defined as a **global** variable rather than a local one. +-- The could avoid the lua garbage collection to accidentally/falsely deallocate the RANGE objects. +-- -- -- -- @field #RANGE @@ -192,7 +213,8 @@ RANGE={ Debug=false, rangename=nil, location=nil, - rangeradius=10000, + rangeradius=5000, + rangezone=nil, strafeTargets={}, bombingTargets={}, nbombtargets=0, @@ -204,7 +226,10 @@ RANGE={ bombPlayerResults = {}, PlayerSettings = {}, dtBombtrack=0.005, + BombtrackThreshold=25000, Tmsg=30, + examinergroupname=nil, + examinerexclusive=nil, strafemaxalt=914, ndisplayresult=10, BombSmokeColor=SMOKECOLOR.Red, @@ -215,8 +240,32 @@ RANGE={ scorebombdistance=1000, TdelaySmoke=3.0, eventmoose=true, + trackbombs=true, + trackrockets=true, + trackmissiles=true, } +--- Default range parameters. +-- @list Defaults +RANGE.Defaults={ + goodhitrange=25, + strafemaxalt=914, + dtBombtrack=0.005, + Tmsg=30, + ndisplayresult=10, + rangeradius=5000, + TdelaySmoke=3.0, + boxlength=3000, + boxwidth=300, + goodpass=20, + goodhitrange=25, + foulline=610, +} + +--- Global list of all defined range names. +-- @field #table Names +RANGE.Names={} + --- Main radio menu. -- @field #table MenuF10 RANGE.MenuF10={} @@ -226,11 +275,14 @@ RANGE.MenuF10={} RANGE.id="RANGE | " --- Range script version. --- @field #number version -RANGE.version="1.0.1" +-- @field #string version +RANGE.version="1.2.3" ---TODO list ---TODO: Add statics for strafe pits. +--TODO list: +--TODO: Add custom weapons, which can be specified by the user. +--TODO: Check if units are still alive. +--DONE: Add statics for strafe pits. +--DONE: Add missiles. --DONE: Convert env.info() to self:T() --DONE: Add user functions. --DONE: Rename private functions, i.e. start with _functionname. @@ -251,10 +303,11 @@ function RANGE:New(rangename) local self=BASE:Inherit(self, BASE: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" -- Debug info. - local text=string.format("RANGE script version %s. Creating new RANGE object. Range name: %s.", RANGE.version, self.rangename) + local text=string.format("RANGE script version %s - creating new RANGE object of name: %s.", RANGE.version, self.rangename) self:E(RANGE.id..text) MESSAGE:New(text, 10):ToAllIf(self.Debug) @@ -274,9 +327,10 @@ function RANGE:Start() local _count=0 for _,_target in pairs(self.bombingTargets) do _count=_count+1 - --_target.name + + -- Get range location. if _location==nil then - _location=_target.point --Core.Point#COORDINATE + _location=_target.target:GetCoordinate() --Core.Point#COORDINATE end end self.nbombtargets=_count @@ -285,6 +339,7 @@ function RANGE:Start() _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() @@ -293,13 +348,20 @@ function RANGE:Start() end self.nstrafetargets=_count - -- Location of the range. We simply take the first unit/target we find. - self.location=_location + -- 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) - return nil + 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. @@ -311,9 +373,6 @@ function RANGE:Start() if self.eventmoose then -- Events are handled my MOOSE. self:T(RANGE.id.."Events are handled by MOOSE.") - --self:HandleEvent(EVENTS.Birth, self._OnBirth) - --self:HandleEvent(EVENTS.Hit, self._OnHit) - --self:HandleEvent(EVENTS.Shot, self._OnShot) self:HandleEvent(EVENTS.Birth) self:HandleEvent(EVENTS.Hit) self:HandleEvent(EVENTS.Shot) @@ -323,6 +382,28 @@ function RANGE:Start() 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()) + + 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 + + -- Debug mode: smoke all targets and range zone. + if self.Debug then + self:_MarkTargetsOnMap() + self:_SmokeBombTargets() + self:_SmokeStrafeTargets() + self:_SmokeStrafeTargetBoxes() + self.rangezone:SmokeZone(SMOKECOLOR.White) + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -332,35 +413,67 @@ end -- @param #RANGE self -- @param #number maxalt Maximum altitude AGL in meters. Default is 914 m= 3000 ft. function RANGE:SetMaxStrafeAlt(maxalt) - self.strafemaxalt=maxalt or 914 + self.strafemaxalt=maxalt or RANGE.Defaults.strafemaxalt 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. function RANGE:SetBombtrackTimestep(dt) - self.dtBombtrack=dt or 0.005 + self.dtBombtrack=dt or RANGE.Defaults.dtBombtrack end --- Set time how long (most) messages are displayed. -- @param #RANGE self -- @param #number time Time in seconds. Default is 30 s. function RANGE:SetMessageTimeDuration(time) - self.Tmsg=time or 30 + self.Tmsg=time or RANGE.Defaults.Tmsg +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. +function RANGE:SetMessageToExaminer(examinergroupname, exclusively) + self.examinergroupname=examinergroupname + self.examinerexclusive=exclusively end --- Set max number of player results that are displayed. -- @param #RANGE self -- @param #number nmax Number of results. Default is 10. function RANGE:SetDisplayedMaxPlayerResults(nmax) - self.ndisplayresult=nmax or 10 + self.ndisplayresult=nmax or RANGE.Defaults.ndisplayresult 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 10 km. +-- @param #number radius Radius in km. Default 5 km. function RANGE:SetRangeRadius(radius) - self.rangeradius=radius*1000 or 10000 + self.rangeradius=radius*1000 or RANGE.Defaults.rangeradius +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. +function RANGE:SetBombtrackThreshold(distance) + self.BombtrackThreshold=distance*1000 or 25*1000 +end + +--- Set range location. If this is not done, one (random) unit position of the range is used to determine the location of the range. +-- The range location determines the position at which the weather data is evaluated. +-- @param #RANGE self +-- @param Core.Point#COORDINATE coordinate Coordinate of the range. +function RANGE:SetRangeLocation(coordinate) + self.location=coordinate +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:SetRangeZone(zone) + self.rangezone=zone end --- Set smoke color for marking bomb targets. By default bomb targets are marked by red smoke. @@ -388,7 +501,7 @@ end -- @param #RANGE self -- @param #number delay Time delay in seconds. Default is 3 seconds. function RANGE:SetSmokeTimeDelay(delay) - self.TdelaySmoke=delay or 3.0 + self.TdelaySmoke=delay or RANGE.Defaults.TdelaySmoke end --- Enable debug modus. @@ -403,24 +516,60 @@ function RANGE:DebugOFF() self.Debug=false end +--- Enables tracking of all bomb types. Note that this is the default setting. +-- @param #RANGE self +function RANGE:TrackBombsON() + self.trackbombs=true +end + +--- Disables tracking of all bomb types. +-- @param #RANGE self +function RANGE:TrackBombsOFF() + self.trackbombs=false +end + +--- Enables tracking of all rocket types. Note that this is the default setting. +-- @param #RANGE self +function RANGE:TrackRocketsON() + self.trackrockets=true +end + +--- Disables tracking of all rocket types. +-- @param #RANGE self +function RANGE:TrackRocketsOFF() + self.trackrockets=false +end + +--- Enables tracking of all missile types. Note that this is the default setting. +-- @param #RANGE self +function RANGE:TrackMissilesON() + self.trackmissiles=true +end + +--- Disables tracking of all missile types. +-- @param #RANGE self +function RANGE:TrackMissilesOFF() + self.trackmissiles=false +end + --- Add new strafe pit. For a strafe pit, hits from guns are counted. One pit can consist of several units. -- Note, an approach is only valid, if the player enters via a zone in front of the pit, which defined by boxlength and boxheading. -- Furthermore, the player must not be too high and fly in the direction of the pit to make a valid target apporoach. -- @param #RANGE self --- @param #table unitnames Table of unit names defining the strafe targets. The first target in the list determines the approach zone (heading and box). +-- @param #table targetnames Table of unit or static names defining the strafe targets. The first target in the list determines the approach zone (heading and box). -- @param #number boxlength (Optional) Length of the approach box in meters. Default is 3000 m. -- @param #number boxwidth (Optional) Width of the approach box in meters. Default is 300 m. -- @param #number heading (Optional) Approach heading in Degrees. Default is heading of the unit as defined in the mission editor. -- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false. -- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. -- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. -function RANGE:AddStrafePit(unitnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) - self:F({unitnames=unitnames, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) +function RANGE:AddStrafePit(targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) + self:F({targetnames=targetnames, boxlength=boxlength, boxwidth=boxwidth, heading=heading, inverseheading=inverseheading, goodpass=goodpass, foulline=foulline}) -- Create table if necessary. - if type(unitnames) ~= "table" then - unitnames={unitnames} + if type(targetnames) ~= "table" then + targetnames={targetnames} end -- Make targets @@ -428,11 +577,34 @@ function RANGE:AddStrafePit(unitnames, boxlength, boxwidth, heading, inversehead local center=nil --Wrapper.Unit#UNIT local ntargets=0 - for _i,_name in ipairs(unitnames) do + for _i,_name in ipairs(targetnames) do - self:T(RANGE.id..string.format("Adding strafe target #%d %s", _i, _name)) - local unit=UNIT:FindByName(_name) + -- Check if we have a static or unit object. + local _isstatic=self:_CheckStatic(_name) + + local unit=nil + if _isstatic==true then + -- Add static object. + self:T(RANGE.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)) + 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) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + + end + + -- Add object to targets. if unit then table.insert(_targets, unit) -- Define center as the first unit we find @@ -440,17 +612,21 @@ function RANGE:AddStrafePit(unitnames, boxlength, boxwidth, heading, inversehead center=unit end ntargets=ntargets+1 - else - local text=string.format("ERROR! Could not find strafe target with name %s.", _name) - self:E(RANGE.id..text) - MESSAGE:New(text, 10):ToAllIf(self.Debug) 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) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + return + end -- Approach box dimensions. - local l=boxlength or 3000 - local w=(boxwidth or 300)/2 + 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() @@ -469,10 +645,10 @@ function RANGE:AddStrafePit(unitnames, boxlength, boxwidth, heading, inversehead end -- Number of hits called a "good" pass. - local goodpass=goodpass or 20 + goodpass=goodpass or RANGE.Defaults.goodpass -- Foule line distance. - local foulline=foulline or 610 + foulline=foulline or RANGE.Defaults.foulline -- Coordinate of the range. local Ccenter=center:GetCoordinate() @@ -499,96 +675,195 @@ function RANGE:AddStrafePit(unitnames, boxlength, boxwidth, heading, inversehead --_polygon:BoundZone() -- Add zone to table. - table.insert(self.strafeTargets, {name=_name, polygon=_polygon, goodPass=goodpass, targets=_targets, foulline=foulline, smokepoints=p, heading=heading}) + table.insert(self.strafeTargets, {name=_name, polygon=_polygon, coordinate= Ccenter, goodPass=goodpass, targets=_targets, foulline=foulline, smokepoints=p, heading=heading}) -- 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, boxlength, boxwidth, goodpass, foulline) + 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) MESSAGE:New(text, 5):ToAllIf(self.Debug) end ---- Add bombing target(s) to range. --- @param #RANGE self --- @param #table unitnames Table containing the unit names acting 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 static (Optional) Target is static. Default false. -function RANGE:AddBombingTargets(unitnames, goodhitrange, static) - self:F({unitnames=unitnames, goodhitrange=goodhitrange, static=static}) - -- Create a table if necessary. - if type(unitnames) ~= "table" then - unitnames={unitnames} - end - - if static == nil or static == false then - static=false - else - static=true - end - - -- Default range is 25 m. - goodhitrange=goodhitrange or 25 - - for _,name in pairs(unitnames) do - local _unit - local _static +--- Add all units of a group as one new strafe target pit. +-- For a strafe pit, hits from guns are counted. One pit can consist of several units. +-- Note, an approach is only valid, if the player enters via a zone in front of the pit, which defined by boxlength and boxheading. +-- Furthermore, the player must not be too high and fly in the direction of the pit to make a valid target apporoach. +-- @param #RANGE self +-- @param Wrapper.Group#GROUP group MOOSE group of unit names defining the strafe target pit. The first unit in the group determines the approach zone (heading and box). +-- @param #number boxlength (Optional) Length of the approach box in meters. Default is 3000 m. +-- @param #number boxwidth (Optional) Width of the approach box in meters. Default is 300 m. +-- @param #number heading (Optional) Approach heading in Degrees. Default is heading of the unit as defined in the mission editor. +-- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false. +-- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20. +-- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line. +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 - if static then - - -- Add static object. Workaround since cargo objects are not yet in database because DCS function does not add those. - local _DCSstatic=StaticObject.getByName(name) - if _DCSstatic and _DCSstatic:isExist() then - self:T(RANGE.id..string.format("Adding DCS static to database. Name = %s.", name)) - _DATABASE:AddStatic(name) - else - self:E(RANGE.id..string.format("ERROR! DCS static DOES NOT exist! Name = %s.", name)) - end - - -- Now we can find it... - _static=STATIC:FindByName(name) - if _static then - self:AddBombingTargetUnit(_static, goodhitrange) - self:T(RANGE.id..string.format("Adding static bombing target %s with hit range %d.", name, goodhitrange)) - else - self:E(RANGE.id..string.format("ERROR! Cound not find static bombing target %s.", name)) - end - - else + -- Get units of group. + local _units=group:GetUnits() - _unit=UNIT:FindByName(name) - if _unit then - self:AddBombingTargetUnit(_unit, goodhitrange) - self:T(RANGE.id..string.format("Adding bombing target %s with hit range %d.", name, goodhitrange)) - else - self:E(RANGE.id..string.format("ERROR! Could not find bombing target %s.", name)) + -- Make table of unit names. + local _names={} + for _,_unit in ipairs(_units) do + + local _unit=_unit --Wrapper.Unit#UNIT + + if _unit and _unit:IsAlive() then + local _name=_unit:GetName() + table.insert(_names,_name) end end + + -- Add strafe pit. + self:AddStrafePit(_names, boxlength, boxwidth, heading, inverseheading, goodpass, foulline) + end +end + +--- Add bombing target(s) to range. +-- @param #RANGE self +-- @param #table targetnames Table containing names of unit or static objects serving as bomb targets. +-- @param #number goodhitrange (Optional) Max distance from target unit (in meters) which is considered as a good hit. Default is 25 m. +-- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +function RANGE:AddBombingTargets(targetnames, goodhitrange, randommove) + self:F({targetnames=targetnames, goodhitrange=goodhitrange, randommove=randommove}) + + -- Create a table if necessary. + if type(targetnames) ~= "table" then + targetnames={targetnames} + end + + -- Default range is 25 m. + goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange + + for _,name in pairs(targetnames) do + + -- Check if we have a static or unit object. + local _isstatic=self:_CheckStatic(name) + + if _isstatic==true then + local _static=STATIC:FindByName(name) + self:T2(RANGE.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:AddBombingTargetUnit(_unit, goodhitrange) + else + self:E(RANGE.id..string.format("ERROR! Could not find bombing target %s.", name)) + end + end end ---- Add a unit as bombing target. +--- Add a unit or static object as bombing target. -- @param #RANGE self --- @param Wrapper.Unit#UNIT unit Unit of the strafe target. +-- @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. -function RANGE:AddBombingTargetUnit(unit, goodhitrange) - self:F({unit=unit, goodhitrange=goodhitrange}) +-- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +function RANGE:AddBombingTargetUnit(unit, goodhitrange, randommove) + self:F({unit=unit, goodhitrange=goodhitrange, randommove=randommove}) - local coord=unit:GetCoordinate() + -- Get name of positionable. local name=unit:GetName() - -- Default range is 25 m. - goodhitrange=goodhitrange or 25 + -- Check if we have a static or unit object. + local _isstatic=self:_CheckStatic(name) - -- Create a zone around the unit. - local Vec2=coord:GetVec2() - local Rzone=ZONE_RADIUS:New(name, Vec2, goodhitrange) + -- Default range is 25 m. + goodhitrange=goodhitrange or RANGE.Defaults.goodhitrange + + -- Set randommove to false if it was not specified. + if randommove==nil or _isstatic==true then + randommove=false + end + + -- Debug or error output. + if _isstatic==true then + self:T(RANGE.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))) + 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)) + end + + -- Get max speed of unit in km/h. + local speed=0 + if _isstatic==false then + speed=self:_GetSpeed(unit) + end -- Insert target to table. - table.insert(self.bombingTargets, {name=name, point=coord, zone=Rzone, target=unit, goodhitrange=goodhitrange}) + table.insert(self.bombingTargets, {name=name, target=unit, goodhitrange=goodhitrange, move=randommove, speed=speed}) end +--- Add all units of a group as bombing targets. +-- @param #RANGE self +-- @param Wrapper.Group#GROUP group Group of bombing targets. +-- @param #number goodhitrange Max distance from unit which is considered as a good hit. +-- @param #boolean randommove If true, unit will move randomly within the range. Default is false. +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 + +end + +--- Measures the foule line distance between two unit or static objects. +-- @param #RANGE self +-- @param #string namepit Name of the strafe pit target object. +-- @param #string namefoulline Name of the fould line distance marker object. +-- @return #number Foul line distance in meters. +function RANGE:GetFoullineDistance(namepit, namefoulline) + self:F({namepit=namepit, namefoulline=namefoulline}) + + -- Check if we have units or statics. + local _staticpit=self:_CheckStatic(namepit) + local _staticfoul=self:_CheckStatic(namefoulline) + + -- Get the unit or static pit object. + local pit=nil + if _staticpit==true then + pit=STATIC:FindByName(namepit, false) + elseif _staticpit==false then + pit=UNIT:FindByName(namepit) + else + self:E(RANGE.id..string.format("ERROR! Pit object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namepit)) + end + + -- Get the unit or static foul line object. + local foul=nil + if _staticfoul==true then + foul=STATIC:FindByName(namefoulline, false) + elseif _staticfoul==false then + foul=UNIT:FindByName(namefoulline) + else + self:E(RANGE.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)) + end + + self:T(RANGE.id..string.format("Foul line distance = %.1f m.", fouldist)) + return fouldist +end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Event Handling @@ -600,11 +875,11 @@ function RANGE:onEvent(Event) self:F3(Event) if Event == nil or Event.initiator == nil then - self:T2("Skipping onEvent. Event or Event.initiator unknown.") + self:T3("Skipping onEvent. Event or Event.initiator unknown.") return true end if Unit.getByName(Event.initiator:getName()) == nil then - self:T2("Skipping onEvent. Initiator unit name unknown.") + self:T3("Skipping onEvent. Initiator unit name unknown.") return true end @@ -646,19 +921,16 @@ function RANGE:onEvent(Event) -- Call event Birth function. if Event.id==world.event.S_EVENT_BIRTH and _playername then self:OnEventBirth(EventData) - --self:_OnBirth(EventData) end -- Call event Shot function. if Event.id==world.event.S_EVENT_SHOT and _playername and Event.weapon then self:OnEventShot(EventData) - --self:_OnShot(EventData) end -- Call event Hit function. if Event.id==world.event.S_EVENT_HIT and _playername and DCStgtunit then self:OnEventHit(EventData) - --self:_OnHit(EventData) end end @@ -668,7 +940,6 @@ end -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventBirth(EventData) ---function RANGE:_OnBirth(EventData) self:F({eventbirth = EventData}) local _unitName=EventData.IniUnitName @@ -690,6 +961,8 @@ function RANGE:OnEventBirth(EventData) self:T(RANGE.id..text) MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:_GetAmmo(_unitName) + -- Reset current strafe status. self.strafeStatus[_uid] = nil @@ -717,7 +990,6 @@ end -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventHit(EventData) ---function RANGE:_OnHit(EventData) self:F({eventhit = EventData}) -- Debug info. @@ -733,7 +1005,7 @@ function RANGE:OnEventHit(EventData) end -- Unit ID - local _unitID = _unit:GetID() + local _unitID = _unit:GetID() -- Target local target = EventData.TgtUnit @@ -743,7 +1015,7 @@ function RANGE:OnEventHit(EventData) local _currentTarget = self.strafeStatus[_unitID] -- Player has rolled in on a strafing target. - if _currentTarget then + if _currentTarget and target:IsAlive() then local playerPos = _unit:GetCoordinate() local targetPos = target:GetCoordinate() @@ -752,7 +1024,7 @@ function RANGE:OnEventHit(EventData) for _,_target in pairs(_currentTarget.zone.targets) do -- Check the the target is the same that was actually hit. - if _target:GetName() == targetname then + if _target and _target:IsAlive() and _target:GetName() == targetname then -- Get distance between player and target. local dist=playerPos:Get2DDistance(targetPos) @@ -768,7 +1040,7 @@ function RANGE:OnEventHit(EventData) else -- Too close to the target. if _currentTarget.pastfoulline==false and _unit and _playername then - local _d=_currentTarget.zone.foulline + 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) @@ -781,15 +1053,17 @@ function RANGE:OnEventHit(EventData) end -- Bombing Targets - for _,_target in pairs(self.bombingTargets) do + 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.name == targetname then + if _target and _target:IsAlive() and _bombtarget.name == targetname then if _unit and _playername then - local playerPos = _unit:GetCoordinate() - local targetPos = target:GetCoordinate() + -- Position of target. + local targetPos = _target:GetCoordinate() -- Message to player. --local text=string.format("%s, direct hit on target %s.", self:_myname(_unitName), targetname) @@ -809,7 +1083,6 @@ end -- @param #RANGE self -- @param Core.Event#EVENTDATA EventData function RANGE:OnEventShot(EventData) ---function RANGE:_OnShot(EventData) self:F({eventshot = EventData}) -- Weapon data. @@ -818,129 +1091,168 @@ function RANGE:OnEventShot(EventData) local _weaponName = _weaponStrArray[#_weaponStrArray] -- Debug info. - self:T3(RANGE.id.."EVENT SHOT: Ini unit = "..EventData.IniUnitName) - self:T3(RANGE.id.."EVENT SHOT: Ini group = "..EventData.IniGroupName) - self:T3(RANGE.id.."EVENT SHOT: Weapon type = ".._weapon) - self:T3(RANGE.id.."EVENT SHOT: Weapon name = ".._weaponName) + 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) - -- Monitor only bombs and rockets. - if (string.match(_weapon, "weapons.bombs") or string.match(_weapon, "weapons.nurs")) then + -- 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") + + -- 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 + + -- 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) - -- Weapon - local _ordnance = EventData.weapon + -- Set this to larger value than the threshold. + local dPR=self.BombtrackThreshold*2 + + -- Distance player to range. + if _unit and _playername then + dPR=_unit:GetCoordinate():Get2DDistance(self.location) + self:T(RANGE.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 -- Tracking info and init of last bomb position. - self:T(RANGE.id..string.format("Tracking %s - %s.", _weapon, _ordnance:getName())) + self:T(RANGE.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} - - -- Get unit name. - local _unitName = EventData.IniUnitName -- Function monitoring the position of a bomb until impact. - local function trackBomb(_previousPos) + local function trackBomb(_ordnance) + + -- When the pcall returns a failure the weapon has hit. + local _status,_bombPos = pcall( + function() + return _ordnance:getPoint() + end) + + self:T3(RANGE.id..string.format("Range %s: Bomb still in air: %s", self.rangename, tostring(_status))) + if _status then - -- Get player unit and name. - local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) - local _callsign=self:_myname(_unitName) + -- Still in the air. Remember this position. + _lastBombPos = {x = _bombPos.x, y = _bombPos.y, z= _bombPos.z } - if _unit and _playername then - - -- When the pcall returns a failure the weapon has hit. - local _status,_bombPos = pcall( - function() - return _ordnance:getPoint() - end) - - if _status then + -- Check again in 0.005 seconds. + return timer.getTime() + self.dtBombtrack - -- Still in the air. Remember this position. - _lastBombPos = {x = _bombPos.x, y = _bombPos.y, z= _bombPos.z } - - -- Check again in 0.005 seconds. - return timer.getTime() + self.dtBombtrack - - else + else + + -- Bomb did hit the ground. + -- Get closet target to last position. + local _closetTarget = nil + local _distance = nil + local _hitquality = "POOR" - -- Bomb did hit the ground. - -- Get closet target to last position. - local _closetTarget = nil - local _distance = nil - local _hitquality = "POOR" - - -- 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) - - -- Smoke impact point of bomb. - if self.PlayerSettings[_playername].smokebombimpact and impactdist approach heading. + if heading>180 then + heading=heading-180 + else + heading=heading+180 + end + + local mycoord=coord:ToStringA2G(_unit, _settings) + _text=_text..string.format("\n- %s: %s - heading %03d",_strafepit.name, mycoord, heading) + end + + self:_DisplayMessageToGroup(_unit,_text, nil, true) + end +end + + --- Report weather conditions at range. Temperature, QFE pressure and wind data. -- @param #RANGE self -- @param #string _unitname Name of the player unit. @@ -1335,7 +1721,7 @@ end -- @param #RANGE self -- @param #string _unitName Name of player unit. function RANGE:_CheckInZone(_unitName) - self:F(_unitName) + self:F2(_unitName) -- Get player unit and name. local _unit, _playername = self:_GetPlayerUnitAndName(_unitName) @@ -1364,7 +1750,7 @@ function RANGE:_CheckInZone(_unitName) -- 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:T(RANGE.id..text) + self:T2(RANGE.id..text) -- Check if player is in strafe zone and below max alt. if unitinzone then @@ -1390,6 +1776,9 @@ function RANGE:_CheckInZone(_unitName) else + -- Get current ammo. + local _ammo=self:_GetAmmo(_unitName) + -- Result. local _result = self.strafeStatus[_unitID] @@ -1403,9 +1792,19 @@ function RANGE:_CheckInZone(_unitName) else _result.text = "POOR PASS" 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) + if shots and accur then + _text=_text..string.format("\nTotal rounds fired %d. Accuracy %.1f %%.", shots, accur) + end -- Send message. self:_DisplayMessageToGroup(_unit, _text) @@ -1442,13 +1841,16 @@ function RANGE:_CheckInZone(_unitName) -- 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:T(RANGE.id..text) + self:T2(RANGE.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, pastfoulline=false } + 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) @@ -1493,7 +1895,7 @@ function RANGE:_AddF10Commands(_unitName) -- Enable switch so we don't do this twice. self.MenuAddedTo[_gid] = true - -- Main F10 menu: F10/On the Range + -- Main F10 menu: F10/On the Range// if RANGE.MenuF10[_gid] == nil then RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range") end @@ -1501,42 +1903,43 @@ function RANGE:_AddF10Commands(_unitName) local _statsPath = missionCommands.addSubMenuForGroup(_gid, "Statistics", _rangePath) local _markPath = missionCommands.addSubMenuForGroup(_gid, "Mark Targets", _rangePath) local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings", _rangePath) - -- F10/On the Range/My Settings/ + local _infoPath = missionCommands.addSubMenuForGroup(_gid, "Range Info", _rangePath) + -- F10/On the Range//My Settings/ local _mysmokePath = missionCommands.addSubMenuForGroup(_gid, "Smoke Color", _settingsPath) local _myflarePath = missionCommands.addSubMenuForGroup(_gid, "Flare Color", _settingsPath) - - --TODO: Convert to MOOSE menu. - -- F10/On the Range/Mark Targets/ + -- F10/On the Range//Mark Targets/ missionCommands.addCommandForGroup(_gid, "Mark On Map", _markPath, self._MarkTargetsOnMap, self, _unitName) missionCommands.addCommandForGroup(_gid, "Illuminate Range", _markPath, self._IlluminateBombTargets, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Strafe Pits", _markPath, self._SmokeStrafeTargetBoxes, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Strafe Tgts", _markPath, self._SmokeStrafeTargets, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Bomb Tgts", _markPath, self._SmokeBombTargets, self, _unitName) - -- F10/On the Range/Stats/ + -- F10/On the Range//Stats/ missionCommands.addCommandForGroup(_gid, "All Strafe Results", _statsPath, self._DisplayStrafePitResults, self, _unitName) missionCommands.addCommandForGroup(_gid, "All Bombing Results", _statsPath, self._DisplayBombingResults, self, _unitName) missionCommands.addCommandForGroup(_gid, "My Strafe Results", _statsPath, self._DisplayMyStrafePitResults, self, _unitName) missionCommands.addCommandForGroup(_gid, "My Bomb Results", _statsPath, self._DisplayMyBombingResults, self, _unitName) missionCommands.addCommandForGroup(_gid, "Reset All Stats", _statsPath, self._ResetRangeStats, self, _unitName) - -- F10/On the Range/My Settings/Smoke Color/ + -- F10/On the Range//My Settings/Smoke Color/ missionCommands.addCommandForGroup(_gid, "Blue Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Blue) missionCommands.addCommandForGroup(_gid, "Green Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Green) missionCommands.addCommandForGroup(_gid, "Orange Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Orange) missionCommands.addCommandForGroup(_gid, "Red Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Red) missionCommands.addCommandForGroup(_gid, "White Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.White) - -- F10/On the Range/My Settings/Flare Color/ + -- F10/On the Range//My Settings/Flare Color/ missionCommands.addCommandForGroup(_gid, "Green Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Green) missionCommands.addCommandForGroup(_gid, "Red Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Red) missionCommands.addCommandForGroup(_gid, "White Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.White) missionCommands.addCommandForGroup(_gid, "Yellow Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Yellow) - -- F10/On the Range/My Settings/ + -- F10/On the Range//My Settings/ missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName) missionCommands.addCommandForGroup(_gid, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName) missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName) - -- F10/On the Range/ - missionCommands.addCommandForGroup(_gid, "Range Information", _rangePath, self._DisplayRangeInfo, self, _unitName) - missionCommands.addCommandForGroup(_gid, "Weather Report", _rangePath, self._DisplayRangeWeather, self, _unitName) + -- F10/On the Range//Range Information + missionCommands.addCommandForGroup(_gid, "General Info", _infoPath, self._DisplayRangeInfo, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Weather Report", _infoPath, self._DisplayRangeWeather, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Bombing Targets", _infoPath, self._DisplayBombTargets, self, _unitName) + missionCommands.addCommandForGroup(_gid, "Strafe Pits", _infoPath, self._DisplayStrafePits, self, _unitName) end else self:T(RANGE.id.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName) @@ -1550,6 +1953,56 @@ end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Helper Functions +--- Get the number of shells a unit currently has. +-- @param #RANGE self +-- @param #string unitname Name of the player unit. +-- @return Number of shells left +function RANGE:_GetAmmo(unitname) + self:F2(unitname) + + -- Init counter. + local ammo=0 + + local unit, playername = self:_GetPlayerUnitAndName(unitname) + + if unit and playername then + + local has_ammo=false + + local ammotable=unit:GetAmmo() + self:T2({ammotable=ammotable}) + + if ammotable ~= nil then + + local weapons=#ammotable + self:T2(RANGE.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) + 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) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + end + end + end + end + + return ammo +end + --- Mark targets on F10 map. -- @param #RANGE self -- @param #string _unitName Name of the player unit. @@ -1557,31 +2010,45 @@ function RANGE:_MarkTargetsOnMap(_unitName) self:F(_unitName) -- Get group. - local group=UNIT:FindByName(_unitName):GetGroup() - - if group then + local group=nil + if _unitName then + group=UNIT:FindByName(_unitName):GetGroup() + end - -- Mark bomb targets. - for _,_target in pairs(self.bombingTargets) do - local coord=_target.point --Core.Point#COORDINATE - coord:MarkToGroup("Bomb target ".._target.name, group) - end - - -- Mark strafe targets. - for _,_strafepit in pairs(self.strafeTargets) do - for _,_target in pairs(_strafepit.targets) do - local coord=_target:GetCoordinate() --Core.Point#COORDINATE - coord:MarkToGroup("Strafe target ".._target:GetName(), group) + -- 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 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 + + -- Mark strafe targets. + for _,_strafepit in pairs(self.strafeTargets) do + for _,_target in pairs(_strafepit.targets) do + local _target=_target --Wrapper.Positionable#POSITIONABLE + if _target and _target:IsAlive() then + local coord=_target:GetCoordinate() --Core.Point#COORDINATE + if group then + coord:MarkToGroup("Strafe target ".._target:GetName(), group) + 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. @@ -1593,9 +2060,12 @@ function RANGE:_IlluminateBombTargets(_unitName) -- All bombing target coordinates. local bomb={} - for _,_target in pairs(self.bombingTargets) do - local coord=_target.point --Core.Point#COORDINATE - table.insert(bomb, coord) + 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 + table.insert(bomb, coord) + end end if #bomb>0 then @@ -1609,8 +2079,11 @@ function RANGE:_IlluminateBombTargets(_unitName) for _,_strafepit in pairs(self.strafeTargets) do for _,_target in pairs(_strafepit.targets) do - local coord=_target:GetCoordinate() --Core.Point#COORDINATE - table.insert(strafe, coord) + local _target=_target --Wrapper.Positionable#POSITIONABLE + if _target and _target:IsAlive() then + local coord=_target:GetCoordinate() --Core.Point#COORDINATE + table.insert(strafe, coord) + end end end @@ -1662,13 +2135,24 @@ function RANGE:_DisplayMessageToGroup(_unit, _text, _time, _clear) -- Group ID. local _gid=_unit:GetGroup():GetID() - if _gid then + if _gid and not self.examinerexclusive then if _clear == true then trigger.action.outTextForGroup(_gid, _text, _time, _clear) else trigger.action.outTextForGroup(_gid, _text, _time) 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 @@ -1741,9 +2225,12 @@ end function RANGE:_SmokeBombTargets(unitname) self:F(unitname) - for _,_target in pairs(self.bombingTargets) do - local coord = _target.point --Core.Point#COORDINATE - coord:Smoke(self.BombSmokeColor) + 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 + coord:Smoke(self.BombSmokeColor) + end end if unitname then @@ -1761,10 +2248,7 @@ function RANGE:_SmokeStrafeTargets(unitname) self:F(unitname) for _,_target in pairs(self.strafeTargets) do - for _,_unit in pairs(_target.targets) do - local coord = _unit:GetCoordinate() --Core.Point#COORDINATE - coord:Smoke(self.StrafeSmokeColor) - end + _target.coordinate:Smoke(self.StrafeSmokeColor) end if unitname then @@ -1877,6 +2361,63 @@ function RANGE:_flarecolor2text(color) return txt end +--- Checks if a static object with a certain name exists. It also added it to the MOOSE data base, if it is not already in there. +-- @param #RANGE self +-- @param #string name Name of the potential static object. +-- @return #boolean Returns true if a static with this name exists. Retruns false if a unit with this name exists. Returns nil if neither unit or static exist. +function RANGE:_CheckStatic(name) + self:F2(name) + + -- Get DCS static object. + local _DCSstatic=StaticObject.getByName(name) + + if _DCSstatic and _DCSstatic:isExist() then + + --Static does exist at least in DCS. Check if it also in the MOOSE DB. + local _MOOSEstatic=STATIC:FindByName(name, false) + + -- If static is not yet in MOOSE DB, we add it. Can happen for cargo statics! + if not _MOOSEstatic then + self:T(RANGE.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)) + 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)) + end + + -- If not unit or static exist, we return nil. + return nil +end + +--- Get max speed of controllable. +-- @param #RANGE self +-- @param Wrapper.Controllable#CONTROLLABLE controllable +-- @return Maximum speed in km/h. +function RANGE:_GetSpeed(controllable) + self:F2(controllable) + + -- Get DCS descriptors + local desc=controllable:GetDesc() + + -- Get speed + local speed=0 + if desc then + speed=desc.speedMax*3.6 + self:T({speed=speed}) + end + + return speed +end + --- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. -- @param #RANGE self -- @param #string _unitName Name of the player unit. @@ -1884,7 +2425,7 @@ end -- @return #string Name of the player. -- @return nil If player does not exist. function RANGE:_GetPlayerUnitAndName(_unitName) - self:F(_unitName) + self:F2(_unitName) if _unitName ~= nil then @@ -1896,7 +2437,7 @@ function RANGE:_GetPlayerUnitAndName(_unitName) local playername=DCSunit:getPlayerName() local unit=UNIT:Find(DCSunit) - self:T({DCSunit=DCSunit, unit=unit, playername=playername}) + self:T2({DCSunit=DCSunit, unit=unit, playername=playername}) if DCSunit and unit and playername then return unit, playername end @@ -1913,7 +2454,7 @@ end -- @param #RANGE self -- @param #string unitname Name of the player unit. function RANGE:_myname(unitname) - self:F(unitname) + self:F2(unitname) local unit=UNIT:FindByName(unitname) local pname=unit:GetPlayerName() @@ -1922,13 +2463,13 @@ function RANGE:_myname(unitname) return string.format("%s (%s)", csign, pname) end ---- http://stackoverflow.com/questions/1426954/split-string-in-lua +--- 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:F({str=str, sep=sep}) + self:F2({str=str, sep=sep}) local result = {} local regex = ("([^%s]+)"):format(sep) diff --git a/Moose Development/Moose/Functional/Scoring.lua b/Moose Development/Moose/Functional/Scoring.lua index cab7839ae..af50957e6 100644 --- a/Moose Development/Moose/Functional/Scoring.lua +++ b/Moose Development/Moose/Functional/Scoring.lua @@ -1,16 +1,33 @@ ---- **Functional** -- (R2.0) - Administer the scoring of player achievements, and create a CSV file logging the scoring events for use at team or squadron websites. +--- **Functional** - Administer the scoring of player achievements, and create a CSV file logging the scoring events for use at team or squadron websites. -- -- === -- --- ![Banner Image](..\Presentations\SCORING\Dia1.JPG) --- +-- ## Features: +-- +-- * Set the scoring scales based on threat level. +-- * Positive scores and negative scores. +-- * A contribution model to score achievements. +-- * Score goals. +-- * Score specific achievements. +-- * Score the hits and destroys of units. +-- * Score the hits and destroys of statics. +-- * Score the hits and destroys of scenery. +-- * Log scores into a CSV file. +-- * Connect to a remote server using JSON and IP. +-- -- === -- --- The @{#SCORING} class administers the scoring of player achievements, +-- ## Missions: +-- +-- [SCO - Scoring](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SCO%20-%20Scoring) +-- +-- === +-- +-- Administers the scoring of player achievements, -- and creates a CSV file logging the scoring events and results for use at team or squadron websites. -- -- SCORING automatically calculates the threat level of the objects hit and destroyed by players, --- which can be @{Unit}, @{Static) and @{Scenery} objects. +-- which can be @{Wrapper.Unit}, @{Static) and @{Scenery} objects. -- -- Positive score points are granted when enemy or neutral targets are destroyed. -- Negative score points or penalties are given when a friendly target is hit or destroyed. @@ -56,9 +73,34 @@ -- Use the radio menu F10 to consult the scores while running the mission. -- Scores can be reported for your user, or an overall score can be reported of all players currently active in the mission. -- --- # 1) @{Scoring#SCORING} class, extends @{Base#BASE} +-- === -- --- ## 1.1) Set the destroy score or penalty scale +-- ### Authors: **FlightControl** +-- +-- ### Contributions: +-- +-- * **Wingthor (TAW)**: Testing & Advice. +-- * **Dutch-Baron (TAW)**: Testing & Advice. +-- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing and Advice. +-- +-- === +-- +-- @module Functional.Scoring +-- @image Scoring.JPG + + +--- @type SCORING +-- @field Players A collection of the current players that have joined the game. +-- @extends Core.Base#BASE + +--- SCORING class +-- +-- # Constructor: +-- +-- local Scoring = SCORING:New( "Scoring File" ) +-- +-- +-- # Set the destroy score or penalty scale: -- -- Score scales can be set for scores granted when enemies or friendlies are destroyed. -- Use the method @{#SCORING.SetScaleDestroyScore}() to set the scale of enemy destroys (positive destroys). @@ -71,12 +113,12 @@ -- The above sets the scale for valid scores to 10. So scores will be given in a scale from 0 to 10. -- The penalties will be given in a scale from 0 to 40. -- --- ## 1.2) Define special targets that will give extra scores. +-- # Define special targets that will give extra scores: -- -- Special targets can be set that will give extra scores to the players when these are destroyed. --- Use the methods @{#SCORING.AddUnitScore}() and @{#SCORING.RemoveUnitScore}() to specify a special additional score for a specific @{Unit}s. +-- Use the methods @{#SCORING.AddUnitScore}() and @{#SCORING.RemoveUnitScore}() to specify a special additional score for a specific @{Wrapper.Unit}s. -- Use the methods @{#SCORING.AddStaticScore}() and @{#SCORING.RemoveStaticScore}() to specify a special additional score for a specific @{Static}s. --- Use the method @{#SCORING.SetGroupGroup}() to specify a special additional score for a specific @{Group}s. +-- Use the method @{#SCORING.SetGroupGroup}() to specify a special additional score for a specific @{Wrapper.Group}s. -- -- local Scoring = SCORING:New( "Scoring File" ) -- Scoring:AddUnitScore( UNIT:FindByName( "Unit #001" ), 200 ) @@ -88,22 +130,22 @@ -- -- Scoring:RemoveUnitScore( UNIT:FindByName( "Unit #001" ) ) -- --- ## 1.3) Define destruction zones that will give extra scores. +-- # Define destruction zones that will give extra scores: -- -- Define zones of destruction. Any object destroyed within the zone of the given category will give extra points. -- Use the method @{#SCORING.AddZoneScore}() to add a @{Zone} for additional scoring. -- Use the method @{#SCORING.RemoveZoneScore}() to remove a @{Zone} for additional scoring. --- There are interesting variations that can be achieved with this functionality. For example, if the @{Zone} is a @{Zone#ZONE_UNIT}, +-- There are interesting variations that can be achieved with this functionality. For example, if the @{Zone} is a @{Core.Zone#ZONE_UNIT}, -- then the zone is a moving zone, and anything destroyed within that @{Zone} will generate points. -- The other implementation could be to designate a scenery target (a building) in the mission editor surrounded by a @{Zone}, -- just large enough around that building. -- --- ## 1.4) Add extra Goal scores upon an event or a condition. +-- # Add extra Goal scores upon an event or a condition: -- -- A mission has goals and achievements. The scoring system provides an API to set additional scores when a goal or achievement event happens. -- Use the method @{#SCORING.AddGoalScore}() to add a score for a Player at any time in your mission. -- --- ## 1.5) (Decommissioned) Configure fratricide level. +-- # (Decommissioned) Configure fratricide level. -- -- **This functionality is decomissioned until the DCS bug concerning Unit:destroy() not being functional in multi player for player units has been fixed by ED**. -- @@ -111,13 +153,13 @@ -- Use the method @{#SCORING.SetFratricide}() to define the level when a player gets kicked. -- By default, the fratricide level is the default penalty mutiplier * 2 for the penalty score. -- --- ## 1.6) Penalty score when a player changes the coalition. +-- # Penalty score when a player changes the coalition. -- -- When a player changes the coalition, he can receive a penalty score. -- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition. -- By default, the penalty for changing coalition is the default penalty scale. -- --- ## 1.8) Define output CSV files. +-- # Define output CSV files. -- -- The CSV file is given the name of the string given in the @{#SCORING.New}{} constructor, followed by the .csv extension. -- The file is incrementally saved in the **\\Saved Games\\DCS\\Logs** folder, and has a time stamp indicating each mission run. @@ -154,7 +196,7 @@ -- The MOOSE designer cannot take any responsibility of any damage inflicted as a result of the de-sanitization. -- That being said, I hope that the SCORING class provides you with a great add-on to score your squad mates achievements. -- --- ## 1.9) Configure messages. +-- # Configure messages. -- -- When players hit or destroy targets, messages are sent. -- Various methods exist to configure: @@ -162,7 +204,7 @@ -- * Which messages are sent upon the event. -- * Which audience receives the message. -- --- ### 1.9.1) Configure the messages sent upon the event. +-- ## Configure the messages sent upon the event. -- -- Use the following methods to configure when to send messages. By default, all messages are sent. -- @@ -171,48 +213,16 @@ -- * @{#SCORING.SetMessagesAddon}(): Configure to send messages for additional score, after a target has been destroyed. -- * @{#SCORING.SetMessagesZone}(): Configure to send messages for additional score, after a target has been destroyed within a given zone. -- --- ### 1.9.2) Configure the audience of the messages. +-- ## Configure the audience of the messages. -- -- Use the following methods to configure the audience of the messages. By default, the messages are sent to all players in the mission. -- -- * @{#SCORING.SetMessagesToAll}(): Configure to send messages to all players. -- * @{#SCORING.SetMessagesToCoalition}(): Configure to send messages to only those players within the same coalition as the player. --- --- --- === --- --- # **API CHANGE HISTORY** --- --- The underlying change log documents the API changes. Please read this carefully. The following notation is used: --- --- * **Added** parts are expressed in bold type face. --- * _Removed_ parts are expressed in italic type face. --- --- Hereby the change log: --- --- 2017-02-26: Initial class and API. -- -- === --- --- # **AUTHORS and CONTRIBUTIONS** --- --- ### Contributions: --- --- * **Wingthor (TAW)**: Testing & Advice. --- * **Dutch-Baron (TAW)**: Testing & Advice. --- * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing and Advice. --- --- ### Authors: --- --- * **FlightControl**: Concept, Design & Programming. -- --- @module Scoring - - ---- The Scoring class --- @type SCORING --- @field Players A collection of the current players that have joined the game. --- @extends Core.Base#BASE +-- @field #SCORING SCORING = { ClassName = "SCORING", ClassID = 0, @@ -239,8 +249,10 @@ local _SCORINGCategory = -- @param #string GameName The name of the game. This name is also logged in the CSV score file. -- @return #SCORING self -- @usage +-- -- -- Define a new scoring object for the mission Gori Valley. -- ScoringObject = SCORING:New( "Gori Valley" ) +-- function SCORING:New( GameName ) -- Inherits from BASE @@ -339,11 +351,11 @@ function SCORING:SetScaleDestroyPenalty( Scale ) return self end ---- Add a @{Unit} for additional scoring when the @{Unit} is destroyed. --- Note that if there was already a @{Unit} declared within the scoring with the same name, --- then the old @{Unit} will be replaced with the new @{Unit}. +--- Add a @{Wrapper.Unit} for additional scoring when the @{Wrapper.Unit} is destroyed. +-- Note that if there was already a @{Wrapper.Unit} declared within the scoring with the same name, +-- then the old @{Wrapper.Unit} will be replaced with the new @{Wrapper.Unit}. -- @param #SCORING self --- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. +-- @param Wrapper.Unit#UNIT ScoreUnit The @{Wrapper.Unit} for which the Score needs to be given. -- @param #number Score The Score value. -- @return #SCORING function SCORING:AddUnitScore( ScoreUnit, Score ) @@ -355,9 +367,9 @@ function SCORING:AddUnitScore( ScoreUnit, Score ) return self end ---- Removes a @{Unit} for additional scoring when the @{Unit} is destroyed. +--- Removes a @{Wrapper.Unit} for additional scoring when the @{Wrapper.Unit} is destroyed. -- @param #SCORING self --- @param Wrapper.Unit#UNIT ScoreUnit The @{Unit} for which the Score needs to be given. +-- @param Wrapper.Unit#UNIT ScoreUnit The @{Wrapper.Unit} for which the Score needs to be given. -- @return #SCORING function SCORING:RemoveUnitScore( ScoreUnit ) @@ -398,9 +410,9 @@ function SCORING:RemoveStaticScore( ScoreStatic ) end ---- Specify a special additional score for a @{Group}. +--- Specify a special additional score for a @{Wrapper.Group}. -- @param #SCORING self --- @param Wrapper.Group#GROUP ScoreGroup The @{Group} for which each @{Unit} a Score is given. +-- @param Wrapper.Group#GROUP ScoreGroup The @{Wrapper.Group} for which each @{Wrapper.Unit} a Score is given. -- @param #number Score The Score value. -- @return #SCORING function SCORING:AddScoreGroup( ScoreGroup, Score ) @@ -714,7 +726,7 @@ end -- A free text can be given that is shown to the players. -- The Score can be both positive and negative. -- @param #SCORING self --- @param Wrapper.Unit#UNIT PlayerUnit The @{Unit} of the Player. Other Properties for the scoring are taken from this PlayerUnit, like coalition, type etc. +-- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the Player. Other Properties for the scoring are taken from this PlayerUnit, like coalition, type etc. -- @param #string GoalTag The string or identifier that is used in the CSV file to identify the goal (sort or group later in Excel). -- @param #string Text A free text that is shown to the players. -- @param #number Score The score can be both positive or negative ( Penalty ). @@ -858,7 +870,7 @@ function SCORING:OnEventBirth( Event ) if Event.IniUnit then if Event.IniObjectCategory == 1 then local PlayerName = Event.IniUnit:GetPlayerName() - if PlayerName ~= "" then + if PlayerName then self:_AddPlayerFromUnit( Event.IniUnit ) self:SetScoringMenu( Event.IniGroup ) end diff --git a/Moose Development/Moose/Functional/Sead.lua b/Moose Development/Moose/Functional/Sead.lua index f1a18b8df..55a791023 100644 --- a/Moose Development/Moose/Functional/Sead.lua +++ b/Moose Development/Moose/Functional/Sead.lua @@ -1,12 +1,41 @@ ---- **Functional** -- Provides defensive behaviour to a set of SAM sites within a running Mission. +--- **Functional** -- Make SAM sites execute evasive and defensive behaviour when being fired upon. -- -- === -- --- @module Sead +-- ## Features: +-- +-- * When SAM sites are being fired upon, the SAMs will take evasive action will reposition themselves when possible. +-- * When SAM sites are being fired upon, the SAMs will take defensive action by shutting down their radars. +-- +-- === +-- +-- ## Missions: +-- +-- [SEV - SEAD Evasion](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SEV%20-%20SEAD%20Evasion) +-- +-- === +-- +-- ### Authors: **FlightControl** +-- +-- === +-- +-- @module Functional.Sead +-- @image SEAD.JPG ---- The SEAD class --- @type SEAD +--- @type SEAD -- @extends Core.Base#BASE + +--- Make SAM sites execute evasive and defensive behaviour when being fired upon. +-- +-- This class is very easy to use. Just setup a SEAD object by using @{#SEAD.New}() and SAMs will evade and take defensive action when being fired upon. +-- +-- # Constructor: +-- +-- Use the @{#SEAD.New}() constructor to create a new SEAD object. +-- +-- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } ) +-- +-- @field #SEAD SEAD = { ClassName = "SEAD", TargetSkill = { diff --git a/Moose Development/Moose/Functional/Suppression.lua b/Moose Development/Moose/Functional/Suppression.lua new file mode 100644 index 000000000..3df26be6d --- /dev/null +++ b/Moose Development/Moose/Functional/Suppression.lua @@ -0,0 +1,1848 @@ +--- **Functional** - Suppress fire of ground units when they get hit. +-- +-- === +-- +-- ## Features: +-- +-- * Hold fire of attacked units when being fired upon. +-- +-- === +-- +-- ## Missions: +-- +-- ## [MOOSE - ALL Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS) +-- +-- === +-- +-- When ground units get hit by (suppressive) enemy fire, they will not be able to shoot back for a certain amount of time. +-- +-- The implementation is based on an idea and script by MBot. See the [DCS forum threat](https://forums.eagle.ru/showthread.php?t=107635) for details. +-- +-- In addition to suppressing the fire, conditions can be specified which let the group retreat to a defined zone, move away from the attacker +-- or hide at a nearby scenery object. +-- +-- ==== +-- +-- # YouTube Channel +-- +-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) +-- +-- === +-- +-- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)** +-- +-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536) +-- +-- === +-- +-- @module Functional.Suppression +-- @image Suppression.JPG + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- SUPPRESSION class +-- @type SUPPRESSION +-- @field #string ClassName Name of the class. +-- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players. +-- @field #boolean flare Flare units when they get hit or die. +-- @field #boolean smoke Smoke places to which the group retreats, falls back or hides. +-- @field #list DCSdesc Table containing all DCS descriptors of the group. +-- @field #string Type Type of the group. +-- @field #number SpeedMax Maximum speed of group in km/h. +-- @field #boolean IsInfantry True if group has attribute Infantry. +-- @field Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the FSM. Must be a ground group. +-- @field #number Tsuppress_ave Average time in seconds a group gets suppressed. Actual value is sampled randomly from a Gaussian distribution. +-- @field #number Tsuppress_min Minimum time in seconds the group gets suppressed. +-- @field #number Tsuppress_max Maximum time in seconds the group gets suppressed. +-- @field #number TsuppressionOver Time at which the suppression will be over. +-- @field #number IniGroupStrength Number of units in a group at start. +-- @field #number Nhit Number of times the group was hit. +-- @field #string Formation Formation which will be used when falling back, taking cover or retreating. Default "Vee". +-- @field #number Speed Speed the unit will use when falling back, taking cover or retreating. Default 999. +-- @field #boolean MenuON If true creates a entry in the F10 menu. +-- @field #boolean FallbackON If true, group can fall back, i.e. move away from the attacking unit. +-- @field #number FallbackWait Time in seconds the unit will wait at the fall back point before it resumes its mission. +-- @field #number FallbackDist Distance in meters the unit will fall back. +-- @field #number FallbackHeading Heading in degrees to which the group should fall back. Default is directly away from the attacking unit. +-- @field #boolean TakecoverON If true, group can hide at a nearby scenery object. +-- @field #number TakecoverWait Time in seconds the group will hide before it will resume its mission. +-- @field #number TakecoverRange Range in which the group will search for scenery objects to hide at. +-- @field Core.Point#COORDINATE hideout Coordinate/place where the group will try to take cover. +-- @field #number PminFlee Minimum probability in percent that a group will flee (fall back or take cover) at each hit event. Default is 10 %. +-- @field #number PmaxFlee Maximum probability in percent that a group will flee (fall back or take cover) at each hit event. Default is 90 %. +-- @field Core.Zone#ZONE RetreatZone Zone to which a group retreats. +-- @field #number RetreatDamage Damage in percent at which the group will be ordered to retreat. +-- @field #number RetreatWait Time in seconds the group will wait in the retreat zone before it resumes its mission. Default two hours. +-- @field #string CurrentAlarmState Alam state the group is currently in. +-- @field #string CurrentROE ROE the group currently has. +-- @field #string DefaultAlarmState Alarm state the group will go to when it is changed back from another state. Default is "Auto". +-- @field #string DefaultROE ROE the group will get once suppression is over. Default is "Free". +-- @field #boolean eventmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. Default true. +-- @extends Core.Fsm#FSM_CONTROLLABLE +-- + +--- Mimic suppressive enemy fire and let groups flee or retreat. +-- +-- ## Suppression Process +-- +-- ![Process](..\Presentations\SUPPRESSION\Suppression_Process.png) +-- +-- The suppression process can be described as follows. +-- +-- ### CombatReady +-- +-- A group starts in the state **CombatReady**. In this state the group is ready to fight. The ROE is set to either "Weapon Free" or "Return Fire". +-- The alarm state is set to either "Auto" or "Red". +-- +-- ### Event Hit +-- The most important event in this scenario is the **Hit** event. This is an event of the FSM and triggered by the DCS event hit. +-- +-- ### Suppressed +-- After the **Hit** event the group changes its state to **Suppressed**. Technically, the ROE of the group is changed to "Weapon Hold". +-- The suppression of the group will last a certain amount of time. It is randomized an will vary each time the group is hit. +-- The expected suppression time is set to 15 seconds by default. But the actual value is sampled from a Gaussian distribution. +-- +-- ![Process](..\Presentations\SUPPRESSION\Suppression_Gaussian.png) +-- +-- The graph shows the distribution of suppression times if a group would be hit 100,000 times. As can be seen, on most hits the group gets +-- suppressed for around 15 seconds. Other values are also possible but they become less likely the further away from the "expected" suppression time they are. +-- Minimal and maximal suppression times can also be specified. By default these are set to 5 and 25 seconds, respectively. This can also be seen in the graph +-- because the tails of the Gaussian distribution are cut off at these values. +-- +-- ### Event Recovered +-- After the suppression time is over, the event **Recovered** is initiated and the group becomes **CombatReady** again. +-- The ROE of the group will be set to "Weapon Free". +-- +-- Of course, it can also happen that a group is hit again while it is still suppressed. In that case a new random suppression time is calculated. +-- If the new suppression time is longer than the remaining suppression of the previous hit, then the group recovers when the suppression time of the last +-- hit has passed. +-- If the new suppression time is shorter than the remaining suppression, the group will recover after the longer time of the first suppression has passed. +-- +-- For example: +-- +-- * A group gets hit the first time and is suppressed for - let's say - 15 seconds. +-- * After 10 seconds, i.e. when 5 seconds of the old suppression are left, the group gets hit a again. +-- * A new suppression time is calculated which can be smaller or larger than the remaining 5 seconds. +-- * If the new suppression time is smaller, e.g. three seconds, than five seconds, the group will recover after the 5 remaining seconds of the first suppression have passed. +-- * If the new suppression time is longer than last suppression time, e.g. 10 seconds, then the group will recover after the 10 seconds of the new hit have passed. +-- +-- Generally speaking, the suppression times are not just added on top of each other. Because this could easily lead to the situation that a group +-- never becomes CombatReady again before it gets destroyed. +-- +-- The mission designer can capture the event **Recovered** by the function @{#SUPPRESSION.OnAfterRecovered}(). +-- +-- ## Flee Events and States +-- Apart from being suppressed the groups can also flee from the enemy under certain conditions. +-- +-- ### Event Retreat +-- The first option is a retreat. This can be enabled by setting a retreat zone, i.e. a trigger zone defined in the mission editor. +-- +-- If the group takes a certain amount of damage, the event **Retreat** will be called and the group will start to move to the retreat zone. +-- The group will be in the state **Retreating**, which means that its ROE is set to "Weapon Hold" and the alarm state is set to "Green". +-- Setting the alarm state to green is necessary to enable the group to move under fire. +-- +-- When the group has reached the retreat zone, the event **Retreated** is triggered and the state will change to **Retreated** (note that both the event and +-- the state of the same name in this case). ROE and alarm state are +-- set to "Return Fire" and "Auto", respectively. The group will stay in the retreat zone and not actively participate in the combat any more. +-- +-- If no option retreat zone has been specified, the option retreat is not available. +-- +-- The mission designer can capture the events **Retreat** and **Retreated** by the functions @{#SUPPRESSION.OnAfterRetreat}() and @{#SUPPRESSION.OnAfterRetreated}(). +-- +-- ### Fallback +-- +-- If a group is attacked by another ground group, it has the option to fall back, i.e. move away from the enemy. The probability of the event **FallBack** to +-- happen depends on the damage of the group that was hit. The more a group gets damaged, the more likely **FallBack** event becomes. +-- +-- If the group enters the state **FallingBack** it will move 100 meters in the opposite direction of the attacking unit. ROE and alarmstate are set to "Weapon Hold" +-- and "Green", respectively. +-- +-- At the fallback point the group will wait for 60 seconds before it resumes its normal mission. +-- +-- The mission designer can capture the event **FallBack** by the function @{#SUPPRESSION.OnAfterFallBack}(). +-- +-- ### TakeCover +-- +-- If a group is hit by either another ground or air unit, it has the option to "take cover" or "hide". This means that the group will move to a random +-- scenery object in it vicinity. +-- +-- Analogously to the fall back case, the probability of a **TakeCover** event to occur, depends on the damage of the group. The more a group is damaged, the more +-- likely it becomes that a group takes cover. +-- +-- When a **TakeCover** event occurs an area with a radius of 300 meters around the hit group is searched for an arbitrary scenery object. +-- If at least one scenery object is found, the group will move there. One it has reached its "hideout", it will wait there for two minutes before it resumes its +-- normal mission. +-- +-- If more than one scenery object is found, the group will move to a random one. +-- If no scenery object is near the group the **TakeCover** event is rejected and the group will not move. +-- +-- The mission designer can capture the event **TakeCover** by the function @{#SUPPRESSION.OnAfterTakeCover}(). +-- +-- ### Choice of FallBack or TakeCover if both are enabled? +-- +-- If both **FallBack** and **TakeCover** events are enabled by the functions @{#SUPPRESSION.Fallback}() and @{#SUPPRESSION.Takecover}() the algorithm does the following: +-- +-- * If the attacking unit is a ground unit, then the **FallBack** event is executed. +-- * Otherwise, i.e. if the attacker is *not* a ground unit, then the **TakeCover** event is triggered. +-- +-- ### FightBack +-- +-- When a group leaves the states **TakingCover** or **FallingBack** the event **FightBack** is triggered. This changes the ROE and the alarm state back to their default values. +-- +-- The mission designer can capture the event **FightBack** by the function @{#SUPPRESSION.OnAfterFightBack}() +-- +-- # Examples +-- +-- ## Simple Suppression +-- This example shows the basic steps to use suppressive fire for a group. +-- +-- ![Process](..\Presentations\SUPPRESSION\Suppression_Example_01.png) +-- +-- +-- # Customization and Fine Tuning +-- The following user functions can be used to change the default values +-- +-- * @{#SUPPRESSION.SetSuppressionTime}() can be used to set the time a goup gets suppressed. +-- * @{#SUPPRESSION.SetRetreatZone}() sets the retreat zone and enables the possiblity for the group to retreat. +-- * @{#SUPPRESSION.SetFallbackDistance}() sets a value how far the unit moves away from the attacker after the fallback event. +-- * @{#SUPPRESSION.SetFallbackWait}() sets the time after which the group resumes its mission after a FallBack event. +-- * @{#SUPPRESSION.SetTakecoverWait}() sets the time after which the group resumes its mission after a TakeCover event. +-- * @{#SUPPRESSION.SetTakecoverRange}() sets the radius in which hideouts are searched. +-- * @{#SUPPRESSION.SetTakecoverPlace}() explicitly sets the place where the group will run at a TakeCover event. +-- * @{#SUPPRESSION.SetMinimumFleeProbability}() sets the minimum probability that a group flees (FallBack or TakeCover) after a hit. Note taht the probability increases with damage. +-- * @{#SUPPRESSION.SetMaximumFleeProbability}() sets the maximum probability that a group flees (FallBack or TakeCover) after a hit. Default is 90%. +-- * @{#SUPPRESSION.SetRetreatDamage}() sets the damage a group/unit can take before it is ordered to retreat. +-- * @{#SUPPRESSION.SetRetreatWait}() sets the time a group waits in the retreat zone after a retreat. +-- * @{#SUPPRESSION.SetDefaultAlarmState}() sets the alarm state a group gets after it becomes CombatReady again. +-- * @{#SUPPRESSION.SetDefaultROE}() set the rules of engagement a group gets after it becomes CombatReady again. +-- * @{#SUPPRESSION.FlareOn}() is mainly for debugging. A flare is fired when a unit is hit, gets suppressed, recovers, dies. +-- * @{#SUPPRESSION.SmokeOn}() is mainly for debugging. Puts smoke on retreat zone, hideouts etc. +-- * @{#SUPPRESSION.MenuON}() is mainly for debugging. Activates a radio menu item where certain functions like retreat etc. can be triggered manually. +-- +-- +-- @field #SUPPRESSION +SUPPRESSION={ + ClassName = "SUPPRESSION", + Debug = false, + flare = false, + smoke = false, + DCSdesc = nil, + Type = nil, + IsInfantry=nil, + SpeedMax = nil, + Tsuppress_ave = 15, + Tsuppress_min = 5, + Tsuppress_max = 25, + TsuppressOver = nil, + IniGroupStrength = nil, + Nhit = 0, + Formation = "Off road", + Speed = 4, + MenuON = false, + FallbackON = false, + FallbackWait = 60, + FallbackDist = 100, + FallbackHeading = nil, + TakecoverON = false, + TakecoverWait = 120, + TakecoverRange = 300, + hideout = nil, + PminFlee = 10, + PmaxFlee = 90, + RetreatZone = nil, + RetreatDamage = nil, + RetreatWait = 7200, + CurrentAlarmState = "unknown", + CurrentROE = "unknown", + DefaultAlarmState = "Auto", + DefaultROE = "Weapon Free", + eventmoose = true, +} + +--- Enumerator of possible rules of engagement. +-- @field #list ROE +SUPPRESSION.ROE={ + Hold="Weapon Hold", + Free="Weapon Free", + Return="Return Fire", +} + +--- Enumerator of possible alarm states. +-- @field #list AlarmState +SUPPRESSION.AlarmState={ + Auto="Auto", + Green="Green", + Red="Red", +} + +--- Main F10 menu for suppresion, i.e. F10/Suppression. +-- @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" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--TODO list +--DONE: Figure out who was shooting and move away from him. +--DONE: Move behind a scenery building if there is one nearby. +--DONE: Retreat to a given zone or point. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Creates a new AI_suppression object. +-- @param #SUPPRESSION self +-- @param Wrapper.Group#GROUP group The GROUP object for which suppression should be applied. +-- @return #SUPPRESSION SUPPRESSION object. +-- @return 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())) + else + self:E(SUPPRESSION.id.."Suppressive fire: 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())) + return nil + end + + -- Set the controllable for the FSM. + self:SetControllable(group) + + -- Get DCS descriptors of group. + local DCSgroup=Group.getByName(group:GetName()) + local DCSunit=DCSgroup:getUnit(1) + self.DCSdesc=DCSunit:getDesc() + + -- Get max speed the group can do and convert to km/h. + self.SpeedMax=self.DCSdesc.speedMaxOffRoad*3.6 + + -- Set speed to maximum. + self.Speed=self.SpeedMax + + -- Is this infantry or not. + self.IsInfantry=DCSunit:hasAttribute("Infantry") + + -- Type of group. + self.Type=group:GetTypeName() + + -- Initial group strength. + self.IniGroupStrength=#group:GetUnits() + + -- Set ROE and Alarm State. + self:SetDefaultROE("Free") + self:SetDefaultAlarmState("Auto") + + -- Transitions + self:AddTransition("*", "Start", "CombatReady") + self:AddTransition("CombatReady", "Hit", "Suppressed") + self:AddTransition("Suppressed", "Hit", "Suppressed") + self:AddTransition("Suppressed", "Recovered", "CombatReady") + self:AddTransition("Suppressed", "TakeCover", "TakingCover") + self:AddTransition("Suppressed", "FallBack", "FallingBack") + self:AddTransition("*", "Retreat", "Retreating") + self:AddTransition("TakingCover", "FightBack", "CombatReady") + self:AddTransition("FallingBack", "FightBack", "CombatReady") + self:AddTransition("Retreating", "Retreated", "Retreated") + self:AddTransition("*", "Dead", "*") + + self:AddTransition("TakingCover", "Hit", "TakingCover") + self:AddTransition("FallingBack", "Hit", "FallingBack") + + --- User function for OnBefore "Hit" event. + -- @function [parent=#SUPPRESSION] OnBeforeHit + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT Unit Unit that was hit. + -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. + -- @return #boolean + + --- User function for OnAfer "Hit" event. + -- @function [parent=#SUPPRESSION] OnAfterHit + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT Unit Unit that was hit. + -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. + + + --- User function for OnBefore "Recovered" event. + -- @function [parent=#SUPPRESSION] OnBeforeRecovered + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean + + --- User function for OnAfter "Recovered" event. + -- @function [parent=#SUPPRESSION] OnAfterRecovered + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- User function for OnBefore "TakeCover" event. + -- @function [parent=#SUPPRESSION] OnBeforeTakeCover + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Hideout Place where the group will hide. + -- @return #boolean + + --- User function for OnAfter "TakeCover" event. + -- @function [parent=#SUPPRESSION] OnAfterTakeCover + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Point#COORDINATE Hideout Place where the group will hide. + + + --- User function for OnBefore "FallBack" event. + -- @function [parent=#SUPPRESSION] OnBeforeFallBack + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. + -- @return #boolean + + --- User function for OnAfter "FallBack" event. + -- @function [parent=#SUPPRESSION] OnAfterFallBack + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. + + + --- User function for OnBefore "Retreat" event. + -- @function [parent=#SUPPRESSION] OnBeforeRetreat + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean + + --- User function for OnAfter "Retreat" event. + -- @function [parent=#SUPPRESSION] OnAfterRetreat + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- User function for OnBefore "Retreated" event. + -- @function [parent=#SUPPRESSION] OnBeforeRetreated + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean + + --- User function for OnAfter "Retreated" event. + -- @function [parent=#SUPPRESSION] OnAfterRetreated + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- User function for OnBefore "FlightBack" event. + -- @function [parent=#SUPPRESSION] OnBeforeFightBack + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @return #boolean + + --- User function for OnAfter "FlightBack" event. + -- @function [parent=#SUPPRESSION] OnAfterFightBack + -- @param #SUPPRESSION self + -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set average, minimum and maximum time a unit is suppressed each time it gets hit. +-- @param #SUPPRESSION self +-- @param #number Tave Average time [seconds] a group will be suppressed. Default is 15 seconds. +-- @param #number Tmin (Optional) Minimum time [seconds] a group will be suppressed. Default is 5 seconds. +-- @param #number Tmax (Optional) Maximum time a group will be suppressed. Default is 25 seconds. +function SUPPRESSION:SetSuppressionTime(Tave, Tmin, Tmax) + self:F({Tave=Tave, Tmin=Tmin, Tmax=Tmax}) + + -- Minimum suppression time is input or default but at least 1 second. + self.Tsuppress_min=Tmin or self.Tsuppress_min + self.Tsuppress_min=math.max(self.Tsuppress_min, 1) + + -- Maximum suppression time is input or dault but at least Tmin. + self.Tsuppress_max=Tmax or self.Tsuppress_max + self.Tsuppress_max=math.max(self.Tsuppress_max, self.Tsuppress_min) + + -- Expected suppression time is input or default but at leat Tmin and at most Tmax. + self.Tsuppress_ave=Tave or self.Tsuppress_ave + self.Tsuppress_ave=math.max(self.Tsuppress_min) + self.Tsuppress_ave=math.min(self.Tsuppress_max) + + self:T(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)) +end + +--- Set the zone to which a group retreats after being damaged too much. +-- @param #SUPPRESSION self +-- @param Core.Zone#ZONE zone MOOSE zone object. +function SUPPRESSION:SetRetreatZone(zone) + self:F({zone=zone}) + self.RetreatZone=zone +end + +--- Turn Debug mode on. Enables messages and more output to DCS log file. +-- @param #SUPPRESSION self +function SUPPRESSION:DebugOn() + self:F() + self.Debug=true +end + +--- Flare units when they are hit, die or recover from suppression. +-- @param #SUPPRESSION self +function SUPPRESSION:FlareOn() + self:F() + self.flare=true +end + +--- Smoke positions where units fall back to, hide or retreat. +-- @param #SUPPRESSION self +function SUPPRESSION:SmokeOn() + self:F() + self.smoke=true +end + +--- Set the formation a group uses for fall back, hide or retreat. +-- @param #SUPPRESSION self +-- @param #string formation Formation of the group. Default "Vee". +function SUPPRESSION:SetFormation(formation) + self:F(formation) + self.Formation=formation or "Vee" +end + +--- Set speed a group moves at for fall back, hide or retreat. +-- @param #SUPPRESSION self +-- @param #number speed Speed in km/h of group. Default max speed the group can do. +function SUPPRESSION:SetSpeed(speed) + self:F(speed) + self.Speed=speed or self.SpeedMax + self.Speed=math.min(self.Speed, self.SpeedMax) +end + +--- Enable fall back if a group is hit. +-- @param #SUPPRESSION self +-- @param #boolean switch Enable=true or disable=false fall back of group. +function SUPPRESSION:Fallback(switch) + self:F(switch) + if switch==nil then + switch=true + end + self.FallbackON=switch +end + +--- Set distance a group will fall back when it gets hit. +-- @param #SUPPRESSION self +-- @param #number distance Distance in meters. +function SUPPRESSION:SetFallbackDistance(distance) + self:F(distance) + self.FallbackDist=distance +end + +--- Set time a group waits at its fall back position before it resumes its normal mission. +-- @param #SUPPRESSION self +-- @param #number time Time in seconds. +function SUPPRESSION:SetFallbackWait(time) + self:F(time) + self.FallbackWait=time +end + +--- Enable take cover option if a unit is hit. +-- @param #SUPPRESSION self +-- @param #boolean switch Enable=true or disable=false fall back of group. +function SUPPRESSION:Takecover(switch) + self:F(switch) + if switch==nil then + switch=true + end + self.TakecoverON=switch +end + +--- Set time a group waits at its hideout position before it resumes its normal mission. +-- @param #SUPPRESSION self +-- @param #number time Time in seconds. +function SUPPRESSION:SetTakecoverWait(time) + self:F(time) + self.TakecoverWait=time +end + +--- Set distance a group searches for hideout places. +-- @param #SUPPRESSION self +-- @param #number range Search range in meters. +function SUPPRESSION:SetTakecoverRange(range) + self:F(range) + self.TakecoverRange=range +end + +--- Set hideout place explicitly. +-- @param #SUPPRESSION self +-- @param Core.Point#COORDINATE Hideout Place where the group will hide after the TakeCover event. +function SUPPRESSION:SetTakecoverPlace(Hideout) + self.hideout=Hideout +end + +--- Set minimum probability that a group flees (falls back or takes cover) after a hit event. Default is 10%. +-- @param #SUPPRESSION self +-- @param #number probability Probability in percent. +function SUPPRESSION:SetMinimumFleeProbability(probability) + self:F(probability) + self.PminFlee=probability or 10 +end + +--- Set maximum probability that a group flees (falls back or takes cover) after a hit event. Default is 90%. +-- @param #SUPPRESSION self +-- @param #number probability Probability in percent. +function SUPPRESSION:SetMaximumFleeProbability(probability) + self:F(probability) + self.PmaxFlee=probability or 90 +end + +--- Set damage threshold before a group is ordered to retreat if a retreat zone was defined. +-- If the group consists of only a singe unit, this referrs to the life of the unit. +-- If the group consists of more than one unit, this referrs to the group strength relative to its initial strength. +-- @param #SUPPRESSION self +-- @param #number damage Damage in percent. If group gets damaged above this value, the group will retreat. Default 50 %. +function SUPPRESSION:SetRetreatDamage(damage) + self:F(damage) + self.RetreatDamage=damage or 50 +end + +--- Set time a group waits in the retreat zone before it resumes its mission. Default is two hours. +-- @param #SUPPRESSION self +-- @param #number time Time in seconds. Default 7200 seconds = 2 hours. +function SUPPRESSION:SetRetreatWait(time) + self:F(time) + self.RetreatWait=time or 7200 +end + +--- Set alarm state a group will get after it returns from a fall back or take cover. +-- @param #SUPPRESSION self +-- @param #string alarmstate Alarm state. Possible "Auto", "Green", "Red". Default is "Auto". +function SUPPRESSION:SetDefaultAlarmState(alarmstate) + self:F(alarmstate) + if alarmstate:lower()=="auto" then + self.DefaultAlarmState=SUPPRESSION.AlarmState.Auto + elseif alarmstate:lower()=="green" then + self.DefaultAlarmState=SUPPRESSION.AlarmState.Green + elseif alarmstate:lower()=="red" then + self.DefaultAlarmState=SUPPRESSION.AlarmState.Red + else + self.DefaultAlarmState=SUPPRESSION.AlarmState.Auto + end +end + +--- Set Rules of Engagement (ROE) a group will get when it recovers from suppression. +-- @param #SUPPRESSION self +-- @param #string roe ROE after suppression. Possible "Free", "Hold" or "Return". Default "Free". +function SUPPRESSION:SetDefaultROE(roe) + self:F(roe) + if roe:lower()=="free" then + self.DefaultROE=SUPPRESSION.ROE.Free + elseif roe:lower()=="hold" then + self.DefaultROE=SUPPRESSION.ROE.Hold + elseif roe:lower()=="return" then + self.DefaultROE=SUPPRESSION.ROE.Return + else + self.DefaultROE=SUPPRESSION.ROE.Free + end +end + +--- Create an F10 menu entry for the suppressed group. The menu is mainly for Debugging purposes. +-- @param #SUPPRESSION self +-- @param #boolean switch Enable=true or disable=false menu group. Default is true. +function SUPPRESSION:MenuOn(switch) + self:F(switch) + if switch==nil then + switch=true + end + self.MenuON=switch +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create F10 main menu, i.e. F10/Suppression. The menu is mainly for Debugging purposes. +-- @param #SUPPRESSION self +function SUPPRESSION:_CreateMenuGroup() + local SubMenuName=self.Controllable:GetName() + local MenuGroup=MENU_MISSION:New(SubMenuName, SUPPRESSION.MenuF10) + MENU_MISSION_COMMAND:New("Fallback!", MenuGroup, self.OrderFallBack, self) + MENU_MISSION_COMMAND:New("Take Cover!", MenuGroup, self.OrderTakeCover, self) + MENU_MISSION_COMMAND:New("Retreat!", MenuGroup, self.OrderRetreat, self) + MENU_MISSION_COMMAND:New("Report Status", MenuGroup, self.Status, self, true) +end + +--- Order group to fall back between 100 and 150 meters in a random direction. +-- @param #SUPPRESSION self +function SUPPRESSION:OrderFallBack() + local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE + local vicinity=group:GetCoordinate():GetRandomVec2InRadius(150, 100) + local coord=COORDINATE:NewFromVec2(vicinity) + self:FallBack(self.Controllable) +end + +--- Order group to take cover at a nearby scenery object. +-- @param #SUPPRESSION self +function SUPPRESSION:OrderTakeCover() + -- Search place to hide or take specified one. + local Hideout=self.hideout + if self.hideout==nil then + Hideout=self:_SearchHideout() + end + -- Trigger TakeCover event. + self:TakeCover(Hideout) +end + +--- Order group to retreat to a pre-defined zone. +-- @param #SUPPRESSION self +function SUPPRESSION:OrderRetreat() + self:Retreat() +end + +--- Status of group. Current ROE, alarm state, life. +-- @param #SUPPRESSION self +-- @param #boolean message Send message to all players. +function SUPPRESSION:Status(message) + + local name=self.Controllable:GetName() + local nunits=#self.Controllable:GetUnits() + local roe=self.CurrentROE + local state=self.CurrentAlarmState + local life_min, life_max, life_ave, life_ave0, groupstrength=self:_GetLife() + + 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) + + MESSAGE:New(text, 10):ToAllIf(message or self.Debug) + self:T(SUPPRESSION.id.."\n"..text) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Start" event. Initialized ROE and alarm state. Starts the event handler. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterStart(Controllable, From, Event, To) + self:_EventFromTo("onafterStart", Event, From, To) + + local text=string.format("Started SUPPRESSION for group %s.", Controllable:GetName()) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + + local rzone="not defined" + if self.RetreatZone then + rzone=self.RetreatZone:GetName() + end + + -- Set retreat damage value if it was not set by user input. + if self.RetreatDamage==nil then + if self.RetreatZone then + if self.IniGroupStrength==1 then + self.RetreatDamage=60.0 -- 40% of life is left. + elseif self.IniGroupStrength==2 then + self.RetreatDamage=50.0 -- 50% of group left, i.e. 1 of 2. We already order a retreat, because if for a group 2 two a zone is defined it would not be used at all. + else + self.RetreatDamage=66.5 -- 34% of the group is left, e.g. 1 of 3,4 or 5, 2 of 6,7 or 8, 3 of 9,10 or 11, 4/12, 4/13, 4/14, 5/15, ... + end + else + self.RetreatDamage=100 -- If no retreat then this should be set to 100%. + end + end + + -- Create main F10 menu if it is not there yet. + if self.MenuON then + if not SUPPRESSION.MenuF10 then + SUPPRESSION.MenuF10 = MENU_MISSION:New("Suppression") + end + self:_CreateMenuGroup() + end + + -- Set the current ROE and alam state. + self:_SetAlarmState(self.DefaultAlarmState) + self:_SetROE(self.DefaultROE) + + local text=string.format("\n******************************************************\n") + text=text..string.format("Suppressed group = %s\n", Controllable:GetName()) + text=text..string.format("Type = %s\n", self.Type) + text=text..string.format("IsInfantry = %s\n", tostring(self.IsInfantry)) + text=text..string.format("Group strength = %d\n", self.IniGroupStrength) + text=text..string.format("Average time = %5.1f seconds\n", self.Tsuppress_ave) + text=text..string.format("Minimum time = %5.1f seconds\n", self.Tsuppress_min) + text=text..string.format("Maximum time = %5.1f seconds\n", self.Tsuppress_max) + text=text..string.format("Default ROE = %s\n", self.DefaultROE) + text=text..string.format("Default AlarmState = %s\n", self.DefaultAlarmState) + text=text..string.format("Fall back ON = %s\n", tostring(self.FallbackON)) + text=text..string.format("Fall back distance = %5.1f m\n", self.FallbackDist) + text=text..string.format("Fall back wait = %5.1f seconds\n", self.FallbackWait) + text=text..string.format("Fall back heading = %s degrees\n", tostring(self.FallbackHeading)) + text=text..string.format("Take cover ON = %s\n", tostring(self.TakecoverON)) + text=text..string.format("Take cover search = %5.1f m\n", self.TakecoverRange) + text=text..string.format("Take cover wait = %5.1f seconds\n", self.TakecoverWait) + text=text..string.format("Min flee probability = %5.1f\n", self.PminFlee) + text=text..string.format("Max flee probability = %5.1f\n", self.PmaxFlee) + text=text..string.format("Retreat zone = %s\n", rzone) + text=text..string.format("Retreat damage = %5.1f %%\n", self.RetreatDamage) + text=text..string.format("Retreat wait = %5.1f seconds\n", self.RetreatWait) + text=text..string.format("Speed = %5.1f km/h\n", self.Speed) + text=text..string.format("Speed max = %5.1f km/h\n", self.SpeedMax) + text=text..string.format("Formation = %s\n", self.Formation) + text=text..string.format("******************************************************\n") + self:T(SUPPRESSION.id..text) + + -- Add event handler. + if self.eventmoose then + self:HandleEvent(EVENTS.Hit, self._OnEventHit) + self:HandleEvent(EVENTS.Dead, self._OnEventDead) + else + world.addEventHandler(self) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Hit" event. (Of course, this is not really before the group got hit.) +-- @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) + + --local Tnow=timer.getTime() + --env.info(SUPPRESSION.id..string.format("Last hit = %s %s", tostring(self.LastHit), tostring(Tnow))) + + return true +end + +--- After "Hit" event. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT Unit Unit that was hit. +-- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked. +function SUPPRESSION:onafterHit(Controllable, From, Event, To, Unit, AttackUnit) + self:_EventFromTo("onafterHit", Event, From, To) + + -- Suppress unit. + if From=="CombatReady" or From=="Suppressed" then + self:_Suppress() + end + + -- Get life of group in %. + local life_min, life_max, life_ave, life_ave0, groupstrength=self:_GetLife() + + -- Damage in %. If group consists only of one unit, we take its life value. + local Damage=100-life_ave0 + + -- Condition for retreat. + local RetreatCondition = Damage >= self.RetreatDamage-0.01 and self.RetreatZone + + -- Probability that a unit flees. The probability increases linearly with the damage of the group/unit. + -- If Damage=0 ==> P=Pmin + -- if Damage=RetreatDamage ==> P=Pmax + -- If no retreat zone has been specified, RetreatDamage is 100. + local Pflee=(self.PmaxFlee-self.PminFlee)/self.RetreatDamage * math.min(Damage, self.RetreatDamage) + self.PminFlee + + -- Evaluate flee condition. + local P=math.random(0,100) + local FleeCondition = P < Pflee + + local text + text=string.format("\nGroup %s: Life min=%5.1f, max=%5.1f, ave=%5.1f, ave0=%5.1f group=%5.1f\n", Controllable:GetName(), life_min, life_max, life_ave, life_ave0, groupstrength) + text=string.format("Group %s: Damage = %8.4f (%8.4f retreat threshold).\n", Controllable:GetName(), Damage, self.RetreatDamage) + text=string.format("Group %s: P_Flee = %5.1f %5.1f=P_rand (P_Flee > Prand ==> Flee)\n", Controllable:GetName(), Pflee, P) + self:T(SUPPRESSION.id..text) + + -- Group is obviously destroyed. + if Damage >= 99.9 then + return + end + + if RetreatCondition then + + -- Trigger Retreat event. + self:Retreat() + + elseif FleeCondition then + + if self.FallbackON and AttackUnit:IsGround() then + + -- Trigger FallBack event. + self:FallBack(AttackUnit) + + elseif self.TakecoverON then + + -- Search place to hide or take specified one. + local Hideout=self.hideout + if self.hideout==nil then + Hideout=self:_SearchHideout() + end + + -- Trigger TakeCover event. + self:TakeCover(Hideout) + end + end + + -- Give info on current status. + if self.Debug then + self:Status() + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Recovered" event. Check if suppression time is over. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean +function SUPPRESSION:onbeforeRecovered(Controllable, From, Event, To) + self:_EventFromTo("onbeforeRecovered", Event, From, To) + + -- Current time. + local Tnow=timer.getTime() + + -- Debug info + self:T(SUPPRESSION.id..string.format("onbeforeRecovered: Time now: %d - Time over: %d", Tnow, self.TsuppressionOver)) + + -- Recovery is only possible if enough time since the last hit has passed. + if Tnow >= self.TsuppressionOver then + return true + else + return false + end + +end + +--- After "Recovered" event. Group has recovered and its ROE is set back to the "normal" unsuppressed state. Optionally the group is flared green. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterRecovered(Controllable, From, Event, To) + self:_EventFromTo("onafterRecovered", Event, From, To) + + if Controllable and Controllable:IsAlive() then + + -- Debug message. + local text=string.format("Group %s has recovered!", Controllable:GetName()) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:T(SUPPRESSION.id..text) + + -- Set ROE back to default. + self:_SetROE() + + -- Flare unit green. + if self.flare or self.Debug then + Controllable:FlareGreen() + end + + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "FightBack" event. ROE and Alarm state are set back to default. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterFightBack(Controllable, From, Event, To) + self:_EventFromTo("onafterFightBack", Event, From, To) + + -- Set ROE and alarm state back to default. + self:_SetROE() + self:_SetAlarmState() +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "FallBack" event. We check that group is not already falling back. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. +-- @return #boolean +function SUPPRESSION:onbeforeFallBack(Controllable, From, Event, To, AttackUnit) + self:_EventFromTo("onbeforeFallBack", Event, From, To) + + --TODO: Add retreat? Only allowd transition is Suppressed-->Fallback. So in principle no need. + if From == "FallingBack" then + return false + else + return true + end +end + +--- After "FallBack" event. We get the heading away from the attacker and route the group a certain distance in that direction. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this. +function SUPPRESSION:onafterFallBack(Controllable, From, Event, To, AttackUnit) + self:_EventFromTo("onafterFallback", Event, From, To) + + -- Debug info + self:T(SUPPRESSION.id..string.format("Group %s is falling back after %d hits.", Controllable:GetName(), self.Nhit)) + + -- Coordinate of the attacker and attacked unit. + local ACoord=AttackUnit:GetCoordinate() + local DCoord=Controllable:GetCoordinate() + + -- Heading from attacker to attacked unit. + local heading=self:_Heading(ACoord, DCoord) + + -- Overwrite heading with user specified heading. + if self.FallbackHeading then + heading=self.FallbackHeading + end + + -- Create a coordinate ~ 100 m in opposite direction of the attacking unit. + local Coord=DCoord:Translate(self.FallbackDist, heading) + + -- Place marker + if self.Debug then + local MarkerID=Coord:MarkToAll("Fall back position for group "..Controllable:GetName()) + end + + -- Smoke the coordinate. + if self.smoke or self.Debug then + Coord:SmokeBlue() + end + + -- Set ROE to weapon hold. + self:_SetROE(SUPPRESSION.ROE.Hold) + + -- Set alarm state to GREEN and let the unit run away. + self:_SetAlarmState(SUPPRESSION.AlarmState.Green) + + -- Make the group run away. + self:_Run(Coord, self.Speed, self.Formation, self.FallbackWait) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "TakeCover" event. Search an area around the group for possible scenery objects where the group can hide. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Hideout Place where the group will hide. +-- @return #boolean +function SUPPRESSION:onbeforeTakeCover(Controllable, From, Event, To, Hideout) + self:_EventFromTo("onbeforeTakeCover", Event, From, To) + + --TODO: Need to test this! + if From=="TakingCover" then + return false + end + + -- Block transition if no hideout place is given. + if Hideout ~= nil then + return true + else + return false + end + +end + +--- After "TakeCover" event. Group will run to a nearby scenery object and "hide" there for a certain time. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Point#COORDINATE Hideout Place where the group will hide. +function SUPPRESSION:onafterTakeCover(Controllable, From, Event, To, Hideout) + self:_EventFromTo("onafterTakeCover", Event, From, To) + + if self.Debug then + local MarkerID=Hideout:MarkToAll(string.format("Hideout for group %s", Controllable:GetName())) + end + + -- Smoke place of hideout. + if self.smoke or self.Debug then + Hideout:SmokeBlue() + end + + -- Set ROE to weapon hold. + self:_SetROE(SUPPRESSION.ROE.Hold) + + -- Set the ALARM STATE to GREEN. Then the unit will move even if it is under fire. + self:_SetAlarmState(SUPPRESSION.AlarmState.Green) + + -- Make the group run away. + self:_Run(Hideout, self.Speed, self.Formation, self.TakecoverWait) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Retreat" event. We check that the group is not already retreating. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @return #boolean True if transition is allowed, False if transition is forbidden. +function SUPPRESSION:onbeforeRetreat(Controllable, From, Event, To) + self:_EventFromTo("onbeforeRetreat", Event, From, To) + + if From=="Retreating" then + local text=string.format("Group %s is already retreating.") + self:T2(SUPPRESSION.id..text) + return false + else + return true + end + +end + +--- After "Retreat" event. Find a random point in the retreat zone and route the group there. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterRetreat(Controllable, From, Event, To) + self:_EventFromTo("onafterRetreat", Event, From, To) + + -- Route the group to a zone. + local text=string.format("Group %s is retreating! Alarm state green.", Controllable:GetName()) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:T(SUPPRESSION.id..text) + + -- Get a random point in the retreat zone. + local ZoneCoord=self.RetreatZone:GetRandomCoordinate() -- Core.Point#COORDINATE + local ZoneVec2=ZoneCoord:GetVec2() + + -- Debug smoke zone and point. + if self.smoke or self.Debug then + ZoneCoord:SmokeBlue() + end + if self.Debug then + self.RetreatZone:SmokeZone(SMOKECOLOR.Red, 12) + end + + -- Set ROE to weapon hold. + self:_SetROE(SUPPRESSION.ROE.Hold) + + -- Set the ALARM STATE to GREEN. Then the unit will move even if it is under fire. + self:_SetAlarmState(SUPPRESSION.AlarmState.Green) + + -- Make unit run to retreat zone and wait there for ~two hours. + self:_Run(ZoneCoord, self.Speed, self.Formation, self.RetreatWait) + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Before "Retreateded" event. Check that the group is really in the retreat zone. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onbeforeRetreated(Controllable, From, Event, To) + self:_EventFromTo("onbeforeRetreated", Event, From, To) + + -- Check that the group is inside the zone. + local inzone=self.RetreatZone:IsVec3InZone(Controllable:GetVec3()) + + return inzone +end + +--- After "Retreateded" event. Group has reached the retreat zone. Set ROE to return fire and alarm state to auto. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterRetreated(Controllable, From, Event, To) + self:_EventFromTo("onafterRetreated", Event, From, To) + + -- Set ROE to weapon return fire. + self:_SetROE(SUPPRESSION.ROE.Return) + + -- Set the ALARM STATE to GREEN. Then the unit will move even if it is under fire. + self:_SetAlarmState(SUPPRESSION.AlarmState.Auto) + + -- TODO: Add hold task? Move from _Run() +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- After "Dead" event, when a unit has died. When all units of a group are dead, FSM is stopped and eventhandler removed. +-- @param #SUPPRESSION self +-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group. +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function SUPPRESSION:onafterDead(Controllable, From, Event, To) + self:_EventFromTo("onafterDead", Event, From, To) + + -- 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) + end + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +--- Event Handler +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Event handler for suppressed groups. +--@param #SUPPRESSION self +function SUPPRESSION:onEvent(Event) + --self:E(event) + + if Event == nil or Event.initiator == nil or Unit.getByName(Event.initiator:getName()) == nil then + return true + end + + local EventData={} + if Event.initiator then + EventData.IniDCSUnit = Event.initiator + EventData.IniUnitName = Event.initiator:getName() + EventData.IniDCSGroup = Event.initiator:getGroup() + EventData.IniGroupName = Event.initiator:getGroup():getName() + EventData.IniGroup = GROUP:FindByName(EventData.IniGroupName) + EventData.IniUnit = UNIT:FindByName(EventData.IniUnitName) + end + + if Event.target then + EventData.TgtDCSUnit = Event.target + EventData.TgtUnitName = Event.target:getName() + EventData.TgtDCSGroup = Event.target:getGroup() + EventData.TgtGroupName = Event.target:getGroup():getName() + EventData.TgtGroup = GROUP:FindByName(EventData.TgtGroupName) + EventData.TgtUnit = UNIT:FindByName(EventData.TgtUnitName) + end + + + -- Event HIT + if Event.id == world.event.S_EVENT_HIT then + self:_OnEventHit(EventData) + end + + -- Event DEAD + if Event.id == world.event.S_EVENT_DEAD then + self:_OnEventDead(EventData) + end + +end + +--- Event handler for Dead event of suppressed groups. +-- @param #SUPPRESSION self +-- @param Core.Event#EVENTDATA EventData +function SUPPRESSION:_OnEventHit(EventData) + self:F(EventData) + + local GroupNameSelf=self.Controllable:GetName() + local GroupNameTgt=EventData.TgtGroupName + local TgtUnit=EventData.TgtUnit + local tgt=EventData.TgtDCSUnit + local IniUnit=EventData.IniUnit + + -- Check that correct group was hit. + if GroupNameTgt == GroupNameSelf then + + self:T(SUPPRESSION.id..string.format("Hit event at t = %5.1f", timer.getTime())) + + -- Flare unit that was hit. + if self.flare or self.Debug then + TgtUnit:FlareRed() + end + + -- Increase Hit counter. + self.Nhit=self.Nhit+1 + + -- Info on hit times. + self:T(SUPPRESSION.id..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)) + + -- FSM Hit event. + self:__Hit(3, TgtUnit, IniUnit) + end + +end + +--- Event handler for Dead event of suppressed groups. +-- @param #SUPPRESSION self +-- @param Core.Event#EVENTDATA EventData +function SUPPRESSION:_OnEventDead(EventData) + + local GroupNameSelf=self.Controllable:GetName() + local GroupNameIni=EventData.IniGroupName + + -- Check for correct group. + if GroupNameIni== GroupNameSelf then + + -- Dead Unit. + local IniUnit=EventData.IniUnit --Wrapper.Unit#UNIT + local IniUnitName=EventData.IniUnitName + + if EventData.IniUnit then + self:T2(SUPPRESSION.id..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)) + end + + if EventData.IniDCSUnit then + self:T2(SUPPRESSION.id..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)) + 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.")) + 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.")) + end + + -- Get status. + self:Status() + + -- FSM Dead event. + self:__Dead(0.1) + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Suppress fire of a unit by setting its ROE to "Weapon Hold". +-- @param #SUPPRESSION self +function SUPPRESSION:_Suppress() + + -- Current time. + local Tnow=timer.getTime() + + -- Controllable + local Controllable=self.Controllable --Wrapper.Controllable#CONTROLLABLE + + -- Group will hold their weapons. + self:_SetROE(SUPPRESSION.ROE.Hold) + + -- Get randomized time the unit is suppressed. + local sigma=(self.Tsuppress_max-self.Tsuppress_min)/4 + local Tsuppress=self:_Random_Gaussian(self.Tsuppress_ave,sigma,self.Tsuppress_min, self.Tsuppress_max) + + -- Time at which the suppression is over. + local renew=true + if self.TsuppressionOver ~= nil then + if Tsuppress+Tnow > self.TsuppressionOver then + self.TsuppressionOver=Tnow+Tsuppress + else + renew=false + end + else + self.TsuppressionOver=Tnow+Tsuppress + end + + -- Recovery event will be called in Tsuppress seconds. + if renew then + self:__Recovered(self.TsuppressionOver-Tnow) + end + + -- Debug message. + local text=string.format("Group %s is suppressed for %d seconds. Suppression ends at %d:%02d.", Controllable:GetName(), Tsuppress, self.TsuppressionOver/60, self.TsuppressionOver%60) + MESSAGE:New(text, 10):ToAllIf(self.Debug) + self:T(SUPPRESSION.id..text) + +end + + +--- Make group run/drive to a certain point. We put in several intermediate waypoints because sometimes the group stops before it arrived at the desired point. +--@param #SUPPRESSION self +--@param Core.Point#COORDINATE fin Coordinate where we want to go. +--@param #number speed Speed of group. Default is 20. +--@param #string formation Formation of group. Default is "Vee". +--@param #number wait Time the group will wait/hold at final waypoint. Default is 30 seconds. +function SUPPRESSION:_Run(fin, speed, formation, wait) + + speed=speed or 20 + formation=formation or "Off road" + wait=wait or 30 + + local group=self.Controllable -- Wrapper.Controllable#CONTROLLABLE + + -- Clear all tasks. + group:ClearTasks() + + -- Current coordinates of group. + local ini=group:GetCoordinate() + + -- Distance between current and final point. + local dist=ini:Get2DDistance(fin) + + -- Heading from ini to fin. + local heading=self:_Heading(ini, fin) + + -- Number of waypoints. + local nx + if dist <= 50 then + nx=2 + elseif dist <= 100 then + nx=3 + elseif dist <= 500 then + nx=4 + else + nx=5 + end + + -- Number of intermediate waypoints. + local dx=dist/(nx-1) + + -- Waypoint and task arrays. + local wp={} + local tasks={} + + -- First waypoint is the current position of the group. + wp[1]=ini:WaypointGround(speed, formation) + tasks[1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, 1, false) + + if self.Debug then + local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)", #wp, self.Controllable:GetName())) + end + + self:T2(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) + + wp[#wp+1]=coord:WaypointGround(speed, formation) + tasks[#tasks+1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, false) + + 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())) + end + + 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 + +--- Function called when group is passing a waypoint. At the last waypoint we set the group back to CombatReady. +--@param Wrapper.Group#GROUP group Group which is passing a waypoint. +--@param #SUPPRESSION Fsm The suppression object. +--@param #number i Waypoint number that has been reached. +--@param #boolean final True if it is the final waypoint. Start Fightback. +function SUPPRESSION._Passing_Waypoint(group, Fsm, i, final) + + -- Debug message. + local text=string.format("Group %s passing waypoint %d (final=%s)", group:GetName(), i, tostring(final)) + MESSAGE:New(text,10):ToAllIf(Fsm.Debug) + if Fsm.Debug then + env.info(SUPPRESSION.id..text) + end + + if final then + if Fsm:is("Retreating") then + -- Retreated-->Retreated. + Fsm:Retreated() + else + -- FightBack-->Combatready: Change alarm state back to default. + Fsm:FightBack() + end + end +end + + +--- Search a place to hide. This is any scenery object in the vicinity. +--@param #SUPPRESSION self +--@return Core.Point#COORDINATE Coordinate of the hideout place. +--@return nil If no scenery object is within search radius. +function SUPPRESSION:_SearchHideout() + -- We search objects in a zone with radius ~300 m around the group. + local Zone = ZONE_GROUP:New("Zone_Hiding", self.Controllable, self.TakecoverRange) + local gpos = self.Controllable:GetCoordinate() + + -- Scan for Scenery objects to run/drive to. + Zone:Scan(Object.Category.SCENERY) + + -- Array with all possible hideouts, i.e. scenery objects in the vicinity of the group. + local hideouts={} + + for SceneryTypeName, SceneryData in pairs(Zone:GetScannedScenery()) do + for SceneryName, SceneryObject in pairs(SceneryData) do + + local SceneryObject = SceneryObject -- Wrapper.Scenery#SCENERY + + -- Position of the scenery object. + local spos=SceneryObject:GetCoordinate() + + -- Distance from group to hideout. + local distance= spos:Get2DDistance(gpos) + + if self.Debug then + -- Place markers on every possible scenery object. + local MarkerID=SceneryObject:GetCoordinate():MarkToAll(string.format("%s scenery object %s", self.Controllable:GetName(),SceneryObject:GetTypeName())) + local text=string.format("%s scenery: %s, Coord %s", self.Controllable:GetName(), SceneryObject:GetTypeName(), SceneryObject:GetCoordinate():ToStringLLDMS()) + self:T2(SUPPRESSION.id..text) + end + + -- Add to table. + table.insert(hideouts, {object=SceneryObject, distance=distance}) + end + end + + -- Get random hideout place. + local Hideout=nil + if #hideouts>0 then + + -- Debug info. + self:T(SUPPRESSION.id.."Number of hideouts "..#hideouts) + + -- Sort results table wrt number of hits. + local _sort = function(a,b) return a.distance < b.distance end + table.sort(hideouts,_sort) + + -- Pick a random location. + --Hideout=hideouts[math.random(#hideouts)].object + + -- Pick closest location. + Hideout=hideouts[1].object:GetCoordinate() + + else + self:E(SUPPRESSION.id.."No hideouts found!") + end + + return Hideout + +end + +--- Get (relative) life in percent of a group. Function returns the value of the units with the smallest and largest life. Also the average value of all groups is returned. +-- @param #SUPPRESSION self +-- @return #number Smallest life value of all units. +-- @return #number Largest life value of all units. +-- @return #number Average life value of all alife groups +-- @return #number Average life value of all groups including already dead ones. +-- @return #number Relative group strength. +function SUPPRESSION:_GetLife() + + local group=self.Controllable --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + local units=group:GetUnits() + + local life_min=nil + local life_max=nil + local life_ave=0 + local life_ave0=0 + local n=0 + + local groupstrength=#units/self.IniGroupStrength*100 + + self.T2(SUPPRESSION.id..string.format("Group %s _GetLife nunits = %d", self.Controllable:GetName(), #units)) + + for _,unit in pairs(units) do + + local unit=unit -- Wrapper.Unit#UNIT + if unit and unit:IsAlive() then + n=n+1 + local life=unit:GetLife()/(unit:GetLife0()+1)*100 + if life_min==nil or life < life_min then + life_min=life + end + if life_max== nil or life > life_max then + life_max=life + end + life_ave=life_ave+life + if self.Debug then + local text=string.format("n=%02d: Life = %3.1f, Life0 = %3.1f, min=%3.1f, max=%3.1f, ave=%3.1f, group=%3.1f", n, unit:GetLife(), unit:GetLife0(), life_min, life_max, life_ave/n,groupstrength) + self:T2(SUPPRESSION.id..text) + end + end + + end + + -- If the counter did not increase (can happen!) return 0 + if n==0 then + return 0,0,0,0,0 + end + + -- Average life relative to initial group strength including the dead ones. + life_ave0=life_ave/self.IniGroupStrength + + -- Average life of all alive units. + life_ave=life_ave/n + + return life_min, life_max, life_ave, life_ave0, groupstrength + else + return 0, 0, 0, 0, 0 + end +end + + +--- Heading from point a to point b in degrees. +--@param #SUPPRESSION self +--@param Core.Point#COORDINATE a Coordinate. +--@param Core.Point#COORDINATE b Coordinate. +--@return #number angle Angle from a to b in degrees. +function SUPPRESSION:_Heading(a, b) + local dx = b.x-a.x + local dy = b.z-a.z + local angle = math.deg(math.atan2(dy,dx)) + if angle < 0 then + angle = 360 + angle + end + return angle +end + +--- Generate Gaussian pseudo-random numbers. +-- @param #SUPPRESSION self +-- @param #number x0 Expectation value of distribution. +-- @param #number sigma (Optional) Standard deviation. Default 10. +-- @param #number xmin (Optional) Lower cut-off value. +-- @param #number xmax (Optional) Upper cut-off value. +-- @return #number Gaussian random number. +function SUPPRESSION:_Random_Gaussian(x0, sigma, xmin, xmax) + + -- Standard deviation. Default 5 if not given. + sigma=sigma or 5 + + local r + local gotit=false + local i=0 + while not gotit do + + -- Uniform numbers in [0,1). We need two. + local x1=math.random() + local x2=math.random() + + -- Transform to Gaussian exp(-(x-x0)²/(2*sigma²). + r = math.sqrt(-2*sigma*sigma * math.log(x1)) * math.cos(2*math.pi * x2) + x0 + + i=i+1 + if (r>=xmin and r<=xmax) or i>100 then + gotit=true + end + end + + return r + +end + +--- Sets the ROE for the group and updates the current ROE variable. +-- @param #SUPPRESSION self +-- @param #string roe ROE the group will get. Possible "Free", "Hold", "Return". Default is self.DefaultROE. +function SUPPRESSION:_SetROE(roe) + local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE + + -- If no argument is given, we take the default ROE. + roe=roe or self.DefaultROE + + -- Update the current ROE. + self.CurrentROE=roe + + -- Set the ROE. + if roe==SUPPRESSION.ROE.Free then + group:OptionROEOpenFire() + elseif roe==SUPPRESSION.ROE.Hold then + group:OptionROEHoldFire() + elseif roe==SUPPRESSION.ROE.Return then + group:OptionROEReturnFire() + else + self:E(SUPPRESSION.id.."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) +end + +--- Sets the alarm state of the group and updates the current alarm state variable. +-- @param #SUPPRESSION self +-- @param #string state Alarm state the group will get. Possible "Auto", "Green", "Red". Default is self.DefaultAlarmState. +function SUPPRESSION:_SetAlarmState(state) + local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE + + -- Input or back to default alarm state. + state=state or self.DefaultAlarmState + + -- Update the current alam state of the group. + self.CurrentAlarmState=state + + -- Set the alarm state. + if state==SUPPRESSION.AlarmState.Auto then + group:OptionAlarmStateAuto() + elseif state==SUPPRESSION.AlarmState.Green then + group:OptionAlarmStateGreen() + elseif state==SUPPRESSION.AlarmState.Red then + group:OptionAlarmStateRed() + else + self:E(SUPPRESSION.id.."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) +end + +--- Print event-from-to string to DCS log file. +-- @param #SUPPRESSION self +-- @param #string BA Before/after info. +-- @param #string Event Event. +-- @param #string From From state. +-- @param #string To To state. +function SUPPRESSION:_EventFromTo(BA, Event, From, To) + local text=string.format("\n%s: %s EVENT %s: %s --> %s", BA, self.Controllable:GetName(), Event, From, To) + self:T2(SUPPRESSION.id..text) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua new file mode 100644 index 000000000..e95ccb6ec --- /dev/null +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -0,0 +1,8182 @@ +--- **Functional** - Simulation of logistic operations. +-- +-- === +-- +-- ## Features: +-- +-- * Holds (virtual) assests in stock and spawns them upon request. +-- * Manages requests of assets from other warehouses. +-- * Queueing system with optional priorization of requests. +-- * Realistic transportation of assets between warehouses. +-- * Different means of automatic transportation (planes, helicopters, APCs, self propelled). +-- * Strategic components such as capturing, defending and destroying warehouses and their associated infrastructure. +-- * Intelligent spawning of aircraft on airports (only if enough parking spots are available). +-- * Possibility to hook into events and customize actions. +-- * Persistence of assets. Warehouse assets can be saved and loaded from file. +-- * Can be easily interfaced to other MOOSE classes. +-- +-- === +-- +-- ## 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** +-- ### Co-author: FlightControl (cargo dispatcher classes) +-- +-- === +-- +-- @module Functional.Warehouse +-- @image Warehouse.JPG + +--- WAREHOUSE class. +-- @type WAREHOUSE +-- @field #string ClassName Name of the class. +-- @field #boolean Debug If true, send debug messages to all. +-- @field #boolean Report If true, send status messages to coalition. +-- @field Wrapper.Static#STATIC warehouse The phyical warehouse structure. +-- @field #string alias Alias of the warehouse. Name its called when sending messages. +-- @field Core.Zone#ZONE zone Zone around the warehouse. If this zone is captured, the warehouse and all its assets goes to the capturing coaliton. +-- @field Wrapper.Airbase#AIRBASE airbase Airbase the warehouse belongs to. +-- @field #string airbasename Name of the airbase associated to the warehouse. +-- @field Core.Point#COORDINATE road Closest point to warehouse on road. +-- @field Core.Point#COORDINATE rail Closest point to warehouse on rail. +-- @field Core.Zone#ZONE spawnzone Zone in which assets are spawned. +-- @field #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 markerid ID of the warehouse marker at the airbase. +-- @field #number dTstatus Time interval in seconds of updating the warehouse status and processing new events. Default 30 seconds. +-- @field #number queueid Unit id of each request in the queue. Essentially a running number starting at one and incremented when a new request is added. +-- @field #table stock Table holding all assets in stock. Table entries are of type @{#WAREHOUSE.Assetitem}. +-- @field #table queue Table holding all queued requests. Table entries are of type @{#WAREHOUSE.Queueitem}. +-- @field #table pending Table holding all pending requests, i.e. those that are currently in progress. Table elements are of type @{#WAREHOUSE.Pendingitem}. +-- @field #table transporting Table holding assets currently transporting cargo assets. +-- @field #table delivered Table holding all delivered requests. Table elements are #boolean. If true, all cargo has been delivered. +-- @field #table defending Table holding all defending requests, i.e. self requests that were if the warehouse is under attack. Table elements are of type @{#WAREHOUSE.Pendingitem}. +-- @field Core.Zone#ZONE portzone Zone defining the port of a warehouse. This is where naval assets are spawned. +-- @field #table shippinglanes Table holding the user defined shipping between warehouses. +-- @field #table offroadpaths Table holding user defined paths from one warehouse to another. +-- @field #boolean autodefence When the warehouse is under attack, automatically spawn assets to defend the warehouse. +-- @field #number spawnzonemaxdist Max distance between warehouse and spawn zone. Default 5000 meters. +-- @field #boolean autosave Automatically save assets to file when mission ends. +-- @field #string autosavepath Path where the asset file is saved on auto save. +-- @field #string autosavefilename 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. +-- @extends Core.Fsm#FSM + +--- Have your assets at the right place at the right time - or not! +-- +-- === +-- +-- # The Warehouse Concept +-- +-- The MOOSE warehouse adds a new logistic component to the DCS World. *Assets*, i.e. ground, airborne and naval units, can be transferred from one place +-- to another in a realistic and highly automatic fashion. In contrast to a "DCS warehouse" these assets have a physical representation in game. In particular, +-- this means they can be destroyed during the transport and add more life to the DCS world. +-- +-- This comes along with some additional interesting stategic aspects since capturing/defending and destroying/protecting an enemy or your +-- own warehous becomes of critical importance for the development of a conflict. +-- +-- In essence, creating an efficient network of warehouses is vital for the success of a battle or even the whole war. Likewise, of course, cutting off the enemy +-- of important supply lines by capturing or destroying warehouses or their associated infrastructure is equally important. +-- +-- ## What is a warehouse? +-- +-- A warehouse is an abstract object represented by a physical (static) building that can hold virtual assets in stock. +-- It can (but it must not) be associated with a particular airbase. The associated airbase can be an airdrome, a Helipad/FARP or a ship. +-- +-- If another warehouse requests assets, the corresponding troops are spawned at the warehouse and being transported to the requestor or go their +-- by themselfs. Once arrived at the requesting warehouse, the assets go into the stock of the requestor and can be activated/deployed when necessary. +-- +-- ## What assets can be stored? +-- +-- Any kind of ground, airborne or naval asset can be stored and are spawned upon request. +-- The fact that the assets live only virtually in stock and are put into the game only when needed has a positive impact on the game performance. +-- It also alliviates the problem of limited parking spots at smaller airbases. +-- +-- ## What means of transportation are available? +-- +-- Firstly, all mobile assets can be send from warehouse to another on their own. +-- +-- * Ground vehicles will use the road infrastructure. So a good road connection for both warehouses is important but also off road connections can be added if necessary. +-- * Airborne units get a flightplan from the airbase of the sending warehouse to the airbase of the receiving warehouse. This already implies that for airborne +-- assets both warehouses need an airbase. If either one of the warehouses does not have an associated airbase, direct transportation of airborne assest is not possible. +-- * Naval units can be exchanged between warehouses which possess a port, which can be defined by the user. Also shipping lanes must be specified manually but the user since DCS does not provide these. +-- * Trains (would) use the available railroad infrastructure and both warehouses must have a connection to the railroad. Unfortunately, however, trains are not yet implemented to +-- a reasonable degree in DCS at the moment and hence cannot be used yet. +-- +-- Furthermore, ground assets can be transferred between warehouses by transport units. These are APCs, helicopters and airplanes. The transportation process is modelled +-- in a realistic way by using the corresponding cargo dispatcher classes, i.e. +-- +-- * @{AI.AI_Cargo_Dispatcher_APC#AI_DISPATCHER_APC} +-- * @{AI.AI_Cargo_Dispatcher_Helicopter#AI_DISPATCHER_HELICOPTER} +-- * @{AI.AI_Cargo_Dispatcher_Airplane#AI_DISPATCHER_AIRPLANE} +-- +-- Depending on which cargo dispatcher is used (ground or airbore), similar considerations like in the self propelled case are necessary. Howver, note that +-- the dispatchers as of yet cannot use user defined off road paths for example since they are classes of their own and use a different routing logic. +-- +-- === +-- +-- # Creating a Warehouse +-- +-- A MOOSE warehouse must be represented in game by a physical *static* object. For example, the mission editor already has warehouse as static object available. +-- This would be a good first choice but any static object will do. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Static.png) +-- +-- The positioning of the warehouse static object is very important for a couple of reasons. Firstly, a warehouse needs a good infrastructure so that spawned assets +-- have a proper road connection or can reach the associated airbase easily. +-- +-- ## Constructor and Start +-- +-- Once the static warehouse object is placed in the mission editor it can be used as a MOOSE warehouse by the @{#WAREHOUSE.New}(*warehousestatic*, *alias*) constructor, +-- like for example: +-- +-- warehouseBatumi=WAREHOUSE:New(STATIC:FindByName("Warehouse Batumi"), "My optional Warehouse Alias") +-- warehouseBatumi:Start() +-- +-- The first parameter *warehousestatic* is the static MOOSE object. By default, the name of the warehouse will be the same as the name given to the static object. +-- The second parameter *alias* is optional and can be used to choose a more convenient name if desired. This will be the name the warehouse calls itself when reporting messages. +-- +-- Note that a warehouse also needs to be started in order to be in service. This is done with the @{#WAREHOUSE.Start}() or @{#WAREHOUSE.__Start}(*delay*) functions. +-- The warehouse is now fully operational and requests are being processed. +-- +-- # Adding Assets +-- +-- Assets can be added to the warehouse stock by using the @{#WAREHOUSE.AddAsset}(*group*, *ngroups*, *forceattribute*, *forcecargobay*, *forceweight*, *loadradius*, *skill*, *liveries*, *assignment*) function. +-- The parameter *group* has to be a MOOSE @{Wrapper.Group#GROUP}. This is also the only mandatory parameters. All other parameters are optional and can be used for fine tuning if +-- nessary. The parameter *ngroups* specifies how many clones of this group are added to the stock. +-- +-- infrantry=GROUP:FindByName("Some Infantry Group") +-- warehouseBatumi:AddAsset(infantry, 5) +-- +-- This will add five infantry groups to the warehouse stock. Note that the group should normally be a late activated template group, +-- which was defined in the mission editor. But you can also add other groups which are already spawned and present in the mission. +-- +-- Also note that the coalition of the template group (red, blue or neutral) does not matter. The coalition of the assets is determined by the coalition of the warehouse owner. +-- In other words, it is no problem to add red groups to blue warehouses and vice versa. The assets will automatically have the coalition of the warehouse. +-- +-- You can add assets with a delay by using the @{#WAREHOUSE.__AddAsset}(*delay*, *group*, *ngroups*, *forceattribute*, *forcecargobay*, *forceweight*, *loadradius*, *skill*, *liveries*, *assignment*), +-- where *delay* is the delay in seconds before the asset is added. +-- +-- In game, the warehouse will get a mark which is regularly updated and showing the currently available assets in stock. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Stock-Marker.png) +-- +-- ## Optional Parameters for Fine Tuning +-- +-- By default, the generalized attribute of the asset is determined automatically from the DCS descriptor attributes. However, this might not always result in the desired outcome. +-- Therefore, it is possible, to force a generalized attribute for the asset with the third optional parameter *forceattribute*, which is of type @{#WAREHOUSE.Attribute}. +-- +-- ### Setting the Generalized Attibute +-- For example, a UH-1H Huey has in DCS the attibute of an attack helicopter. But of course, it can also transport cargo. If you want to use it for transportation, you can specify this +-- manually when the asset is added +-- +-- warehouseBatumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) +-- +-- This becomes important when assets are requested from other warehouses as described below. In this case, the five Hueys are now marked as transport helicopters and +-- not attack helicopters. +-- +-- ### Setting the Cargo Bay Weight Limit +-- You can ajust the cargo bay weight limit, in case it is not calculated correctly automatically. For example, the cargo bay of a C-17A is much smaller in DCS than that of a C-130, which is +-- unrealistic. This can be corrected by the *forcecargobay* parmeter which is here set to 77,000 kg +-- +-- warehouseBatumi:AddAsset("C-17A", nil, nil, 77000) +-- +-- The size of the cargo bay is only important when the group is used as transport carrier for other assets. +-- +-- ### Setting the Weight +-- If an asset shall be transported by a carrier it important to note that - as in real life - a carrier can only carry cargo up to a certain weight. The weight of the +-- units is automatically determined from the DCS descriptor table. +-- However, in the current DCS version (2.5.3) a mortar unit has a weight of 5 tons. This confuses the transporter logic, because it appears to be too have for, e.g. all APCs. +-- +-- As a workaround, you can manually adjust the weight by the optional *forceweight* parameter: +-- +-- warehouseBatumi:AddAsset("Mortar Alpha", nil, nil, nil, 210) +-- +-- In this case we set it to 210 kg. Note, the weight value set is meant for *each* unit in the group. Therefore, a group consisting of three mortars will have a total weight +-- of 630 kg. This is important as groups cannot be split between carrier units when transporting, i.e. the total weight of the whole group must be smaller than the +-- cargo bay of the transport carrier. +-- +-- ### Setting the Load Radius +-- Boading and loading of cargo into a carrier is modeled in a realistic fashion in the AI\_CARGO\DISPATCHER classes, which are used inernally by the WAREHOUSE class. +-- Meaning that troops (cargo) will board, i.e. run or drive to the carrier, and only once they are in close proximity to the transporter they will be loaded (disappear). +-- +-- Unfortunately, there are some situations where problems can occur. For example, in DCS tanks have the strong tentendcy not to drive around obstacles but rather to roll over them. +-- I have seen cases where an aircraft of the same coalition as the tank was in its way and the tank drove right through the plane waiting on a parking spot and destroying it. +-- +-- As a workaround it is possible to set a larger load radius so that the cargo units are despawned further away from the carrier via the optional **loadradius** parameter: +-- +-- warehouseBatumi:AddAsset("Leopard 2", nil, nil, nil, nil, 250) +-- +-- Adding the asset like this will cause the units to be loaded into the carrier already at a distance of 250 meters. +-- +-- ### Setting the AI Skill +-- +-- By default, the asset has the skill of its template group. The optional parameter *skill* allows to set a different skill when the asset is added. See the +-- [hoggit page](https://wiki.hoggitworld.com/view/DCS_enum_AI) possible values of this enumerator. +-- For example you can use +-- +-- warehouseBatumi:AddAsset("Leopard 2", nil, nil, nil, nil, nil, AI.Skill.EXCELLENT) +-- +-- do set the skill of the asset to excellent. +-- +-- ### Setting Liveries +-- +-- By default ,the asset uses the livery of its template group. The optional parameter *liveries* allows to define one or multiple liveries. +-- If multiple liveries are given in form of a table of livery names, each asset gets a random one. +-- +-- For example +-- +-- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, "China UN") +-- +-- would spawn the asset with a chinese UN livery. +-- +-- Or +-- +-- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, {"China UN", "German"}) +-- +-- would spawn the asset with either a chinese UN or German livery. Mind the curly brackets **{}** when you want to specify multiple liveries. +-- +-- Four each unit type, the livery names can be found in the DCS root folder under Bazar\Liveries. You have to use the name of the livery subdirectory. The names of the liveries +-- as displayed in the mission editor might be different and won't work in general. +-- +-- ### Setting an Assignment +-- +-- Assets can be added with a specific assignment given as a text, e.g. +-- +-- warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, nil, "Go to Warehouse Kobuleti") +-- +-- This is helpful to establish supply chains once an asset has arrived at its (first) destination and is meant to be forwarded to another warehouse. +-- +-- ## Retrieving the Asset +-- +-- Once a an asset is added to a warehouse, the @{#WAREHOUSE.NewAsset} event is triggered. You can hook into this event with the @{#WAREHOUSE.OnAfterNewAsset}(*asset*, *assignment*) function. +-- +-- The first parameter *asset* is a table of type @{#WAREHOUSE.Assetitem} and contains a lot of information about the asset. The seconed parameter *assignment* is optional and is the specific +-- assignment the asset got when it was added. +-- +-- Note that the assignment is can also be the assignment that was specified when adding a request (see next section). Once an asset that was requested from another warehouse and an assignment +-- was specified in the @{#WAREHOUSE.AddRequest} function, the assignment can be checked when the asset has arrived and is added to the receiving warehouse. +-- +-- === +-- +-- # Requesting Assets +-- +-- Assets of the warehouse can be requested by other MOOSE warehouses. A request will first be scrutinized to check if can be fulfilled at all. If the request is valid, it is +-- put into the warehouse queue and processed as soon as possible. +-- +-- A request can be added by the @{#WAREHOUSE.AddRequest}(*warehouse*, *AssetDescriptor*, *AssetDescriptorValue*, *nAsset*, *TransportType*, *nTransport*, *Prio*, *Assignment*) function. +-- The parameters are +-- +-- * *warehouse*: The requesting MOOSE @{#WAREHOUSE}. Assets will be delivered there. +-- * *AssetDescriptor*: The descriptor to describe the asset "type". See the @{#WAREHOUSE.Descriptor} enumerator. For example, assets requested by their generalized attibute. +-- * *AssetDescriptorValue*: The value of the asset descriptor. +-- * *nAsset*: (Optional) Number of asset group requested. Default is one group. +-- * *TransportType*: (Optional) The transport method used to deliver the assets to the requestor. Default is that assets go to the requesting warehouse on their own. +-- * *nTransport*: (Optional) Number of asset groups used to transport the cargo assets from A to B. Default is one group. +-- * *Prio*: (Optional) A number between 1 (high) and 100 (low) describing the priority of the request. Request with high priority are processed first. Default is 50, i.e. medium priority. +-- * *Assignment*: (Optional) A free to choose string describing the assignment. For self requests, this can be used to assign the spawned groups to specific tasks. +-- +-- ## Requesting by Generalized Attribute +-- +-- Generalized attributes are similar to [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). However, they are a bit more general and +-- an asset can only have one generalized attribute by which it is characterized. +-- +-- For example: +-- +-- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.APC, 2) +-- +-- Here, warehouse Kobuleti requests 5 infantry groups from warehouse Batumi. These "cargo" assets should be transported from Batumi to Kobuleti by 2 APCS. +-- Note that the warehouse at Batumi needs to have at least five infantry groups and two APC groups in their stock if the request can be processed. +-- If either to few infantry or APC groups are available when the request is made, the request is held in the warehouse queue until enough cargo and +-- transport assets are available. +-- +-- Also note that the above request is for five infantry groups. So any group in stock that has the generalized attribute "GROUND_INFANTRY" can be selected for the request. +-- +-- ### Generalized Attributes +-- +-- Currently implemented are: +-- +-- * @{#WAREHOUSE.Attribute.AIR_TRANSPORTPLANE} Airplane with transport capability. This can be used to transport other assets. +-- * @{#WAREHOUSE.Attribute.AIR_AWACS} Airborne Early Warning and Control System. +-- * @{#WAREHOUSE.Attribute.AIR_FIGHTER} Fighter, interceptor, ... airplane. +-- * @{#WAREHOUSE.Attribute.AIR_BOMBER} Aircraft which can be used for strategic bombing. +-- * @{#WAREHOUSE.Attribute.AIR_TANKER} Airplane which can refuel other aircraft. +-- * @{#WAREHOUSE.Attribute.AIR_TRANSPORTHELO} Helicopter with transport capability. This can be used to transport other assets. +-- * @{#WAREHOUSE.Attribute.AIR_ATTACKHELO} Attack helicopter. +-- * @{#WAREHOUSE.Attribute.AIR_UAV} Unpiloted Aerial Vehicle, e.g. drones. +-- * @{#WAREHOUSE.Attribute.AIR_OTHER} Any airborne unit that does not fall into any other airborne category. +-- * @{#WAREHOUSE.Attribute.GROUND_APC} Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets. +-- * @{#WAREHOUSE.Attribute.GROUND_TRUCK} Unarmed ground vehicles, which has the DCS "Truck" attribute. +-- * @{#WAREHOUSE.Attribute.GROUND_INFANTRY} Ground infantry assets. +-- * @{#WAREHOUSE.Attribute.GROUND_ARTILLERY} Artillery assets. +-- * @{#WAREHOUSE.Attribute.GROUND_TANK} Tanks (modern or old). +-- * @{#WAREHOUSE.Attribute.GROUND_TRAIN} Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently. +-- * @{#WAREHOUSE.Attribute.GROUND_EWR} Early Warning Radar. +-- * @{#WAREHOUSE.Attribute.GROUND_AAA} Anti-Aircraft Artillery. +-- * @{#WAREHOUSE.Attribute.GROUND_SAM} Surface-to-Air Missile system or components. +-- * @{#WAREHOUSE.Attribute.GROUND_OTHER} Any ground unit that does not fall into any other ground category. +-- * @{#WAREHOUSE.Attribute.NAVAL_AIRCRAFTCARRIER} Aircraft carrier. +-- * @{#WAREHOUSE.Attribute.NAVAL_WARSHIP} War ship, i.e. cruisers, destroyers, firgates and corvettes. +-- * @{#WAREHOUSE.Attribute.NAVAL_ARMEDSHIP} Any armed ship that is not an aircraft carrier, a cruiser, destroyer, firgatte or corvette. +-- * @{#WAREHOUSE.Attribute.NAVAL_UNARMEDSHIP} Any unarmed naval vessel. +-- * @{#WAREHOUSE.Attribute.NAVAL_OTHER} Any naval unit that does not fall into any other naval category. +-- * @{#WAREHOUSE.Attribute.OTHER_UNKNOWN} Anything that does not fall into any other category. +-- +-- ## Requesting a Specific Unit Type +-- +-- A more specific request could look like: +-- +-- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.UNITTYPE, "A-10C", 2) +-- +-- Here, Kobuleti requests a specific unit type, in particular two groups of A-10Cs. Note that the spelling is important as it must exacly be the same as +-- what one get's when using the DCS unit type. +-- +-- ## Requesting a Specific Group +-- +-- An even more specific request would be: +-- +-- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Group Name as in ME", 3) +-- +-- In this case three groups named "Group Name as in ME" are requested. This explicitly request the groups named like that in the Mission Editor. +-- +-- ## Requesting a General Category +-- +-- On the other hand, very general and unspecifc requests can be made by the categroy descriptor. The descriptor value parameter can be any [group category](https://wiki.hoggitworld.com/view/DCS_Class_Group), i.e. +-- +-- * Group.Category.AIRPLANE for fixed wing aircraft, +-- * Group.Category.HELICOPTER for helicopters, +-- * Group.Category.GROUND for all ground troops, +-- * Group.Category.SHIP for naval assets, +-- * Group.Category.TRAIN for trains (not implemented and not working in DCS yet). +-- +-- For example, +-- +-- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, 10) +-- +-- means that Kubuleti requests 10 ground groups and does not care which ones. This could be a mix of infantry, APCs, trucks etc. +-- +-- **Note** that these general requests should be made with *great care* due to the fact, that depending on what a warehouse has in stock a lot of different unit types can be spawned. +-- +-- ## Requesting Relative Quantities +-- +-- In addition to requesting absolute numbers of assets it is possible to request relative amounts of assets currently in stock. To this end the @{#WAREHOUSE.Quantity} enumerator +-- was introduced: +-- +-- * @{#WAREHOUSE.Quantity.ALL} +-- * @{#WAREHOUSE.Quantity.HALF} +-- * @{#WAREHOUSE.Quantity.QUARTER} +-- * @{#WAREHOUSE.Quantity.THIRD} +-- * @{#WAREHOUSE.Quantity.THREEQUARTERS} +-- +-- For example, +-- +-- warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.HELICOPTER, WAREHOUSE.Quantity.HALF) +-- +-- means that Kobuleti warehouse requests half of all available helicopters which Batumi warehouse currently has in stock. +-- +-- # Employing Assets - The Self Request +-- +-- Transferring assets from one warehouse to another is important but of course once the the assets are at the "right" place it is equally important that they +-- can be employed for specific tasks and assignments. +-- +-- Assets in the warehouses stock can be used for user defined tasks quite easily. They can be spawned into the game by a "***self request***", i.e. the warehouse +-- requests the assets from itself: +-- +-- warehouseBatumi:AddRequest(warehouseBatumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5) +-- +-- Note that the *sending* and *requesting* warehouses are *identical* in this case. +-- +-- This would simply spawn five infantry groups in the spawn zone of the Batumi warehouse if/when they are available. +-- +-- ## Accessing the Assets +-- +-- If a warehouse requests assets from itself, it triggers the event **SelfReqeuest**. The mission designer can capture this event with the associated +-- @{#WAREHOUSE.OnAfterSelfRequest}(*From*, *Event*, *To*, *groupset*, *request*) function. +-- +-- --- OnAfterSelfRequest user function. Access groups spawned from the warehouse for further tasking. +-- -- @param #WAREHOUSE self +-- -- @param #string From From state. +-- -- @param #string Event Event. +-- -- @param #string To To state. +-- -- @param Core.Set#SET_GROUP groupset The set of cargo groups that was delivered to the warehouse itself. +-- -- @param #WAREHOUSE.Pendingitem request Pending self request. +-- function WAREHOUSE:OnAfterSelfRequest(From, Event, To, groupset, request) +-- local groupset=groupset --Core.Set#SET_GROUP +-- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem +-- +-- for _,group in pairs(groupset:GetSetObjects()) do +-- local group=group --Wrapper.Group#GROUP +-- group:SmokeGreen() +-- end +-- +-- end +-- +-- The variable *groupset* is a @{Core.Set#SET_GROUP} object and holds all asset groups from the request. The code above shows, how the mission designer can access the groups +-- for further tasking. Here, the groups are only smoked but, of course, you can use them for whatever assignment you fancy. +-- +-- Note that airborne groups are spawned in **uncontrolled state** and need to be activated first before they can begin with their assigned tasks and missions. +-- This can be done with the @{Wrapper.Controllable#CONTROLLABLE.StartUncontrolled} function as demonstrated in the example section below. +-- +-- === +-- +-- # Infrastructure +-- +-- A good infrastructure is important for a warehouse to be efficient. Therefore, the location of a warehouse should be chosen with care. +-- This can also help to avoid many DCS related issues such as units getting stuck in buildings, blocking taxi ways etc. +-- +-- ## Spawn Zone +-- +-- By default, the zone were ground assets are spawned is a circular zone around the physical location of the warehouse with a radius of 200 meters. However, the location of the +-- spawn zone can be set by the @{#WAREHOUSE.SetSpawnZone}(*zone*) functions. It is advisable to choose a zone which is clear of obstacles. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Batumi.png) +-- +-- The parameter *zone* is a MOOSE @{Core.Zone#ZONE} object. So one can, e.g., use trigger zones defined in the mission editor. If a cicular zone is not desired, one +-- can use a polygon zone (see @{Core.Zone#ZONE_POLYGON}). +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_SpawnPolygon.png) +-- +-- ## Road Connections +-- +-- Ground assets will use a road connection to travel from one warehouse to another. Therefore, a proper road connection is necessary. +-- +-- By default, the closest point on road to the center of the spawn zone is chosen as road connection automatically. But only, if distance between the spawn zone +-- and the road connection is less than 3 km. +-- +-- The user can set the road connection manually with the @{#WAREHOUSE.SetRoadConnection} function. This is only functional for self propelled assets at the moment +-- and not if using the AI dispatcher classes since these have a different logic to find the route. +-- +-- ## Off Road Connections +-- +-- For ground troops it is also possible to define off road paths between warehouses if no proper road connection is available or should not be used. +-- +-- An off road path can be defined via the @{#WAREHOUSE.AddOffRoadPath}(*remotewarehouse*, *group*, *oneway*) function, where +-- *remotewarehouse* is the warehouse to which the path leads. +-- The parameter *group* is a *late activated* template group. The waypoints of this group are used to define the path between the two warehouses. +-- By default, the reverse paths is automatically added to get *from* the remote warehouse *to* this warehouse unless the parameter *oneway* is set to *true*. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Off-Road_Paths.png) +-- +-- **Note** that if an off road connection is defined between two warehouses this becomes the default path, i.e. even if there is a path *on road* possible +-- this will not be used. +-- +-- Also note that you can define multiple off road connections between two warehouses. If there are multiple paths defined, the connection is chosen randomly. +-- It is also possible to add the same path multiple times. By this you can influence the probability of the chosen path. For example Path1(A->B) has been +-- added two times while Path2(A->B) was added only once. Hence, the group will choose Path1 with a probability of 66.6 % while Path2 is only chosen with +-- a probability of 33.3 %. +-- +-- ## Rail Connections +-- +-- A rail connection is automatically defined as the closest point on a railway measured from the center of the spawn zone. But only, if the distance is less than 3 km. +-- +-- The mission designer can manually specify a rail connection with the @{#WAREHOUSE.SetRailConnection} function. +-- +-- **NOTE** however, that trains in DCS are currently not implemented in a way so that they can be used. +-- +-- ## Air Connections +-- +-- In order to use airborne assets, a warehouse needs to have an associated airbase. This can be an airdrome, a FARP/HELOPAD or a ship. +-- +-- If there is an airbase within 3 km range of the warehouse it is automatically set as the associated airbase. A user can set an airbase manually +-- with the @{#WAREHOUSE.SetAirbase} function. Keep in mind that sometimes ground units need to walk/drive from the spawn zone to the airport +-- to get to their transport carriers. +-- +-- ## Naval Connections +-- +-- Natively, DCS does not have the concept of a port/habour or shipping lanes. So in order to have a meaningful transfer of naval units between warehouses, these have to be +-- defined by the mission designer. +-- +-- ### Defining a Port +-- +-- A port in this context is the zone where all naval assets are spawned. This zone can be defined with the function @{#WAREHOUSE.SetPortZone}(*zone*), where the parameter +-- *zone* is a MOOSE zone. So again, this can be create from a trigger zone defined in the mission editor or if a general shape is desired by a @{Core.Zone#ZONE_POLYGON}. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_PortZone.png) +-- +-- ### Defining Shipping Lanes +-- +-- A shipping lane between to warehouses can be defined by the @{#WAREHOUSE.AddShippingLane}(*remotewarehouse*, *group*, *oneway*) function. The first parameter *remotewarehouse* +-- is the warehouse which should be connected to the present warehouse. +-- +-- The parameter *group* should be a late activated group defined in the mission editor. The waypoints of this group are used as waypoints of the shipping lane. +-- +-- By default, the reverse lane is automatically added to the remote warehouse. This can be disabled by setting the *oneway* parameter to *true*. +-- +-- Similar to off road connections, you can also define multiple shipping lanes between two warehouse ports. If there are multiple lanes defined, one is chosen randomly. +-- It is possible to add the same lane multiple times. By this you can influence the probability of the chosen lane. For example Lane_1(A->B) has been +-- added two times while Lane_2(A->B) was added only once. Therefore, the ships will choose Lane_1 with a probability of 66.6 % while Path_2 is only chosen with +-- a probability of 33.3 %. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_ShippingLane.png) +-- +-- === +-- +-- # Why is my request not processed? +-- +-- For each request, the warehouse class logic does a lot of consistancy and validation checks under the hood. +-- This helps to circumvent a lot of DCS issues and shortcomings. For example, it is checked that enough free +-- parking spots at an airport are available *before* the assets are spawned. +-- However, this also means that sometimes a request is deemed to be *invalid* in which case they are deleted +-- from the queue or considered to be valid but cannot be executed at this very moment. +-- +-- ## Invalid Requests +-- +-- Invalid request are requests which can **never** be processes because there is some logical or physical argument against it. +-- (Or simply because that feature was not implemented (yet).) +-- +-- * All airborne assets need an associated airbase of any kind on the sending *and* receiving warhouse. +-- * Airplanes need an airdrome at the sending and receiving warehouses. +-- * Not enough parking spots of the right terminal type at the sending warehouse. This avoids planes spawning on runways or on top of each other. +-- * No parking spots of the right terminal type at the receiving warehouse. This avoids DCS despawning planes on landing if they have no valid parking spot. +-- * Ground assets need a road connection between both warehouses or an off-road path needs to be added manually. +-- * Ground assets cannot be send directly to ships, i.e. warehouses on ships. +-- * Naval units need a user defined shipping lane between both warehouses. +-- * Warehouses need a user defined port zone to spawn naval assets. +-- * The receiving warehouse is destroyed or stopped. +-- * If transport by airplane, both warehouses must have and airdrome. +-- * If transport by APC, both warehouses must have a road connection. +-- * If transport by helicopter, the sending airbase must have an associated airbase (airdrome or FARP). +-- +-- All invalid requests are cancelled and **removed** from the warehouse queue! +-- +-- ## Temporarily Unprocessable Requests +-- +-- Temporarily unprocessable requests are possible in priciple, 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 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! +-- +-- For example, a TPz Fuchs has a cargo bay large enough to carry up to 10 soldiers at once, which is a realistic number. +-- If a group consisting of more than ten soldiers needs to be transported, it cannot be loaded into the APC. +-- Even if two APCs are available, which could in principle carry up to 20 soldiers, a group of, let's say 12 soldiers will not +-- be split into a group of ten soldiers using the first APC and a group two soldiers using the second APC. +-- +-- In other words, **there must be at least one carrier unit available that has a cargo bay large enough to load the heaviest cargo group!** +-- The warehouse logic will automatically search all available transport assets for a large enough carrier. +-- But if none is available, the request will be queued until a suitable carrier becomes available. +-- +-- The only realistic solution in this case is to either provide a transport carrier with a larger cargo bay or to reduce the number of soldiers +-- in the group. +-- +-- A better way would be to have two groups of max. 10 soldiers each and one TPz Fuchs for transport. In this case, the first group is +-- loaded and transported to the receiving warehouse. Once this is done, the carrier will drive back and pick up the remaining +-- group. +-- +-- As an artificial workaround one can manually set the cargo bay size to a larger value or alternatively reduce the weight of the cargo +-- when adding the assets via the @{#WAREHOUSE.AddAsset} function. This might even be unavoidable if, for example, a SAM group +-- should be transported since SAM sites only work when all units are in the same group. +-- +-- ## Processing Speed +-- +-- A warehouse has a limited speed to process requests. Each time the status of the warehouse is updated only one requests is processed. +-- The time interval between status updates is 30 seconds by default and can be adjusted via the @{#WAREHOUSE.SetStatusUpdate}(*interval*) function. +-- However, the status is also updated on other occasions, e.g. when a new request was added. +-- +-- === +-- +-- # Strategic Considerations +-- +-- Due to the fact that a warehouse holds (or can hold) a lot of valuable assets, it makes a (potentially) juicy target for enemy attacks. +-- There are several interesting situations, which can occurr. +-- +-- ## Capturing a Warehouses Airbase +-- +-- If a warehouse has an associated airbase, it can be captured by the enemy. In this case, the warehouse looses its ability so employ all airborne assets and is also cut-off +-- from supply by airplanes. Supply of ground troops via helicopters is still possible, because they deliver the troops into the spawn zone. +-- +-- Technically, the capturing of the airbase is triggered by the DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) event. +-- So the capturing takes place when only enemy ground units are in the airbase zone whilst no ground units of the present airbase owner are in that zone. +-- +-- The warehouse will also create an event **AirbaseCaptured**, which can be captured by the @{#WAREHOUSE.OnAfterAirbaseCaptured} function. So the warehouse chief can react on +-- this attack and for example deploy ground groups to re-capture its airbase. +-- +-- When an airbase is re-captured the event **AirbaseRecaptured** is triggered and can be captured by the @{#WAREHOUSE.OnAfterAirbaseRecaptured} function. +-- This can be used to put the defending assets back into the warehouse stock. +-- +-- ## Capturing the Warehouse +-- +-- A warehouse can be captured by the enemy coalition. If enemy ground troops enter the warehouse zone the event **Attacked** is triggered which can be captured by the +-- @{#WAREHOUSE.OnAfterAttacked} event. By default the warehouse zone circular zone with a radius of 500 meters located at the center of the physical warehouse. +-- The warehouse zone can be set via the @{#WAREHOUSE.SetWarehouseZone}(*zone*) function. The parameter *zone* must also be a cirular zone. +-- +-- The @{#WAREHOUSE.OnAfterAttacked} function can be used by the mission designer to react to the enemy attack. For example by deploying some or all ground troops +-- currently in stock to defend the warehouse. Note that the warehouse also has a self defence option which can be enabled by the @{#WAREHOUSE.SetAutoDefenceOn}() +-- function. In this case, the warehouse will automatically spawn all ground troops. If the spawn zone is further away from the warehouse zone, all mobile troops +-- are routed to the warehouse zone. The self request which is triggered on an automatic defence has the assignment "AutoDefence". So you can use this to +-- give orders to the groups that were spawned using the @{#WAREHOUSE.OnAfterSelfRequest} function. +-- +-- If only ground troops of the enemy coalition are present in the warehouse zone, the warehouse and all its assets falls into the hands of the enemy. +-- In this case the event **Captured** is triggered which can be captured by the @{#WAREHOUSE.OnAfterCaptured} function. +-- +-- The warehouse turns to the capturing coalition, i.e. its physical representation, and all assets as well. In 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.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 +-- 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. +-- +-- === +-- +-- # Hook in and Take Control +-- +-- The Finite State Machine implementation allows mission designers to hook into important events and add their own code. +-- Most of these events have already been mentioned but here is the list at a glance: +-- +-- * "NotReadyYet" --> "Start" --> "Running" (Starting the warehouse) +-- * "*" --> "Status" --> "*" (status updated in regular intervals) +-- * "*" --> "AddAsset" --> "*" (adding a new asset to the warehouse stock) +-- * "*" --> "NewAsset" --> "*" (a new asset has been added to the warehouse stock) +-- * "*" --> "AddRequest" --> "*" (adding a request for the warehouse assets) +-- * "Running" --> "Request" --> "*" (a request is processed when the warehouse is running) +-- * "Attacked" --> "Request" --> "*" (a request is processed when the warehouse is attacked) +-- * "*" --> "Arrived" --> "*" (asset group has arrived at its destination) +-- * "*" --> "Delivered" --> "*" (all assets of a request have been delivered) +-- * "Running" --> "SelfRequest" --> "*" (warehouse is requesting asset from itself when running) +-- * "Attacked" --> "SelfRequest" --> "*" (warehouse is requesting asset from itself while under attack) +-- * "*" --> "Attacked" --> "Attacked" (warehouse is being attacked) +-- * "Attacked" --> "Defeated" --> "Running" (an attack was defeated) +-- * "Attacked" --> "Captured" --> "Running" (warehouse was captured by the enemy) +-- * "*" --> "AirbaseCaptured" --> "*" (airbase belonging to the warehouse was captured by the enemy) +-- * "*" --> "AirbaseRecaptured" --> "*" (airbase was re-captured) +-- * "*" --> "AssetDead" --> "*" (a whole asset group is dead) +-- * "*" --> "Destroyed" --> "Destroyed" (warehouse was destroyed) +-- * "Running" --> "Pause" --> "Paused" (warehouse is paused) +-- * "Paused" --> "Unpause" --> "Running" (warehouse is unpaused) +-- * "*" --> "Stop" --> "Stopped" (warehouse is stopped) +-- +-- The transitions are of the general form "From State" --> "Event" --> "To State". The "*" star denotes that the transition is possible from *any* state. +-- Some transitions, however, are only allowed from certain "From States". For example, no requests can be processed if the warehouse is in "Paused" or "Destroyed" or "Stopped" state. +-- +-- Mission designers can capture the events with OnAfterEvent functions, e.g. @{#WAREHOUSE.OnAfterDelivered} or @{#WAREHOUSE.OnAfterAirbaseCaptured}. +-- +-- === +-- +-- # Persistence of Assets +-- +-- Assets in stock of a warehouse can be saved to a file on your hard drive and then loaded from that file at a later point. This enables to restart the mission +-- and restore the warehouse stock. +-- +-- ## Prerequisites +-- +-- **Important** By default, DCS does not allow for writing data to files. Therefore, one first has to comment out the line "sanitizeModule('io')", i.e. +-- +-- do +-- sanitizeModule('os') +-- --sanitizeModule('io') +-- sanitizeModule('lfs') +-- require = nil +-- loadlib = nil +-- end +-- +-- in the file "MissionScripting.lua", which is located in the subdirectory "Scripts" of your DCS installation root directory. +-- +-- ### Don't! +-- +-- Do not use **semi-colons** or **equal signs** in the group names of your assets as these are used as separators in the saved and loaded files texts. +-- If you do, it will cause problems and give you a headache! +-- +-- ## Save Assets +-- +-- Saving asset data to file is achieved by the @{WAREHOUSE.Save}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the +-- warehouse data is saved. If you do not specify a path, the file is saved your the DCS installation root directory. +-- The parameter *filename* is optional and defines the name of the saved file. By default this is automatically created from the warehouse id and name, for example +-- "Warehouse-1234_Batumi.txt". +-- +-- warehouseBatumi:Save("D:\\My Warehouse Data\\") +-- +-- This will save all asset data to in "D:\\My Warehouse Data\\Warehouse-1234_Batumi.txt". +-- +-- ### Automatic Save at Mission End +-- +-- The assets can be saved automatically when the mission is ended via the @{WAREHOUSE.SetSaveOnMissionEnd}(*path*, *filename*) function, i.e. +-- +-- warehouseBatumi:SetSaveOnMissionEnd("D:\\My Warehouse Data\\") +-- +-- ## Load Assets +-- +-- Loading assets data from file is achieved by the @{WAREHOUSE.Load}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the +-- warehouse data is loaded from. If you do not specify a path, the file is loaded from your the DCS installation root directory. +-- The parameter *filename* is optional and defines the name of the file to load. By default this is automatically generated from the warehouse id and name, for example +-- "Warehouse-1234_Batumi.txt". +-- +-- Note that the warehouse **must not be started** and in the *Running* state in order to load the assets. In other words, loading should happen after the +-- @{#WAREHOUSE.New} command is specified in the code but before the @{#WAREHOUSE.Start} command is given. +-- +-- Loading the assets is done by +-- +-- warehouseBatumi:New(STATIC:FindByName("Warehouse Batumi")) +-- warehouseBatumi:Load("D:\\My Warehouse Data\\") +-- warehouseBatumi:Start() +-- +-- This sequence loads all assets from file. If a warehouse was captured in the last mission, it also respawns the static warehouse structure with the right coaliton. +-- However, it due to DCS limitations it is not possible to set the airbase coalition. This has to be done manually in the mission editor. Or alternatively, one could +-- spawn some ground units via a self request and let them capture the airbase. +-- +-- === +-- +-- # Examples +-- +-- This section shows some examples how the WAREHOUSE class is used in practice. This is one of the best ways to explain things, in my opinion. +-- +-- But first, let me introduce a convenient way to define several warehouses in a table. This is absolutely *not necessary* but quite handy if you have +-- multiple WAREHOUSE objects in your mission. +-- +-- ## Example 0: Setting up a Warehouse Array +-- +-- If you have multiple warehouses, you can put them in a table. This makes it easier to access them or to loop over them. +-- +-- -- Define Warehouses. +-- local warehouse={} +-- -- Blue warehouses +-- warehouse.Senaki = WAREHOUSE:New(STATIC:FindByName("Warehouse Senaki"), "Senaki") --Functional.Warehouse#WAREHOUSE +-- warehouse.Batumi = WAREHOUSE:New(STATIC:FindByName("Warehouse Batumi"), "Batumi") --Functional.Warehouse#WAREHOUSE +-- warehouse.Kobuleti = WAREHOUSE:New(STATIC:FindByName("Warehouse Kobuleti"), "Kobuleti") --Functional.Warehouse#WAREHOUSE +-- warehouse.Kutaisi = WAREHOUSE:New(STATIC:FindByName("Warehouse Kutaisi"), "Kutaisi") --Functional.Warehouse#WAREHOUSE +-- warehouse.Berlin = WAREHOUSE:New(STATIC:FindByName("Warehouse Berlin"), "Berlin") --Functional.Warehouse#WAREHOUSE +-- warehouse.London = WAREHOUSE:New(STATIC:FindByName("Warehouse London"), "London") --Functional.Warehouse#WAREHOUSE +-- warehouse.Stennis = WAREHOUSE:New(STATIC:FindByName("Warehouse Stennis"), "Stennis") --Functional.Warehouse#WAREHOUSE +-- warehouse.Pampa = WAREHOUSE:New(STATIC:FindByName("Warehouse Pampa"), "Pampa") --Functional.Warehouse#WAREHOUSE +-- -- Red warehouses +-- warehouse.Sukhumi = WAREHOUSE:New(STATIC:FindByName("Warehouse Sukhumi"), "Sukhumi") --Functional.Warehouse#WAREHOUSE +-- warehouse.Gudauta = WAREHOUSE:New(STATIC:FindByName("Warehouse Gudauta"), "Gudauta") --Functional.Warehouse#WAREHOUSE +-- warehouse.Sochi = WAREHOUSE:New(STATIC:FindByName("Warehouse Sochi"), "Sochi") --Functional.Warehouse#WAREHOUSE +-- +-- Remarks: +-- +-- * I defined the array as local, i.e. local warehouse={}. This is personal preference and sometimes causes trouble with the lua garbage collection. You can also define it as a global array/table! +-- * The "--Functional.Warehouse#WAREHOUSE" at the end is only to have the LDT intellisense working correctly. If you don't use LDT (which you should!), it can be omitted. +-- +-- **NOTE** that all examples below need this bit or code at the beginning - or at least the warehouses which are used. +-- +-- The example mission is based on the same template mission, which has defined a lot of airborne, ground and naval assets as templates. Only few of those are used here. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Assets.png) +-- +-- ## Example 1: Self Request +-- +-- Ground troops are taken from the Batumi warehouse stock and spawned in its spawn zone. After a short delay, they are added back to the warehouse stock. +-- Also a new request is made. Hence, the groups will be spawned, added back to the warehouse, spawned again and so on and so forth... +-- +-- -- Start warehouse Batumi. +-- warehouse.Batumi:Start() +-- +-- -- Add five groups of infantry as assets. +-- warehouse.Batumi:AddAsset(GROUP:FindByName("Infantry Platoon Alpha"), 5) +-- +-- -- Add self request for three infantry at Batumi. +-- warehouse.Batumi:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 3) +-- +-- +-- --- Self request event. Triggered once the assets are spawned in the spawn zone or at the airbase. +-- function warehouse.Batumi:OnAfterSelfRequest(From, Event, To, groupset, request) +-- local mygroupset=groupset --Core.Set#SET_GROUP +-- +-- -- Loop over all groups spawned from that request. +-- for _,group in pairs(mygroupset:GetSetObjects()) do +-- local group=group --Wrapper.Group#GROUP +-- +-- -- Gree smoke on spawned group. +-- group:SmokeGreen() +-- +-- -- Put asset back to stock after 10 seconds. +-- warehouse.Batumi:__AddAsset(10, group) +-- end +-- +-- -- Add new self request after 20 seconds. +-- warehouse.Batumi:__AddRequest(20, warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 3) +-- +-- end +-- +-- ## Example 2: Self propelled Ground Troops +-- +-- Warehouse Berlin, which is a FARP near Batumi, requests infantry and troop transports from the warehouse at Batumi. +-- The groups are spawned at Batumi and move by themselfs from Batumi to Berlin using the roads. +-- Once the troops have arrived at Berlin, the troops are automatically added to the warehouse stock of Berlin. +-- While on the road, Batumi has requested back two APCs from Berlin. Since Berlin does not have the assets in stock, +-- the request is queued. After the troops have arrived, Berlin is sending back the APCs to Batumi. +-- +-- -- Start Warehouse at Batumi. +-- warehouse.Batumi:Start() +-- +-- -- Add 20 infantry groups and ten APCs as assets at Batumi. +-- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) +-- warehouse.Batumi:AddAsset("TPz Fuchs", 10) +-- +-- -- Start Warehouse Berlin. +-- warehouse.Berlin:Start() +-- +-- -- Warehouse Berlin requests 10 infantry groups and 5 APCs from warehouse Batumi. +-- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 10) +-- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, 5) +-- +-- -- Request from Batumi for 2 APCs. Initially these are not in stock. When they become available, the request is executed. +-- warehouse.Berlin:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, 2) +-- +-- ## Example 3: Self Propelled Airborne Assets +-- +-- Warehouse Senaki receives a high priority request from Kutaisi for one Yak-52s. At the same time, Kobuleti requests half of +-- all available Yak-52s. Request from Kutaisi is first executed and then Kobuleti gets half of the remaining assets. +-- Additionally, London requests one third of all available UH-1H Hueys from Senaki. +-- Once the units have arrived they are added to the stock of the receiving warehouses and can be used for further assignments. +-- +-- -- Start warehouses +-- warehouse.Senaki:Start() +-- warehouse.Kutaisi:Start() +-- warehouse.Kobuleti:Start() +-- warehouse.London:Start() +-- +-- -- Add assets to Senaki warehouse. +-- warehouse.Senaki:AddAsset("Yak-52", 10) +-- warehouse.Senaki:AddAsset("Huey", 6) +-- +-- -- Kusaisi requests 3 Yak-52 form Senaki while Kobuleti wants all the rest. +-- warehouse.Senaki:AddRequest(warehouse.Kutaisi, WAREHOUSE.Descriptor.GROUPNAME, "Yak-52", 1, nil, nil, 10) +-- warehouse.Senaki:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Yak-52", WAREHOUSE.Quantity.HALF, nil, nil, 70) +-- +-- -- FARP London wants 1/3 of the six available Hueys. +-- warehouse.Senaki:AddRequest(warehouse.London, WAREHOUSE.Descriptor.GROUPNAME, "Huey", WAREHOUSE.Quantity.THIRD) +-- +-- ## Example 4: Transport of Assets by APCs +-- +-- Warehouse at FARP Berlin requests five infantry groups from Batumi. These assets shall be transported using two APC groups. +-- Infantry and APC are spawned in the spawn zone at Batumi. The APCs have a cargo bay large enough to pick up four of the +-- five infantry groups in the first run and will bring them to Berlin. There, they unboard and walk to the warehouse where they will be added to the stock. +-- Meanwhile the APCs go back to Batumi and one will pick up the last remaining soldiers. +-- Once the APCs have completed their mission, they return to Batumi and are added back to stock. +-- +-- -- Start Warehouse at Batumi. +-- warehouse.Batumi:Start() +-- +-- -- Start Warehouse Berlin. +-- warehouse.Berlin:Start() +-- +-- -- Add 20 infantry groups and five APCs as assets at Batumi. +-- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) +-- warehouse.Batumi:AddAsset("TPz Fuchs", 5) +-- +-- -- Warehouse Berlin requests 5 infantry groups from warehouse Batumi using 2 APCs for transport. +-- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.APC, 2) +-- +--## Example 5: Transport of Assets by Helicopters +-- +-- Warehouse at FARP Berlin requests five infantry groups from Batumi. They shall be transported by all available transport helicopters. +-- Note that the UH-1H Huey in DCS is an attack and not a transport helo. So the warehouse logic would be default also +-- register it as an @{#WAREHOUSE.Attribute.AIR_ATTACKHELICOPTER}. In order to use it as a transport we need to force +-- it to be added as transport helo. +-- Also note that even though all (here five) helos are requested, only two of them are employed because this number is sufficient to +-- transport all requested assets in one go. +-- +-- -- Start Warehouses. +-- warehouse.Batumi:Start() +-- warehouse.Berlin:Start() +-- +-- -- Add 20 infantry groups as assets at Batumi. +-- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) +-- +-- -- Add five Hueys for transport. Note that a Huey in DCS is an attack and not a transport helo. So we force this attribute! +-- warehouse.Batumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) +-- +-- -- Warehouse Berlin requests 5 infantry groups from warehouse Batumi using all available helos for transport. +-- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.HELICOPTER, WAREHOUSE.Quantity.ALL) +-- +--## Example 6: Transport of Assets by Airplanes +-- +-- Warehoues Kobuleti requests all (three) APCs from Batumi using one airplane for transport. +-- The available C-130 is able to carry one APC at a time. So it has to commute three times between Batumi and Kobuleti to deliver all requested cargo assets. +-- Once the cargo is delivered, the C-130 transport returns to Batumi and is added back to stock. +-- +-- -- Start warehouses. +-- warehouse.Batumi:Start() +-- warehouse.Kobuleti:Start() +-- +-- -- Add assets to Batumi warehouse. +-- warehouse.Batumi:AddAsset("C-130", 1) +-- warehouse.Batumi:AddAsset("TPz Fuchs", 3) +-- +-- warehouse.Batumi:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, WAREHOUSE.Quantity.ALL, WAREHOUSE.TransportType.AIRPLANE) +-- +-- ## Example 7: Capturing Airbase and Warehouse +-- +-- A red BMP has made it through our defence lines and drives towards our unprotected airbase at Senaki. +-- Once the BMP captures the airbase (DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) is evaluated) +-- the warehouse at Senaki lost its air infrastructure and it is not possible any more to spawn airborne units. All requests for airborne units are rejected and cancelled in this case. +-- +-- The red BMP then drives further to the warehouse. Once it enters the warehouse zone (500 m radius around the warehouse building), the warehouse is +-- considered to be under attack. This triggers the event **Attacked**. The @{#WAREHOUSE.OnAfterAttacked} function can be used to react to this situation. +-- Here, we only broadcast a distress call and launch a flare. However, it would also be reasonable to spawn all or selected ground troops in order to defend +-- the warehouse. Note, that the warehouse has a self defence option which can be activated via the @{#WAREHOUSE.SetAutoDefenceOn}() function. If activated, +-- *all* ground assets are automatically spawned and assigned to defend the warehouse. Once/if the attack is defeated, these assets go automatically back +-- into the warehouse stock. +-- +-- If the red coalition manages to capture our warehouse, all assets go into their possession. Now red tries to steal three F/A-18 flights and send them to +-- Sukhumi. These aircraft will be spawned and begin to taxi. However, ... +-- +-- A blue Bradley is in the area and will attemt to recapture the warehouse. It might also catch the red F/A-18s before they take off. +-- +-- -- Start warehouses. +-- warehouse.Senaki:Start() +-- warehouse.Sukhumi:Start() +-- +-- -- Add some assets. +-- warehouse.Senaki:AddAsset("TPz Fuchs", 5) +-- warehouse.Senaki:AddAsset("Infantry Platoon Alpha", 10) +-- warehouse.Senaki:AddAsset("F/A-18C 2ship", 10) +-- +-- -- Enable auto defence, i.e. spawn all group troups into the spawn zone. +-- --warehouse.Senaki:SetAutoDefenceOn() +-- +-- -- Activate Red BMP trying to capture the airfield and the warehouse. +-- local red1=GROUP:FindByName("Red BMP-80 Senaki"):Activate() +-- +-- -- The red BMP first drives to the airbase which gets captured and changes from blue to red. +-- -- This triggers the "AirbaseCaptured" event where you can hook in and do things. +-- function warehouse.Senaki:OnAfterAirbaseCaptured(From, Event, To, Coalition) +-- -- This request cannot be processed since the warehouse has lost its airbase. In fact it is deleted from the queue. +-- warehouse.Senaki:AddRequest(warehouse.Senaki,WAREHOUSE.Descriptor.CATEGORY, Group.Category.AIRPLANE, 1) +-- end +-- +-- -- Now the red BMP also captures the warehouse. This triggers the "Captured" event where you can hook in. +-- -- So now the warehouse and the airbase are both red and aircraft can be spawned again. +-- function warehouse.Senaki:OnAfterCaptured(From, Event, To, Coalition, Country) +-- -- These units will be spawned as red units because the warehouse has just been captured. +-- if Coalition==coalition.side.RED then +-- -- Sukhumi tries to "steals" three F/A-18 from Senaki and brings them to Sukhumi. +-- -- Well, actually the aircraft wont make it because blue1 will kill it on the taxi way leaving a blood bath. But that's life! +-- warehouse.Senaki:AddRequest(warehouse.Sukhumi, WAREHOUSE.Descriptor.CATEGORY, Group.Category.AIRPLANE, 3) +-- warehouse.Senaki.warehouse:SmokeRed() +-- elseif Coalition==coalition.side.BLUE then +-- warehouse.Senaki.warehouse:SmokeBlue() +-- end +-- +-- -- Activate a blue vehicle to re-capture the warehouse. It will drive to the warehouse zone and kill the red intruder. +-- local blue1=GROUP:FindByName("blue1"):Activate() +-- end +-- +-- ## Example 8: Destroying a Warehouse +-- +-- FARP Berlin requests a Huey from Batumi warehouse. This helo is deployed and will be delivered. +-- After 30 seconds into the mission we create and (artificial) big explosion - or a terrorist attack if you like - which completely destroys the +-- the warehouse at Batumi. All assets are gone and requests cannot be processed anymore. +-- +-- -- Start Batumi and Berlin warehouses. +-- warehouse.Batumi:Start() +-- warehouse.Berlin:Start() +-- +-- -- Add some assets. +-- warehouse.Batumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) +-- warehouse.Berlin:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO) +-- +-- -- Big explosion at the warehose. It has a very nice damage model by the way :) +-- local function DestroyWarehouse() +-- warehouse.Batumi:GetCoordinate():Explosion(999) +-- end +-- SCHEDULER:New(nil, DestroyWarehouse, {}, 30) +-- +-- -- First request is okay since warehouse is still alive. +-- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1) +-- +-- -- These requests should both not be processed any more since the warehouse at Batumi is destroyed. +-- warehouse.Batumi:__AddRequest(35, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1) +-- warehouse.Berlin:__AddRequest(40, warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1) +-- +-- ## Example 9: Self Propelled Naval Assets +-- +-- Kobuleti requests all naval assets from Batumi. +-- However, before naval assets can be exchanged, both warehouses need a port and at least one shipping lane defined by the user. +-- See the @{#WAREHOUSE.SetPortZone}() and @{#WAREHOUSE.AddShippingLane}() functions. +-- We do not want to spawn them all at once, because this will probably be a disaster +-- in the port zone. Therefore, each ship is spawned with a delay of five minutes. +-- +-- Batumi has quite a selection of different ships (for testing). +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Naval_Assets.png) +-- +-- -- Start warehouses. +-- warehouse.Batumi:Start() +-- warehouse.Kobuleti:Start() +-- +-- -- Define ports. These are polygon zones created by the waypoints of late activated units. +-- warehouse.Batumi:SetPortZone(ZONE_POLYGON:NewFromGroupName("Warehouse Batumi Port Zone", "Warehouse Batumi Port Zone")) +-- warehouse.Kobuleti:SetPortZone(ZONE_POLYGON:NewFromGroupName("Warehouse Kobuleti Port Zone", "Warehouse Kobuleti Port Zone")) +-- +-- -- Shipping lane. Again, the waypoints of late activated units are taken as points defining the shipping lane. +-- -- Some units will take lane 1 while others will take lane two. But both lead from Batumi to Kobuleti port. +-- warehouse.Batumi:AddShippingLane(warehouse.Kobuleti, GROUP:FindByName("Warehouse Batumi-Kobuleti Shipping Lane 1")) +-- warehouse.Batumi:AddShippingLane(warehouse.Kobuleti, GROUP:FindByName("Warehouse Batumi-Kobuleti Shipping Lane 2")) +-- +-- -- Large selection of available naval units in DCS. +-- warehouse.Batumi:AddAsset("Speedboat") +-- warehouse.Batumi:AddAsset("Perry") +-- warehouse.Batumi:AddAsset("Normandy") +-- warehouse.Batumi:AddAsset("Stennis") +-- warehouse.Batumi:AddAsset("Carl Vinson") +-- warehouse.Batumi:AddAsset("Tarawa") +-- warehouse.Batumi:AddAsset("SSK 877") +-- warehouse.Batumi:AddAsset("SSK 641B") +-- warehouse.Batumi:AddAsset("Grisha") +-- warehouse.Batumi:AddAsset("Molniya") +-- warehouse.Batumi:AddAsset("Neustrashimy") +-- warehouse.Batumi:AddAsset("Rezky") +-- warehouse.Batumi:AddAsset("Moskva") +-- warehouse.Batumi:AddAsset("Pyotr Velikiy") +-- warehouse.Batumi:AddAsset("Kuznetsov") +-- warehouse.Batumi:AddAsset("Zvezdny") +-- warehouse.Batumi:AddAsset("Yakushev") +-- warehouse.Batumi:AddAsset("Elnya") +-- warehouse.Batumi:AddAsset("Ivanov") +-- warehouse.Batumi:AddAsset("Yantai") +-- warehouse.Batumi:AddAsset("Type 052C") +-- warehouse.Batumi:AddAsset("Guangzhou") +-- +-- -- Get Number of ships at Batumi. +-- local nships=warehouse.Batumi:GetNumberOfAssets(WAREHOUSE.Descriptor.CATEGORY, Group.Category.SHIP) +-- +-- -- Send one ship every 3 minutes (ships do not evade each other well, so we need a bit space between them). +-- for i=1, nships do +-- warehouse.Batumi:__AddRequest(180*(i-1)+10, warehouse.Kobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.SHIP, 1) +-- end +-- +-- ## Example 10: Warehouse on Aircraft Carrier +-- +-- This example shows how to spawn assets from a warehouse located on an aircraft carrier. The warehouse must still be represented by a +-- physical static object. However, on a carrier space is limit so we take a smaller static. In priciple one could also take something +-- like a windsock. +-- +-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Carrier.png) +-- +-- USS Stennis requests F/A-18s from Batumi. At the same time Kobuleti requests F/A-18s from the Stennis which currently does not have any. +-- So first, Batumi delivers the fighters to the Stennis. After they arrived they are deployed again and send to Kobuleti. +-- +-- -- Start warehouses. +-- warehouse.Batumi:Start() +-- warehouse.Stennis:Start() +-- warehouse.Kobuleti:Start() +-- +-- -- Add F/A-18 2-ship flight to Batmi. +-- warehouse.Batumi:AddAsset("F/A-18C 2ship", 1) +-- +-- -- USS Stennis requests F/A-18 from Batumi. +-- warehouse.Batumi:AddRequest(warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "F/A-18C 2ship") +-- +-- -- Kobuleti requests F/A-18 from USS Stennis. +-- warehouse.Stennis:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "F/A-18C 2ship") +-- +-- ## Example 11: Aircraft Carrier - Rescue Helo and Escort +-- +-- After 10 seconds we make a self request for a rescue helicopter. Note, that the @{#WAREHOUSE.AddRequest} function has a parameter which lets you +-- specify an "Assignment". This can be later used to identify the request and take the right actions. +-- +-- Once the request is processed, the @{#WAREHOUSE.OnAfterSelfRequest} function is called. This is where we hook in and postprocess the spawned assets. +-- In particular, we use the @{AI.AI_Formation#AI_FORMATION} class to make some nice escorts for our carrier. +-- +-- When the resue helo is spawned, we can check that this is the correct asset and make the helo go into formation with the carrier. +-- Once the helo runs out of fuel, it will automatically return to the ship and land. For the warehouse, this means that the "cargo", i.e. the helicopter +-- has been delivered - assets can be delivered to other warehouses and to the same warehouse - hence a *self* request. +-- When that happens, the **Delivered** event is triggered and the @{#WAREHOUSE.OnAfterDelivered} function called. This can now be used to spawn +-- a fresh helo. Effectively, there we created an infinite, never ending loop. So a rescue helo will be up at all times. +-- +-- After 30 and 45 seconds requests for five groups of armed speedboats are made. These will be spawned in the port zone right behind the carrier. +-- The first five groups will go port of the carrier an form a left wing formation. The seconds groups will to the analogue on the starboard side. +-- **Note** that in order to spawn naval assets a warehouse needs a port (zone). Since the carrier and hence the warehouse is mobile, we define a moving +-- zone as @{Core.Zone#ZONE_UNIT} with the carrier as reference unit. The "port" of the Stennis at its stern so all naval assets are spawned behing the carrier. +-- +-- -- Start warehouse on USS Stennis. +-- warehouse.Stennis:Start() +-- +-- -- Aircraft carrier gets a moving zone right behind it as port. +-- warehouse.Stennis:SetPortZone(ZONE_UNIT:New("Warehouse Stennis Port Zone", UNIT:FindByName("USS Stennis"), 100, {rho=250, theta=180, relative_to_unit=true})) +-- +-- -- Add speedboat assets. +-- warehouse.Stennis:AddAsset("Speedboat", 10) +-- warehouse.Stennis:AddAsset("CH-53E", 1) +-- +-- -- Self request of speed boats. +-- warehouse.Stennis:__AddRequest(10, warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "CH-53E", 1, nil, nil, nil, "Rescue Helo") +-- warehouse.Stennis:__AddRequest(30, warehouse.Stennis, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.NAVAL_ARMEDSHIP, 5, nil, nil, nil, "Speedboats Left") +-- warehouse.Stennis:__AddRequest(45, warehouse.Stennis, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.NAVAL_ARMEDSHIP, 5, nil, nil, nil, "Speedboats Right") +-- +-- --- Function called after self request +-- function warehouse.Stennis:OnAfterSelfRequest(From, Event, To,_groupset, request) +-- local groupset=_groupset --Core.Set#SET_GROUP +-- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem +-- +-- -- USS Stennis is the mother ship. +-- local Mother=UNIT:FindByName("USS Stennis") +-- +-- -- Get assignment of the request. +-- local assignment=warehouse.Stennis:GetAssignment(request) +-- +-- if assignment=="Speedboats Left" then +-- +-- -- Define AI Formation object. +-- -- Note that this has to be a global variable or the garbage collector will remove it for some reason! +-- CarrierFormationLeft = AI_FORMATION:New(Mother, groupset, "Left Formation with Carrier", "Escort Carrier.") +-- +-- -- Formation parameters. +-- CarrierFormationLeft:FormationLeftWing(200 ,50, 0, 0, 500, 50) +-- CarrierFormationLeft:__Start(2) +-- +-- for _,group in pairs(groupset:GetSetObjects()) do +-- local group=group --Wrapper.Group#GROUP +-- group:FlareRed() +-- end +-- +-- elseif assignment=="Speedboats Right" then +-- +-- -- Define AI Formation object. +-- -- Note that this has to be a global variable or the garbage collector will remove it for some reason! +-- CarrierFormationRight = AI_FORMATION:New(Mother, groupset, "Right Formation with Carrier", "Escort Carrier.") +-- +-- -- Formation parameters. +-- CarrierFormationRight:FormationRightWing(200 ,50, 0, 0, 500, 50) +-- CarrierFormationRight:__Start(2) +-- +-- for _,group in pairs(groupset:GetSetObjects()) do +-- local group=group --Wrapper.Group#GROUP +-- group:FlareGreen() +-- end +-- +-- elseif assignment=="Rescue Helo" then +-- +-- -- Start uncontrolled helo. +-- local group=groupset:GetFirst() --Wrapper.Group#GROUP +-- group:StartUncontrolled() +-- +-- -- Define AI Formation object. +-- CarrierFormationHelo = AI_FORMATION:New(Mother, groupset, "Helo Formation with Carrier", "Fly Formation.") +-- +-- -- Formation parameters. +-- CarrierFormationHelo:FormationCenterWing(-150, 50, 20, 50, 100, 50) +-- CarrierFormationHelo:__Start(2) +-- +-- end +-- +-- --- When the helo is out of fuel, it will return to the carrier and should be delivered. +-- function warehouse.Stennis:OnAfterDelivered(From,Event,To,request) +-- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem +-- +-- -- So we start another request. +-- if request.assignment=="Rescue Helo" then +-- warehouse.Stennis:__AddRequest(10, warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "CH-53E", 1, nil, nil, nil, "Rescue Helo") +-- end +-- end +-- +-- end +-- +-- ## Example 12: Pause a Warehouse +-- +-- This example shows how to pause and unpause a warehouse. In paused state, requests will not be processed but assets can be added and requests be added. +-- +-- * Warehouse Batumi is paused after 10 seconds. +-- * Request from Berlin after 15 which will not be processed. +-- * New tank assets for Batumi after 20 seconds. This is possible also in paused state. +-- * Batumi unpaused after 30 seconds. Queued request from Berlin can be processed. +-- * Berlin is paused after 60 seconds. +-- * Berlin requests tanks from Batumi after 90 seconds. Request is not processed because Berlin is paused and not running. +-- * Berlin is unpaused after 120 seconds. Queued request for tanks from Batumi can not be processed. +-- +-- Here is the code: +-- +-- -- Start Warehouse at Batumi. +-- warehouse.Batumi:Start() +-- +-- -- Start Warehouse Berlin. +-- warehouse.Berlin:Start() +-- +-- -- Add 20 infantry groups and 5 tank platoons as assets at Batumi. +-- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20) +-- +-- -- Pause the warehouse after 10 seconds +-- warehouse.Batumi:__Pause(10) +-- +-- -- Add a request from Berlin after 15 seconds. A request can be added but not be processed while warehouse is paused. +-- warehouse.Batumi:__AddRequest(15, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 1) +-- +-- -- New asset added after 20 seconds. This is possible even if the warehouse is paused. +-- warehouse.Batumi:__AddAsset(20, "Abrams", 5) +-- +-- -- Unpause warehouse after 30 seconds. Now the request from Berlin can be processed. +-- warehouse.Batumi:__Unpause(30) +-- +-- -- Pause warehouse Berlin +-- warehouse.Berlin:__Pause(60) +-- +-- -- After 90 seconds request from Berlin for tanks. +-- warehouse.Batumi:__AddRequest(90, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TANK, 1) +-- +-- -- After 120 seconds unpause Berlin. +-- warehouse.Berlin:__Unpause(120) +-- +-- ## Example 13: Battlefield Air Interdiction +-- +-- This example show how to couple the WAREHOUSE class with the @{AI.AI_Bai} class. +-- Four enemy targets have been located at the famous Kobuleti X. All three available Viggen 2-ship flights are assigned to kill at least one of the BMPs to complete their mission. +-- +-- -- Start Warehouse at Kobuleti. +-- warehouse.Kobuleti:Start() +-- +-- -- Add three 2-ship groups of Viggens. +-- warehouse.Kobuleti:AddAsset("Viggen 2ship", 3) +-- +-- -- Self request for all Viggen assets. +-- warehouse.Kobuleti:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Viggen 2ship", WAREHOUSE.Quantity.ALL, nil, nil, nil, "BAI") +-- +-- -- Red targets at Kobuleti X (late activated). +-- local RedTargets=GROUP:FindByName("Red IVF Alpha") +-- +-- -- Activate the targets. +-- RedTargets:Activate() +-- +-- -- Do something with the spawned aircraft. +-- function warehouse.Kobuleti:OnAfterSelfRequest(From,Event,To,groupset,request) +-- local groupset=groupset --Core.Set#SET_GROUP +-- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem +-- +-- if request.assignment=="BAI" then +-- +-- for _,group in pairs(groupset:GetSetObjects()) do +-- local group=group --Wrapper.Group#GROUP +-- +-- -- Start uncontrolled aircraft. +-- group:StartUncontrolled() +-- +-- local BAI=AI_BAI_ZONE:New(ZONE:New("Patrol Zone Kobuleti"), 500, 1000, 500, 600, ZONE:New("Patrol Zone Kobuleti")) +-- +-- -- Tell the program to use the object (in this case called BAIPlane) as the group to use in the BAI function +-- BAI:SetControllable(group) +-- +-- -- Function checking if targets are still alive +-- local function CheckTargets() +-- local nTargets=RedTargets:GetSize() +-- local nInitial=RedTargets:GetInitialSize() +-- local nDead=nInitial-nTargets +-- local nRequired=1 -- Let's make this easy. +-- if RedTargets:IsAlive() and nDead < nRequired then +-- MESSAGE:New(string.format("BAI Mission: %d of %d red targets still alive. At least %d targets need to be eliminated.", nTargets, nInitial, nRequired), 5):ToAll() +-- else +-- MESSAGE:New("BAI Mission: The required red targets are destroyed.", 30):ToAll() +-- BAI:__Accomplish(1) -- Now they should fly back to the patrolzone and patrol. +-- end +-- end +-- +-- -- Start scheduler to monitor number of targets. +-- local Check, CheckScheduleID = SCHEDULER:New(nil, CheckTargets, {}, 60, 60) +-- +-- -- When the targets in the zone are destroyed, (see scheduled function), the planes will return home ... +-- function BAI:OnAfterAccomplish( Controllable, From, Event, To ) +-- MESSAGE:New( "BAI Mission: Sending the Viggens back to base.", 30):ToAll() +-- Check:Stop(CheckScheduleID) +-- BAI:__RTB(1) +-- end +-- +-- -- Start BAI +-- BAI:Start() +-- +-- -- Engage after 5 minutes. +-- BAI:__Engage(300) +-- +-- -- RTB after 30 min max. +-- BAI:__RTB(-30*60) +-- +-- end +-- end +-- +-- end +-- +-- ## Example 14: Strategic Bombing +-- +-- This example shows how to employ stategic bombers in a mission. Three B-52s are lauched at Kobuleti with the assignment to wipe out the enemy warehouse at Sukhumi. +-- The bombers will get a flight path and make their approach from the South at an altitude of 5000 m ASL. After their bombing run, they will return to Kobuleti and +-- added back to stock. +-- +-- -- Start warehouses +-- warehouse.Kobuleti:Start() +-- warehouse.Sukhumi:Start() +-- +-- -- Add a strategic bomber assets +-- warehouse.Kobuleti:AddAsset("B-52H", 3) +-- +-- -- Request bombers for specific task of bombing Sukhumi warehouse. +-- warehouse.Kobuleti:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_BOMBER, WAREHOUSE.Quantity.ALL, nil, nil, nil, "Bomb Sukhumi") +-- +-- -- Specify assignment after bombers have been spawned. +-- function warehouse.Kobuleti:OnAfterSelfRequest(From, Event, To, groupset, request) +-- local groupset=groupset --Core.Set#SET_GROUP +-- +-- -- Get assignment of this request. +-- local assignment=warehouse.Kobuleti:GetAssignment(request) +-- +-- if assignment=="Bomb Sukhumi" then +-- +-- for _,_group in pairs(groupset:GetSet()) do +-- local group=_group --Wrapper.Group#GROUP +-- +-- -- Start uncontrolled aircraft. +-- group:StartUncontrolled() +-- +-- -- Target coordinate! +-- local ToCoord=warehouse.Sukhumi:GetCoordinate():SetAltitude(5000) +-- +-- -- Home coordinate. +-- local HomeCoord=warehouse.Kobuleti:GetCoordinate():SetAltitude(3000) +-- +-- -- Task bomb Sukhumi warehouse using all bombs (2032) from direction 180 at altitude 5000 m. +-- local task=group:TaskBombing(warehouse.Sukhumi:GetCoordinate():GetVec2(), false, "All", nil , 180, 5000, 2032) +-- +-- -- Define waypoints. +-- local WayPoints={} +-- +-- -- Take off position. +-- WayPoints[1]=warehouse.Kobuleti:GetCoordinate():WaypointAirTakeOffParking() +-- -- Begin bombing run 20 km south of target. +-- WayPoints[2]=ToCoord:Translate(20*1000, 180):WaypointAirTurningPoint(nil, 600, {task}, "Bombing Run") +-- -- Return to base. +-- WayPoints[3]=HomeCoord:WaypointAirTurningPoint() +-- -- Land at homebase. Bombers are added back to stock and can be employed in later assignments. +-- WayPoints[4]=warehouse.Kobuleti:GetCoordinate():WaypointAirLanding() +-- +-- -- Route bombers. +-- group:Route(WayPoints) +-- end +-- +-- end +-- end +-- +-- ## Example 15: Defining Off-Road Paths +-- +-- For self propelled assets it is possible to define custom off-road paths from one warehouse to another via the @{#WAREHOUSE.AddOffRoadPath} function. +-- The waypoints of a path are taken from late activated units. In this example, two paths have been defined between the warehouses Kobuleti and FARP London. +-- Trucks are spawned at each warehouse and are guided along the paths to the other warehouse. +-- Note that if more than one path was defined, each asset group will randomly select its route. +-- +-- -- Start warehouses +-- warehouse.Kobuleti:Start() +-- warehouse.London:Start() +-- +-- -- Define a polygon zone as spawn zone at Kobuleti. +-- warehouse.Kobuleti:SetSpawnZone(ZONE_POLYGON:New("Warehouse Kobuleti Spawn Zone", GROUP:FindByName("Warehouse Kobuleti Spawn Zone"))) +-- +-- -- Add assets. +-- warehouse.Kobuleti:AddAsset("M978", 20) +-- warehouse.London:AddAsset("M818", 20) +-- +-- -- Off two road paths from Kobuleti to London. The reverse path from London to Kobuleti is added automatically. +-- warehouse.Kobuleti:AddOffRoadPath(warehouse.London, GROUP:FindByName("Warehouse Kobuleti-London OffRoad Path 1")) +-- warehouse.Kobuleti:AddOffRoadPath(warehouse.London, GROUP:FindByName("Warehouse Kobuleti-London OffRoad Path 2")) +-- +-- -- London requests all available trucks from Kobuleti. +-- warehouse.Kobuleti:AddRequest(warehouse.London, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TRUCK, WAREHOUSE.Quantity.ALL) +-- +-- -- Kobuleti requests all available trucks from London. +-- warehouse.London:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TRUCK, WAREHOUSE.Quantity.HALF) +-- +-- ## Example 16: Resupply of Dead Assets +-- +-- Warehouse at FARP Berlin is located at the front line and sends infantry groups to the battle zone. +-- Whenever a group dies, a new group is send from the warehouse to the battle zone. +-- Additionally, for each dead group, Berlin requests resupply from Batumi. +-- +-- -- Start warehouses. +-- warehouse.Batumi:Start() +-- warehouse.Berlin:Start() +-- +-- -- Front line warehouse. +-- warehouse.Berlin:AddAsset("Infantry Platoon Alpha", 6) +-- +-- -- Resupply warehouse. +-- warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 50) +-- +-- -- Battle zone near FARP Berlin. This is where the action is! +-- local BattleZone=ZONE:New("Virtual Battle Zone") +-- +-- -- Send infantry groups to the battle zone. Two groups every ~60 seconds. +-- for i=1,2 do +-- local time=(i-1)*60+10 +-- warehouse.Berlin:__AddRequest(time, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 2, nil, nil, nil, "To Battle Zone") +-- end +-- +-- -- Take care of the spawned units. +-- function warehouse.Berlin:OnAfterSelfRequest(From,Event,To,groupset,request) +-- local groupset=groupset --Core.Set#SET_GROUP +-- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem +-- +-- -- Get assignment of this request. +-- local assignment=warehouse.Berlin:GetAssignment(request) +-- +-- if assignment=="To Battle Zone" then +-- +-- for _,group in pairs(groupset:GetSet()) do +-- local group=group --Wrapper.Group#GROUP +-- +-- -- Route group to Battle zone. +-- local ToCoord=BattleZone:GetRandomCoordinate() +-- group:RouteGroundOnRoad(ToCoord, group:GetSpeedMax()*0.8) +-- +-- -- After 3-5 minutes we create an explosion to destroy the group. +-- SCHEDULER:New(nil, Explosion, {group, 50}, math.random(180, 300)) +-- end +-- +-- end +-- +-- end +-- +-- -- An asset has died ==> request resupply for it. +-- function warehouse.Berlin:OnAfterAssetDead(From, Event, To, asset, request) +-- local asset=asset --Functional.Warehouse#WAREHOUSE.Assetitem +-- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem +-- +-- -- Get assignment. +-- local assignment=warehouse.Berlin:GetAssignment(request) +-- +-- -- Request resupply for dead asset from Batumi. +-- warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, asset.attribute, nil, nil, nil, nil, "Resupply") +-- +-- -- Send asset to Battle zone either now or when they arrive. +-- warehouse.Berlin:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, asset.attribute, 1, nil, nil, nil, assignment) +-- end +-- +-- ## Example 17: Supply Chains +-- +-- Our remote warehouse "Pampa" south of Batumi needs assets but does not have any air infrastructure (FARP or airdrome). +-- Leopard 2 tanks are transported from Kobuleti to Batumi using two C-17As. From there they go be themselfs to Pampa. +-- Eight infantry groups and two mortar groups are also being transferred from Kobuleti to Batumi by helicopter. +-- The infantry has a higher priority and will be transported first using all available Mi-8 helicopters. +-- Once infantry has arrived at Batumi, it will walk by itself to warehouse Pampa. +-- The mortars can only be transported once the Mi-8 helos are available again, i.e. when the infantry has been delivered. +-- Once the mortars arrive at Batumi, they will be transported by APCs to Pampa. +-- +-- -- Start warehouses. +-- warehouse.Kobuleti:Start() +-- warehouse.Batumi:Start() +-- warehouse.Pampa:Start() +-- +-- -- Add assets to Kobuleti warehouse, which is our main hub. +-- warehouse.Kobuleti:AddAsset("C-130", 2) +-- warehouse.Kobuleti:AddAsset("C-17A", 2, nil, 77000) +-- warehouse.Kobuleti:AddAsset("Mi-8", 2, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, nil, nil, nil, AI.Skill.EXCELLENT, {"Germany", "United Kingdom"}) +-- warehouse.Kobuleti:AddAsset("Leopard 2", 10, nil, nil, 62000, 500) +-- warehouse.Kobuleti:AddAsset("Mortar Alpha", 10, nil, nil, 210) +-- warehouse.Kobuleti:AddAsset("Infantry Platoon Alpha", 20) +-- +-- -- Transports at Batumi. +-- warehouse.Batumi:AddAsset("SPz Marder", 2) +-- warehouse.Batumi:AddAsset("TPz Fuchs", 2) +-- +-- -- Tanks transported by plane from from Kobuleti to Batumi. +-- warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TANK, 2, WAREHOUSE.TransportType.AIRPLANE, 2, 10, "Assets for Pampa") +-- -- Artillery transported by helicopter from Kobuleti to Batumi. +-- warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_ARTILLERY, 2, WAREHOUSE.TransportType.HELICOPTER, 2, 30, "Assets for Pampa via APC") +-- -- Infantry transported by helicopter from Kobuleti to Batumi. +-- warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 8, WAREHOUSE.TransportType.HELICOPTER, 2, 20, "Assets for Pampa") +-- +-- --- Function handling assets delivered from Kobuleti warehouse. +-- function warehouse.Kobuleti:OnAfterDelivered(From, Event, To, request) +-- local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem +-- +-- -- Get assignment. +-- local assignment=warehouse.Kobuleti:GetAssignment(request) +-- +-- -- Check if these assets were meant for Warehouse Pampa. +-- if assignment=="Assets for Pampa via APC" then +-- -- Forward everything that arrived at Batumi to Pampa via APC. +-- warehouse.Batumi:AddRequest(warehouse.Pampa, WAREHOUSE.Descriptor.ATTRIBUTE, request.cargoattribute, request.ndelivered, WAREHOUSE.TransportType.APC, WAREHOUSE.Quantity.ALL) +-- end +-- end +-- +-- -- Forward all mobile ground assets to Pampa once they arrived. +-- function warehouse.Batumi:OnAfterNewAsset(From, Event, To, asset, assignment) +-- local asset=asset --Functional.Warehouse#WAREHOUSE.Assetitem +-- if assignment=="Assets for Pampa" then +-- if asset.category==Group.Category.GROUND and asset.speedmax>0 then +-- warehouse.Batumi:AddRequest(warehouse.Pampa, WAREHOUSE.Descriptor.GROUPNAME, asset.templatename) +-- end +-- end +-- end +-- +-- +-- @field #WAREHOUSE +WAREHOUSE = { + ClassName = "WAREHOUSE", + Debug = false, + Report = true, + warehouse = nil, + alias = nil, + zone = nil, + airbase = nil, + airbasename = nil, + road = nil, + rail = nil, + spawnzone = nil, + wid = nil, + uid = nil, + markerid = nil, + dTstatus = 30, + queueid = 0, + stock = {}, + queue = {}, + pending = {}, + transporting = {}, + delivered = {}, + defending = {}, + portzone = nil, + shippinglanes = {}, + offroadpaths = {}, + autodefence = false, + spawnzonemaxdist = 5000, + autosave = false, + autosavepath = nil, + autosavefile = nil, + saveparking = false, + isunit = false, +} + +--- Item of the warehouse stock table. +-- @type WAREHOUSE.Assetitem +-- @field #number uid Unique id of the asset. +-- @field #string templatename Name of the template group. +-- @field #table template The spawn template of the group. +-- @field DCS#Group.Category category Category of the group. +-- @field #string unittype Type of the first unit of the group as obtained by the Object.getTypeName() DCS API function. +-- @field #number nunits Number of units in the group. +-- @field #number range Range of the unit in meters. +-- @field #number speedmax Maximum speed in km/h the group can do. +-- @field #number size Maximum size in length and with of the asset in meters. +-- @field #number weight The weight of the whole asset group in kilo gramms. +-- @field DCS#Object.Desc DCSdesc All DCS descriptors. +-- @field #WAREHOUSE.Attribute attribute Generalized attribute of the group. +-- @field #table cargobay Array of cargo bays of all units in an asset group. +-- @field #number cargobaytot Total weight in kg that fits in the cargo bay of all asset group units. +-- @field #number cargobaymax Largest cargo bay of all units in the group. +-- @field #number loadradius Distance when cargo is loaded into the carrier. +-- @field DCS#AI.Skill skill Skill of AI unit. +-- @field #string livery Livery of the asset. +-- @field #string assignment Assignment of the asset. This could, e.g., be used in the @{#WAREHOUSE.OnAfterNewAsset) funktion. + +--- Item of the warehouse queue table. +-- @type WAREHOUSE.Queueitem +-- @field #number uid Unique id of the queue item. +-- @field #WAREHOUSE warehouse Requesting warehouse. +-- @field #WAREHOUSE.Descriptor assetdesc Descriptor of the requested asset. Enumerator of type @{#WAREHOUSE.Descriptor}. +-- @field assetdescval Value of the asset descriptor. Type depends on "assetdesc" descriptor. +-- @field #number nasset Number of asset groups requested. +-- @field #WAREHOUSE.TransportType transporttype Transport unit type. +-- @field #number ntransport Max. number of transport units requested. +-- @field #string assignment A keyword or text that later be used to identify this request and postprocess the assets. +-- @field #number prio Priority of the request. Number between 1 (high) and 100 (low). +-- @field Wrapper.Airbase#AIRBASE airbase The airbase beloning to requesting warehouse if any. +-- @field DCS#Airbase.Category category Category of the requesting airbase, i.e. airdrome, helipad/farp or ship. +-- @field #boolean toself Self request, i.e. warehouse requests assets from itself. +-- @field #table assets Table of self propelled (or cargo) and transport assets. Each element of the table is a @{#WAREHOUSE.Assetitem} and can be accessed by their asset ID. +-- @field #table cargoassets Table of cargo (or self propelled) assets. Each element of the table is a @{#WAREHOUSE.Assetitem}. +-- @field #number cargoattribute Attribute of cargo assets of type @{#WAREHOUSE.Attribute}. +-- @field #number cargocategory Category of cargo assets of type @{#WAREHOUSE.Category}. +-- @field #table transportassets Table of transport carrier assets. Each element of the table is a @{#WAREHOUSE.Assetitem}. +-- @field #number transportattribute Attribute of transport assets of type @{#WAREHOUSE.Attribute}. +-- @field #number transportcategory Category of transport assets of type @{#WAREHOUSE.Category}. + +--- Item of the warehouse pending queue table. +-- @type WAREHOUSE.Pendingitem +-- @field #number timestamp Absolute mission time in seconds when the request was processed. +-- @field #table assetproblem Table with assets that might have problems (damage or stuck). +-- @field Core.Set#SET_GROUP cargogroupset Set of cargo groups do be delivered. +-- @field #number ndelivered Number of groups delivered to destination. +-- @field Core.Set#SET_GROUP transportgroupset Set of cargo transport carrier groups. +-- @field Core.Set#SET_CARGO transportcargoset Set of cargo objects. +-- @field #table carriercargo Table holding the cargo groups of each carrier unit. +-- @field #number ntransporthome Number of transports back home. +-- @extends #WAREHOUSE.Queueitem + +--- Descriptors enumerator describing the type of the asset. +-- @type WAREHOUSE.Descriptor +-- @field #string GROUPNAME Name of the asset template. +-- @field #string UNITTYPE Typename of the DCS unit, e.g. "A-10C". +-- @field #string ATTRIBUTE Generalized attribute @{#WAREHOUSE.Attribute}. +-- @field #string CATEGORY Asset category of type DCS#Group.Category, i.e. GROUND, AIRPLANE, HELICOPTER, SHIP, TRAIN. +WAREHOUSE.Descriptor = { + GROUPNAME="templatename", + UNITTYPE="unittype", + ATTRIBUTE="attribute", + CATEGORY="category", +} + +--- Generalized asset attributes. Can be used to request assets with certain general characteristics. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit. +-- @type WAREHOUSE.Attribute +-- @field #string AIR_TRANSPORTPLANE Airplane with transport capability. This can be used to transport other assets. +-- @field #string AIR_AWACS Airborne Early Warning and Control System. +-- @field #string AIR_FIGHTER Fighter, interceptor, ... airplane. +-- @field #string AIR_BOMBER Aircraft which can be used for strategic bombing. +-- @field #string AIR_TANKER Airplane which can refuel other aircraft. +-- @field #string AIR_TRANSPORTHELO Helicopter with transport capability. This can be used to transport other assets. +-- @field #string AIR_ATTACKHELO Attack helicopter. +-- @field #string AIR_UAV Unpiloted Aerial Vehicle, e.g. drones. +-- @field #string AIR_OTHER Any airborne unit that does not fall into any other airborne category. +-- @field #string GROUND_APC Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets. +-- @field #string GROUND_TRUCK Unarmed ground vehicles, which has the DCS "Truck" attribute. +-- @field #string GROUND_INFANTRY Ground infantry assets. +-- @field #string GROUND_ARTILLERY Artillery assets. +-- @field #string GROUND_TANK Tanks (modern or old). +-- @field #string GROUND_TRAIN Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently. +-- @field #string GROUND_EWR Early Warning Radar. +-- @field #string GROUND_AAA Anti-Aircraft Artillery. +-- @field #string GROUND_SAM Surface-to-Air Missile system or components. +-- @field #string GROUND_OTHER Any ground unit that does not fall into any other ground category. +-- @field #string NAVAL_AIRCRAFTCARRIER Aircraft carrier. +-- @field #string NAVAL_WARSHIP War ship, i.e. cruisers, destroyers, firgates and corvettes. +-- @field #string NAVAL_ARMEDSHIP Any armed ship that is not an aircraft carrier, a cruiser, destroyer, firgatte or corvette. +-- @field #string NAVAL_UNARMEDSHIP Any unarmed naval vessel. +-- @field #string NAVAL_OTHER Any naval unit that does not fall into any other naval category. +-- @field #string OTHER_UNKNOWN Anything that does not fall into any other category. +WAREHOUSE.Attribute = { + AIR_TRANSPORTPLANE="Air_TransportPlane", + AIR_AWACS="Air_AWACS", + AIR_FIGHTER="Air_Fighter", + AIR_BOMBER="Air_Bomber", + AIR_TANKER="Air_Tanker", + AIR_TRANSPORTHELO="Air_TransportHelo", + AIR_ATTACKHELO="Air_AttackHelo", + AIR_UAV="Air_UAV", + AIR_OTHER="Air_OtherAir", + GROUND_APC="Ground_APC", + GROUND_TRUCK="Ground_Truck", + GROUND_INFANTRY="Ground_Infantry", + GROUND_ARTILLERY="Ground_Artillery", + GROUND_TANK="Ground_Tank", + GROUND_TRAIN="Ground_Train", + GROUND_EWR="Ground_EWR", + GROUND_AAA="Ground_AAA", + GROUND_SAM="Ground_SAM", + GROUND_OTHER="Ground_OtherGround", + NAVAL_AIRCRAFTCARRIER="Naval_AircraftCarrier", + NAVAL_WARSHIP="Naval_WarShip", + NAVAL_ARMEDSHIP="Naval_ArmedShip", + NAVAL_UNARMEDSHIP="Naval_UnarmedShip", + NAVAL_OTHER="Naval_OtherNaval", + OTHER_UNKNOWN="Other_Unknown", +} + +--- Cargo transport type. Defines how assets are transported to their destination. +-- @type WAREHOUSE.TransportType +-- @field #string AIRPLANE Transports are carried out by airplanes. +-- @field #string HELICOPTER Transports are carried out by helicopters. +-- @field #string APC Transports are conducted by APCs. +-- @field #string SHIP Transports are conducted by ships. Not implemented yet. +-- @field #string TRAIN Transports are conducted by trains. Not implemented yet. Also trains are buggy in DCS. +-- @field #string SELFPROPELLED Assets go to their destination by themselves. No transport carrier needed. +WAREHOUSE.TransportType = { + AIRPLANE = "Air_TransportPlane", + HELICOPTER = "Air_TransportHelo", + APC = "Ground_APC", + TRAIN = "Ground_Train", + SHIP = "Naval_UnarmedShip", + SELFPROPELLED = "Selfpropelled", +} + +--- Warehouse quantity enumerator for selecting number of assets, e.g. all, half etc. of what is in stock rather than an absolute number. +-- @type WAREHOUSE.Quantity +-- @field #string ALL All "all" assets currently in stock. +-- @field #string THREEQUARTERS Three quarters "3/4" of assets in stock. +-- @field #string HALF Half "1/2" of assets in stock. +-- @field #string THIRD One third "1/3" of assets in stock. +-- @field #string QUARTER One quarter "1/4" of assets in stock. +WAREHOUSE.Quantity = { + ALL = "all", + THREEQUARTERS = "3/4", + HALF = "1/2", + THIRD = "1/3", + QUARTER = "1/4", +} + +--- Warehouse database. Note that this is a global array to have easier exchange between warehouses. +-- @type WAREHOUSE.db +-- @field #number AssetID Unique ID of each asset. This is a running number, which is increased each time a new asset is added. +-- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}.# +-- @field #number WarehouseID Unique ID of the warehouse. Running number. +-- @field #table Warehouses Table holding all defined @{#WAREHOUSE} objects by their unique ids. +WAREHOUSE.db = { + AssetID = 0, + Assets = {}, + WarehouseID = 0, + Warehouses = {} +} + +--- Warehouse class version. +-- @field #string version +WAREHOUSE.version="0.6.7" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO: Warehouse todo list. +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add check if assets "on the move" are stationary. Can happen if ground units get stuck in buildings. If stationary auto complete transport by adding assets to request warehouse? Time? +-- TODO: Optimize findpathonroad. Do it only once (first time) and safe paths between warehouses similar to off-road paths. +-- 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: 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. +-- DONE: Test capturing a neutral warehouse. +-- DONE: Add save/load capability of warehouse <==> percistance after mission restart. Difficult in lua! +-- DONE: Get cargo bay and weight from CARGO_GROUP and GROUP. No necessary any more! +-- DONE: Add possibility to set weight and cargo bay manually in AddAsset function as optional parameters. +-- DONE: Check overlapping aircraft sometimes. +-- DONE: Case when all transports are killed and there is still cargo to be delivered. Put cargo back into warehouse. Should be done now! +-- DONE: Add transport units from dispatchers back to warehouse stock once they completed their mission. +-- DONE: Write documentation. +-- DONE: Add AAA, SAMs and UAVs to generalized attributes. +-- DONE: Add warehouse quantity enumerator. +-- DONE: Test mortars. Immobile units need a transport. +-- DONE: Set ROE for spawned groups. +-- DONE: Add offroad lanes between warehouses if road connection is not available. +-- DONE: Add possibility to add active groups. Need to create a pseudo template before destroy. <== Does not seem to be necessary any more. +-- DONE: Add a time stamp when an asset is added to the stock and for requests. +-- DONE: How to get a specific request once the cargo is delivered? Make addrequest addasset non FSM function? Callback for requests like in SPAWN? +-- DONE: Add autoselfdefence switch and user function. Default should be off. +-- DONE: Warehouse re-capturing not working?! +-- DONE: Naval assets dont go back into stock once arrived. +-- DONE: Take cargo weight into consideration, when selecting transport assets. +-- DONE: Add ports for spawning naval assets. +-- DONE: Add shipping lanes between warehouses. +-- DONE: Handle cases with immobile units <== should be handled by dispatcher classes. +-- DONE: Handle cases for aircraft carriers and other ships. Place warehouse on carrier possible? On others probably not - exclude them? +-- DONE: Add general message function for sending to coaliton or debug. +-- DONE: Fine tune event handlers. +-- DONE: Improve generalized attributes. +-- DONE: If warehouse is destroyed, all asssets are gone. +-- DONE: Add event handlers. +-- DONE: Add AI_CARGO_AIRPLANE +-- DONE: Add AI_CARGO_APC +-- DONE: Add AI_CARGO_HELICOPTER +-- DONE: Switch to AI_CARGO_XXX_DISPATCHER +-- DONE: Add queue. +-- DONE: Put active groups into the warehouse, e.g. when they were transported to this warehouse. +-- NOGO: Spawn warehouse assets as uncontrolled or AI off and activate them when requested. +-- DONE: How to handle multiple units in a transport group? <== Cargo dispatchers. +-- DONE: Add phyical object. +-- DONE: If warehosue is captured, change warehouse and assets to other coalition. +-- NOGO: Use RAT for routing air units. Should be possible but might need some modifications of RAT, e.g. explit spawn place. But flight plan should be better. +-- DONE: Can I make a request with specific assets? E.g., once delivered, make a request for exactly those assests that were in the original request. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor(s) +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- The WAREHOUSE constructor. Creates a new WAREHOUSE object from a static object. Parameters like the coalition and country are taken from the static object structure. +-- @param #WAREHOUSE self +-- @param Wrapper.Static#STATIC warehouse The physical structure representing the warehouse. +-- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static +-- @return #WAREHOUSE self +function WAREHOUSE:New(warehouse, alias) + BASE:T({warehouse=warehouse}) + + -- Check if just a string was given and convert to static. + if type(warehouse)=="string" then + warehouse=UNIT:FindByName(warehouse) + if warehouse==nil then + env.info(string.format("No warehouse unit with name %s found trying static.", warehouse)) + warehouse=STATIC:FindByName(warehouse, true) + self.isunit=false + else + self.isunit=true + end + end + + -- Nil check. + if warehouse==nil then + BASE:E("ERROR: Warehouse does not exist!") + return nil + end + + -- Set alias. + self.alias=alias or warehouse:GetName() + + -- Print version. + env.info(string.format("Adding warehouse v%s for structure %s with alias %s", WAREHOUSE.version, warehouse:GetName(), self.alias)) + + -- Inherit everthing from FSM class. + local self = BASE:Inherit(self, FSM:New()) -- #WAREHOUSE + + -- Set some string id for output to DCS.log file. + self.wid=string.format("WAREHOUSE %s | ", self.alias) + + -- Set some variables. + self.warehouse=warehouse + + -- Increase global warehouse counter. + WAREHOUSE.db.WarehouseID=WAREHOUSE.db.WarehouseID+1 + + -- Set unique ID for this warehouse. + self.uid=WAREHOUSE.db.WarehouseID + + -- As Kalbuth found out, this would fail when using SPAWNSTATIC https://forums.eagle.ru/showthread.php?p=3703488#post3703488 + --self.uid=tonumber(warehouse:GetID()) + + -- Closest of the same coalition but within a certain range. + local _airbase=self:GetCoordinate():GetClosestAirbase(nil, self:GetCoalition()) + if _airbase and _airbase:GetCoordinate():Get2DDistance(self:GetCoordinate()) < 3000 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) + + -- Add warehouse to database. + WAREHOUSE.db.Warehouses[self.uid]=self + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("NotReadyYet") + + -- Add FSM transitions. + -- From State --> Event --> To State + self:AddTransition("NotReadyYet", "Load", "Loaded") -- Load the warehouse state from scatch. + self:AddTransition("Stopped", "Load", "Loaded") -- Load the warehouse state stopped state. + self:AddTransition("NotReadyYet", "Start", "Running") -- Start the warehouse from scratch. + self:AddTransition("Loaded", "Start", "Running") -- Start the warehouse when loaded from disk. + self:AddTransition("*", "Status", "*") -- Status update. + self:AddTransition("*", "AddAsset", "*") -- Add asset to warehouse stock. + self:AddTransition("*", "NewAsset", "*") -- New asset was added to warehouse stock. + self:AddTransition("*", "AddRequest", "*") -- New request from other warehouse. + self:AddTransition("Running", "Request", "*") -- Process a request. Only in running mode. + self:AddTransition("Attacked", "Request", "*") -- Process a request. Only in running mode. + self:AddTransition("*", "Unloaded", "*") -- Cargo has been unloaded from the carrier (unused ==> unnecessary?). + self:AddTransition("*", "Arrived", "*") -- Cargo or transport group has arrived. + self:AddTransition("*", "Delivered", "*") -- All cargo groups of a request have been delivered to the requesting warehouse. + self:AddTransition("Running", "SelfRequest", "*") -- Request to warehouse itself. Requested assets are only spawned but not delivered anywhere. + self:AddTransition("Attacked", "SelfRequest", "*") -- Request to warehouse itself. Also possible when warehouse is under attack! + self:AddTransition("Running", "Pause", "Paused") -- Pause the processing of new requests. Still possible to add assets and requests. + self:AddTransition("Paused", "Unpause", "Running") -- Unpause the warehouse. Queued requests are processed again. + self:AddTransition("*", "Stop", "Stopped") -- Stop the warehouse. + self:AddTransition("Stopped", "Restart", "Running") -- Restart the warehouse when it was stopped before. + self:AddTransition("Loaded", "Restart", "Running") -- Restart the warehouse when assets were loaded from file before. + self:AddTransition("*", "Save", "*") -- Save the warehouse state to disk. + self:AddTransition("*", "Attacked", "Attacked") -- Warehouse is under attack by enemy coalition. + self:AddTransition("Attacked", "Defeated", "Running") -- Attack by other coalition was defeated! + self:AddTransition("*", "ChangeCountry", "*") -- Change country (and coalition) of the warehouse. Warehouse is respawned! + self:AddTransition("Attacked", "Captured", "Running") -- Warehouse was captured by another coalition. It must have been attacked first. + self:AddTransition("*", "AirbaseCaptured", "*") -- Airbase was captured by other coalition. + self:AddTransition("*", "AirbaseRecaptured", "*") -- Airbase was re-captured from other coalition. + self:AddTransition("*", "AssetDead", "*") -- An asset group died. + self:AddTransition("*", "Destroyed", "Destroyed") -- Warehouse was destroyed. All assets in stock are gone and warehouse is stopped. + + ------------------------ + --- Pseudo Functions --- + ------------------------ + + --- Triggers the FSM event "Start". Starts the warehouse. Initializes parameters and starts event handlers. + -- @function [parent=#WAREHOUSE] Start + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Start" after a delay. Starts the warehouse. Initializes parameters and starts event handlers. + -- @function [parent=#WAREHOUSE] __Start + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Stop". Stops the warehouse and all its event handlers. All waiting and pending queue items are deleted as well and all assets are removed from stock. + -- @function [parent=#WAREHOUSE] Stop + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Stop" after a delay. Stops the warehouse and all its event handlers. All waiting and pending queue items are deleted as well and all assets are removed from stock. + -- @function [parent=#WAREHOUSE] __Stop + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Restart". Restarts the warehouse from stopped state by reactivating the event handlers *only*. + -- @function [parent=#WAREHOUSE] Restart + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Restart" after a delay. Restarts the warehouse from stopped state by reactivating the event handlers *only*. + -- @function [parent=#WAREHOUSE] __Restart + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Pause". Pauses the warehouse. Assets can still be added and requests be made. However, requests are not processed. + -- @function [parent=#WAREHOUSE] Pause + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Pause" after a delay. Pauses the warehouse. Assets can still be added and requests be made. However, requests are not processed. + -- @function [parent=#WAREHOUSE] __Pause + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + --- Triggers the FSM event "Unpause". Unpauses the warehouse. Processing of queued requests is resumed. + -- @function [parent=#WAREHOUSE] UnPause + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Unpause" after a delay. Unpauses the warehouse. Processing of queued requests is resumed. + -- @function [parent=#WAREHOUSE] __Unpause + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + + --- Triggers the FSM event "Status". Queue is updated and requests are executed. + -- @function [parent=#WAREHOUSE] Status + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Status" after a delay. Queue is updated and requests are executed. + -- @function [parent=#WAREHOUSE] __Status + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + + --- Trigger the FSM event "AddAsset". Add a group to the warehouse stock. + -- @function [parent=#WAREHOUSE] AddAsset + -- @param #WAREHOUSE self + -- @param Wrapper.Group#GROUP group Group to be added as new asset. + -- @param #number ngroups (Optional) Number of groups to add to the warehouse stock. Default is 1. + -- @param #WAREHOUSE.Attribute forceattribute (Optional) Explicitly force a generalized attribute for the asset. This has to be an @{#WAREHOUSE.Attribute}. + -- @param #number forcecargobay (Optional) Explicitly force cargobay weight limit in kg for cargo carriers. This is for each *unit* of the group. + -- @param #number forceweight (Optional) Explicitly force weight in kg of each unit in the group. + -- @param #number loadradius (Optional) The distance in meters when the cargo is loaded into the carrier. Default is the bounding box size of the carrier. + -- @param DCS#AI.Skill skill Skill of the asset. + -- @param #table liveries Table of livery names. When the asset is spawned one livery is chosen randomly. + -- @param #string assignment A free to choose string specifying an assignment for the asset. This can be used with the @{#WAREHOUSE.OnAfterNewAsset} function. + + --- Trigger the FSM event "AddAsset" with a delay. Add a group to the warehouse stock. + -- @function [parent=#WAREHOUSE] __AddAsset + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Group#GROUP group Group to be added as new asset. + -- @param #number ngroups (Optional) Number of groups to add to the warehouse stock. Default is 1. + -- @param #WAREHOUSE.Attribute forceattribute (Optional) Explicitly force a generalized attribute for the asset. This has to be an @{#WAREHOUSE.Attribute}. + -- @param #number forcecargobay (Optional) Explicitly force cargobay weight limit in kg for cargo carriers. This is for each *unit* of the group. + -- @param #number forceweight (Optional) Explicitly force weight in kg of each unit in the group. + -- @param #number loadradius (Optional) The distance in meters when the cargo is loaded into the carrier. Default is the bounding box size of the carrier. + -- @param DCS#AI.Skill skill Skill of the asset. + -- @param #table liveries Table of livery names. When the asset is spawned one livery is chosen randomly. + -- @param #string assignment A free to choose string specifying an assignment for the asset. This can be used with the @{#WAREHOUSE.OnAfterNewAsset} function. + + + --- Triggers the FSM delayed event "NewAsset" when a new asset has been added to the warehouse stock. + -- @function [parent=#WAREHOUSE] NewAsset + -- @param #WAREHOUSE self + -- @param #WAREHOUSE.Assetitem asset The new asset. + -- @param #string assignment (Optional) Assignment text for the asset. + + --- Triggers the FSM delayed event "NewAsset" when a new asset has been added to the warehouse stock. + -- @function [parent=#WAREHOUSE] __NewAsset + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #WAREHOUSE.Assetitem asset The new asset. + -- @param #string assignment (Optional) Assignment text for the asset. + + --- On after "NewAsset" event user function. A new asset has been added to the warehouse stock. + -- @function [parent=#WAREHOUSE] OnAfterNewAsset + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Assetitem asset The asset that has just been added. + -- @param #string assignment (Optional) Assignment text for the asset. + + + --- Triggers the FSM event "AddRequest". Add a request to the warehouse queue, which is processed when possible. + -- @function [parent=#WAREHOUSE] AddRequest + -- @param #WAREHOUSE self + -- @param #WAREHOUSE warehouse The warehouse requesting supply. + -- @param #WAREHOUSE.Descriptor AssetDescriptor Descriptor describing the asset that is requested. + -- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc. + -- @param #number nAsset Number of groups requested that match the asset specification. + -- @param #WAREHOUSE.TransportType TransportType Type of transport. + -- @param #number nTransport Number of transport units requested. + -- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low. + -- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets. + + --- Triggers the FSM event "AddRequest" with a delay. Add a request to the warehouse queue, which is processed when possible. + -- @function [parent=#WAREHOUSE] __AddRequest + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #WAREHOUSE warehouse The warehouse requesting supply. + -- @param #WAREHOUSE.Descriptor AssetDescriptor Descriptor describing the asset that is requested. + -- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc. + -- @param #number nAsset Number of groups requested that match the asset specification. + -- @param #WAREHOUSE.TransportType TransportType Type of transport. + -- @param #number nTransport Number of transport units requested. + -- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low. + -- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets. + + + --- Triggers the FSM event "Request". Executes a request from the queue if possible. + -- @function [parent=#WAREHOUSE] Request + -- @param #WAREHOUSE self + -- @param #WAREHOUSE.Queueitem Request Information table of the request. + + --- Triggers the FSM event "Request" after a delay. Executes a request from the queue if possible. + -- @function [parent=#WAREHOUSE] __Request + -- @param #WAREHOUSE self + -- @param #number Delay Delay in seconds. + -- @param #WAREHOUSE.Queueitem Request Information table of the request. + + + --- Triggers the FSM event "Arrived" when a group has arrived at the destination warehouse. + -- This function should always be called from the sending and not the receiving warehouse. + -- If the group is a cargo asset, it is added to the receiving warehouse. If the group is a transporter it + -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once + -- all cargo was delivered. + -- @function [parent=#WAREHOUSE] Arrived + -- @param #WAREHOUSE self + -- @param Wrapper.Group#GROUP group Group that has arrived. + + --- Triggers the FSM event "Arrived" after a delay when a group has arrived at the destination. + -- This function should always be called from the sending and not the receiving warehouse. + -- If the group is a cargo asset, it is added to the receiving warehouse. If the group is a transporter it + -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once + -- @function [parent=#WAREHOUSE] __Arrived + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param Wrapper.Group#GROUP group Group that has arrived. + + --- On after "Arrived" event user function. Called when a group has arrived at its destination. + -- @function [parent=#WAREHOUSE] OnAfterArrived + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#GROUP group Group that has arrived. + + + --- Triggers the FSM event "Delivered". All (cargo) assets of a request have been delivered to the receiving warehouse. + -- @function [parent=#WAREHOUSE] Delivered + -- @param #WAREHOUSE self + -- @param #WAREHOUSE.Pendingitem request Pending request that was now delivered. + + --- Triggers the FSM event "Delivered" after a delay. A group has been delivered from the warehouse to another warehouse. + -- @function [parent=#WAREHOUSE] __Delivered + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #WAREHOUSE.Pendingitem request Pending request that was now delivered. + + --- On after "Delivered" event user function. Called when a group has been delivered from the warehouse to another warehouse. + -- @function [parent=#WAREHOUSE] OnAfterDelivered + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Pendingitem request Pending request that was now delivered. + + + --- Triggers the FSM event "SelfRequest". Request was initiated from the warehouse to itself. Groups are just spawned at the warehouse or the associated airbase. + -- If the warehouse is currently under attack when the self request is made, the self request is added to the defending table. One the attack is defeated, + -- this request is used to put the groups back into the warehouse stock. + -- @function [parent=#WAREHOUSE] SelfRequest + -- @param #WAREHOUSE self + -- @param Core.Set#SET_GROUP groupset The set of cargo groups that was delivered to the warehouse itself. + -- @param #WAREHOUSE.Pendingitem request Pending self request. + + --- Triggers the FSM event "SelfRequest" with a delay. Request was initiated from the warehouse to itself. Groups are just spawned at the warehouse or the associated airbase. + -- If the warehouse is currently under attack when the self request is made, the self request is added to the defending table. One the attack is defeated, + -- this request is used to put the groups back into the warehouse stock. + -- @function [parent=#WAREHOUSE] __SelfRequest + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param Core.Set#SET_GROUP groupset The set of cargo groups that was delivered to the warehouse itself. + -- @param #WAREHOUSE.Pendingitem request Pending self request. + + --- On after "SelfRequest" event. Request was initiated from the warehouse to itself. Groups are simply spawned at the warehouse or the associated airbase. + -- All requested assets are passed as a @{Core.Set#SET_GROUP} and can be used for further tasks or in other MOOSE classes. + -- Note that airborne assets are spawned in uncontrolled state so they do not simply "fly away" after spawning. + -- + -- @usage + -- --- Self request event. Triggered once the assets are spawned in the spawn zone or at the airbase. + -- function mywarehouse:OnAfterSelfRequest(From, Event, To, groupset, request) + -- local groupset=groupset --Core.Set#SET_GROUP + -- + -- -- Loop over all groups spawned from that request. + -- for _,group in pairs(groupset:GetSetObjects()) do + -- local group=group --Wrapper.Group#GROUP + -- + -- -- Gree smoke on spawned group. + -- group:SmokeGreen() + -- + -- -- Activate uncontrolled airborne group if necessary. + -- group:StartUncontrolled() + -- end + -- end + -- + -- @function [parent=#WAREHOUSE] OnAfterSelfRequest + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Core.Set#SET_GROUP groupset The set of (cargo) groups that was delivered to the warehouse itself. + -- @param #WAREHOUSE.Pendingitem request Pending self request. + + + --- Triggers the FSM event "Attacked" when a warehouse is under attack by an another coalition. + -- @function [parent=#WAREHOUSE] Attacked + -- @param #WAREHOUSE self + -- @param DCS#coalition.side Coalition Coalition side which is attacking the warehouse, i.e. a number of @{DCS#coalition.side} enumerator. + -- @param DCS#country.id Country Country ID, which is attacking the warehouse, i.e. a number @{DCS#country.id} enumerator. + + --- Triggers the FSM event "Attacked" with a delay when a warehouse is under attack by an another coalition. + -- @function [parent=#WAREHOUSE] __Attacked + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param DCS#coalition.side Coalition Coalition side which is attacking the warehouse, i.e. a number of @{DCS#coalition.side} enumerator. + -- @param DCS#country.id Country Country ID, which is attacking the warehouse, i.e. a number @{DCS#country.id} enumerator. + + --- On after "Attacked" event user function. Called when a warehouse (zone) is under attack by an enemy. + -- @function [parent=#WAREHOUSE] OnAfterAttacked + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param DCS#coalition.side Coalition Coalition side which is attacking the warehouse, i.e. a number of @{DCS#coalition.side} enumerator. + -- @param DCS#country.id Country Country ID, which is attacking the warehouse, i.e. a number @{DCS#country.id} enumerator. + + + --- Triggers the FSM event "Defeated" when an attack from an enemy was defeated. + -- @function [parent=#WAREHOUSE] Defeated + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Defeated" with a delay when an attack from an enemy was defeated. + -- @function [parent=#WAREHOUSE] __Defeated + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + --- On after "Defeated" event user function. Called when an enemy attack was defeated. + -- @function [parent=#WAREHOUSE] OnAfterDefeate + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "ChangeCountry" so the warehouse is respawned with the new country. + -- @function [parent=#WAREHOUSE] ChangeCountry + -- @param #WAREHOUSE self + -- @param DCS#country.id Country New country id of the warehouse. + + --- Triggers the FSM event "ChangeCountry" after a delay so the warehouse is respawned with the new country. + -- @function [parent=#WAREHOUSE] __ChangeCountry + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param DCS#country.id Country Country id which has captured the warehouse. + + --- On after "ChangeCountry" event user function. Called when the warehouse has changed its country. + -- @function [parent=#WAREHOUSE] OnAfterChangeCountry + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param DCS#country.id Country New country id of the warehouse, i.e. a number @{DCS#country.id} enumerator. + + + --- Triggers the FSM event "Captured" when a warehouse has been captured by another coalition. + -- @function [parent=#WAREHOUSE] Captured + -- @param #WAREHOUSE self + -- @param DCS#coalition.side Coalition Coalition side which captured the warehouse. + -- @param DCS#country.id Country Country id which has captured the warehouse. + + --- Triggers the FSM event "Captured" with a delay when a warehouse has been captured by another coalition. + -- @function [parent=#WAREHOUSE] __Captured + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param DCS#coalition.side Coalition Coalition side which captured the warehouse. + -- @param DCS#country.id Country Country id which has captured the warehouse. + + --- On after "Captured" event user function. Called when the warehouse has been captured by an enemy coalition. + -- @function [parent=#WAREHOUSE] OnAfterCaptured + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param DCS#coalition.side Coalition Coalition side which captured the warehouse, i.e. a number of @{DCS#coalition.side} enumerator. + -- @param DCS#country.id Country Country id which has captured the warehouse, i.e. a number @{DCS#country.id} enumerator. + -- + + --- Triggers the FSM event "AirbaseCaptured" when the airbase of the warehouse has been captured by another coalition. + -- @function [parent=#WAREHOUSE] AirbaseCaptured + -- @param #WAREHOUSE self + -- @param DCS#coalition.side Coalition Coalition side which captured the airbase, i.e. a number of @{DCS#coalition.side} enumerator. + + --- Triggers the FSM event "AirbaseCaptured" with a delay when the airbase of the warehouse has been captured by another coalition. + -- @function [parent=#WAREHOUSE] __AirbaseCaptured + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param DCS#coalition.side Coalition Coalition side which captured the airbase, i.e. a number of @{DCS#coalition.side} enumerator. + + --- On after "AirbaseCaptured" even user function. Called when the airbase of the warehouse has been captured by another coalition. + -- @function [parent=#WAREHOUSE] OnAfterAirbaseCaptured + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param DCS#coalition.side Coalition Coalition side which captured the airbase, i.e. a number of @{DCS#coalition.side} enumerator. + + + --- Triggers the FSM event "AirbaseRecaptured" when the airbase of the warehouse has been re-captured from the other coalition. + -- @param #WAREHOUSE self + -- @function [parent=#WAREHOUSE] AirbaseRecaptured + -- @param DCS#coalition.side Coalition Coalition which re-captured the airbase, i.e. the same as the current warehouse owner coalition. + + --- Triggers the FSM event "AirbaseRecaptured" with a delay when the airbase of the warehouse has been re-captured from the other coalition. + -- @function [parent=#WAREHOUSE] __AirbaseRecaptured + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param DCS#coalition.side Coalition Coalition which re-captured the airbase, i.e. the same as the current warehouse owner coalition. + + --- On after "AirbaseRecaptured" event user function. Called when the airbase of the warehouse has been re-captured from the other coalition. + -- @function [parent=#WAREHOUSE] OnAfterAirbaseRecaptured + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param DCS#coalition.side Coalition Coalition which re-captured the airbase, i.e. the same as the current warehouse owner coalition. + + + --- Triggers the FSM event "AssetDead" when an asset group has died. + -- @function [parent=#WAREHOUSE] AssetDead + -- @param #WAREHOUSE self + -- @param #WAREHOUSE.Assetitem asset The asset that is dead. + -- @param #WAREHOUSE.Pendingitem request The request of the dead asset. + + --- Triggers the delayed FSM event "AssetDead" when an asset group has died. + -- @function [parent=#WAREHOUSE] __AssetDead + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #WAREHOUSE.Assetitem asset The asset that is dead. + -- @param #WAREHOUSE.Pendingitem request The request of the dead asset. + + --- On after "AssetDead" event user function. Called when an asset group died. + -- @function [parent=#WAREHOUSE] OnAfterAssetDead + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #WAREHOUSE.Assetitem asset The asset that is dead. + -- @param #WAREHOUSE.Pendingitem request The request of the dead asset. + + + --- Triggers the FSM event "Destroyed" when the warehouse was destroyed. Services are stopped. + -- @function [parent=#WAREHOUSE] Destroyed + -- @param #WAREHOUSE self + + --- Triggers the FSM event "Destroyed" with a delay when the warehouse was destroyed. Services are stopped. + -- @function [parent=#WAREHOUSE] __Destroyed + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + + --- On after "Destroyed" event user function. Called when the warehouse was destroyed. Services are stopped. + -- @function [parent=#WAREHOUSE] OnAfterDestroyed + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + + + --- Triggers the FSM event "Save" when the warehouse assets are saved to file on disk. + -- @function [parent=#WAREHOUSE] Save + -- @param #WAREHOUSE self + -- @param #string path Path where the file is saved. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. + + --- Triggers the FSM event "Save" with a delay when the warehouse assets are saved to a file. + -- @function [parent=#WAREHOUSE] __Save + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #string path Path where the file is saved. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. + + --- On after "Save" event user function. Called when the warehouse assets are saved to disk. + -- @function [parent=#WAREHOUSE] OnAfterSave + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path Path where the file is saved. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. + + + --- Triggers the FSM event "Load" when the warehouse is loaded from a file on disk. + -- @function [parent=#WAREHOUSE] Load + -- @param #WAREHOUSE self + -- @param #string path Path where the file is located. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. + + --- Triggers the FSM event "Load" with a delay when the warehouse assets are loaded from disk. + -- @function [parent=#WAREHOUSE] __Load + -- @param #WAREHOUSE self + -- @param #number delay Delay in seconds. + -- @param #string path Path where the file is located. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. + + --- On after "Load" event user function. Called when the warehouse assets are loaded from disk. + -- @function [parent=#WAREHOUSE] OnAfterLoad + -- @param #WAREHOUSE self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param #string path Path where the file is located. Default is the DCS installation root directory. + -- @param #string filename (Optional) File name. Default is WAREHOUSE-_.txt. + + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set debug mode on. Error messages will be displayed on screen, units will be smoked at some events. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetDebugOn() + self.Debug=true + return self +end + +--- Set debug mode off. This is the default +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetDebugOff() + self.Debug=false + return self +end + +--- Set report on. Messages at events will be displayed on screen to the coalition owning the warehouse. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetReportOn() + self.Report=true + return self +end + +--- Set report off. Warehouse does not report about its status and at certain events. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetReportOff() + self.Report=false + return self +end + +--- Enable safe parking option, i.e. parking spots at an airbase will be considered as occupied when a client aircraft is parked there (even if the client slot is not taken by a player yet). +-- Note that also incoming aircraft can reserve/occupie parking spaces. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetSafeParkingOn() + self.safeparking=true + return self +end + +--- Disable safe parking option. Note that is the default setting. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetSafeParkingOff() + self.safeparking=false + return self +end + + +--- Set interval of status updates. Note that normally only one request can be processed per time interval. +-- @param #WAREHOUSE self +-- @param #number timeinterval Time interval in seconds. +-- @return #WAREHOUSE self +function WAREHOUSE:SetStatusUpdate(timeinterval) + self.dTstatus=timeinterval + return self +end + +--- Set a zone where the (ground) assets of the warehouse are spawned once requested. +-- @param #WAREHOUSE self +-- @param Core.Zone#ZONE zone The spawn zone. +-- @param #number maxdist (Optional) Maximum distance in meters between spawn zone and warehouse. Units are not spawned if distance is larger. Default is 5000 m. +-- @return #WAREHOUSE self +function WAREHOUSE:SetSpawnZone(zone, maxdist) + self.spawnzone=zone + self.spawnzonemaxdist=maxdist or 5000 + return self +end + + +--- Set a warehouse zone. If this zone is captured, the warehouse and all its assets fall into the hands of the enemy. +-- @param #WAREHOUSE self +-- @param Core.Zone#ZONE zone The warehouse zone. Note that this **cannot** be a polygon zone! +-- @return #WAREHOUSE self +function WAREHOUSE:SetWarehouseZone(zone) + self.zone=zone + return self +end + +--- Set auto defence on. When the warehouse is under attack, all ground assets are spawned automatically and will defend the warehouse zone. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetAutoDefenceOn() + self.autodefence=true + return self +end + +--- Set auto defence off. This is the default. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE self +function WAREHOUSE:SetAutoDefenceOff() + self.autodefence=false + return self +end + +--- Set auto defence off. This is the default. +-- @param #WAREHOUSE self +-- @param #string path Path where to save the asset data file. +-- @param #string filename File name. Default is generated automatically from warehouse id. +-- @return #WAREHOUSE self +function WAREHOUSE:SetSaveOnMissionEnd(path, filename) + self.autosave=true + self.autosavepath=path + self.autosavefile=filename + return self +end + + +--- Set the airbase belonging to this warehouse. +-- Note that it has to be of the same coalition as the warehouse. +-- Also, be reasonable and do not put it too far from the phyiscal warehouse structure because you troops might have a long way to get to their transports. +-- @param #WAREHOUSE self +-- @param Wrapper.Airbase#AIRBASE airbase The airbase object associated to this warehouse. +-- @return #WAREHOUSE self +function WAREHOUSE:SetAirbase(airbase) + self.airbase=airbase + if airbase~=nil then + self.airbasename=airbase:GetName() + else + self.airbasename=nil + end + return self +end + +--- Set the connection of the warehouse to the road. +-- Ground assets spawned in the warehouse spawn zone will first go to this point and from there travel on road to the requesting warehouse. +-- Note that by default the road connection is set to the closest point on road from the center of the spawn zone if it is withing 3000 meters. +-- Also note, that if the parameter "coordinate" is passed as nil, any road connection is disabled and ground assets cannot travel of be transportet on the ground. +-- @param #WAREHOUSE self +-- @param Core.Point#COORDINATE coordinate The road connection. Technically, the closest point on road from this coordinate is determined by DCS API function. So this point must not be exactly on the road. +-- @return #WAREHOUSE self +function WAREHOUSE:SetRoadConnection(coordinate) + if coordinate then + self.road=coordinate:GetClosestPointToRoad() + else + self.road=false + end + return self +end + +--- Set the connection of the warehouse to the railroad. +-- This is the place where train assets or transports will be spawned. +-- @param #WAREHOUSE self +-- @param Core.Point#COORDINATE coordinate The railroad connection. Technically, the closest point on rails from this coordinate is determined by DCS API function. So this point must not be exactly on the a railroad connection. +-- @return #WAREHOUSE self +function WAREHOUSE:SetRailConnection(coordinate) + if coordinate then + self.rail=coordinate:GetClosestPointToRoad(true) + else + self.rail=false + end + return self +end + +--- Set the port zone for this warehouse. +-- The port zone is the zone, where all naval assets of the warehouse are spawned. +-- @param #WAREHOUSE self +-- @param Core.Zone#ZONE zone The zone defining the naval port of the warehouse. +-- @return #WAREHOUSE self +function WAREHOUSE:SetPortZone(zone) + self.portzone=zone + return self +end + +--- Add a shipping lane from this warehouse to another remote warehouse. +-- Note that both warehouses must have a port zone defined before a shipping lane can be added! +-- Shipping lane is taken from the waypoints of a (late activated) template group. So set up a group, e.g. a ship or a helicopter, and place its +-- waypoints along the shipping lane you want to add. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE remotewarehouse The remote warehouse to where the shipping lane is added +-- @param Wrapper.Group#GROUP group Waypoints of this group will define the shipping lane between to warehouses. +-- @param #boolean oneway (Optional) If true, the lane can only be used from this warehouse to the other but not other way around. Default false. +-- @return #WAREHOUSE self +function WAREHOUSE:AddShippingLane(remotewarehouse, group, oneway) + + -- Check that port zones are defined. + if self.portzone==nil or remotewarehouse.portzone==nil then + local text=string.format("ERROR: Sending or receiving warehouse does not have a port zone defined. Adding shipping lane not possible!") + self:_ErrorMessage(text, 5) + return self + end + + -- Initial and final coordinates are random points within the port zones. + local startcoord=self.portzone:GetRandomCoordinate() + local finalcoord=remotewarehouse.portzone:GetRandomCoordinate() + + -- Create new lane from waypoints of the template group. + local lane=self:_NewLane(group, startcoord, finalcoord) + + -- Debug info. Marks along shipping lane. + if self.Debug then + for i=1,#lane do + local coord=lane[i] --Core.Point#COORDINATE + local text=string.format("Shipping lane %s to %s. Point %d.", self.alias, remotewarehouse.alias, i) + coord:MarkToCoalition(text, self:GetCoalition()) + end + end + + -- Name of the remote warehouse. + local remotename=remotewarehouse.warehouse:GetName() + + -- Create new table if no shipping lane exists yet. + if self.shippinglanes[remotename]==nil then + self.shippinglanes[remotename]={} + end + + -- Add shipping lane. + table.insert(self.shippinglanes[remotename], lane) + + -- Add shipping lane in the opposite direction. + if not oneway then + remotewarehouse:AddShippingLane(self, group, true) + end + + return self +end + + +--- Add an off-road path from this warehouse to another and back. +-- The start and end points are automatically set to one random point in the respective spawn zones of the two warehouses. +-- By default, the reverse path is also added as path from the remote warehouse to this warehouse. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE remotewarehouse The remote warehouse to which the path leads. +-- @param Wrapper.Group#GROUP group Waypoints of this group will define the path between to warehouses. +-- @param #boolean oneway (Optional) If true, the path can only be used from this warehouse to the other but not other way around. Default false. +-- @return #WAREHOUSE self +function WAREHOUSE:AddOffRoadPath(remotewarehouse, group, oneway) + + -- Initial and final points are random points within the spawn zone. + local startcoord=self.spawnzone:GetRandomCoordinate() + local finalcoord=remotewarehouse.spawnzone:GetRandomCoordinate() + + -- Create new path from template group waypoints. + local path=self:_NewLane(group, startcoord, finalcoord) + + if path==nil then + self:E(self.wid.."ERROR: Offroad path could not be added. Group present in ME?") + return + end + + -- Debug info. Marks along path. + if path and self.Debug then + for i=1,#path do + local coord=path[i] --Core.Point#COORDINATE + local text=string.format("Off road path from %s to %s. Point %d.", self.alias, remotewarehouse.alias, i) + coord:MarkToCoalition(text, self:GetCoalition()) + end + end + + -- Name of the remote warehouse. + local remotename=remotewarehouse.warehouse:GetName() + + -- Create new table if no shipping lane exists yet. + if self.offroadpaths[remotename]==nil then + self.offroadpaths[remotename]={} + end + + -- Add off road path. + table.insert(self.offroadpaths[remotename], path) + + -- Add off road path in the opposite direction (if not forbidden). + if not oneway then + remotewarehouse:AddOffRoadPath(self, group, true) + end + + return self +end + +--- Create a new path from a template group. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group Group used for extracting the waypoints. +-- @param Core.Point#COORDINATE startcoord First coordinate. +-- @param Core.Point#COORDINATE finalcoord Final coordinate. +-- @return #table Table with route points. +function WAREHOUSE:_NewLane(group, startcoord, finalcoord) + + local lane=nil + + if group then + + -- Get route from template. + local lanepoints=group:GetTemplateRoutePoints() + + -- First and last waypoints + local laneF=lanepoints[1] + local laneL=lanepoints[#lanepoints] + + -- Get corresponding coordinates. + local coordF=COORDINATE:New(laneF.x, 0, laneF.y) + local coordL=COORDINATE:New(laneL.x, 0, laneL.y) + + -- Figure out which point is closer to the port of this warehouse. + local distF=startcoord:Get2DDistance(coordF) + local distL=startcoord:Get2DDistance(coordL) + + -- Add the lane. Need to take care of the wrong "direction". + lane={} + if distF0 then + + -- Check if coalition is right. + local samecoalition=anycoalition or Coalition==warehouse:GetCoalition() + + -- Check that warehouse is in service. + if samecoalition and not (warehouse:IsNotReadyYet() or warehouse:IsStopped() or warehouse:IsDestroyed()) then + + -- Get number of assets. Whole stock is returned if no descriptor/value is given. + local nassets=warehouse:GetNumberOfAssets(Descriptor, DescriptorValue) + + --env.info(string.format(" FF warehouse %s nassets = %d for %s=%s", warehouse.alias, nassets, tostring(Descriptor), tostring(DescriptorValue))) + + -- Assume we have enough. + local enough=true + -- If specifc assets need to be present... + if Descriptor and DescriptorValue then + -- Check that enough assets (default 1) are available. + enough = nassets>=MinAssets + end + + -- Check distance. + if enough and (distmin==nil or dist0 then + text=text..string.format("\n- %d of %d transports returned home. Casualties %d.", ntransporthome, ntransporttot, ntransportdead) + end + self:_InfoMessage(text, 20) + + -- Mark request for deletion. + table.insert(done, request) + + else + ----------------------------------- + -- No cargo but still transports -- + ----------------------------------- + + -- This is difficult! How do I know if transports were unused? They could also be just on their way back home. + -- ==> Need to do a lot of checks. + + -- All transports are dead but there is still cargo left ==> Put cargo back into stock. + for _,_group in pairs(request.transportgroupset:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + + -- Check if group is alive. + if group and group:IsAlive() then + + -- Check if group is in the spawn zone? + local category=group:GetCategory() + + -- Get current speed. + local speed=group:GetVelocityKMH() + local notmoving=speed<1 + + -- Closest airbase. + local airbase=group:GetCoordinate():GetClosestAirbase():GetName() + local athomebase=self.airbase and self.airbase:GetName()==airbase + + -- On ground + local onground=not group:InAir() + + -- In spawn zone. + local inspawnzone=group:IsPartlyOrCompletelyInZone(self.spawnzone) + + -- Check conditions for being back home. + local ishome=false + if category==Group.Category.GROUND or category==Group.Category.HELICOPTER then + -- Units go back to the spawn zone, helicopters land and they should not move any more. + ishome=inspawnzone and onground and notmoving + elseif category==Group.Category.AIRPLANE then + -- Planes need to be on ground at their home airbase and should not move any more. + ishome=athomebase and onground and notmoving + end + + -- Debug text. + local text=string.format("Group %s: speed=%d km/h, onground=%s , airbase=%s, spawnzone=%s ==> ishome=%s", group:GetName(), speed, tostring(onground), airbase, tostring(inspawnzone), tostring(ishome)) + self:T(self.wid..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) + + -- Debug smoke. + if self.Debug then + group:SmokeRed() + end + + -- Group arrived. + self:Arrived(group) + end + end + end + + end + + else + + if ntransport==0 and request.ntransport>0 then + ----------------------------------- + -- Still cargo but no transports -- + ----------------------------------- + + local ncargoalive=0 + + -- All transports are dead but there is still cargo left ==> Put cargo back into stock. + for _,_group in pairs(request.cargogroupset:GetSetObjects()) do + --local group=group --Wrapper.Group#GROUP + + -- These groups have been respawned as cargo, i.e. their name changed! + local groupname=_group:GetName() + local group=GROUP:FindByName(groupname.."#CARGO") + + -- Check if group is alive. + if group and group:IsAlive() then + + -- Check if group is in spawn zone? + if group:IsPartlyOrCompletelyInZone(self.spawnzone) then + -- Debug smoke. + if self.Debug then + group:SmokeBlue() + end + -- Add asset group back to stock. + self:AddAsset(group) + ncargoalive=ncargoalive+1 + end + end + + end + + -- Info message. + self:_InfoMessage(string.format("Warehouse %s: All transports of request id=%s dead! Putting remaining %s cargo assets back into warehouse!", self.alias, request.uid, ncargoalive)) + end + + end + + end -- loop over requests + + -- Remove pending requests if done. + for _,request in pairs(done) do + self:_DeleteQueueItem(request, self.pending) + end +end + +--- Function that checks if an asset group is still okay. +-- @param #WAREHOUSE self +function WAREHOUSE:_CheckAssetStatus() + + -- Check if a unit of the group has problems. + local function _CheckGroup(_request, _group) + local request=_request --#WAREHOUSE.Pendingitem + local group=_group --Wrapper.Group#GROUP + + if group and group:IsAlive() then + + -- Category of group. + local category=group:GetCategory() + + for _,_unit in pairs(group:GetUnits()) do + local unit=_unit --Wrapper.Unit#UNIT + + if unit and unit:IsAlive() then + local unitid=unit:GetID() + local life9=unit:GetLife() + local life0=unit:GetLife0() + local life=life9/life0*100 + local speed=unit:GetVelocityMPS() + local onground=unit:InAir() + + local problem=false + if life<10 then + self:T(string.format("Unit %s is heavily damaged!", unit:GetName())) + end + if speed<1 and unit:GetSpeedMax()>1 and onground then + self:T(string.format("Unit %s is not moving!", unit:GetName())) + problem=true + end + + if problem then + if request.assetproblem[unitid] then + local deltaT=timer.getAbsTime()-request.assetproblem[unitid] + if deltaT>300 then + --Todo: which event to generate? Removeunit or Dead/Creash or both? + unit:Destroy() + end + else + request.assetproblem[unitid]=timer.getAbsTime() + end + end + end + + end + end + end + + + for _,request in pairs(self.pending) do + local request=request --#WAREHOUSE.Pendingitem + + -- Cargo groups. + if request.cargogroupset then + for _,_group in pairs(request.cargogroupset:GetSet()) do + local group=_group --Wrapper.Group#GROUP + + _CheckGroup(request, group) + + end + end + + -- Transport groups. + if request.transportgroupset then + for _,group in pairs(request.transportgroupset:GetSet()) do + + _CheckGroup(request, group) + end + end + + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after "AddAsset" event. Add a group to the warehouse stock. If the group is alive, it is destroyed. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group Group or template group to be added to the warehouse stock. +-- @param #number ngroups Number of groups to add to the warehouse stock. Default is 1. +-- @param #WAREHOUSE.Attribute forceattribute (Optional) Explicitly force a generalized attribute for the asset. This has to be an @{#WAREHOUSE.Attribute}. +-- @param #number forcecargobay (Optional) Explicitly force cargobay weight limit in kg for cargo carriers. This is for each *unit* of the group. +-- @param #number forceweight (Optional) Explicitly force weight in kg of each unit in the group. +-- @param #number loadradius (Optional) Radius in meters when the cargo is loaded into the carrier. +-- @param DCS#AI.Skill skill Skill of the asset. +-- @param #table liveries Table of livery names. When the asset is spawned one livery is chosen randomly. +-- @param #string assignment A free to choose string specifying an assignment for the asset. This can be used with the @{#WAREHOUSE.OnAfterNewAsset} function. +function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, skill, liveries, assignment) + 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))) + 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))) + else + self:T(warehouse.wid..string.format("WARNING: Group %s is neither cargo nor transport!", group:GetName())) + end + + -- If no assignment was given we take the assignment of the request if there is any. + if assignment==nil and request.assignment~=nil then + assignment=request.assignment + end + + end + end + + -- Get the asset from the global DB. + local asset=self:FindAssetInDB(group) + + -- 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 + self:_DebugMessage(string.format("Warehouse %s: Adding KNOWN asset uid=%d with attribute=%s to stock.", self.alias, asset.uid, asset.attribute), 5) + table.insert(self.stock, asset) + self:NewAsset(asset, assignment or "") + else + self:_ErrorMessage(string.format("ERROR: Known asset could not be found in global warehouse db!"), 0) + end + + else + ------------------------- + -- This is a NEW asset -- + ------------------------- + + -- Debug info. + self:_DebugMessage(string.format("Warehouse %s: Adding %d NEW assets of group %s to stock.", self.alias, n, tostring(group:GetName())), 5) + + -- This is a group that is not in the db yet. Add it n times. + local assets=self:_RegisterAsset(group, n, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill) + + -- Add created assets to stock of this warehouse. + for _,asset in pairs(assets) do + table.insert(self.stock, asset) + self:NewAsset(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) + -- 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({unknowngroup=group}) + end + + -- Update status. + --self:__Status(-1) +end + +--- Register new asset in globase warehouse data base. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group that will be added to the warehouse stock. +-- @param #number ngroups Number of groups to be added. +-- @param #string forceattribute Forced generalized attribute. +-- @param #number forcecargobay Cargo bay weight limit in kg. +-- @param #number forceweight Weight of units in kg. +-- @param #number loadradius Radius in meters when cargo is loaded into the carrier. +-- @param #table liveries Table of liveries. +-- @param DCS#AI.Skill skill Skill of AI. +-- @return #table A table containing all registered assets. +function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill) + self:F({groupname=group:GetName(), ngroups=ngroups, forceattribute=forceattribute, forcecargobay=forcecargobay, forceweight=forceweight}) + + -- Set default. + local n=ngroups or 1 + + -- Get the size of an object. + local function _GetObjectSize(DCSdesc) + if DCSdesc.box then + local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x) --length + local y=DCSdesc.box.max.y+math.abs(DCSdesc.box.min.y) --height + local z=DCSdesc.box.max.z+math.abs(DCSdesc.box.min.z) --width + return math.max(x,z), x , y, z + end + return 0,0,0,0 + end + + -- Get name of template group. + local templategroupname=group:GetName() + + local Descriptors=group:GetUnit(1):GetDesc() + local Category=group:GetCategory() + local TypeName=group:GetTypeName() + local SpeedMax=group:GetSpeedMax() + local RangeMin=group:GetRange() + local smax,sx,sy,sz=_GetObjectSize(Descriptors) + + -- Get weight and cargo bay size in kg. + local weight=0 + local cargobay={} + local cargobaytot=0 + local cargobaymax=0 + for _i,_unit in pairs(group:GetUnits()) do + local unit=_unit --Wrapper.Unit#UNIT + local Desc=unit:GetDesc() + + -- Weight. We sum up all units in the group. + local unitweight=forceweight or Desc.massEmpty + if unitweight then + weight=weight+unitweight + end + + local cargomax=0 + local massfuel=Desc.fuelMassMax or 0 + local massempty=Desc.massEmpty or 0 + local massmax=Desc.massMax or 0 + + -- Calcuate cargo bay limit value. + cargomax=massmax-massfuel-massempty + self:T3(self.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)) + + -- Cargo bay size. + local bay=forcecargobay or unit:GetCargoBayFreeWeight() + + -- Add bay size to table. + table.insert(cargobay, bay) + + -- Sum up total bay size. + cargobaytot=cargobaytot+bay + + -- Get max bay size. + if bay>cargobaymax then + cargobaymax=bay + end + end + + -- Set/get the generalized attribute. + local attribute=forceattribute or self:_GetAttribute(group) + + -- Table for returned assets. + local assets={} + + -- Add this n times to the table. + for i=1,n do + local asset={} --#WAREHOUSE.Assetitem + + -- Increase asset unique id counter. + WAREHOUSE.db.AssetID=WAREHOUSE.db.AssetID+1 + + -- Set parameters. + asset.uid=WAREHOUSE.db.AssetID + asset.templatename=templategroupname + asset.template=UTILS.DeepCopy(_DATABASE.Templates.Groups[templategroupname].Template) + asset.category=Category + asset.unittype=TypeName + asset.nunits=#asset.template.units + asset.range=RangeMin + asset.speedmax=SpeedMax + asset.size=smax + asset.weight=weight + asset.DCSdesc=Descriptors + asset.attribute=attribute + asset.cargobay=cargobay + asset.cargobaytot=cargobaytot + asset.cargobaymax=cargobaymax + asset.loadradius=loadradius + if liveries then + asset.livery=liveries[math.random(#liveries)] + end + asset.skill=skill + + if i==1 then + self:_AssetItemInfo(asset) + end + + -- Add asset to global db. + WAREHOUSE.db.Assets[asset.uid]=asset + + -- Add asset to the table that is retured. + table.insert(assets,asset) + end + + return assets +end + +--- Asset item characteristics. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Assetitem asset The asset for which info in printed in trace mode. +function WAREHOUSE:_AssetItemInfo(asset) + -- Info about asset: + local text=string.format("\nNew asset with id=%d for warehouse %s:\n", asset.uid, self.alias) + text=text..string.format("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) + self:T({DCSdesc=asset.DCSdesc}) + self:T3({Template=asset.template}) +end + +--- On after "NewAsset" event. A new asset has been added to the warehouse stock. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #WAREHOUSE.Assetitem asset The asset that has just been added. +-- @parma #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)) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On before "AddRequest" event. Checks some basic properties of the given parameters. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #WAREHOUSE warehouse The warehouse requesting supply. +-- @param #WAREHOUSE.Descriptor AssetDescriptor Descriptor describing the asset that is requested. +-- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc. +-- @param #number nAsset Number of groups requested that match the asset specification. +-- @param #WAREHOUSE.TransportType TransportType Type of transport. +-- @param #number nTransport Number of transport units requested. +-- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low. +-- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets. +-- @return #boolean If true, request is okay at first glance. +function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescriptor, AssetDescriptorValue, nAsset, TransportType, nTransport, Assignment, Prio) + + -- Request is okay. + local okay=true + + if AssetDescriptor==WAREHOUSE.Descriptor.ATTRIBUTE then + + -- Check if a valid attibute was given. + local gotit=false + for _,attribute in pairs(WAREHOUSE.Attribute) do + if AssetDescriptorValue==attribute then + gotit=true + end + end + if not gotit then + self:_ErrorMessage("ERROR: Invalid request. Asset attribute is unknown!", 5) + okay=false + end + + elseif AssetDescriptor==WAREHOUSE.Descriptor.CATEGORY then + + -- Check if a valid category was given. + local gotit=false + for _,category in pairs(Group.Category) do + if AssetDescriptorValue==category then + gotit=true + end + end + if not gotit then + self:_ErrorMessage("ERROR: Invalid request. Asset category is unknown!", 5) + okay=false + end + + elseif AssetDescriptor==WAREHOUSE.Descriptor.GROUPNAME then + + if type(AssetDescriptorValue)~="string" then + self:_ErrorMessage("ERROR: Invalid request. Asset template name must be passed as a string!", 5) + okay=false + end + + elseif AssetDescriptor==WAREHOUSE.Descriptor.UNITTYPE then + + if type(AssetDescriptorValue)~="string" then + self:_ErrorMessage("ERROR: Invalid request. Asset unit type must be passed as a string!", 5) + okay=false + end + + else + self:_ErrorMessage("ERROR: Invalid request. Asset descriptor is not ATTRIBUTE, CATEGORY, GROUPNAME or UNITTYPE!", 5) + okay=false + end + + -- Warehouse is stopped? + if self:IsStopped() then + self:_ErrorMessage("ERROR: Invalid request. Warehouse is stopped!", 0) + okay=false + end + + -- Warehouse is destroyed? + if self:IsDestroyed() then + self:_ErrorMessage("ERROR: Invalid request. Warehouse is destroyed!", 0) + okay=false + end + + return okay +end + +--- On after "AddRequest" event. Add a request to the warehouse queue, which is processed when possible. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #WAREHOUSE warehouse The warehouse requesting supply. +-- @param #WAREHOUSE.Descriptor AssetDescriptor Descriptor describing the asset that is requested. +-- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc. +-- @param #number nAsset Number of groups requested that match the asset specification. +-- @param #WAREHOUSE.TransportType TransportType Type of transport. +-- @param #number nTransport Number of transport units requested. +-- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low. +-- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets. +function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor, AssetDescriptorValue, nAsset, TransportType, nTransport, Prio, Assignment) + + -- Defaults. + nAsset=nAsset or 1 + TransportType=TransportType or WAREHOUSE.TransportType.SELFPROPELLED + Prio=Prio or 50 + if nTransport==nil then + if TransportType==WAREHOUSE.TransportType.SELFPROPELLED then + nTransport=0 + else + nTransport=1 + end + end + + -- Self request? + local toself=false + if self.warehouse:GetName()==warehouse.warehouse:GetName() then + toself=true + end + + -- Increase id. + self.queueid=self.queueid+1 + + -- Request queue table item. + local request={ + uid=self.queueid, + prio=Prio, + warehouse=warehouse, + assetdesc=AssetDescriptor, + assetdescval=AssetDescriptorValue, + nasset=nAsset, + transporttype=TransportType, + ntransport=nTransport, + assignment=tostring(Assignment), + airbase=warehouse:GetAirbase(), + category=warehouse:GetAirbaseCategory(), + ndelivered=0, + ntransporthome=0, + assets={}, + toself=toself, + } --#WAREHOUSE.Queueitem + + -- Add request to queue. + table.insert(self.queue, request) + + local text=string.format("Warehouse %s: New request from warehouse %s.\nDescriptor %s=%s, #assets=%s; Transport=%s, #transports =%s.", + self.alias, warehouse.alias, request.assetdesc, tostring(request.assetdescval), tostring(request.nasset), request.transporttype, tostring(request.ntransport)) + self:_DebugMessage(text, 5) + + -- Update status + --self:__Status(-1) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On before "Request" event. Checks if the request can be fulfilled. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #WAREHOUSE.Queueitem Request Information table of the request. +-- @return #boolean If true, request is granted. +function WAREHOUSE:onbeforeRequest(From, Event, To, Request) + self:T3({warehouse=self.alias, request=Request}) + + -- Distance from warehouse to requesting warehouse. + local distance=self:GetCoordinate():Get2DDistance(Request.warehouse:GetCoordinate()) + + -- Shortcut to cargoassets. + local _assets=Request.cargoassets + + if Request.nasset==0 then + local text=string.format("Warehouse %s: Request denied! Zero assets were requested.", self.alias) + self:_InfoMessage(text, 10) + return false + end + + -- Check if destination is in range for all requested assets. + for _,_asset in pairs(_assets) do + local asset=_asset --#WAREHOUSE.Assetitem + + -- Check if destination is in range. + if asset.range1 then + group:RouteGroundTo(self.spawnzone:GetRandomCoordinate(), speedmax*0.5, AI.Task.VehicleFormation.RANK, 3) + else + -- Immobile ground unit ==> directly put it into the warehouse. + self:Arrived(group) + end + elseif group:IsAir() then + -- Not sure if air units will be allowed as cargo even though it might be possible. Best put them into warehouse immediately. + self:Arrived(group) + elseif group:IsShip() then + -- Not sure if naval units will be allowed as cargo even though it might be possible. Best put them into warehouse immediately. + self:Arrived(group) + end + + else + self:E(self.wid..string.format("ERROR unloaded Cargo group is not alive!")) + end +end + +--- On after "Arrived" event. Triggered when a group has arrived at its destination warehouse. +-- The routine should be called by the warehouse sending this asset and not by the receiving warehouse. +-- It is checked if this asset is cargo (or self propelled) or transport. If it is cargo it is put into the stock of receiving warehouse. +-- If it is a transporter it is put back into the sending warehouse since transports are supposed to return their home warehouse. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Wrapper.Group#GROUP group The group that was delivered. +function WAREHOUSE:onafterArrived(From, Event, To, group) + + -- Debug message and smoke. + if self.Debug then + group:SmokeOrange() + end + + -- Get pending request this group belongs to. + local request=self:_GetRequestOfGroup(group, self.pending) + + if request then + + -- Get the right warehouse to put the asset into + -- Transports go back to the warehouse which called this function while cargo goes into the receiving warehouse. + local warehouse=request.warehouse + local istransport=self:_GroupIsTransport(group,request) + if istransport==true then + warehouse=self + elseif istransport==false then + warehouse=request.warehouse + else + self:E(self.wid..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) + 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))) + 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))) + else + self:E(warehouse.wid..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. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #WAREHOUSE.Pendingitem request The pending request that is finished and deleted from the pending queue. +function WAREHOUSE:onafterDelivered(From, Event, To, request) + + -- Debug info + local text=string.format("Warehouse %s: All assets delivered to warehouse %s!", self.alias, request.warehouse.alias) + self:_InfoMessage(text, 5) + + -- Make some noise :) + if self.Debug then + self:_Fireworks(request.warehouse:GetCoordinate()) + end + + -- Set delivered status for this request uid. + self.delivered[request.uid]=true + +end + + +--- On after "SelfRequest" event. Request was initiated to the warehouse itself. Groups are just spawned at the warehouse or the associated airbase. +-- If the warehouse is currently under attack when the self request is made, the self request is added to the defending table. One the attack is defeated, +-- this request is used to put the groups back into the warehouse stock. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param Core.Set#SET_GROUP groupset The set of asset groups that was delivered to the warehouse itself. +-- @param #WAREHOUSE.Pendingitem request Pending self request. +function WAREHOUSE:onafterSelfRequest(From, Event, To, groupset, request) + + -- Debug info. + self:_DebugMessage(string.format("Assets spawned at warehouse %s after self request!", self.alias)) + + -- Debug info. + for _,_group in pairs(groupset:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + if self.Debug then + group:FlareGreen() + end + end + + -- Add a "defender request" to be able to despawn all assets once defeated. + if self:IsAttacked() then + + -- Route (mobile) ground troops to warehouse zone if they are not alreay there. + if self.autodefence then + for _,_group in pairs(groupset:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + local speedmax=group:GetSpeedMax() + if group:IsGround() and speedmax>1 and group:IsNotInZone(self.zone) then + group:RouteGroundTo(self.zone:GetRandomCoordinate(), 0.8*speedmax, "Off Road") + end + end + end + + -- Add request to defenders. + table.insert(self.defending, request) + end + +end + +--- On after "Attacked" event. Warehouse is under attack by an another coalition. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#coalition.side Coalition which is attacking the warehouse. +-- @param DCS#country.id Country which is attacking the warehouse. +function WAREHOUSE:onafterAttacked(From, Event, To, Coalition, Country) + + -- Warning. + local text=string.format("Warehouse %s: We are under attack!", self.alias) + self:_InfoMessage(text) + + -- Debug smoke. + if self.Debug then + self:GetCoordinate():SmokeOrange() + end + + -- Spawn all ground units in the spawnzone? + if self.autodefence then + local nground=self:GetNumberOfAssets(WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND) + local text=string.format("Warehouse auto defence activated.\n") + + if nground>0 then + text=text..string.format("Deploying all %d ground assets.", nground) + + -- Add self request. + self:AddRequest(self, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, WAREHOUSE.Quantity.ALL, nil, nil , 0, "AutoDefence") + else + text=text..string.format("No ground assets currently available.") + end + self:_InfoMessage(text) + else + local text=string.format("Warehouse auto defence inactive.") + self:I(self.wid..text) + end +end + +--- On after "Defeated" event. Warehouse defeated an attack by another coalition. Defender assets are added back to warehouse stock. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function WAREHOUSE:onafterDefeated(From, Event, To) + + -- Message. + local text=string.format("Warehouse %s: Enemy attack was defeated!", self.alias) + self:_InfoMessage(text) + + -- Debug smoke. + if self.Debug then + self:GetCoordinate():SmokeGreen() + end + + -- Auto defence: put assets back into stock. + if self.autodefence then + for _,request in pairs(self.defending) do + + -- Route defenders back to warehoue (for visual reasons only) and put them back into stock. + for _,_group in pairs(request.cargogroupset:GetSetObjects()) do + local group=_group --Wrapper.Group#GROUP + + -- Get max speed of group and route it back slowly to the warehouse. + local speed=group:GetSpeedMax() + if group:IsGround() and speed>1 then + group:RouteGroundTo(self:GetCoordinate(), speed*0.3) + end + + -- Add asset group back to stock after 60 seconds. + self:__AddAsset(60, group) + end + + end + + self.defending=nil + self.defending={} + end +end + + +--- On before "ChangeCountry" event. Checks whether a change of country is necessary by comparing the actual country to the the requested one. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#country.id Country which has captured the warehouse. +function WAREHOUSE:onbeforeChangeCountry(From, Event, To, Country) + + local currentCountry=self:GetCountry() + + -- Message. + local text=string.format("Warehouse %s: request to change country %d-->%d", self.alias, currentCountry, Country) + self:_DebugMessage(text, 10) + + -- Check if current or requested coalition or country match. + if currentCountry~=Country then + return true + end + + return false +end + + +--- On after "ChangeCountry" event. Warehouse is respawned with the specified country. All queued requests are deleted and the owned airbase is reset if the coalition is changed by changing the +-- country. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#country.id Country 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={} + + -- Airbase could have been captured before and already belongs to the new coalition. + local airbase=AIRBASE:FindByName(self.airbasename) + local airbasecoaltion=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 + end + + -- Debug smoke. + if self.Debug then + if CoalitionNew==coalition.side.RED then + self:GetCoordinate():SmokeRed() + elseif CoalitionNew==coalition.side.BLUE then + self:GetCoordinate():SmokeBlue() + end + end + +end + +--- On after "Captured" event. Warehouse has been captured by another coalition. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#coalition.side Coalition which captured the warehouse. +-- @param DCS#country.id Country which has captured the warehouse. +function WAREHOUSE:onafterCaptured(From, Event, To, Coalition, Country) + + -- Message. + local text=string.format("Warehouse %s: We were captured by enemy coalition (side=%d)!", self.alias, Coalition) + self:_InfoMessage(text) + + -- Change coalition and country of warehouse static. + self:ChangeCoaliton(Coalition, Country) + +end + + +--- On after "AirbaseCaptured" event. Airbase of warehouse has been captured by another coalition. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#coalition.side Coalition which captured the warehouse. +function WAREHOUSE:onafterAirbaseCaptured(From, Event, To, Coalition) + + -- Message. + local text=string.format("Warehouse %s: Our airbase %s was captured by the enemy (coalition=%d)!", self.alias, self.airbasename, Coalition) + self:_InfoMessage(text) + + -- Debug smoke. + if self.Debug then + if Coalition==coalition.side.RED then + self.airbase:GetCoordinate():SmokeRed() + elseif Coalition==coalition.side.BLUE then + self.airbase:GetCoordinate():SmokeBlue() + end + end + + -- Set airbase to nil and category to no airbase. + self.airbase=nil +end + +--- On after "AirbaseRecaptured" event. Airbase of warehouse has been re-captured from other coalition. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param DCS#coalition.side Coalition Coalition side which originally captured the warehouse. +function WAREHOUSE:onafterAirbaseRecaptured(From, Event, To, Coalition) + + -- Message. + local text=string.format("Warehouse %s: We recaptured our airbase %s from the enemy (coalition=%d)!", self.alias, self.airbasename, Coalition) + self:_InfoMessage(text) + + -- Set airbase and category. + self.airbase=AIRBASE:FindByName(self.airbasename) + + -- Debug smoke. + if self.Debug then + if Coalition==coalition.side.RED then + self.airbase:GetCoordinate():SmokeRed() + elseif Coalition==coalition.side.BLUE then + self.airbase:GetCoordinate():SmokeBlue() + end + end + +end + + +--- On after "AssetDead" event triggerd when an asset group died. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #WAREHOUSE.Assetitem asset The asset that is dead. +-- @param #WAREHOUSE.Pendingitem request The request of the dead asset. +function WAREHOUSE:onafterAssetDead(From, Event, To, asset, request) + local text=string.format("Asset %s from request id=%d is dead!", asset.templatename, request.uid) + self:T(self.wid..text) + self:_DebugMessage(text) +end + + +--- On after "Destroyed" event. Warehouse was destroyed. All services are stopped. Warehouse is going to "Stopped" state in one minute. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function WAREHOUSE:onafterDestroyed(From, Event, To) + + -- Message. + local text=string.format("Warehouse %s was destroyed! Assets lost %d.", self.alias, #self.stock) + 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={} + + --self.stock=nil + --self.stock={} + +end + + +--- On after "Save" event. Warehouse assets are saved to file on disk. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory. +-- @param #string filename (Optional) Name of the file containing the asset data. +function WAREHOUSE:onafterSave(From, Event, To, path, filename) + + local function _savefile(filename, data) + local f = assert(io.open(filename, "wb")) + f:write(data) + f:close() + end + + -- Set file name. + filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Info + local text=string.format("Saving warehouse assets to file %s", filename) + MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report) + self:I(self.wid..text) + + local warehouseassets="" + warehouseassets=warehouseassets..string.format("coalition=%d\n", self:GetCoalition()) + warehouseassets=warehouseassets..string.format("country=%d\n", self:GetCountry()) + + -- Loop over all assets in stock. + for _,_asset in pairs(self.stock) do + local asset=_asset -- #WAREHOUSE.Assetitem + + -- Loop over asset parameters. + local assetstring="" + for key,value in pairs(asset) do + + -- Only save keys which are needed to restore the asset. + if key=="templatename" or key=="attribute" or key=="cargobay" or key=="weight" or key=="loadradius" or key=="livery" or key=="skill" or key=="assignment" then + local name + if type(value)=="table" then + name=string.format("%s=%s;", key, value[1]) + else + name=string.format("%s=%s;", key, value) + end + assetstring=assetstring..name + end + self:I(string.format("Loaded asset: %s", assetstring)) + end + + -- Add asset string. + warehouseassets=warehouseassets..assetstring.."\n" + end + + -- Save file. + _savefile(filename, warehouseassets) + +end + + +--- On before "Load" event. Checks if the file the warehouse data should be loaded from exists. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path Path where the file is loaded from. +-- @param #string filename (Optional) Name of the file containing the asset data. +function WAREHOUSE:onbeforeLoad(From, Event, To, path, filename) + + + local function _fileexists(name) + local f=io.open(name,"r") + if f~=nil then + io.close(f) + return true + else + return false + end + end + + -- Set file name. + filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Check if file exists. + local exists=_fileexists(filename) + + if exists then + return true + else + self:_ErrorMessage(string.format("ERROR: file %s does not exist! Cannot load assets.", filename), 60) + return false + end + +end + + +--- On after "Load" event. Warehouse assets are loaded from file on disk. +-- @param #WAREHOUSE self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +-- @param #string path Path where the file is loaded from. +-- @param #string filename (Optional) Name of the file containing the asset data. +function WAREHOUSE:onafterLoad(From, Event, To, path, filename) + + local function _loadfile(filename) + local f = assert(io.open(filename, "rb")) + local data = f:read("*all") + f:close() + return data + end + + -- Set file name. + filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias) + + -- Set path. + if path~=nil then + filename=path.."\\"..filename + end + + -- Info + local text=string.format("Loading warehouse assets from file %s", filename) + MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report) + self:I(self.wid..text) + + -- Load asset data from file. + local data=_loadfile(filename) + + -- Split by line break. + local assetdata=UTILS.Split(data,"\n") + + -- Coalition and coutrny. + local Coalition + local Country + + -- Loop over asset lines. + local assets={} + for _,asset in pairs(assetdata) do + + -- Parameters are separated by semi-colons + local descriptors=UTILS.Split(asset,";") + + local asset={} + local isasset=false + for _,descriptor in pairs(descriptors) do + + local keyval=UTILS.Split(descriptor,"=") + + if #keyval==2 then + + if keyval[1]=="coalition" then + -- Get coalition side. + Coalition=tonumber(keyval[2]) + elseif keyval[1]=="country" then + -- Get country id. + Country=tonumber(keyval[2]) + else + + -- This is an asset. + isasset=true + + local key=keyval[1] + local val=keyval[2] + + --env.info(string.format("FF asset key=%s val=%s", key, val)) + + -- Livery or skill could be "nil". + if val=="nil" then + val=nil + end + + -- Convert string to number where necessary. + if key=="cargobay" or key=="weight" or key=="loadradius" then + asset[key]=tonumber(val) + else + asset[key]=val + end + end + + end + end + + -- Add to table. + if isasset then + table.insert(assets, asset) + end + end + + -- Respawn warehouse with prev coalition if necessary. + if Country~=self:GetCountry() then + self:T(self.wid..string.format("Changing warehouse country %d-->%d on loading assets.", self:GetCountry(), Country)) + self:ChangeCountry(Country) + end + + for _,_asset in pairs(assets) do + local asset=_asset --#WAREHOUSE.Assetitem + + local group=GROUP:FindByName(asset.templatename) + if group then + self:AddAsset(group, 1, asset.attribute, asset.cargobay, asset.weight, asset.loadradius, asset.skill, asset.livery, asset.assignment) + else + self:E(string.format("ERROR: Group %s doest not exit. Cannot be loaded as asset.", tostring(asset.templatename))) + end + end + +end + +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Spawn functions +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Spawns requested assets at warehouse or associated airbase. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Queueitem Request Information table of the request. +-- @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 + + -- 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 {} + 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 + + -- Get stock item. + local _assetitem=_assetstock[i] --#WAREHOUSE.Assetitem + + -- Alias of the group. + local _alias=self:_Alias(_assetitem, Request) + + -- 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 + + -- Spawn air units. + if Parking[_assetitem.uid] then + _group=self:_SpawnAssetAircraft(_alias,_assetitem, Request, Parking[_assetitem.uid], UnControlled) + else + _group=self:_SpawnAssetAircraft(_alias,_assetitem, Request, nil, UnControlled) + end + + elseif _assetitem.category==Group.Category.TRAIN then + + -- Spawn train. + if self.rail then + --TODO: Rail should only get one asset because they would spawn on top! + end + + self:E(self.wid.."ERROR: Spawning of TRAIN assets not possible yet!") + + elseif _assetitem.category==Group.Category.SHIP then + + -- Spawn naval assets. + _group=self:_SpawnAssetGroundNaval(_alias,_assetitem, Request, self.portzone) + + else + self:E(self.wid.."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 + + +--- Spawn a ground or naval asset in the corresponding spawn zone of the warehouse. +-- @param #WAREHOUSE self +-- @param #string alias Alias name of the asset group. +-- @param #WAREHOUSE.Assetitem asset Ground asset that will be spawned. +-- @param #WAREHOUSE.Queueitem request Request belonging to this asset. Needed for the name/alias. +-- @param Core.Zone#ZONE spawnzone Zone where the assets should be spawned. +-- @param #boolean aioff If true, AI of ground units are set to off. +-- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned. +function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aioff) + + if asset and (asset.category==Group.Category.GROUND or asset.category==Group.Category.SHIP) then + + -- Prepare spawn template. + local template=self:_SpawnAssetPrepareTemplate(asset, alias) + + -- Initial spawn point. + template.route.points[1]={} + + -- Get a random coordinate in the spawn zone. + local coord=spawnzone:GetRandomCoordinate() + + -- Translate the position of the units. + for i=1,#template.units do + + -- Unit template. + local unit = template.units[i] + + -- Translate position. + local SX = unit.x or 0 + local SY = unit.y or 0 + local BX = asset.template.route.points[1].x + local BY = asset.template.route.points[1].y + local TX = coord.x + (SX-BX) + local TY = coord.z + (SY-BY) + + template.units[i].x = TX + template.units[i].y = TY + + if asset.livery then + unit.livery_id = asset.livery + end + if asset.skill then + unit.skill= asset.skill + end + + end + + template.route.points[1].x = coord.x + template.route.points[1].y = coord.z + + template.x = coord.x + template.y = coord.z + template.alt = coord.y + + -- Spawn group. + local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP + + -- Activate group. Should only be necessary for late activated groups. + --group:Activate() + + -- Switch AI off if desired. This works only for ground and naval groups. + if aioff then + group:SetAIOff() + end + + return group + end + + return nil +end + +--- Spawn an aircraft asset (plane or helo) at the airbase associated with the warehouse. +-- @param #WAREHOUSE self +-- @param #string alias Alias name of the asset group. +-- @param #WAREHOUSE.Assetitem asset Ground asset that will be spawned. +-- @param #WAREHOUSE.Queueitem request Request belonging to this asset. Needed for the name/alias. +-- @param #table parking Parking data for this asset. +-- @param #boolean uncontrolled Spawn aircraft in uncontrolled state. +-- @param #boolean hotstart Spawn aircraft with engines already on. Default is a cold start with engines off. +-- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned. +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) + + 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] + + if AirbaseCategory == Airbase.Category.HELIPAD or AirbaseCategory == Airbase.Category.SHIP then + + -- Helipads we take the position of the airbase location, since the exact location of the spawn point does not make sense. + local coord=self.airbase:GetCoordinate() + + unit.x=coord.x + unit.y=coord.z + unit.alt=coord.y + + unit.parking_id = nil + unit.parking = nil + + else + + local coord=parking[i].Coordinate --Core.Point#COORDINATE + local terminal=parking[i].TerminalID --#number + + if self.Debug then + coord:MarkToAll(string.format("Spawnplace unit %s terminal %d.", unit.name, terminal)) + end + + unit.x=coord.x + unit.y=coord.z + unit.alt=coord.y + + unit.parking_id = nil + unit.parking = terminal + + end + + if asset.livery then + unit.livery_id = asset.livery + end + if asset.skill then + unit.skill= asset.skill + end + + 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 + + +--- Prepare a spawn template for the asset. Deep copy of asset template, adjusting template and unit names, nillifying group and unit ids. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Assetitem asset Ground asset that will be spawned. +-- @param #string alias Alias name of the group. +-- @return #table Prepared new spawn template. +function WAREHOUSE:_SpawnAssetPrepareTemplate(asset, alias) + + -- Create an own copy of the template! + local template=UTILS.DeepCopy(asset.template) + + -- Set unique name. + template.name=alias + + -- Set current(!) coalition and country. + template.CoalitionID=self:GetCoalition() + template.CountryID=self:GetCountry() + + -- Nillify the group ID. + template.groupId=nil + + -- 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 + + -- Set and empty route. + template.route = {} + template.route.routeRelativeTOT=true + template.route.points = {} + + -- Handle units. + for i=1,#template.units do + + -- Unit template. + local unit = template.units[i] + + -- Nillify the unit ID. + unit.unitId=nil + + -- Set unit name: -01, -02, ... + unit.name=string.format("%s-%02d", template.name , i) + + end + + return template +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Routing functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Route ground units to destination. ROE is set to return fire and alarm state to green. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The ground group to be routed +-- @param #WAREHOUSE.Queueitem request The request for this group. +-- @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. + local hasoffroad=self:HasConnectionOffRoad(request.warehouse, self.Debug) + + -- Check if any off road paths have be defined. They have priority! + if hasoffroad then + + -- Get off road path to remote warehouse. If more have been defined, pick one randomly. + local remotename=request.warehouse.warehouse:GetName() + local path=self.offroadpaths[remotename][math.random(#self.offroadpaths[remotename])] + + -- Loop over user defined shipping lanes. + for i=1,#path do + + -- Shortcut and coordinate intellisense. + local coord=path[i] --Core.Point#COORDINATE + + -- Get waypoint for coordinate. + local Waypoint=coord:WaypointGround(_speed, "Off Road") + + -- Add waypoint to route. + table.insert(Waypoints, Waypoint) + end + + else + + -- Waypoints for road-to-road connection. + Waypoints = group:TaskGroundOnRoad(request.warehouse.road, _speed, "Off Road", false, self.road) + + -- First waypoint = current position of the group. + local FromWP=group:GetCoordinate():WaypointGround(_speed, "Off Road") + table.insert(Waypoints, 1, FromWP) + + -- Final coordinate. + local ToWP=request.warehouse.spawnzone:GetRandomCoordinate():WaypointGround(_speed, "Off Road") + table.insert(Waypoints, #Waypoints+1, ToWP) + + end + + -- Task function triggering the arrived event at the last waypoint. + local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) + + -- Put task function on last waypoint. + local Waypoint = Waypoints[#Waypoints] + group:SetTaskWaypoint(Waypoint, TaskFunction) + + -- Route group to destination. + group:Route(Waypoints, 1) + + -- Set ROE and alaram state. + group:OptionROEReturnFire() + group:OptionAlarmStateGreen() + end +end + +--- Route naval units along user defined shipping lanes to destination warehouse. ROE is set to return fire. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The naval group to be routed +-- @param #WAREHOUSE.Queueitem request The request for this group. +function WAREHOUSE:_RouteNaval(group, request) + + -- Check if we have a group and it is alive. + if group and group:IsAlive() then + + -- Set speed to 80% of max possible. + local _speed=group:GetSpeedMax()*0.8 + + -- Get shipping lane to remote warehouse. If more have been defined, pick one randomly. + local remotename=request.warehouse.warehouse:GetName() + local lane=self.shippinglanes[remotename][math.random(#self.shippinglanes[remotename])] + + if lane then + + -- Route waypoints. + local Waypoints={} + + -- Loop over user defined shipping lanes. + for i=1,#lane do + + -- Shortcut and coordinate intellisense. + local coord=lane[i] --Core.Point#COORDINATE + + -- Get waypoint for coordinate. + local Waypoint=coord:WaypointGround(_speed) + + -- Add waypoint to route. + table.insert(Waypoints, Waypoint) + end + + -- Task function triggering the arrived event at the last waypoint. + local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group) + + -- Put task function on last waypoint. + local Waypoint = Waypoints[#Waypoints] + group:SetTaskWaypoint(Waypoint, TaskFunction) + + -- Route group to destination. + group:Route(Waypoints, 1) + + -- Set ROE (Naval units dont have and alaram state.) + group:OptionROEReturnFire() + + else + -- This should not happen! Existance of shipping lane was checked before executing this request. + self:E(self.wid..string.format("ERROR: No shipping lane defined for Naval asset!")) + end + + end +end + + +--- Route the airplane from one airbase another. Activates uncontrolled aircraft and sets ROE/ROT for ferry flights. +-- ROE is set to return fire and ROT to passive defence. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP aircraft Airplane group to be routed. +function WAREHOUSE:_RouteAir(aircraft) + + if aircraft and aircraft:IsAlive()~=nil then + + -- Debug info. + self:T2(self.wid..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()))) + + -- Set ROE and alaram state. + aircraft:OptionROEReturnFire() + aircraft:OptionROTPassiveDefense() + + else + self:E(string.format("ERROR: aircraft %s cannot be routed since it does not exist or is not alive %s!", tostring(aircraft:GetName()), tostring(aircraft:IsAlive()))) + end +end + +--- Route trains to their destination - or at least to the closest point on rail of the desired final destination. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP Group The train group. +-- @param Core.Point#COORDINATE Coordinate of the destination. Tail will be routed to the closest point +-- @param #number Speed Speed in km/h to drive to the destination coordinate. Default is 60% of max possible speed the unit can go. +function WAREHOUSE:_RouteTrain(Group, Coordinate, Speed) + + if Group and Group:IsAlive() then + + local _speed=Speed or Group:GetSpeedMax()*0.6 + + -- Create a + local Waypoints = Group:TaskGroundOnRailRoads(Coordinate, Speed) + + -- Task function triggering the arrived event at the last waypoint. + local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", Group) + + -- Put task function on last waypoint. + local Waypoint = Waypoints[#Waypoints] + Group:SetTaskWaypoint( Waypoint, TaskFunction ) + + -- Route group to destination. + Group:Route(Waypoints, 1) + end +end + +--- Task function for last waypoint. Triggering the "Arrived" event. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group that arrived. +function WAREHOUSE:_Arrived(group) + self:_DebugMessage(string.format("Group %s arrived!", tostring(group:GetName()))) + + if group then + --Trigger "Arrived event. + self:__Arrived(1, group) + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Event handler functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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)) + + 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)) + else + --self:T3({wid=wid, uid=self.uid, match=(wid==self.uid), tw=type(wid), tu=type(self.uid)}) + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function handling the event when a (warehouse) unit starts its engines. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data. +function WAREHOUSE:_OnEventEngineStartup(EventData) + self:T3(self.wid..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)) + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function handling the event when a (warehouse) unit takes off. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data. +function WAREHOUSE:_OnEventTakeOff(EventData) + self:T3(self.wid..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)) + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function handling the event when a (warehouse) unit lands. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data. +function WAREHOUSE:_OnEventLanding(EventData) + self:T3(self.wid..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)) + + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Function handling the event when a (warehouse) unit shuts down its engines. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data. +function WAREHOUSE:_OnEventEngineShutdown(EventData) + self:T3(self.wid..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)) + end + end +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Arrived event if an air unit/group arrived at its destination. This can be an engine shutdown or a landing event. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data table. +function WAREHOUSE:_OnEventArrived(EventData) + + if EventData and EventData.IniUnit then + + -- Unit that arrived. + local unit=EventData.IniUnit + + -- Check if unit is alive and on the ground. Engine shutdown can also be triggered in other situations! + if unit and unit:IsAlive()==true and unit:InAir()==false then + + -- Get group. + local group=EventData.IniGroup + + -- Get unique IDs from group name. + local wid,aid,rid=self:_GetIDsFromGroup(group) + + -- If all IDs are good we can assume it is a warehouse asset. + if wid~=nil and aid~=nil and rid~=nil then + + -- Check that warehouse ID is right. + if self.uid==wid then + + local request=self:_GetRequestOfGroup(group, self.pending) + 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) + + end + + end + + else + self:T3(string.format("Group that arrived did not belong to a warehouse. Warehouse ID=%s, Asset ID=%s, Request ID=%s.", tostring(wid), tostring(aid), tostring(rid))) + end + end + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Warehouse event handling function. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data. +function WAREHOUSE:_OnEventCrashOrDead(EventData) + self:T3(self.wid..string.format("Warehouse %s captured event dead or crash!", self.alias)) + + if EventData then + + -- Check if warehouse was destroyed. We compare the name of the destroyed unit. + if EventData.IniUnitName then + local warehousename=self.warehouse:GetName() + if EventData.IniUnitName==warehousename then + self:_DebugMessage(string.format("Warehouse %s alias %s was destroyed!", warehousename, self.alias)) + + -- Trigger Destroyed event. + self:Destroyed() + end + 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. + if EventData.IniGroup then + + -- Group initiating the event. + local group=EventData.IniGroup + + -- Get warehouse, asset and request IDs from the group name. + local wid,aid,rid=self:_GetIDsFromGroup(group) + + -- Check that we have the right warehouse. + if wid==self.uid then + + -- Debug message. + self:T(self.wid..string.format("Warehouse %s captured event dead or crash of its asset unit %s.", self.alias, EventData.IniUnitName)) + + -- Loop over all pending requests and get the one belonging to this unit. + for _,request in pairs(self.pending) do + local request=request --#WAREHOUSE.Pendingitem + + -- This is the right request. + if request.uid==rid then + + -- Update cargo and transport group sets of this request. We need to know if this job is finished. + self:_UnitDead(EventData.IniUnit, request) + + end + end + end + end + end +end + +--- A unit of a group just died. Update group sets in request. +-- This is important in order to determine if a job is done and can be removed from the (pending) queue. +-- @param #WAREHOUSE self +-- @param Wrapper.Unit#UNIT deadunit Unit that died. +-- @param #WAREHOUSE.Pendingitem request Request that needs to be updated. +function WAREHOUSE:_UnitDead(deadunit, request) + + -- Flare unit + deadunit:FlareRed() + + -- Group the dead unit belongs to. + local group=deadunit:GetGroup() + + -- Check if this was the last unit of the group ==> whole group 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 + 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)))) + if self.Debug then + group:SmokeWhite() + end + -- Trigger AssetDead event. + 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. + 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. + 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())) + end + + else + + --- + -- Complicated case: Dead unit could be: + -- 1.) A Cargo unit (e.g. waiting to be picked up). + -- 2.) A Transport unit which itself holds cargo groups. + --- + + -- Check if this a cargo or transport group. + local istransport=self:_GroupIsTransport(group,request) + + if istransport==true then + + -- Get the carrier unit table holding the cargo groups inside this carrier. + local cargogroupnames=request.carriercargo[unitname] + + if cargogroupnames then + + -- Loop over all groups inside the destroyed carrier ==> all dead. + for _,cargoname in pairs(cargogroupnames) do + request.cargogroupset:Remove(cargoname, NoTriggerEvent) + self:T(self.wid..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 + + 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())) + -- 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())) + end + end + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Warehouse event handling function. +-- Handles the case when the airbase associated with the warehous is captured. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data. +function WAREHOUSE:_OnEventBaseCaptured(EventData) + self:T3(self.wid..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)) + + -- So what can happen? + -- Warehouse is blue, airbase is blue and belongs to warehouse and red captures it ==> self.airbase=nil + -- Warehouse is blue, airbase is blue self.airbase is nil and blue (re-)captures it ==> self.airbase=Event.Place + if self.airbase==nil then + -- New coalition is the same as of the warehouse ==> warehouse previously lost this airbase and now it was re-captured. + if NewCoalitionAirbase == self:GetCoalition() then + self:AirbaseRecaptured(NewCoalitionAirbase) + end + else + -- Captured airbase belongs to this warehouse but was captured by other coaltion. + if NewCoalitionAirbase ~= self:GetCoalition() then + self:AirbaseCaptured(NewCoalitionAirbase) + end + end + + end + end +end + +--- Warehouse event handling function. +-- Handles the case when the mission is ended. +-- @param #WAREHOUSE self +-- @param Core.Event#EVENTDATA EventData Event data. +function WAREHOUSE:_OnEventMissionEnd(EventData) + self:T3(self.wid..string.format("Warehouse %s captured event mission end!",self.alias)) + + if self.autosave then + self:Save(self.autosavepath, self.autosavefile) + end +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Helper functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Checks if the warehouse zone was conquered by antoher coalition. +-- @param #WAREHOUSE self +function WAREHOUSE:_CheckConquered() + + -- Get coordinate and radius to check. + local coord=self.zone:GetCoordinate() + local radius=self.zone:GetRadius() + + -- Scan units in zone. + local gotunits,_,_,units,_,_=coord:ScanObjects(radius, true, false, false) + + local Nblue=0 + local Nred=0 + local Nneutral=0 + + local CountryBlue=nil + local CountryRed=nil + local CountryNeutral=nil + + if gotunits then + -- Loop over all units. + for _,_unit in pairs(units) do + local unit=_unit --Wrapper.Unit#UNIT + + local distance=coord:Get2DDistance(unit:GetCoordinate()) + + -- Filter only alive groud units. Also check distance again, because the scan routine might give some larger distances. + if unit:IsGround() and unit:IsAlive() and distance <= radius then + + -- Get coalition and country. + local _coalition=unit:GetCoalition() + local _country=unit:GetCountry() + + -- Debug info. + self:T2(self.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)) + + -- Add up units for each side. + if _coalition==coalition.side.BLUE then + Nblue=Nblue+1 + CountryBlue=_country + elseif _coalition==coalition.side.RED then + Nred=Nred+1 + CountryRed=_country + else + Nneutral=Nneutral+1 + CountryNeutral=_country + end + + end + end + end + + -- Debug info. + self:T(self.wid..string.format("Ground troops in warehouse zone: blue=%d, red=%d, neutral=%d", Nblue, Nred, Nneutral)) + + + -- Figure out the new coalition if any. + -- Condition is that only units of one coalition are within the zone. + local newcoalition=self:GetCoalition() + local newcountry=self:GetCountry() + if Nblue>0 and Nred==0 and Nneutral==0 then + -- Only blue units in zone ==> Zone goes to blue. + newcoalition=coalition.side.BLUE + newcountry=CountryBlue + elseif Nblue==0 and Nred>0 and Nneutral==0 then + -- Only red units in zone ==> Zone goes to red. + newcoalition=coalition.side.RED + newcountry=CountryRed + elseif Nblue==0 and Nred==0 and Nneutral>0 then + -- Only neutral units in zone but neutrals do not attack or even capture! + --newcoalition=coalition.side.NEUTRAL + --newcountry=CountryNeutral + end + + -- Coalition has changed ==> warehouse was captured! This should be before the attack check. + if self:IsAttacked() and newcoalition ~= self:GetCoalition() then + self:Captured(newcoalition, newcountry) + return + end + + -- Before a warehouse can be captured, it has to be attacked. + -- That is, even if only enemy units are present it is not immediately captured in order to spawn all ground assets for defence. + if self:GetCoalition()==coalition.side.BLUE then + -- Blue warehouse is running and we have red units in the zone. + if self:IsRunning() and Nred>0 then + self:Attacked(coalition.side.RED, CountryRed) + end + -- Blue warehouse was under attack by blue but no more blue units in zone. + if self:IsAttacked() and Nred==0 then + self:Defeated() + end + elseif self:GetCoalition()==coalition.side.RED then + -- Red Warehouse is running and we have blue units in the zone. + if self:IsRunning() and Nblue>0 then + self:Attacked(coalition.side.BLUE, CountryBlue) + end + -- Red warehouse was under attack by blue but no more blue units in zone. + if self:IsAttacked() and Nblue==0 then + self:Defeated() + end + elseif self:GetCoalition()==coalition.side.NEUTRAL then + -- Neutrals dont attack! + if self:IsRunning() and Nred>0 then + self:Attacked(coalition.side.RED, CountryRed) + elseif self:IsRunning() and Nblue>0 then + self:Attacked(coalition.side.BLUE, CountryBlue) + end + end + +end + +--- Checks if the associated airbase still belongs to the warehouse. +-- @param #WAREHOUSE self +function WAREHOUSE:_CheckAirbaseOwner() + -- The airbasename is set at start and not deleted if the airbase was captured. + if self.airbasename then + + local airbase=AIRBASE:FindByName(self.airbasename) + local airbasecurrentcoalition=airbase:GetCoalition() + + if self.airbase then + + -- Warehouse has lost its airbase. + if self:GetCoalition()~=airbasecurrentcoalition then + self.airbase=nil + end + + else + + -- Warehouse has re-captured the airbase. + if self:GetCoalition()==airbasecurrentcoalition then + self.airbase=airbase + end + + end + + end +end + +--- Checks if the request can be fulfilled in general. If not, it is removed from the queue. +-- Check if departure and destination bases are of the right type. +-- @param #WAREHOUSE self +-- @param #table queue The queue which is holding the requests to check. +-- @return #boolean If true, request can be executed. If false, something is not right. +function WAREHOUSE:_CheckRequestConsistancy(queue) + self:T3(self.wid..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)) + + -- 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.")) + 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())) + 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 + 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 + 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) + else + self:T3(self.wid..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:_DeleteQueueItem(_request, self.queue) + end + +end + +--- Check if a request is valid in general. If not, it will be removed from the queue. +-- This routine needs to have at least one asset in stock that matches the request descriptor in order to determine whether the request category of troops. +-- If no asset is in stock, the request will remain in the queue but cannot be executed. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Queueitem request The request to be checked. +-- @return #boolean If true, request can be executed. If false, something is not right. +function WAREHOUSE:_CheckRequestValid(request) + + -- Check if number of requested assets is in stock. + local _assets,_nassets,_enough=self:_FilterStock(self.stock, request.assetdesc, request.assetdescval, request.nasset) + + -- No assets in stock? Checks cannot be performed. + if #_assets==0 then + return true + end + + -- Convert relative to absolute number if necessary. + local nasset=request.nasset + if type(request.nasset)=="string" then + nasset=self:_QuantityRel2Abs(request.nasset,_nassets) + end + + -- Debug check, request.nasset might be a string Quantity enumerator. + local text=string.format("Request valid? Number of assets: requested=%s=%d, selected=%d, total=%d, enough=%s.", tostring(request.nasset), nasset,#_assets,_nassets, tostring(_enough)) + self:T(text) + + -- First asset. Is representative for all filtered items in stock. + local asset=_assets[1] --#WAREHOUSE.Assetitem + + -- Asset is air, ground etc. + local asset_plane = asset.category==Group.Category.AIRPLANE + local asset_helo = asset.category==Group.Category.HELICOPTER + local asset_ground = asset.category==Group.Category.GROUND + local asset_train = asset.category==Group.Category.TRAIN + local asset_naval = asset.category==Group.Category.SHIP + + -- General air request. + local asset_air=asset_helo or asset_plane + + -- Assume everything is okay. + local valid=true + + -- Category of the requesting warehouse airbase. + local requestcategory=request.warehouse:GetAirbaseCategory() + + if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then + ------------------------------------------- + -- Case where the units go my themselves -- + ------------------------------------------- + + if asset_air then + + if asset_plane then + + -- No airplane to or from FARPS. + if requestcategory==Airbase.Category.HELIPAD or self:GetAirbaseCategory()==Airbase.Category.HELIPAD then + self:E("ERROR: Incorrect request. Asset airplane requested but warehouse or requestor is HELIPAD/FARP!") + valid=false + end + + -- Category SHIP is not general enough! Fighters can go to carriers. Which fighters, is there an attibute? + -- Also for carriers, attibute? + + elseif asset_helo then + + -- Helos need a FARP or AIRBASE or SHIP for spawning. Also at the the receiving warehouse. So even if they could go there they "cannot" be spawned again. + -- Unless I allow spawning of helos in the the spawn zone. But one should place at least a FARP there. + if self:GetAirbaseCategory()==-1 or requestcategory==-1 then + self:E("ERROR: Incorrect request. Helos need a AIRBASE/HELIPAD/SHIP as home/destination base!") + valid=false + end + + end + + -- All aircraft need an airbase of any type at depature and destination. + if self.airbase==nil or request.airbase==nil then + + self:E("ERROR: Incorrect request. Either warehouse or requesting warehouse does not have any kind of airbase!") + valid=false + + else + + -- Check if enough parking spots are available. This checks the spots available in general, i.e. not the free spots. + -- TODO: For FARPS/ships, is it possible to send more assets than parking spots? E.g. a FARPS has only four (or even one). + -- TODO: maybe only check if spots > 0 for the necessary terminal type? At least for FARPS. + + -- Get necessary terminal type. + local termtype_dep=self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) + local termtype_des=self:_GetTerminal(asset.attribute, request.warehouse:GetAirbaseCategory()) + + -- Get number of parking spots. + local np_departure=self.airbase:GetParkingSpotsNumber(termtype_dep) + local np_destination=request.airbase:GetParkingSpotsNumber(termtype_des) + + -- Debug info. + self:T(string.format("Asset attribute = %s, DEPARTURE: terminal type = %d, spots = %d, DESTINATION: terminal type = %d, spots = %d", asset.attribute, termtype_dep, np_departure, termtype_des, np_destination)) + + -- Not enough parking at sending warehouse. + --if (np_departure < request.nasset) and not (self.category==Airbase.Category.SHIP or self.category==Airbase.Category.HELIPAD) then + if np_departure < nasset then + self:E(string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype_dep, np_departure, nasset)) + valid=false + end + + -- No parking at requesting warehouse. + if np_destination == 0 then + self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse. Available spots = %d!", termtype_des, np_destination)) + valid=false + end + + end + + elseif asset_ground then + + -- Check that both spawn zones are not in water. + local inwater=self.spawnzone:GetCoordinate():IsSurfaceTypeWater() or request.warehouse.spawnzone:GetCoordinate():IsSurfaceTypeWater() + + if inwater then + self:E("ERROR: Incorrect request. Ground asset requested but at least one spawn zone is in water!") + valid=false + end + + -- No ground assets directly to or from ships. + -- TODO: May needs refinement if warehouse is on land and requestor is ship in harbour?! + --if (requestcategory==Airbase.Category.SHIP or self:GetAirbaseCategory()==Airbase.Category.SHIP) then + -- self:E("ERROR: Incorrect request. Ground asset requested but warehouse or requestor is SHIP!") + -- valid=false + --end + + if asset_train then + + -- Check if there is a valid path on rail. + local hasrail=self:HasConnectionRail(request.warehouse) + if not hasrail then + self:E("ERROR: Incorrect request. No valid path on rail for train assets!") + valid=false + end + + else + + if self.warehouse:GetName()~=request.warehouse.warehouse:GetName() then + + -- Check if there is a valid path on road. + local hasroad=self:HasConnectionRoad(request.warehouse) + + -- Check if there is a valid off road path. + local hasoffroad=self:HasConnectionOffRoad(request.warehouse) + + if not (hasroad or hasoffroad) then + self:E("ERROR: Incorrect request. No valid path on or off road for ground assets!") + valid=false + end + + end + + end + + elseif asset_naval then + + -- Check shipping lane. + local shippinglane=self:HasConnectionNaval(request.warehouse) + + if not shippinglane then + self:E("ERROR: Incorrect request. No shipping lane has been defined between warehouses!") + valid=false + end + + end + + else + ------------------------------- + -- Assests need a transport --- + ------------------------------- + + if request.transporttype==WAREHOUSE.TransportType.AIRPLANE then + + -- Airplanes only to AND from airdromes. + if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME or requestcategory~=Airbase.Category.AIRDROME then + self:E("ERROR: Incorrect request. Warehouse or requestor does not have an airdrome. No transport by plane possible!") + valid=false + end + + --TODO: Not sure if there are any transport planes that can land on a carrier? + + elseif request.transporttype==WAREHOUSE.TransportType.APC then + + -- Transport by ground units. + + -- No transport to or from ships + if self:GetAirbaseCategory()==Airbase.Category.SHIP or requestcategory==Airbase.Category.SHIP then + self:E("ERROR: Incorrect request. Warehouse or requestor is SHIP. No transport by APC possible!") + valid=false + end + + -- Check if there is a valid path on road. + local hasroad=self:HasConnectionRoad(request.warehouse) + if not hasroad then + self:E("ERROR: Incorrect request. No valid path on road for ground transport assets!") + valid=false + end + + elseif request.transporttype==WAREHOUSE.TransportType.HELICOPTER then + + -- Transport by helicopters ==> need airbase for spawning but not for delivering to the spawn zone of the receiver. + if self:GetAirbaseCategory()==-1 then + self:E("ERROR: Incorrect request. Warehouse has no airbase. Transport by helicopter not possible!") + valid=false + end + + elseif request.transporttype==WAREHOUSE.TransportType.SHIP 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) + + -- Convert relative to absolute number if necessary. + local nasset=request.ntransport + if type(request.ntransport)=="string" then + nasset=self:_QuantityRel2Abs(request.ntransport,_nassets) + end + + -- Debug check, request.nasset might be a string Quantity enumerator. + local text=string.format("Request valid? Number of transports: requested=%s=%d, selected=%d, total=%d, enough=%s.", tostring(request.ntransport), nasset,#_assets,_nassets, tostring(_enough)) + self:T(text) + + -- Get necessary terminal type for helos or transport aircraft. + local termtype=self:_GetTerminal(request.transporttype, self:GetAirbaseCategory()) + + -- Get number of parking spots. + local np_departure=self.airbase:GetParkingSpotsNumber(termtype) + + -- Debug info. + self:T(self.wid..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)) + 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)) + + -- No parking at requesting warehouse. + if np_destination == 0 then + self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse for transports. Available spots = %d!", termtype, np_destination)) + valid=false + end + end + + end + + + end + + -- Add request as unvalid and delete it later. + if valid==false then + self:E(self.wid..string.format("ERROR: Got invalid request id=%d.", request.uid)) + else + self:T3(self.wid..string.format("Request id=%d valid :)", request.uid)) + end + + return valid +end + + +--- Checks if the request can be fulfilled right now. +-- Check for current parking situation, number of assets and transports currently in stock. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Queueitem request The request to be checked. +-- @return #boolean If true, request can be executed. If false, something is not right. +function WAREHOUSE:_CheckRequestNow(request) + + -- Check if receiving warehouse is running. We do allow self requests if the warehouse is under attack though! + if (request.warehouse:IsRunning()==false) and not (request.toself and self:IsAttacked()) then + local text=string.format("Warehouse %s: Request denied! Receiving warehouse %s is not running. Current state %s.", self.alias, request.warehouse.alias, request.warehouse:GetState()) + self:_InfoMessage(text, 5) + + return false + end + + -- If no transport is requested, assets need to be mobile unless it is a self request. + local onlymobile=false + if type(request.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) + return false + end + + local _transports + local _assetattribute + local _assetcategory + + -- Check if at least one (cargo) asset is available. + if _nassets>0 then + + -- Get the attibute of the requested asset. + _assetattribute=_assets[1].attribute + _assetcategory=_assets[1].category + + -- Check available parking for air asset units. + if 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 + + -- 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 + + 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) + return false + end + + end + + end + + + -- Set chosen cargo assets. + request.cargoassets=_assets + request.cargoattribute=_assets[1].attribute + request.cargocategory=_assets[1].category + request.nasset=#_assets + + -- Debug info: + local text=string.format("Selected cargo assets, attibute=%s, category=%d:\n", request.cargoattribute, request.cargocategory) + for _i,_asset in pairs(_assets) do + local asset=_asset --#WAREHOUSE.Assetitem + text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d",_i, asset.templatename, asset.unittype, asset.category, asset.nunits) + end + self:T(self.wid..text) + + if request.transporttype ~= WAREHOUSE.TransportType.SELFPROPELLED then + + -- Set chosen transport assets. + request.transportassets=_transports + request.transportattribute=_transports[1].attribute + request.transportcategory=_transports[1].category + request.ntransport=#_transports + + -- Debug info: + local text=string.format("Selected transport assets, attibute=%s, category=%d:\n", request.transportattribute, request.transportcategory) + for _i,_asset in pairs(_transports) do + local asset=_asset --#WAREHOUSE.Assetitem + text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d\n",_i, asset.templatename, asset.unittype, asset.category, asset.nunits) + end + self:T(self.wid..text) + + end + + return true +end + +---Get (optimized) transport carriers for the given assets to be transported. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Pendingitem Chosen request. +function WAREHOUSE:_GetTransportsForAssets(request) + + -- Get all transports of the requested type in stock. + local transports=self:_FilterStock(self.stock, WAREHOUSE.Descriptor.ATTRIBUTE, request.transporttype) + + -- Copy asset. + local cargoassets=UTILS.DeepCopy(request.cargoassets) + local cargoset=request.transportcargoset + + -- TODO: Get weight and cargo bay from CARGO_GROUP + --local cargogroup=CARGO_GROUP:New(CargoGroup,Type,Name,LoadRadius,NearRadius) + --cargogroup:GetWeight() + + -- Sort transport carriers w.r.t. cargo bay size. + local function sort_transports(a,b) + return a.cargobaymax>b.cargobaymax + end + + -- Sort cargo assets w.r.t. weight in assending order. + local function sort_cargoassets(a,b) + return a.weight>b.weight + end + + -- Sort tables. + table.sort(transports, sort_transports) + table.sort(cargoassets, sort_cargoassets) + + -- Total cargo bay size of all groups. + self:T2(self.wid.."Transport capability:") + local totalbay=0 + for i=1,#transports do + local transport=transports[i] --#WAREHOUSE.Assetitem + for j=1,transport.nunits do + totalbay=totalbay+transport.cargobay[j] + self:T2(self.wid..string.format("Cargo bay = %d (unit=%d)", transport.cargobay[j], j)) + end + end + self:T2(self.wid..string.format("Total capacity = %d", totalbay)) + + -- Total cargo weight of all assets to transports. + self:T2(self.wid.."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)) + + -- Transports used. + local used_transports={} + + -- Loop over all transport groups, largest cargobaymax to smallest. + for i=1,#transports do + + -- Shortcut for carrier and cargo bay + local transport=transports[i] + + -- Cargo put into carrier. + local putintocarrier={} + + -- Cargo assigned to this transport group? + local used=false + + -- Loop over all units + for k=1,transport.nunits do + + -- Get cargo bay of this carrier. + local cargobay=transport.cargobay[k] + + -- Loop over cargo assets. + for j,asset in pairs(cargoassets) do + local asset=asset --#WAREHOUSE.Assetitem + + -- How many times does the cargo fit into the carrier? + local delta=cargobay-asset.weight + --env.info(string.format("k=%d, j=%d delta=%d cargobay=%d weight=%d", k, j, delta, cargobay, asset.weight)) + + --self:E(self.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)) + + -- 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)) + + -- 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)) + end + + end -- loop over assets + end -- loop over units + + -- Remove cargo assets from list. Needs to be done back-to-front in order not to confuse the loop. + for j=#putintocarrier,1, -1 do + + local nput=putintocarrier[j] + local cargo=cargoassets[nput] + + -- Need to check if multiple units in a group and the group has already been removed! + -- TODO: This might need to be improved but is working okay so far. + if cargo then + -- Remove this group because it was used. + self:T2(self.wid..string.format("Cargo id=%d assigned for carrier id=%d", cargo.uid, transport.uid)) + table.remove(cargoassets, nput) + end + end + + -- Cargo was assined for this carrier. + if used then + table.insert(used_transports, transport) + end + + -- Convert relative quantity (all, half) to absolute number if necessary. + local ntrans=self:_QuantityRel2Abs(request.ntransport, #transports) + + -- Max number of transport groups reached? + if #used_transports >= ntrans then + request.ntransport=#used_transports + break + end + end + + -- Debug info. + local text=string.format("Used Transports for request %d to warehouse %s:\n", request.uid, request.warehouse.alias) + local totalcargobay=0 + for _i,_transport in pairs(used_transports) do + local transport=_transport --#WAREHOUSE.Assetitem + text=text..string.format("%d) %s: cargobay tot = %d kg, cargobay max = %d kg, nunits=%d\n", _i, transport.unittype, transport.cargobaytot, transport.cargobaymax, transport.nunits) + totalcargobay=totalcargobay+transport.cargobaytot + --for _,cargobay in pairs(transport.cargobay) do + -- env.info(string.format("cargobay %d", cargobay)) + --end + end + text=text..string.format("Total cargo bay capacity = %.1f kg\n", totalcargobay) + text=text..string.format("Total cargo weight = %.1f kg\n", totalcargoweight) + text=text..string.format("Minimum number of runs = %.1f", totalcargoweight/totalcargobay) + self:_DebugMessage(text) + + return used_transports +end + +---Relative to absolute quantity. +-- @param #WAREHOUSE self +-- @param #string relative Relative number in terms of @{#WAREHOUSE.Quantity}. +-- @param #number ntot Total number. +-- @return #number Absolute number. +function WAREHOUSE:_QuantityRel2Abs(relative, ntot) + + local nabs=0 + + -- Handle string input for nmax. + if type(relative)=="string" then + if relative==WAREHOUSE.Quantity.ALL then + nabs=ntot + elseif relative==WAREHOUSE.Quantity.THREEQUARTERS then + nabs=UTILS.Round(ntot*3/4) + elseif relative==WAREHOUSE.Quantity.HALF then + nabs=UTILS.Round(ntot/2) + elseif relative==WAREHOUSE.Quantity.THIRD then + nabs=UTILS.Round(ntot/3) + elseif relative==WAREHOUSE.Quantity.QUARTER then + nabs=UTILS.Round(ntot/4) + else + nabs=math.min(1, ntot) + end + else + nabs=relative + end + + self:T2(self.wid..string.format("Relative %s: tot=%d, abs=%.2f", tostring(relative), ntot, nabs)) + + return nabs +end + +---Sorts the queue and checks if the request can be fulfilled. +-- @param #WAREHOUSE self +-- @return #WAREHOUSE.Queueitem Chosen request. +function WAREHOUSE:_CheckQueue() + + -- Sort queue wrt to first prio and then qid. + self:_SortQueue() + + -- Search for a request we can execute. + local request=nil --#WAREHOUSE.Queueitem + + local invalid={} + local gotit=false + for _,_qitem in ipairs(self.queue) do + local qitem=_qitem --#WAREHOUSE.Queueitem + + -- Check if request is valid in general. + local valid=self:_CheckRequestValid(qitem) + + -- Check if request is possible now. + local okay=false + if valid then + okay=self:_CheckRequestNow(qitem) + else + -- Remember invalid request and delete later in order not to confuse the loop. + table.insert(invalid, qitem) + end + + -- Get the first valid request that can be executed now. + if okay and valid and not gotit then + request=qitem + gotit=true + break + end + end + + -- Delete invalid requests. + for _,_request in pairs(invalid) do + self:T(self.wid..string.format("Deleting invalid request id=%d.",_request.uid)) + self:_DeleteQueueItem(_request, self.queue) + end + + -- Execute request. + return request +end + +--- Simple task function. Can be used to call a function which has the warehouse and the executing group as parameters. +-- @param #WAREHOUSE self +-- @param #string Function The name of the function to call passed as string. +-- @param Wrapper.Group#GROUP group The group which is meant. +function WAREHOUSE:_SimpleTaskFunction(Function, group) + self:F2({Function}) + + -- Name of the warehouse (static) object. + local warehouse=self.warehouse:GetName() + local groupname=group:GetName() + + -- Task script. + local DCSScript = {} + --DCSScript[#DCSScript+1] = string.format('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 "...". + 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 + +--- Get the proper terminal type based on generalized attribute of the group. +--@param #WAREHOUSE self +--@param #WAREHOUSE.Attribute _attribute Generlized attibute of unit. +--@param #number _category Airbase category. +--@return Wrapper.Airbase#AIRBASE.TerminalType Terminal type for this group. +function WAREHOUSE:_GetTerminal(_attribute, _category) + + -- Default terminal is "large". + local _terminal=AIRBASE.TerminalType.OpenBig + + if _attribute==WAREHOUSE.Attribute.AIR_FIGHTER then + -- Fighter ==> small. + _terminal=AIRBASE.TerminalType.FighterAircraft + elseif _attribute==WAREHOUSE.Attribute.AIR_BOMBER or _attribute==WAREHOUSE.Attribute.AIR_TRANSPORTPLANE or _attribute==WAREHOUSE.Attribute.AIR_TANKER or _attribute==WAREHOUSE.Attribute.AIR_AWACS then + -- Bigger aircraft. + _terminal=AIRBASE.TerminalType.OpenBig + elseif _attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO then + -- Helicopter. + _terminal=AIRBASE.TerminalType.HelicopterUsable + else + --_terminal=AIRBASE.TerminalType.OpenMedOrBig + end + + -- For ships, we allow medium spots for all fixed wing aircraft. There are smaller tankers and AWACS aircraft that can use a carrier. + if _category==Airbase.Category.SHIP then + if not (_attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO) then + _terminal=AIRBASE.TerminalType.OpenMedOrBig + end + end + + return _terminal +end + + +--- Seach unoccupied parking spots at the airbase for a list of assets. For each asset group a list of parking spots is returned. +-- During the search also the not yet spawned asset aircraft are considered. +-- If not enough spots for all asset units could be found, the routine returns nil! +-- @param #WAREHOUSE self +-- @param Wrapper.Airbase#AIRBASE airbase The airbase where we search for parking spots. +-- @param #table assets A table of assets for which the parking spots are needed. +-- @return #table Table of coordinates and terminal IDs of free parking spots. Each table entry has the elements .Coordinate and .TerminalID. +function WAREHOUSE:_FindParkingForAssets(airbase, assets) + + -- Init default + local scanradius=100 + local scanunits=true + local scanstatics=true + local scanscenery=false + local verysafe=false + + -- Function calculating the overlap of two (square) objects. + local function _overlap(l1,l2,dist) + local safedist=(l1/2+l2/2)*1.05 -- 5% safety margine added to safe distance! + local safe = (dist > safedist) + self:T3(string.format("l1=%.1f l2=%.1f s=%.1f d=%.1f ==> safe=%s", l1,l2,safedist,dist,tostring(safe))) + return safe + end + + -- Get 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. + for _,_unit in pairs(_units) do + local unit=_unit --Wrapper.Unit#UNIT + local _coord=unit:GetCoordinate() + local _size=self:_GetObjectSize(unit:GetDCSObject()) + local _name=unit:GetName() + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"}) + end + + -- Check all statics. + for _,static in pairs(_statics) do + local _vec3=static:getPoint() + local _coord=COORDINATE:NewFromVec3(_vec3) + local _name=static:getName() + local _size=self:_GetObjectSize(static) + table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="static"}) + end + + -- Check all scenery. + for _,scenery in pairs(_sceneries) do + local _vec3=scenery:getPoint() + local _coord=COORDINATE:NewFromVec3(_vec3) + local _name=scenery:getTypeName() + local _size=self:_GetObjectSize(scenery) + table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="scenery"}) + end + + end + + -- Parking data for all assets. + local parking={} + + -- 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, self:GetAirbaseCategory()) + + -- Asset specific parking. + parking[_asset.uid]={} + + -- Loop over all units - each one needs a spot. + for i=1,_asset.nunits do + + -- Loop over all parking spots. + local gotit=false + for _,_parkingspot in pairs(parkingdata) do + local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot + + -- Check correct terminal type for asset. We don't want helos in shelters etc. + if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) 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)) + + local free=true + local problem=nil + + -- Safe parking using TO_AC from DCS result. + if self.safeparking and _toac then + free=false + self:T("Parking spot %d is occupied by other aircraft taking off or landing.", _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)) + free=false + problem=obstacle + problem.dist=dist + break + else + --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", _asset.templatename, _asset.uid, _termid, dist)) + end + + end + + -- Check if spot is free + if free then + + -- Add parkingspot for this asset unit. + table.insert(parking[_asset.uid], parkingspot) + + self:T(self.wid..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)) + 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)) + return nil + end + end -- loop over asset units + end -- loop over asset groups + + return parking +end + + +--- Get the request belonging to a group. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group from which the info is gathered. +-- @param #table queue Queue holding all requests. +-- @return #WAREHOUSE.Pendingitem The request belonging to this group. +function WAREHOUSE:_GetRequestOfGroup(group, queue) + + -- Get warehouse, asset and request ID from group name. + local wid,aid,rid=self:_GetIDsFromGroup(group) + + -- Find the request. + for _,_request in pairs(queue) do + local request=_request --#WAREHOUSE.Queueitem + if request.uid==rid then + return request + end + end + +end + +--- Is the group a used as transporter for a given request? +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group from which the info is gathered. +-- @param #WAREHOUSE.Pendingitem request Request. +-- @return #boolean True if group is transport, false if group is cargo and nil otherwise. +function WAREHOUSE:_GroupIsTransport(group, request) + + -- 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 + + 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 + else + return group:GetName() + end +end + + +--- Get warehouse id, asset id and request id from group name (alias). +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group The group from which the info is gathered. +-- @return #number Warehouse ID. +-- @return #number Asset ID. +-- @return #number Request ID. +function WAREHOUSE:_GetIDsFromGroup(group) + + ---@param #string text The text to analyse. + local function analyse(text) + + -- Get rid of #0001 tail from spawn. + local unspawned=UTILS.Split(text, "#")[1] + + -- Split keywords. + local keywords=UTILS.Split(unspawned, "_") + local _wid=nil -- warehouse UID + local _aid=nil -- asset UID + local _rid=nil -- request UID + + -- Loop over keys. + for _,keys in pairs(keywords) do + local str=UTILS.Split(keys, "-") + local key=str[1] + local val=str[2] + if key:find("WID") then + _wid=tonumber(val) + elseif key:find("AID") then + _aid=tonumber(val) + elseif key:find("RID") then + _rid=tonumber(val) + end + end + + return _wid,_aid,_rid + end + + if group then + + -- Group name + local name=group:GetName() + + -- Get ids + local wid,aid,rid=analyse(name) + + -- 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))) + + return wid,aid,rid + else + self:E("WARNING: Group not found in GetIDsFromGroup() function!") + end + +end + +--- Filter stock assets by table entry. +-- @param #WAREHOUSE self +-- @param #table stock Table holding all assets in stock of the warehouse. Each entry is of type @{#WAREHOUSE.Assetitem}. +-- @param #string descriptor Descriptor describing the filtered assets. +-- @param attribute Value of the descriptor. +-- @param #number nmax (Optional) Maximum number of items that will be returned. Default nmax=nil is all matching items are returned. +-- @param #boolean mobile (Optional) If true, filter only mobile assets. +-- @return #table Filtered stock items table. +-- @return #number Total number of (requested) assets available. +-- @return #boolean If true, enough assets are available. +function WAREHOUSE:_FilterStock(stock, descriptor, attribute, nmax, mobile) + + -- Default all. + nmax=nmax or WAREHOUSE.Quantity.ALL + if mobile==nil then + mobile=false + end + + -- Filtered array. + local filtered={} + + -- Count total number in stock. + local ntot=0 + for _,_asset in ipairs(stock) do + local asset=_asset --#WAREHOUSE.Assetitem + local ismobile=asset.speedmax>0 + if asset[descriptor]==attribute then + if (mobile==true and ismobile) or mobile==false then + ntot=ntot+1 + end + end + end + + -- Treat case where ntot=0, i.e. no assets at all. + if ntot==0 then + return filtered, ntot, false + end + + -- Convert relative to absolute number if necessary. + nmax=self:_QuantityRel2Abs(nmax,ntot) + + -- Loop over stock items. + for _i,_asset in ipairs(stock) do + local asset=_asset --#WAREHOUSE.Assetitem + + -- Check if asset has the right attribute. + if asset[descriptor]==attribute then + + -- Check if asset has to be mobile. + if (mobile and asset.speedmax>0) or (not mobile) then + + -- Add asset to filtered table. + table.insert(filtered, asset) + + -- Break loop if nmax was reached. + if nmax~=nil and #filtered>=nmax then + return filtered, ntot, true + end + + end + end + end + + return filtered, ntot, ntot>=nmax +end + +--- Check if a group has a generalized attribute. +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group MOOSE group object. +-- @param #WAREHOUSE.Attribute attribute Attribute to check. +-- @return #boolean True if group has the specified attribute. +function WAREHOUSE:_HasAttribute(group, attribute) + + if group then + local groupattribute=self:_GetAttribute(group) + return groupattribute==attribute + end + + return false +end + +--- Get the generalized attribute of a group. +-- Note that for a heterogenious group, the attribute is determined from the attribute of the first unit! +-- @param #WAREHOUSE self +-- @param Wrapper.Group#GROUP group MOOSE group object. +-- @return #WAREHOUSE.Attribute Generalized attribute of the group. +function WAREHOUSE:_GetAttribute(group) + + -- Default + local attribute=WAREHOUSE.Attribute.OTHER_UNKNOWN --#WAREHOUSE.Attribute + + if group then + + ----------- + --- Air --- + ----------- + -- Planes + local transportplane=group:HasAttribute("Transports") and group:HasAttribute("Planes") + local awacs=group:HasAttribute("AWACS") + local fighter=group:HasAttribute("Fighters") or group:HasAttribute("Interceptors") or group:HasAttribute("Multirole fighters") or (group:HasAttribute("Bombers") and not group:HasAttribute("Strategic bombers")) + local bomber=group:HasAttribute("Strategic bombers") + local tanker=group:HasAttribute("Tankers") + local uav=group:HasAttribute("UAVs") + -- Helicopters + local transporthelo=group:HasAttribute("Transport helicopters") + local attackhelicopter=group:HasAttribute("Attack helicopters") + + -------------- + --- Ground --- + -------------- + -- Ground + local apc=group:HasAttribute("Infantry carriers") + local truck=group:HasAttribute("Trucks") and group:GetCategory()==Group.Category.GROUND + local infantry=group:HasAttribute("Infantry") + local artillery=group:HasAttribute("Artillery") + local tank=group:HasAttribute("Old Tanks") or group:HasAttribute("Modern Tanks") + local aaa=group:HasAttribute("AAA") + local ewr=group:HasAttribute("EWR") + local sam=group:HasAttribute("SAM elements") and (not group:HasAttribute("AAA")) + -- Train + local train=group:GetCategory()==Group.Category.TRAIN + + ------------- + --- Naval --- + ------------- + -- Ships + local aircraftcarrier=group:HasAttribute("Aircraft Carriers") + local warship=group:HasAttribute("Heavy armed ships") + local armedship=group:HasAttribute("Armed ships") + local unarmedship=group:HasAttribute("Unarmed ships") + + + -- Define attribute. Order is important. + if transportplane then + attribute=WAREHOUSE.Attribute.AIR_TRANSPORTPLANE + elseif awacs then + attribute=WAREHOUSE.Attribute.AIR_AWACS + elseif fighter then + attribute=WAREHOUSE.Attribute.AIR_FIGHTER + elseif bomber then + attribute=WAREHOUSE.Attribute.AIR_BOMBER + elseif tanker then + attribute=WAREHOUSE.Attribute.AIR_TANKER + elseif transporthelo then + attribute=WAREHOUSE.Attribute.AIR_TRANSPORTHELO + elseif attackhelicopter then + attribute=WAREHOUSE.Attribute.AIR_ATTACKHELO + elseif uav then + attribute=WAREHOUSE.Attribute.AIR_UAV + elseif apc then + attribute=WAREHOUSE.Attribute.GROUND_APC + elseif infantry then + attribute=WAREHOUSE.Attribute.GROUND_INFANTRY + elseif artillery then + attribute=WAREHOUSE.Attribute.GROUND_ARTILLERY + elseif tank then + attribute=WAREHOUSE.Attribute.GROUND_TANK + elseif aaa then + attribute=WAREHOUSE.Attribute.GROUND_AAA + elseif ewr then + attribute=WAREHOUSE.Attribute.GROUND_EWR + elseif sam then + attribute=WAREHOUSE.Attribute.GROUND_SAM + elseif truck then + attribute=WAREHOUSE.Attribute.GROUND_TRUCK + elseif train then + attribute=WAREHOUSE.Attribute.GROUND_TRAIN + elseif aircraftcarrier then + attribute=WAREHOUSE.Attribute.NAVAL_AIRCRAFTCARRIER + elseif warship then + attribute=WAREHOUSE.Attribute.NAVAL_WARSHIP + elseif armedship then + attribute=WAREHOUSE.Attribute.NAVAL_ARMEDSHIP + elseif unarmedship then + attribute=WAREHOUSE.Attribute.NAVAL_UNARMEDSHIP + else + if group:IsGround() then + attribute=WAREHOUSE.Attribute.GROUND_OTHER + elseif group:IsShip() then + attribute=WAREHOUSE.Attribute.NAVAL_OTHER + elseif group:IsAir() then + attribute=WAREHOUSE.Attribute.AIR_OTHER + else + attribute=WAREHOUSE.Attribute.OTHER_UNKNOWN + end + end + end + + return attribute +end + +--- Size of the bounding box of a DCS object derived from the DCS descriptor table. If boundinb box is nil, a size of zero is returned. +-- @param #WAREHOUSE self +-- @param DCS#Object DCSobject The DCS object for which the size is needed. +-- @return #number Max size of object in meters (length (x) or width (z) components not including height (y)). +-- @return #number Length (x component) of size. +-- @return #number Height (y component) of size. +-- @return #number Width (z component) of size. +function WAREHOUSE:_GetObjectSize(DCSobject) + local DCSdesc=DCSobject:getDesc() + if DCSdesc.box then + local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x) --length + local y=DCSdesc.box.max.y+math.abs(DCSdesc.box.min.y) --height + local z=DCSdesc.box.max.z+math.abs(DCSdesc.box.min.z) --width + return math.max(x,z), x , y, z + end + return 0,0,0,0 +end + +--- Returns the number of assets for each generalized attribute. +-- @param #WAREHOUSE self +-- @param #table stock The stock of the warehouse. +-- @return #table Data table holding the numbers, i.e. data[attibute]=n. +function WAREHOUSE:GetStockInfo(stock) + + local _data={} + for _j,_attribute in pairs(WAREHOUSE.Attribute) do + + local n=0 + for _i,_item in pairs(stock) do + local _ite=_item --#WAREHOUSE.Assetitem + if _ite.attribute==_attribute then + n=n+1 + end + end + + _data[_attribute]=n + end + + return _data +end + +--- Delete an asset item from stock. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Assetitem stockitem Asset item to delete from stock table. +function WAREHOUSE:_DeleteStockItem(stockitem) + for i=1,#self.stock do + local item=self.stock[i] --#WAREHOUSE.Assetitem + if item.uid==stockitem.uid then + table.remove(self.stock,i) + break + end + end +end + +--- Delete item from queue. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Queueitem qitem Item of queue to be removed. +-- @param #table queue The queue from which the item should be deleted. +function WAREHOUSE:_DeleteQueueItem(qitem, queue) + self:F({qitem=qitem, queue=queue}) + + for i=1,#queue do + local _item=queue[i] --#WAREHOUSE.Queueitem + if _item.uid==qitem.uid then + self:T(self.wid..string.format("Deleting queue item id=%d.", qitem.uid)) + table.remove(queue,i) + break + end + end +end + +--- Sort requests queue wrt prio and request uid. +-- @param #WAREHOUSE self +function WAREHOUSE:_SortQueue() + self:F3() + -- Sort. + local function _sort(a, b) + return (a.prio < b.prio) or (a.prio==b.prio and a.uid < b.uid) + end + table.sort(self.queue, _sort) +end + +--- Prints the queue to DCS.log file. +-- @param #WAREHOUSE self +-- @param #table queue Queue to print. +-- @param #string name Name of the queue for info reasons. +function WAREHOUSE:_PrintQueue(queue, name) + + local total="Empty" + if #queue>0 then + total=string.format("Total = %d", #queue) + end + + -- Init string. + local text=string.format("%s at %s: %s",name, self.alias, total) + + for i,qitem in ipairs(queue) do + local qitem=qitem --#WAREHOUSE.Pendingitem + + local uid=qitem.uid + local prio=qitem.prio + local clock="N/A" + if qitem.timestamp then + clock=tostring(UTILS.SecondsToClock(qitem.timestamp)) + end + local assignment=tostring(qitem.assignment) + local requestor=qitem.warehouse.alias + local airbasename=qitem.warehouse:GetAirbaseName() + local requestorAirbaseCat=qitem.warehouse:GetAirbaseCategory() + local assetdesc=qitem.assetdesc + local assetdescval=qitem.assetdescval + local nasset=tostring(qitem.nasset) + local ndelivered=tostring(qitem.ndelivered) + local ncargogroupset="N/A" + if qitem.cargogroupset then + ncargogroupset=tostring(qitem.cargogroupset:Count()) + end + local transporttype="N/A" + if qitem.transporttype then + transporttype=qitem.transporttype + end + local ntransport="N/A" + if qitem.ntransport then + ntransport=tostring(qitem.ntransport) + end + local ntransportalive="N/A" + if qitem.transportgroupset then + ntransportalive=tostring(qitem.transportgroupset:Count()) + end + local ntransporthome="N/A" + if qitem.ntransporthome then + ntransporthome=tostring(qitem.ntransporthome) + end + + -- Output text: + text=text..string.format( + "\n%d) UID=%d, Prio=%d, Clock=%s, Assignment=%s | Requestor=%s [Airbase=%s, category=%d] | Assets(%s)=%s: #requested=%s / #alive=%s / #delivered=%s | Transport=%s: #requested=%s / #alive=%s / #home=%s", + i, uid, prio, clock, assignment, requestor, airbasename, requestorAirbaseCat, assetdesc, assetdescval, nasset, ncargogroupset, ndelivered, transporttype, ntransport, ntransportalive, ntransporthome) + + end + + self:I(self.wid..text) +end + +--- Display status of warehouse. +-- @param #WAREHOUSE self +function WAREHOUSE:_DisplayStatus() + local text=string.format("\n------------------------------------------------------\n") + text=text..string.format("Warehouse %s status: %s\n", self.alias, self:GetState()) + text=text..string.format("------------------------------------------------------\n") + text=text..string.format("Coalition name = %s\n", self:GetCoalitionName()) + text=text..string.format("Country name = %s\n", self:GetCountryName()) + text=text..string.format("Airbase name = %s (category=%d)\n", self:GetAirbaseName(), self:GetAirbaseCategory()) + text=text..string.format("Queued requests = %d\n", #self.queue) + text=text..string.format("Pending requests = %d\n", #self.pending) + text=text..string.format("------------------------------------------------------\n") + text=text..self:_GetStockAssetsText() + self:T(text) +end + +--- Get text about warehouse stock. +-- @param #WAREHOUSE self +-- @param #boolean messagetoall If true, send message to all. +-- @return #string Text about warehouse stock +function WAREHOUSE:_GetStockAssetsText(messagetoall) + + -- Get assets in stock. + local _data=self:GetStockInfo(self.stock) + + -- Text. + local text="Stock:\n" + local total=0 + for _attribute,_count in pairs(_data) do + if _count>0 then + local attribute=tostring(UTILS.Split(_attribute, "_")[2]) + text=text..string.format("%s = %d\n", attribute,_count) + total=total+_count + end + end + text=text..string.format("===================\n") + text=text..string.format("Total = %d\n", total) + text=text..string.format("------------------------------------------------------\n") + + -- Send message? + MESSAGE:New(text, 10):ToAllIf(messagetoall) + + return text +end + +--- Create or update mark text at warehouse, which is displayed in F10 map showing how many assets of each type are in stock. +-- Only the coaliton of the warehouse owner is able to see it. +-- @param #WAREHOUSE self +-- @return #string Text about warehouse stock +function WAREHOUSE:_UpdateWarehouseMarkText() + + -- 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 + +--- Display stock items of warehouse. +-- @param #WAREHOUSE self +-- @param #table stock Table holding all assets in stock of the warehouse. Each entry is of type @{#WAREHOUSE.Assetitem}. +function WAREHOUSE:_DisplayStockItems(stock) + + local text=self.wid..string.format("Warehouse %s stock assets:", self.alias) + for _i,_stock in pairs(stock) do + local mystock=_stock --#WAREHOUSE.Assetitem + local name=mystock.templatename + local category=mystock.category + local cargobaymax=mystock.cargobaymax + local cargobaytot=mystock.cargobaytot + local nunits=mystock.nunits + local range=mystock.range + local size=mystock.size + local speed=mystock.speedmax + local uid=mystock.uid + local unittype=mystock.unittype + local weight=mystock.weight + local attribute=mystock.attribute + text=text..string.format("\n%02d) uid=%d, name=%s, unittype=%s, category=%d, attribute=%s, nunits=%d, speed=%.1f km/h, range=%.1f km, size=%.1f m, weight=%.1f kg, cargobax max=%.1f kg tot=%.1f kg", + _i, uid, name, unittype, category, attribute, nunits, speed, range/1000, size, weight, cargobaymax, cargobaytot) + end + + self:T3(text) +end + +--- Fireworks! +-- @param #WAREHOUSE self +-- @param Core.Point#COORDINATE coord +function WAREHOUSE:_Fireworks(coord) + + -- Place. + coord=coord or self:GetCoordinate() + + -- Fireworks! + for i=1,91 do + local color=math.random(0,3) + coord:Flare(color, i-1) + end +end + +--- Info Message. Message send to coalition if reports or debug mode activated (and duration > 0). Text self:I(text) added to DCS.log file. +-- @param #WAREHOUSE self +-- @param #string text The text of the error message. +-- @param #number duration Message display duration in seconds. Default 20 sec. If duration is zero, no message is displayed. +function WAREHOUSE:_InfoMessage(text, duration) + duration=duration or 20 + if duration>0 then + MESSAGE:New(text, duration):ToCoalitionIf(self:GetCoalition(), self.Debug or self.Report) + end + self:I(self.wid..text) +end + + +--- Debug message. Message send to all if debug mode is activated (and duration > 0). Text self:T(text) added to DCS.log file. +-- @param #WAREHOUSE self +-- @param #string text The text of the error message. +-- @param #number duration Message display duration in seconds. Default 20 sec. If duration is zero, no message is displayed. +function WAREHOUSE:_DebugMessage(text, duration) + duration=duration or 20 + if duration>0 then + MESSAGE:New(text, duration):ToAllIf(self.Debug) + end + self:T(self.wid..text) +end + +--- Error message. Message send to all (if duration > 0). Text self:E(text) added to DCS.log file. +-- @param #WAREHOUSE self +-- @param #string text The text of the error message. +-- @param #number duration Message display duration in seconds. Default 20 sec. If duration is zero, no message is displayed. +function WAREHOUSE:_ErrorMessage(text, duration) + duration=duration or 20 + if duration>0 then + MESSAGE:New(text, duration):ToAll() + end + self:E(self.wid..text) +end + + +--- Calculate the maximum height an aircraft can reach for the given parameters. +-- @param #WAREHOUSE self +-- @param #number D Total distance in meters from Departure to holding point at destination. +-- @param #number alphaC Climb angle in rad. +-- @param #number alphaD Descent angle in rad. +-- @param #number Hdep AGL altitude of departure point. +-- @param #number Hdest AGL altitude of destination point. +-- @param #number Deltahhold Relative altitude of holding point above destination. +-- @return #number Maximum height the aircraft can reach. +function WAREHOUSE:_GetMaxHeight(D, alphaC, alphaD, Hdep, Hdest, Deltahhold) + + local Hhold=Hdest+Deltahhold + local hdest=Hdest-Hdep + local hhold=hdest+Deltahhold + + local Dp=math.sqrt(D^2 + hhold^2) + + local alphaS=math.atan(hdest/D) -- slope angle + local alphaH=math.atan(hhold/D) -- angle to holding point (could be necative!) + + local alphaCp=alphaC-alphaH -- climb angle with slope + local alphaDp=alphaD+alphaH -- descent angle with slope + + -- ASA triangle. + local gammap=math.pi-alphaCp-alphaDp + local sCp=Dp*math.sin(alphaDp)/math.sin(gammap) + local sDp=Dp*math.sin(alphaCp)/math.sin(gammap) + + -- Max height from departure. + local hmax=sCp*math.sin(alphaC) + + -- Debug info. + if self.Debug then + env.info(string.format("Hdep = %.3f km", Hdep/1000)) + env.info(string.format("Hdest = %.3f km", Hdest/1000)) + env.info(string.format("DetaHold= %.3f km", Deltahhold/1000)) + env.info() + env.info(string.format("D = %.3f km", D/1000)) + env.info(string.format("Dp = %.3f km", Dp/1000)) + env.info() + env.info(string.format("alphaC = %.3f Deg", math.deg(alphaC))) + env.info(string.format("alphaCp = %.3f Deg", math.deg(alphaCp))) + env.info() + env.info(string.format("alphaD = %.3f Deg", math.deg(alphaD))) + env.info(string.format("alphaDp = %.3f Deg", math.deg(alphaDp))) + env.info() + env.info(string.format("alphaS = %.3f Deg", math.deg(alphaS))) + env.info(string.format("alphaH = %.3f Deg", math.deg(alphaH))) + env.info() + env.info(string.format("sCp = %.3f km", sCp/1000)) + env.info(string.format("sDp = %.3f km", sDp/1000)) + env.info() + env.info(string.format("hmax = %.3f km", hmax/1000)) + env.info() + + -- Descent height + local hdescent=hmax-hhold + + local dClimb = hmax/math.tan(alphaC) + local dDescent = (hmax-hhold)/math.tan(alphaD) + local dCruise = D-dClimb-dDescent + + env.info(string.format("hmax = %.3f km", hmax/1000)) + env.info(string.format("hdescent = %.3f km", hdescent/1000)) + env.info(string.format("Dclimb = %.3f km", dClimb/1000)) + env.info(string.format("Dcruise = %.3f km", dCruise/1000)) + env.info(string.format("Ddescent = %.3f km", dDescent/1000)) + env.info() + end + + return hmax +end + + +--- Make a flight plan from a departure to a destination airport. +-- @param #WAREHOUSE self +-- @param #WAREHOUSE.Assetitem asset +-- @param Wrapper.Airbase#AIRBASE departure Departure airbase. +-- @param Wrapper.Airbase#AIRBASE destination Destination airbase. +-- @return #table Table of flightplan waypoints. +-- @return #table Table of flightplan coordinates. +function WAREHOUSE:_GetFlightplan(asset, departure, destination) + + -- Parameters in SI units (m/s, m). + local Vmax=asset.speedmax/3.6 + local Range=asset.range + local category=asset.category + local ceiling=asset.DCSdesc.Hmax + local Vymax=asset.DCSdesc.VyMax + + -- Max cruise speed 90% of max speed. + local VxCruiseMax=0.90*Vmax + + -- Min cruise speed 70% of max cruise or 600 km/h whichever is lower. + local VxCruiseMin = math.min(VxCruiseMax*0.70, 166) + + -- Cruise speed (randomized). Expectation value at midpoint between min and max. + local VxCruise = UTILS.RandomGaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin, (VxCruiseMax-VxCruiseMax)/4, VxCruiseMin, VxCruiseMax) + + -- Climb speed 90% ov Vmax but max 720 km/h. + local VxClimb = math.min(Vmax*0.90, 200) + + -- Descent speed 60% of Vmax but max 500 km/h. + local VxDescent = math.min(Vmax*0.60, 140) + + -- Holding speed is 90% of descent speed. + local VxHolding = VxDescent*0.9 + + -- Final leg is 90% of holding speed. + local VxFinal = VxHolding*0.9 + + -- Reasonably civil climb speed Vy=1500 ft/min = 7.6 m/s but max aircraft specific climb rate. + local VyClimb=math.min(7.6, Vymax) + + -- Climb angle in rad. + --local AlphaClimb=math.asin(VyClimb/VxClimb) + local AlphaClimb=math.rad(4) + + -- Descent angle in rad. Moderate 4 degrees. + local AlphaDescent=math.rad(4) + + -- Expected cruise level (peak of Gaussian distribution) + local FLcruise_expect=150*RAT.unit.FL2m + if category==Group.Category.HELICOPTER then + FLcruise_expect=1000 -- 1000 m ASL + end + + ------------------------- + --- DEPARTURE AIRPORT --- + ------------------------- + + -- Coordinates of departure point. + local Pdeparture=departure:GetCoordinate() + + -- Height ASL of departure point. + local H_departure=Pdeparture.y + + --------------------------- + --- DESTINATION AIRPORT --- + --------------------------- + + -- Position of destination airport. + local Pdestination=destination:GetCoordinate() + + -- Height ASL of destination airport/zone. + local H_destination=Pdestination.y + + ----------------------------- + --- DESCENT/HOLDING POINT --- + ----------------------------- + + -- Get a random point between 5 and 10 km away from the destination. + local Rhmin=5000 + local Rhmax=10000 + + -- For helos we set a distance between 500 to 1000 m. + if category==Group.Category.HELICOPTER then + Rhmin=500 + Rhmax=1000 + end + + -- Coordinates of the holding point. y is the land height at that point. + local Pholding=Pdestination:GetRandomCoordinateInRadius(Rhmax, Rhmin) + + -- Distance from holding point to final destination (not used). + local d_holding=Pholding:Get2DDistance(Pdestination) + + -- AGL height of holding point. + local H_holding=Pholding.y + + --------------- + --- GENERAL --- + --------------- + + -- We go directly to the holding point not the destination airport. From there, planes are guided by DCS to final approach. + local heading=Pdeparture:HeadingTo(Pholding) + local d_total=Pdeparture:Get2DDistance(Pholding) + + ------------------------------ + --- Holding Point Altitude --- + ------------------------------ + + -- Holding point altitude. For planes between 1600 and 2400 m AGL. For helos 160 to 240 m AGL. + local h_holding=1200 + if category==Group.Category.HELICOPTER then + h_holding=150 + end + h_holding=UTILS.Randomize(h_holding, 0.2) + + -- Max holding altitude. + local DeltaholdingMax=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, 0) + + if h_holding>DeltaholdingMax then + h_holding=math.abs(DeltaholdingMax) + end + + -- This is the height ASL of the holding point we want to fly to. + local Hh_holding=H_holding+h_holding + + --------------------------- + --- Max Flight Altitude --- + --------------------------- + + -- Get max flight altitude relative to H_departure. + local h_max=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, h_holding) + + -- Max flight level ASL aircraft can reach for given angles and distance. + local FLmax = h_max+H_departure + + --CRUISE + -- Min cruise alt is just above holding point at destination or departure height, whatever is larger. + local FLmin=math.max(H_departure, Hh_holding) + + -- Ensure that FLmax not above its service ceiling. + FLmax=math.min(FLmax, ceiling) + + -- If the route is very short we set FLmin a bit lower than FLmax. + if FLmin>FLmax then + FLmin=FLmax + end + + -- Expected cruise altitude - peak of gaussian distribution. + if FLcruise_expectFLmax then + FLcruise_expect=FLmax + end + + -- Set cruise altitude. Selected from Gaussian distribution but limited to FLmin and FLmax. + local FLcruise=UTILS.RandomGaussian(FLcruise_expect, math.abs(FLmax-FLmin)/4, FLmin, FLmax) + + -- Climb and descent heights. + local h_climb = FLcruise - H_departure + local h_descent = FLcruise - Hh_holding + + -- Get distances. + local d_climb = h_climb/math.tan(AlphaClimb) + local d_descent = h_descent/math.tan(AlphaDescent) + local d_cruise = d_total-d_climb-d_descent + + -- Debug. + local text=string.format("Flight plan:\n") + text=text..string.format("Vx max = %.2f km/h\n", Vmax*3.6) + text=text..string.format("Vx climb = %.2f km/h\n", VxClimb*3.6) + text=text..string.format("Vx cruise = %.2f km/h\n", VxCruise*3.6) + text=text..string.format("Vx descent = %.2f km/h\n", VxDescent*3.6) + text=text..string.format("Vx holding = %.2f km/h\n", VxHolding*3.6) + text=text..string.format("Vx final = %.2f km/h\n", VxFinal*3.6) + text=text..string.format("Vy max = %.2f m/s\n", Vymax) + text=text..string.format("Vy climb = %.2f m/s\n", VyClimb) + text=text..string.format("Alpha Climb = %.2f Deg\n", math.deg(AlphaClimb)) + text=text..string.format("Alpha Descent = %.2f Deg\n", math.deg(AlphaDescent)) + text=text..string.format("Dist climb = %.3f km\n", d_climb/1000) + text=text..string.format("Dist cruise = %.3f km\n", d_cruise/1000) + text=text..string.format("Dist descent = %.3f km\n", d_descent/1000) + text=text..string.format("Dist total = %.3f km\n", d_total/1000) + text=text..string.format("h_climb = %.3f km\n", h_climb/1000) + text=text..string.format("h_desc = %.3f km\n", h_descent/1000) + text=text..string.format("h_holding = %.3f km\n", h_holding/1000) + text=text..string.format("h_max = %.3f km\n", h_max/1000) + text=text..string.format("FL min = %.3f km\n", FLmin/1000) + text=text..string.format("FL expect = %.3f km\n", FLcruise_expect/1000) + text=text..string.format("FL cruise * = %.3f km\n", FLcruise/1000) + text=text..string.format("FL max = %.3f km\n", FLmax/1000) + text=text..string.format("Ceiling = %.3f km\n", ceiling/1000) + text=text..string.format("Max range = %.3f km\n", Range/1000) + self:T(self.wid..text) + + -- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back. + if d_cruise<0 then + d_cruise=100 + end + + ------------------------ + --- Create Waypoints --- + ------------------------ + + -- Waypoints and coordinates + local wp={} + local c={} + + --- Departure/Take-off + c[#c+1]=Pdeparture + wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb, 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") + + --- 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") + + --- 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") + + + -- Mark points at waypoints for debugging. + if self.Debug then + for i,coord in pairs(c) do + local coord=coord --Core.Point#COORDINATE + local dist=0 + if i>1 then + dist=coord:Get2DDistance(c[i-1]) + end + coord:MarkToAll(string.format("Waypoint %i, distance = %.2f km",i, dist/1000)) + end + end + + return wp,c +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--[[ + --- 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 e093eb3ef..61625bc4f 100644 --- a/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneCaptureCoalition.lua @@ -1,18 +1,40 @@ ---- **Functional** -- (R2.3) Models the process to zone guarding and capturing. +--- **Functional** -- Models the process to zone guarding and capturing. -- -- === -- --- ![Banner Image](..\Presentations\ZONE_CAPTURE_COALITION\Dia1.JPG) +-- ## Features: +-- +-- * Models the possible state transitions between the Guarded, Attacked, Empty and Captured states. +-- * A zone has an owning coalition, that means that at a specific point in time, a zone can be owned by the red or blue coalition. +-- * Provide event handlers to tailor the actions when a zone changes coalition or state. -- -- === -- --- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/CAZ - Capture Zones) +-- ## Missions: -- --- - CAZ-000 - Capture Zone: Demonstrates the basic concept of capturing a zone. +-- [CAZ - Capture Zones](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/CAZ%20-%20Capture%20Zones) -- -- === -- --- ### [YouTube Playlist](https://www.youtube.com/watch?v=0m6K6Yxa-os&list=PL7ZUrU4zZUl0qqJsfa8DPvZWDY-OyDumE) +-- # Player Experience +-- +-- ![States](..\Presentations\ZONE_CAPTURE_COALITION\Dia3.JPG) +-- +-- The above models the possible state transitions between the **Guarded**, **Attacked**, **Empty** and **Captured** states. +-- A zone has an __owning coalition__, that means that at a specific point in time, a zone can be owned by the red or blue coalition. +-- +-- The Zone can be in the state **Guarded** by the __owning coalition__, which is the coalition that initially occupies the zone with units of its coalition. +-- Once units of an other coalition are entering the Zone, the state will change to **Attacked**. As long as these units remain in the zone, the state keeps set to Attacked. +-- When all units are destroyed in the Zone, the state will change to **Empty**, which expresses that the Zone is empty, and can be captured. +-- When units of the other coalition are in the Zone, and no other units of the owning coalition is in the Zone, the Zone is captured, and its state will change to **Captured**. +-- +-- The zone needs to be monitored regularly for the presence of units to interprete the correct state transition required. +-- This monitoring process MUST be started using the @{#ZONE_CAPTURE_COALITION.Start}() method. +-- Otherwise no monitoring will be active and the zone will stay in the current state forever. +-- +-- === +-- +-- ## [YouTube Playlist](https://www.youtube.com/watch?v=0m6K6Yxa-os&list=PL7ZUrU4zZUl0qqJsfa8DPvZWDY-OyDumE) -- -- === -- @@ -21,7 +43,8 @@ -- -- === -- --- @module ZoneCaptureCoalition +-- @module Functional.ZoneCaptureCoalition +-- @image Capture_Zones.JPG do -- ZONE_CAPTURE_COALITION @@ -29,70 +52,79 @@ do -- ZONE_CAPTURE_COALITION -- @extends Functional.ZoneGoalCoalition#ZONE_GOAL_COALITION - --- # ZONE\_CAPTURE\_COALITION class, extends @{ZoneGoalCoalition#ZONE_GOAL_COALITION} - -- - -- Models the process to capture a Zone for a Coalition, which is guarded by another Coalition. + --- Models the process to capture a Zone for a Coalition, which is guarded by another Coalition. -- This is a powerful concept that allows to create very dynamic missions based on the different state transitions of various zones. -- - -- --- - -- - -- ![Banner Image](..\Presentations\ZONE_CAPTURE_COALITION\Dia1.JPG) - -- - -- --- + -- === -- - -- # 0. Player Experience + -- In order to use ZONE_CAPTURE_COALITION, you need to: -- - -- ![States](..\Presentations\ZONE_CAPTURE_COALITION\Dia3.JPG) - -- - -- The above models the possible state transitions between the **Guarded**, **Attacked**, **Empty** and **Captured** states. - -- A zone has an __owning coalition__, that means that at a specific point in time, a zone can be owned by the red or blue coalition. - -- - -- The Zone can be in the state **Guarded** by the __owning coalition__, which is the coalition that initially occupies the zone with units of its coalition. - -- Once units of an other coalition are entering the Zone, the state will change to **Attacked**. As long as these units remain in the zone, the state keeps set to Attacked. - -- When all units are destroyed in the Zone, the state will change to **Empty**, which expresses that the Zone is empty, and can be captured. - -- When units of the other coalition are in the Zone, and no other units of the owning coalition is in the Zone, the Zone is captured, and its state will change to **Captured**. - -- - -- The zone needs to be monitored regularly for the presence of units to interprete the correct state transition required. - -- This monitoring process MUST be started using the @{#ZONE_CAPTURE_COALITION.Start}() method. - -- Otherwise no monitoring will be active and the zone will stay in the current state forever. - -- See further in chapter 3.3 for more information about this. - -- - -- ## 1. ZONE\_CAPTURE\_COALITION constructor - -- - -- * @{#ZONE_CAPTURE_COALITION.New}(): Creates a new ZONE\_CAPTURE\_COALITION object. - -- - -- In order to use ZONE\_CAPTURE\_COALITION, you need to: - -- - -- - Create a @{Zone} object from one of the ZONE\_ classes. Note that ZONE\_POLYGON\_ classes are not yet functional. The only functional ZONE\_ classses are those derived from a ZONE\_RADIUS. + -- * Create a @{Zone} object from one of the ZONE_ classes. + -- Note that ZONE_POLYGON_ classes are not yet functional. + -- The only functional ZONE_ classses are those derived from a ZONE_RADIUS. + -- * Set the state of the zone. Most of the time, Guarded would be the initial state. + -- * Start the zone capturing **monitoring process**. + -- This will check the presence of friendly and/or enemy units within the zone and will transition the state of the zone when the tactical situation changed. + -- The frequency of the monitoring must not be real-time, a 30 second interval to execute the checks is sufficient. -- -- ![New](..\Presentations\ZONE_CAPTURE_COALITION\Dia5.JPG) -- - -- Ensure that during the life cycle of the ZONE\_CAPTURE\_COALITION object, the object keeps alive. - -- It is best to declare the object globally within your script. + -- ### Important: -- - -- ## 2. ZONE\_CAPTURE\_COALITION is a finite state machine (FSM). + -- You must start the monitoring process within your code, or there won't be any state transition checks executed. + -- See further the start/stop monitoring process. + -- + -- ### Important: + -- + -- Ensure that the object containing the ZONE_CAPTURE_COALITION object is persistent. + -- Otherwise the garbage collector of lua will remove the object and the monitoring process will stop. + -- This will result in your object to be destroyed (removed) from internal memory and there won't be any zone state transitions anymore detected! + -- So use the `local` keyword in lua with thought! Most of the time, you can declare your object gobally. + -- + -- + -- + -- # Example: + -- + -- -- Define a new ZONE object, which is based on the trigger zone `CaptureZone`, which is defined within the mission editor. + -- CaptureZone = ZONE:New( "CaptureZone" ) + -- + -- -- Here we create a new ZONE_CAPTURE_COALITION object, using the :New constructor. + -- ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( CaptureZone, coalition.side.RED ) + -- + -- -- Set the zone to Guarding state. + -- ZoneCaptureCoalition:__Guard( 1 ) + -- + -- -- Start the zone monitoring process in 30 seconds and check every 30 seconds. + -- ZoneCaptureCoalition:Start( 30, 30 ) + -- + -- + -- # Constructor: + -- + -- Use the @{#ZONE_CAPTURE_COALITION.New}() constructor to create a new ZONE_CAPTURE_COALITION object. + -- + -- # ZONE_CAPTURE_COALITION is a finite state machine (FSM). -- -- ![States](..\Presentations\ZONE_CAPTURE_COALITION\Dia4.JPG) -- - -- ### 2.1 ZONE\_CAPTURE\_COALITION States + -- ## ZONE_CAPTURE_COALITION States -- -- * **Captured**: The Zone has been captured by an other coalition. -- * **Attacked**: The Zone is currently intruded by an other coalition. There are units of the owning coalition and an other coalition in the Zone. -- * **Guarded**: The Zone is guarded by the owning coalition. There is no other unit of an other coalition in the Zone. -- * **Empty**: The Zone is empty. There is not valid unit in the Zone. -- - -- ### 2.2 ZONE\_CAPTURE\_COALITION Events + -- ## 2.2 ZONE_CAPTURE_COALITION Events -- -- * **Capture**: The Zone has been captured by an other coalition. -- * **Attack**: The Zone is currently intruded by an other coalition. There are units of the owning coalition and an other coalition in the Zone. -- * **Guard**: The Zone is guarded by the owning coalition. There is no other unit of an other coalition in the Zone. -- * **Empty**: The Zone is empty. There is not valid unit in the Zone. -- - -- ## 3. "Script It" + -- # "Script It" -- - -- ZONE\_CAPTURE\_COALITION allows to take action on the various state transitions and add your custom code and logic. + -- ZONE_CAPTURE_COALITION allows to take action on the various state transitions and add your custom code and logic. -- - -- ### 3.1. Take action using state- and event handlers. + -- ## Take action using state- and event handlers. -- -- ![States](..\Presentations\ZONE_CAPTURE_COALITION\Dia6.JPG) -- @@ -109,8 +141,6 @@ do -- ZONE_CAPTURE_COALITION -- - On Before the event is triggered. Return false to cancel the transition. -- - On After the event is triggered. -- - -- - -- -- ![States](..\Presentations\ZONE_CAPTURE_COALITION\Dia7.JPG) -- -- Each handler can receive optionally 3 parameters: @@ -131,7 +161,7 @@ do -- ZONE_CAPTURE_COALITION -- -- This code checks that when the __Guarded__ state has been reached, that if the **From** state was __Empty__, then display a message. -- - -- ### 3.2. Example Event Handler. + -- ## Example Event Handler. -- -- --- @param Functional.ZoneCaptureCoalition#ZONE_CAPTURE_COALITION self -- function ZoneCaptureCoalition:OnEnterGuarded( From, Event, To ) @@ -150,7 +180,7 @@ do -- ZONE_CAPTURE_COALITION -- end -- end -- - -- ### 3.3. Stop and Start the zone monitoring process. + -- ## Stop and Start the zone monitoring process. -- -- At regular intervals, the state of the zone needs to be monitored. -- The zone needs to be scanned for the presence of units within the zone boundaries. @@ -162,8 +192,8 @@ do -- ZONE_CAPTURE_COALITION -- -- Therefore, the mission designer is given 2 methods that allow to take control of the CPU utilization efficiency: -- - -- - @{#ZONE_CAPTURE_COALITION.Start()}(): This starts the monitoring process. - -- - @{#ZONE_CAPTURE_COALITION.Stop()}(): This stops the monitoring process. + -- * @{#ZONE_CAPTURE_COALITION.Start}(): This starts the monitoring process. + -- * @{#ZONE_CAPTURE_COALITION.Stop}(): This stops the monitoring process. -- -- ### IMPORTANT -- @@ -171,9 +201,9 @@ do -- ZONE_CAPTURE_COALITION -- The monitoring process is NOT started by default!!!** -- -- - -- ## 4. Full Example + -- # Full Example -- - -- The following annotated code shows a real example of how ZONE\_CAPTURE\_COALITION can be applied. + -- The following annotated code shows a real example of how ZONE_CAPTURE_COALITION can be applied. -- -- The concept is simple. -- @@ -334,7 +364,7 @@ do -- ZONE_CAPTURE_COALITION do - --- Captured State Handler OnLeave for ZONE\_CAPTURE\_COALITION + --- Captured State Handler OnLeave for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnLeaveCaptured -- @param #ZONE_CAPTURE_COALITION self -- @param #string From @@ -342,7 +372,7 @@ do -- ZONE_CAPTURE_COALITION -- @param #string To -- @return #boolean - --- Captured State Handler OnEnter for ZONE\_CAPTURE\_COALITION + --- Captured State Handler OnEnter for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnEnterCaptured -- @param #ZONE_CAPTURE_COALITION self -- @param #string From @@ -354,7 +384,7 @@ do -- ZONE_CAPTURE_COALITION do - --- Attacked State Handler OnLeave for ZONE\_CAPTURE\_COALITION + --- Attacked State Handler OnLeave for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnLeaveAttacked -- @param #ZONE_CAPTURE_COALITION self -- @param #string From @@ -362,7 +392,7 @@ do -- ZONE_CAPTURE_COALITION -- @param #string To -- @return #boolean - --- Attacked State Handler OnEnter for ZONE\_CAPTURE\_COALITION + --- Attacked State Handler OnEnter for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnEnterAttacked -- @param #ZONE_CAPTURE_COALITION self -- @param #string From @@ -373,7 +403,7 @@ do -- ZONE_CAPTURE_COALITION do - --- Guarded State Handler OnLeave for ZONE\_CAPTURE\_COALITION + --- Guarded State Handler OnLeave for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnLeaveGuarded -- @param #ZONE_CAPTURE_COALITION self -- @param #string From @@ -381,7 +411,7 @@ do -- ZONE_CAPTURE_COALITION -- @param #string To -- @return #boolean - --- Guarded State Handler OnEnter for ZONE\_CAPTURE\_COALITION + --- Guarded State Handler OnEnter for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnEnterGuarded -- @param #ZONE_CAPTURE_COALITION self -- @param #string From @@ -393,7 +423,7 @@ do -- ZONE_CAPTURE_COALITION do - --- Empty State Handler OnLeave for ZONE\_CAPTURE\_COALITION + --- Empty State Handler OnLeave for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnLeaveEmpty -- @param #ZONE_CAPTURE_COALITION self -- @param #string From @@ -401,7 +431,7 @@ do -- ZONE_CAPTURE_COALITION -- @param #string To -- @return #boolean - --- Empty State Handler OnEnter for ZONE\_CAPTURE\_COALITION + --- Empty State Handler OnEnter for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnEnterEmpty -- @param #ZONE_CAPTURE_COALITION self -- @param #string From @@ -412,7 +442,7 @@ do -- ZONE_CAPTURE_COALITION self:AddTransition( "*", "Guard", "Guarded" ) - --- Guard Handler OnBefore for ZONE\_CAPTURE\_COALITION + --- Guard Handler OnBefore for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnBeforeGuard -- @param #ZONE_CAPTURE_COALITION self -- @param #string From @@ -420,25 +450,25 @@ do -- ZONE_CAPTURE_COALITION -- @param #string To -- @return #boolean - --- Guard Handler OnAfter for ZONE\_CAPTURE\_COALITION + --- Guard Handler OnAfter for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnAfterGuard -- @param #ZONE_CAPTURE_COALITION self -- @param #string From -- @param #string Event -- @param #string To - --- Guard Trigger for ZONE\_CAPTURE\_COALITION + --- Guard Trigger for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] Guard -- @param #ZONE_CAPTURE_COALITION self - --- Guard Asynchronous Trigger for ZONE\_CAPTURE\_COALITION + --- Guard Asynchronous Trigger for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] __Guard -- @param #ZONE_CAPTURE_COALITION self -- @param #number Delay self:AddTransition( "*", "Empty", "Empty" ) - --- Empty Handler OnBefore for ZONE\_CAPTURE\_COALITION + --- Empty Handler OnBefore for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnBeforeEmpty -- @param #ZONE_CAPTURE_COALITION self -- @param #string From @@ -446,18 +476,18 @@ do -- ZONE_CAPTURE_COALITION -- @param #string To -- @return #boolean - --- Empty Handler OnAfter for ZONE\_CAPTURE\_COALITION + --- Empty Handler OnAfter for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnAfterEmpty -- @param #ZONE_CAPTURE_COALITION self -- @param #string From -- @param #string Event -- @param #string To - --- Empty Trigger for ZONE\_CAPTURE\_COALITION + --- Empty Trigger for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] Empty -- @param #ZONE_CAPTURE_COALITION self - --- Empty Asynchronous Trigger for ZONE\_CAPTURE\_COALITION + --- Empty Asynchronous Trigger for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] __Empty -- @param #ZONE_CAPTURE_COALITION self -- @param #number Delay @@ -465,7 +495,7 @@ do -- ZONE_CAPTURE_COALITION self:AddTransition( { "Guarded", "Empty" }, "Attack", "Attacked" ) - --- Attack Handler OnBefore for ZONE\_CAPTURE\_COALITION + --- Attack Handler OnBefore for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnBeforeAttack -- @param #ZONE_CAPTURE_COALITION self -- @param #string From @@ -473,25 +503,25 @@ do -- ZONE_CAPTURE_COALITION -- @param #string To -- @return #boolean - --- Attack Handler OnAfter for ZONE\_CAPTURE\_COALITION + --- Attack Handler OnAfter for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnAfterAttack -- @param #ZONE_CAPTURE_COALITION self -- @param #string From -- @param #string Event -- @param #string To - --- Attack Trigger for ZONE\_CAPTURE\_COALITION + --- Attack Trigger for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] Attack -- @param #ZONE_CAPTURE_COALITION self - --- Attack Asynchronous Trigger for ZONE\_CAPTURE\_COALITION + --- Attack Asynchronous Trigger for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] __Attack -- @param #ZONE_CAPTURE_COALITION self -- @param #number Delay self:AddTransition( { "Guarded", "Attacked", "Empty" }, "Capture", "Captured" ) - --- Capture Handler OnBefore for ZONE\_CAPTURE\_COALITION + --- Capture Handler OnBefore for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnBeforeCapture -- @param #ZONE_CAPTURE_COALITION self -- @param #string From @@ -499,22 +529,26 @@ do -- ZONE_CAPTURE_COALITION -- @param #string To -- @return #boolean - --- Capture Handler OnAfter for ZONE\_CAPTURE\_COALITION + --- Capture Handler OnAfter for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] OnAfterCapture -- @param #ZONE_CAPTURE_COALITION self -- @param #string From -- @param #string Event -- @param #string To - --- Capture Trigger for ZONE\_CAPTURE\_COALITION + --- Capture Trigger for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] Capture -- @param #ZONE_CAPTURE_COALITION self - --- Capture Asynchronous Trigger for ZONE\_CAPTURE\_COALITION + --- Capture Asynchronous Trigger for ZONE_CAPTURE_COALITION -- @function [parent=#ZONE_CAPTURE_COALITION] __Capture -- @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 ) + return self end @@ -759,5 +793,20 @@ do -- ZONE_CAPTURE_COALITION end end + --- @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() + end + end + + end + + end diff --git a/Moose Development/Moose/Functional/ZoneGoal.lua b/Moose Development/Moose/Functional/ZoneGoal.lua index 5bca12a4e..0caabc8a3 100644 --- a/Moose Development/Moose/Functional/ZoneGoal.lua +++ b/Moose Development/Moose/Functional/ZoneGoal.lua @@ -11,7 +11,8 @@ -- -- === -- --- @module ZoneGoal +-- @module Functional.ZoneGoal +-- @image MOOSE.JPG do -- Zone @@ -19,9 +20,7 @@ do -- Zone -- @extends Core.Fsm#FSM - --- # ZONE_GOAL class, extends @{Fsm#FSM} - -- - -- ZONE_GOAL 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 @@ -36,7 +35,7 @@ do -- Zone -- -- ### 2.2 ZONE_GOAL Events -- - -- * DestroyedUnit: A @{Unit} is destroyed in the Zone. The event will only get triggered if the method @{#ZONE_GOAL.MonitorDestroyedUnits}() is used. + -- * DestroyedUnit: A @{Wrapper.Unit} is destroyed in the Zone. The event will only get triggered if the method @{#ZONE_GOAL.MonitorDestroyedUnits}() is used. -- -- @field #ZONE_GOAL ZONE_GOAL = { diff --git a/Moose Development/Moose/Functional/ZoneGoalCargo.lua b/Moose Development/Moose/Functional/ZoneGoalCargo.lua index e5ea44310..9ae8ae613 100644 --- a/Moose Development/Moose/Functional/ZoneGoalCargo.lua +++ b/Moose Development/Moose/Functional/ZoneGoalCargo.lua @@ -11,7 +11,8 @@ -- -- === -- --- @module ZoneGoalCargo +-- @module Functional.ZoneGoalCargo +-- @image MOOSE.JPG do -- ZoneGoal @@ -19,9 +20,7 @@ do -- ZoneGoal -- @extends Functional.ZoneGoal#ZONE_GOAL - --- # ZONE_GOAL_CARGO class, extends @{ZoneGoal#ZONE_GOAL} - -- - -- ZONE_GOAL_CARGO models processes that have a Goal with a defined achievement involving a Zone and Cargo. + --- Models processes that have a Goal with a defined achievement involving a Zone and Cargo. -- Derived classes implement the ways how the achievements can be realized. -- -- ## 1. ZONE_GOAL_CARGO constructor diff --git a/Moose Development/Moose/Functional/ZoneGoalCoalition.lua b/Moose Development/Moose/Functional/ZoneGoalCoalition.lua index 91c7df0dc..ba86ffe95 100644 --- a/Moose Development/Moose/Functional/ZoneGoalCoalition.lua +++ b/Moose Development/Moose/Functional/ZoneGoalCoalition.lua @@ -11,7 +11,8 @@ -- -- === -- --- @module ZoneGoalCoalition +-- @module Functional.ZoneGoalCoalition +-- @image MOOSE.JPG do -- ZoneGoal @@ -19,9 +20,7 @@ do -- ZoneGoal -- @extends Functional.ZoneGoal#ZONE_GOAL - --- # ZONE_GOAL_COALITION class, extends @{ZoneGoal#ZONE_GOAL} - -- - -- ZONE_GOAL_COALITION models processes that have a Goal with a defined achievement involving a Zone for a Coalition. + --- ZONE_GOAL_COALITION models processes that have a Goal with a defined achievement involving a Zone for a Coalition. -- Derived classes implement the ways how the achievements can be realized. -- -- ## 1. ZONE_GOAL_COALITION constructor diff --git a/Moose Development/Moose/Globals.lua b/Moose Development/Moose/Globals.lua index 5e1e394cc..05f3b8cf3 100644 --- a/Moose Development/Moose/Globals.lua +++ b/Moose Development/Moose/Globals.lua @@ -13,3 +13,6 @@ _DATABASE = DATABASE:New() -- Core.Database#DATABASE _SETTINGS = SETTINGS:Set() _SETTINGS:SetPlayerMenuOn() +_DATABASE:_RegisterCargos() +_DATABASE:_RegisterZones() + diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua new file mode 100644 index 000000000..729e53218 --- /dev/null +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -0,0 +1,9375 @@ +--- **Ops** - (R2.5) - Manages aircraft recoveries for carrier operations. +-- +-- 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 (WIP). +-- * Different skill levels from on-the-fly tips for flight students to ziplip for pros. +-- * Define recovery time windows with individual recovery cases. +-- * 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. +-- * F10 radio menu including carrier info (weather, radio frequencies, TACAN/ICLS channels), player LSO grades, +-- help function (player aircraft attitude, marking of pattern zones etc). +-- * Recovery tanker and refueling option via integration of @{Ops.RecoveryTanker} class. +-- * Rescue helicopter option via @{Ops.RescueHelo} class. +-- * Many parameters customizable by convenient user API functions. +-- * Multiple carrier support due to object oriented approach. +-- * Unlimited number of players. +-- * Finite State Machine (FSM) implementation. +-- +-- **Supported Carriers:** +-- +-- * [USS John C. Stennis](https://en.wikipedia.org/wiki/USS_John_C._Stennis) (CVN-74) +-- +-- **Supported Aircraft:** +-- +-- * [F/A-18C Hornet Lot 20](https://forums.eagle.ru/forumdisplay.php?f=557) (Player & AI) +-- * [A-4E Skyhawk Community Mod](https://forums.eagle.ru/showthread.php?t=224989) (Player & AI) +-- * F/A-18C Hornet (AI) +-- * F-14A Tomcat (AI) +-- * E-2D Hawkeye (AI) +-- * S-3B Viking & tanker version (AI) +-- +-- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) as aircraft and the USS John C. Stennis as carrier. +-- The A-4E community mod is also supported in priciple but may need further tweaking of parameters. +-- +-- The implemenation is kept general. So other aircraft and carriers possible in future. [*Winter is coming!*](https://forums.eagle.ru/forumdisplay.php?f=395) +-- But each aircraft or carrier needs a different set of optimized individual parameters. +-- +-- **PLEASE NOTE** that his class is work in progress and in an early **alpha** stage. Many/most things work already very nicely but there a lot of cases I did not run into yet. +-- Therefore, your *constructive* feedback is both necessary and appreciated! +-- +-- ### Some Open Questions? +-- +-- * What are the conditions for a foul deck wave off? +-- * What is the next step after a pattern wave off during Case II or III recovery? +-- * What is the condition for a "fly through" (\\ or /) LSO grade? +-- * The above question is one of many regarding LSO grade. If you have more info, please share. +-- +-- If you know the answer to any of this, please get in touch with me! +-- The necessary infrastructure to implement it is most likely already there, but I am not 100% sure about the exact conditions. +-- +-- === +-- +-- ### 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 MOOSE.JPG + +--- 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 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 specifc 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 Core.Radio#RADIO LSORadio Radio for LSO calls. +-- @field #number LSOFreq LSO radio frequency in MHz. +-- @field #string LSOModu LSO radio modulation "AM" or "FM". +-- @field Core.Radio#RADIO MarshalRadio Radio for carrier calls. +-- @field #number MarshalFreq Marshal radio frequency in MHz. +-- @field #string MarshalModu Marshal radio modulation "AM" or "FM". +-- @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 Core.Zone#ZONE_UNIT zoneInitial Zone usually 3 NM astern of carrier where pilots start their CASE I pattern. +-- @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 brak 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 RQMarshal Radio queue of marshal. +-- @field #table RQLSO Radio queue of LSO. +-- @field #number Nmaxpattern Max number of aircraft in landing pattern. +-- @field #boolean handleai If true (default), handle AI aircraft. +-- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier. +-- @field Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the 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. +-- @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, +-- * **CASE II** during daytime but poor visibility conditions, +-- * **CASE III** during nighttime recoveries. +-- +-- 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 altiude. The holding altitude of the first stack is 2000 ft. The inverval 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. +-- +-- ### 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 positon 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 interval 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 & hook down). +-- +-- At 3 NM distance to the carrier, the aircraft should intercept the 3.5 degrees glide slope at the "*Bullseye*". From there the pilot should "follow the needes" of the ICLS. +-- +-- ## CASE II +-- +-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case2.png) +-- +-- Case II is the common recovery procedure at daytime if visibilty 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 angles +-- are 0 (no offset), +-15 or +-30 degrees. The AIRBOSS class supports all these scenarios which are used during Case II and III recoveries. +-- +-- +-- # 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. +-- +-- If no recovery window is set like in the basic example, a window will automatically open 15 minutes after mission start and close again after three hours. +-- The next section explains how to set your own recovery times. +-- +-- ## 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 regarless of +-- whether a window is open or not and will be alowed to enter the pattern (if not already full). This will probably change in the future. +-- +-- At the moment there is no autmatic recovery case set depending on weather or daytime. So it is the AIRBOSS (you) who needs to make that descision. +-- 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 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. +-- +-- ## Main Menu +-- +-- 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** +-- +-- ### 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. +-- +-- 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. +-- +-- ### Request Refueling +-- +-- If a recovery tanker was setup via the @{#AIRBOSS.SetRecoveryTanker} function, the player can request refueling. If the tanker is ready, refueling is granted and the player +-- can leave the marshal stack for refueling. The stack will collapse and the player needs to request marshal again, when refueling is finished. +-- +-- ## Help Menu +-- +-- This menu provides commands to help the player. +-- +-- ### Skill Level Submenu +-- +-- 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 pros. +-- +-- ### Mark Zones Submenu +-- +-- 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. +-- +-- ### My Status +-- +-- This command provides information about the current player status. For example, his current step in the pattern. +-- +-- ### Attitude Monitor +-- +-- This command displays the current aircraft attitude of the player in short intervals as message on the screen. +-- It provides information about current pitch, roll, yaw, lineup and glideslope error, orientation of the plane wrt to carrier etc. +-- +-- ### LSO Radio Check +-- +-- LSO will transmit a short message on his radio frequency. See @{#AIRBOSS.SetLSORadio}. +-- +-- ### Marshal Radio Check +-- +-- Marshal will transmit a short message on his radio frequency. See @{#AIRBOSS.SetMarshalRadio}. +-- +-- ### [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 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. +-- +-- ## Kneeboard Menu +-- +-- The Kneeboard menu provides information about the carrier, weather and player results. +-- +-- ### Results Submenu +-- +-- 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 +-- +-- Information about the current carrier status is displayed. This includes current BRC, FB, LSO and Marshal frequences, list of next recovery windows. +-- +-- ### Weather Report +-- +-- Displays information about the current weather at the carrier such as QFE, wind and temperature. +-- +-- ### Set Section +-- +-- With this command, you can define a section of human flights. The player how issues the command becomes the section lead and all other human players +-- within a radius of 200 meters become members of the section. +-- +-- # 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 acknoledge 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 wirth 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 +-- +-- * **X** At the Start +-- * **IM** In the Middle +-- * **IC** In Close +-- * **AR** At the Ramp +-- * **IW** In the Wiress +-- +-- 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 **L**ow: H, L +-- * Too **F**ast or too **SLO**w: F, SLO +-- +-- 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 sigifiantly off from the ideal parameters in close or at the ramp, the LSO will wave the player off. +-- +-- ## Pattern Wave Off +-- +-- 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 happend. +-- * 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. +-- * 1.0 Points **WO**: "Wave-Off": Player got waved off in the final parts of the groove. +-- * 1.0 Points **PWO**: "Pattern Wave-Off", when pilot was far away from where he should be in the pattern. For example, being long in the groove gives a "LIG PWO". +-- * 0.0 Point **CUT**: "Cut pass", when player was waved off but landed anyway. +-- +-- # 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. +-- +-- ## 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. +-- +-- # 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, + 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, + radiotimer = nil, + zoneCCA = nil, + zoneCCZ = nil, + zoneInitial = 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 = {}, + RQMarshal = {}, + RQLSO = {}, + Nmaxpattern = nil, + handleai = nil, + tanker = nil, + warehouse = nil, + Corientation = nil, + Corientlast = nil, + Cposition = nil, + defaultskill = nil, + adinfinitum = nil, + magvar = nil, + Tcollapse = nil, +} + +--- Player aircraft types capable of landing on carriers. +-- @type AIRBOSS.AircraftPlayer +-- @field #string AV8B AV-8B Night Harrier (not yet supported). +-- @field #string HORNET F/A-18C Lot 20 Hornet. +-- @field #string A4EC Community A-4E-C mod. +AIRBOSS.AircraftPlayer={ + --AV8B="AV8BNA", + HORNET="FA-18C_hornet", + A4EC="A-4E-C", +} + +--- Aircraft types capable of landing on carrier (human+AI). +-- @type AIRBOSS.AircraftCarrier +-- @field #string AV8B AV-8B Night Harrier (not yet supported). +-- @field #string HORNET F/A-18C Lot 20 Hornet. +-- @field #string A4EC Community A-4E mod. +-- @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 FA18C F/A-18C Hornet (AI). +-- @field #string F14A F-14A Tomcat (AI). +AIRBOSS.AircraftCarrier={ + --AV8B="AV8BNA", + HORNET="FA-18C_hornet", + A4EC="A-4E-C", + S3B="S-3B", + S3BTANKER="S-3B Tanker", + E2D="E-2C", + FA18C="F/A-18C", + F14A="F-14A", +} + +--- Carrier types. +-- @type AIRBOSS.CarrierType +-- @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={ + 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. + +--- 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 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_RB "Groove Roger Ball". +-- @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_IW "Groove In the Wires". +-- @field #string DEBRIEF "Debrief". +AIRBOSS.PatternStep={ + UNDEFINED="Undefined", + REFUELING="Refueling", + SPINNING="Spinning", + COMMENCING="Commencing", + HOLDING="Holding", + 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_RB="Groove Roger Ball", + GROOVE_IM="Groove In the Middle", + GROOVE_IC="Groove In Close", + GROOVE_AR="Groove At the Ramp", + GROOVE_IW="Groove In the Wires", + DEBRIEF="Debrief", +} + +--- Radio sound file and subtitle. +-- @type AIRBOSS.RadioCall +-- @field #string file Sound file name without suffix. +-- @field #string suffix File suffix/extention, 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. + +--- LSO radio calls. +-- @type AIRBOSS.LSOCall +-- @field #AIRBOSS.RadioCall RADIOCHECK "Paddles, radio check" call. +-- @field #AIRBOSS.RadioCall RIGHTFORLINEUP "Right for line up" call. +-- @field #AIRBOSS.RadioCall COMELEFT "Come left" call. +-- @field #AIRBOSS.RadioCall HIGH "You're high" call. +-- @field #AIRBOSS.RadioCall LOW "You're low" call. +-- @field #AIRBOSS.RadioCall POWER "Power" call. +-- @field #AIRBOSS.RadioCall FAST "You're fast" call. +-- @field #AIRBOSS.RadioCall SLOW "You're slow" call. +-- @field #AIRBOSS.RadioCall PADDLESCONTACT "Paddles, contact" call. +-- @field #AIRBOSS.RadioCall CALLTHEBALL "Call the Ball" +-- @field #AIRBOSS.RadioCall ROGERBALL "Roger ball" call. +-- @field #AIRBOSS.RadioCall WAVEOFF "Wave off" call +-- @field #AIRBOSS.RadioCall BOLTER "Bolter, Bolter" call +-- @field #AIRBOSS.RadioCall LONGINGROOVE "You're long in the groove" call. +-- @field #AIRBOSS.RadioCall DEPARTANDREENTER "Depart and re-enter" call. +-- @field #AIRBOSS.RadioCall WELCOMEABOARD "Welcome aboard" 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. +AIRBOSS.LSOCall={ + RADIOCHECK={ + file="LSO-RadioCheck", + suffix="ogg", + loud=false, + subtitle="Paddles, radio check", + duration=1.1, + }, + RIGHTFORLINEUP={ + file="LSO-RightForLineup", + suffix="ogg", + loud=true, + subtitle="Right for line up", + duration=0.80, + }, + COMELEFT={ + file="LSO-ComeLeft", + suffix="ogg", + loud=true, + subtitle="Come left", + duration=0.60, + }, + HIGH={ + file="LSO-High", + suffix="ogg", + loud=true, + subtitle="You're high", + duration=0.65, + }, + LOW={ + file="LSO-Low", + suffix="ogg", + loud=true, + subtitle="You're low", + duration=0.50, + }, + POWER={ + file="LSO-Power", + suffix="ogg", + loud=true, + subtitle="Power", + duration=0.50, --0.45 was too short + }, + SLOW={ + file="LSO-Slow", + suffix="ogg", + loud=true, + subtitle="You're slow", + duration=0.65, + }, + FAST={ + file="LSO-Fast", + suffix="ogg", + loud=true, + subtitle="You're fast", + duration=0.7, + }, + CALLTHEBALL={ + file="LSO-CallTheBall", + suffix="ogg", + loud=false, + subtitle="Call the ball", + duration=0.6, + }, + ROGERBALL={ + file="LSO-RogerBall", + suffix="ogg", + loud=false, + subtitle="Roger ball", + duration=0.7, + }, + WAVEOFF={ + file="LSO-WaveOff", + suffix="ogg", + loud=false, + subtitle="Wave off", + duration=0.6, + }, + BOLTER={ + file="LSO-BolterBolter", + suffix="ogg", + loud=false, + subtitle="Bolter, Bolter", + duration=0.75, + }, + LONGINGROOVE={ + file="LSO-LongInTheGroove", + suffix="ogg", + loud=false, + subtitle="You're long in the groove", + duration=1.2, + }, + DEPARTANDREENTER={ + file="LSO-DepartAndReenter", + suffix="ogg", + loud=false, + subtitle="Depart and re-enter", + duration=1.1, + }, + PADDLESCONTACT={ + file="LSO-PaddlesContact", + suffix="ogg", + loud=false, + subtitle="Paddles, contact", + duration=1.0, + }, + WELCOMEABOARD={ + file="LSO-WelcomeAboard", + suffix="ogg", + loud=false, + subtitle="Welcome aboard", + duration=0.9, + }, + 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.38, + }, + 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, --0.38 too short + }, +} + +--- Marshal radio calls. +-- @type AIRBOSS.MarshalCall +-- @field #AIRBOSS.RadioCall RADIOCHECK "Radio check" call. +-- @field #AIRBOSS.RadioCall SAYNEEDLES "Say needles" call. +-- @field #AIRBOSS.RadioCall FLYNEEDLES "Fly your needles" 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. +AIRBOSS.MarshalCall={ + RADIOCHECK={ + file="MARSHAL-RadioCheck", + suffix="ogg", + loud=false, + subtitle="Radio check", + duration=1.0, + }, + SAYNEEDLES={ + file="MARSHAL-SayNeedles", + suffix="ogg", + loud=false, + subtitle="Say needles", + duration=0.9, + }, + FLYNEEDLES={ + file="MARSHAL-FlyYourNeedles", + suffix="ogg", + loud=false, + subtitle="Fly your needles", + duration=0.9, + }, + -- TODO: Other voice overs for marshal. + 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.38, + }, + 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, --0.38 too short + }, +} + +--- Difficulty level. +-- @type AIRBOSS.Difficulty +-- @field #string EASY Flight Stutdent. 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. + +--- Groove position. +-- @type AIRBOSS.GroovePos +-- @field #string X0 Entering the groove. +-- @field #string XX At the start, i.e. 3/4 from the run down. +-- @field #string RB Roger ball. +-- @field #string IM In the middle. +-- @field #string IC In close. +-- @field #string AR At the ramp. +-- @field #string IW In the wires. +AIRBOSS.GroovePos={ + X0="X0", + XX="X", + RB="RB", + IM="IM", + IC="IC", + AR="AR", + IW="IW", +} + +--- Groove data. +-- @type AIRBOSS.GrooveData +-- @field #number Step Current step. +-- @field #number AoA Angle of Attack. +-- @field #number Alt Altitude in meters. +-- @field #number GSE Glide slope error in degrees. +-- @field #number LUE Lineup error in degrees. +-- @field #number Roll Roll angle. +-- @field #number Rhdg Relative heading player to carrier. 0=parallel, +-90=perpendicular. +-- @field #number TGroove Time stamp when pilot entered the groove. + +--- LSO grade +-- @type AIRBOSS.LSOgrade +-- @field #string grade LSO grade, i.e. _OK_, OK, (OK), --, CUT +-- @field #number points Points received. +-- @field #string details Detailed flight analysis. +-- @field #number wire Wire caught. +-- @field #number Tgroove Time in the groove in seconds. + +--- 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 Time the flight was added to the queue. +-- @field Core.UserFlag#USERFLAG flag User flag for triggering events for the flight. +-- @field #boolean ai If true, flight is AI. +-- @field #boolean player If true, flight is a human player. +-- @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. + +--- Parameters of an element in a flight group. +-- @type AIRBOSS.FlightElement +-- @field Wrapper.Unit#UNIT unit Aircraft 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. + +--- Player data table holding all important parameters of each player. +-- @type AIRBOSS.PlayerData +-- @field Wrapper.Unit#UNIT unit Aircraft of the player. +-- @field #string name Player name. +-- @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 #table grades LSO grades of player passes. +-- @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 patternwo If true, player was waved of during the pattern. +-- @field #boolean lig If true, player was long in the groove. +-- @field #number Tlso Last time the LSO gave an advice. +-- @field #number Tgroove Time in the groove in seconds. +-- @field #number wire Wire caught by player when trapped. +-- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elemets are of type @{#AIRBOSS.GrooveData}. +-- @field #table menu F10 radio menu +-- @extends #AIRBOSS.FlightGroup + +--- Main radio menu. +-- @field #table MenuF10 +AIRBOSS.MenuF10={} + +--- Airboss class version. +-- @field #string version +AIRBOSS.version="0.6.3" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Check player heading at zones, e.g. initial. +-- TODO: Include recovery tanker into next stack calculation. Angels six should be empty. +-- TODO: Get charly time estimate function. +-- TODO: Player eject and crash debrief "gradings". +-- TODO: Subtitles off options on player level. +-- TODO: PWO during case 2/3. Also when too close to other player. +-- TODO: Option to filter AI groups for recovery. +-- TODO: Spin pattern. Add radio menu entry. Not sure what to add though?! +-- TODO: Persistence of results. +-- 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 happend 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 covery 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. +-- TONE: 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 + + -- Debug trace. + if false then + self.Debug=true + BASE:TraceOnOff(true) + BASE:TraceClass(self.ClassName) + BASE:TraceLevel(1) + end + + -- Set some string id for output to DCS.log file. + self.lid=string.format("AIRBOSS %s | ", carriername) + + -- 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) + + ------------- + --- Defaults: + ------------- + + -- Set up Airboss radio. + self.MarshalRadio=RADIO:New(self.carrier) + self.MarshalRadio:SetAlias("MARSHAL") + self:SetMarshalRadio() + + -- Set up LSO radio. + self.LSORadio=RADIO:New(self.carrier) + self.LSORadio:SetAlias("LSO") + self:SetLSORadio() + + -- 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() + + -- Set max aircraft in landing pattern. + self:SetMaxLandingPattern() + + -- Set AI handling On. + self:SetHandleAION() + + -- Default recovery case. This sets self.defaultcase and self.case. + self:SetRecoveryCase(1) + + -- Set holding offset to 0 degrees. This set self.defaultoffset and self.holdingoffset. + self:SetHoldingOffsetAngle() + + -- Default player skill EASY. + self:SetDefaultPlayerSkill(AIRBOSS.Difficulty.EASY) + + -- 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) + + -- Init carrier parameters. + if self.carriertype==AIRBOSS.CarrierType.STENNIS then + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.VINSON then + -- TODO: Carl Vinson parameters. + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.TARAWA then + -- TODO: Tarawa parameters. + self:_InitStennis() + elseif self.carriertype==AIRBOSS.CarrierType.KUZNETSOV then + -- Kusnetsov parameters - maybe... + self:_InitStennis() + else + self:E(self.lid.."ERROR: Unknown carrier type!") + return nil + end + + -- CASE I/II moving zone: Zone 2.75 NM astern and 0.1 NM starboard of the carrier with a diameter of 1 NM. + self.zoneInitial=ZONE_UNIT:New("Initial Zone", self.carrier, UTILS.NMToMeters(0.5), {dx=-UTILS.NMToMeters(2.75), dy=UTILS.NMToMeters(0.1), relative_to_unit=true}) + + -- Smoke zones. + if self.Debug and false then + local case=2 + 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.Red, 45) + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + 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:FlareGreen() + + -- 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() + + -- 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() + + -- 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 + + -- If calls should be part of self and individual for different carriers. + --[[ + -- Init default sound files. + for _name,_sound in pairs(AIRBOSS.LSOCall) do + local sound=_sound --#AIRBOSS.RadioCall + local text=string.format() + sound.subtitle=1 + sound.loud=1 + --self.radiocall[_name]=sound + end + ]] + + -- Debug: + if false then + local text="Playing default sound files:" + for _name,_call in pairs(AIRBOSS.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, 10) + + -- Also play the loud version. + if call.loud then + self:RadioTransmission(self.LSORadio, call, true, 10) + end + end + self:I(self.lid..text) + end + + + ----------------------- + --- FSM Transitions --- + ----------------------- + + -- Start State. + self:SetStartState("Stopped") + + -- Add FSM transitions. + -- From State --> Event --> To State + 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("*", "Status", "*") -- Update status of players and queues. + self:AddTransition("*", "RecoveryCase", "*") -- Switch to another case recovery. + 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. + + + --- 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 #number delay Delay in seconds. + -- @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 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. + + + --- 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] __Case + -- @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 "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 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 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 offet 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 + +--- 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. +-- @return #AIRBOSS self +function AIRBOSS:AddRecoveryWindow(starttime, stoptime, case, holdingoffset) + + -- Absolute mission time in seconds. + local Tnow=timer.getAbsTime() + + -- 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 windows rejected.", UTILS.SecondsToClock(Tstart), UTILS.SecondsToClock(Tstop))) + return self + end + if Tstop<=Tnow then + self:E(string.format("ERROR: Recovery stop time %s already over. Tnow=%s! Recovery windows 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 + + -- Recovery window. + local recovery={} --#AIRBOSS.Recovery + recovery.START=Tstart + recovery.STOP=Tstop + recovery.CASE=case + recovery.OFFSET=holdingoffset + recovery.OPEN=false + recovery.OVER=false + + -- Add to table + table.insert(self.recoverytimes, recovery) + + return self +end + +--- Disable automatic TACAN activation +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:SetTACANoff() + self.TACANon=false +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 +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 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 + self.LSOModu=modulation or "AM" + + if modulation=="FM" then + self.LSOModu=radio.modulation.FM + else + self.LSOModu=radio.modulation.AM + end + + self.LSORadio:SetFrequency(self.LSOFreq) + self.LSORadio:SetModulation(self.LSOModu) + + 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 + self.MarshalModu=modulation or "AM" + + if modulation=="FM" then + self.MarshalModu=radio.modulation.FM + else + self.MarshalModu=radio.modulation.AM + end + + self.MarshalRadio:SetFrequency(self.MarshalFreq) + self.MarshalRadio:SetModulation(self.MarshalModu) + + return self +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. +-- @return #AIRBOSS self +function AIRBOSS:SetMaxLandingPattern(nmax) + self.Nmaxpattern=nmax or 4 + 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 #ARIBOSS 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 #ARIBOSS self +function AIRBOSS:SetRecoveryTanker(recoverytanker) + self.tanker=recoverytanker + return self +end + +--- Define warehouse associated with the carrier. +-- @param #AIRBOSS self +-- @param Functional.Warehouse#WAREHOUSE warehouse Warehouse object of the carrier. +-- @return #ARIBOSS self +function AIRBOSS:SetWarehouse(warehouse) + self.warehouse=warehouse + 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 #ARIBOSS 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 + +--- 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 + +--- 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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- FSM event functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests 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.", AIRBOSS.version, self.carrier:GetName(), self.carriertype)) + + -- Current map. + local theatre=env.mission.theatre + self:T2(self.lid..string.format("Theatre = %s", tostring(theatre))) + + -- Activate TACAN. + if self.TACANon then + self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true) + end + + -- Activate ICLS. + if self.ICLSon then + self.beacon:ActivateICLS(self.ICLSchannel, self.ICLSmorse) + end + + -- Handle events. + self:HandleEvent(EVENTS.Birth) + self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.Crash) + self:HandleEvent(EVENTS.Ejection) + + -- Time stamp for checking queues. + self.Tqueue=timer.getTime() + + -- Schedule radio queue checks. + self.RQLid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQLSO, "LSO"}, 1, 0.01) + self.RQMid=self.radiotimer:Schedule(self, self._CheckRadioQueue, {self.RQMarshal, "MARSHAL"}, 1, 0.01) + + -- Initial carrier position and orientation. + self.Cposition=self:GetCoordinate() + self.Corientation=self.carrier:GetOrientationX() + self.Corientlast=self.Corientation + self.Tpupdate=timer.getTime() + + -- Init patrol route of carrier. + self:_PatrolRoute() + + -- Check if no recovery window is set. + if #self.recoverytimes==0 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 + + -- Start status check in 1 second. + self:__Status(1) +end + +--- On after Status event. Checks for new flights, updates queue and checks player status. +-- @param #AIRBOSS self +-- @param #string From From state. +-- @param #string Event Event. +-- @param #string To To state. +function AIRBOSS:onafterStatus(From, Event, To) + + -- Get current time. + local time=timer.getTime() + + -- Update marshal and pattern queue every 30 seconds. + if time-self.Tqueue>30 then + + -- Get time. + local clock=UTILS.SecondsToClock(timer.getAbsTime()) + + -- Debug info. + local text=string.format("Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s", + clock, self:GetState(), self.case, self.carrier:GetVelocityKNOTS(), self:GetHeading(), self.currentwp, UTILS.SecondsToClock(self:_GetETAatNextWP())) + self:T(self.lid..text) + + -- Check recovery times and start/stop recovery mode if necessary. + self:_CheckRecoveryTimes() + + -- Scan carrier zone for new aircraft. + self:_ScanCarrierZone() + + -- Check marshal and pattern queues. + self:_CheckQueue() + + -- Check if marshal pattern of AI needs an update. + self:_CheckPatternUpdate() + + -- Time stamp. + self.Tqueue=time + end + + -- Check player status. + self:_CheckPlayerStatus() + + -- Check AI landing pattern status + self:_CheckAIStatus() + + -- Call status every 0.5 seconds. + self:__Status(-0.5) +end + +--- 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 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 + +--- 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 landing pattern. + for _,_flight in pairs(self.Qpattern) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Only AI! + if flight.ai then + + -- Loop over all units in AI flight. + for _,_element in pairs(flight.elements) do + local element=_element --#AIRBOSS.FlightElement + + -- Unit + local unit=element.unit + + -- Get lineup and distance to carrier. + local lineup=self:_Lineup(unit, true) + + -- Distance in NM. + local distance=UTILS.MetersToNM(unit:GetCoordinate():Get2DDistance(self:GetCoordinate())) + + -- Altitude in ft. + local alt=UTILS.MetersToFeet(unit:GetAltitude()) + + -- Check if parameters are right and flight is in the groove. + if lineup<2 and distance<=0.75 and alt<500 and not element.ballcall then + + -- Paddles: Call the ball! + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.CALLTHEBALL, false, 0) + + -- Pilot: "405, Hornet Ball, 3.2" + -- TODO: Voice over. + local text=string.format("%s Ball, %.1f.", self:_GetACNickname(unit:GetTypeName()), self:_GetFuelState(unit)/1000) + self:MessageToPattern(text, element.onboard, "", 3, false, 0, true) + + -- Debug message. + MESSAGE:New(string.format("%s, %s", element.onboard, text), 15, "DEBUG"):ToAllIf(self.Debug) + + -- Paddles: Roger ball after 3 seconds. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL, false, 3) + + -- Flight element called the ball. + element.ballcall=true + + -- This is for the whole flight. Maybe we need it. + flight.ballcall=true + end + + end + end + end + +end + +--- Check if player in the landing pattern is too close to another aircarft in the pattern. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData player Player data. +function AIRBOSS:_CheckPlayerPatternDistance(player) + + -- Nothing to do since we check only in the pattern. + if #self.Qpattern==0 then + return + end + + --- Function that checks if unit1 is too close to unit2. + local function _checkclose(_unit1, _unit2) + + local unit1=_unit1 --Wrapper.Unit#UNIT + local unit2=_unit2 --Wrapper.Unit#UNIT + + if (not unit1) or (not unit2) then + return false + end + + -- Check that this is not the same unit. + if unit1:GetName()==unit2:GetName() then + return false + end + + -- Return false when unit2 is not in air? Could be on the carrier. + if not unit2:InAir() then + return false + end + + -- Positions of units. + local c1=unit1:GetCoordinate() + local c2=unit2:GetCoordinate() + + -- Vector from unit1 to unit2 + local vec12={x=c2.x-c1.x, y=0, z=c2.z-c1.z} --DCS#Vec3 + + -- Distance between units. + local dist=UTILS.VecNorm(vec12) + + -- Orientation of unit 1 in space. + local vec1=unit1:GetOrientationX() + vec1.y=0 + + -- Get angle between the two orientation vectors. Does the player aircraft nose point into the direction of the other aircraft? (Could be behind him!) + local rhdg=math.deg(math.acos(UTILS.VecDot(vec12,vec1)/UTILS.VecNorm(vec12)/UTILS.VecNorm(vec1))) + + -- Check altitude difference? + local dalt=math.abs(c2.y-c1.y) + + -- 650 feet ~= 200 meters distance between flights + local dcrit=UTILS.FeetToMeters(650) + + -- Direction in 30 degrees cone and distance < 200 meters and altitude difference <50 + -- TODO: Test parameter values. + if math.abs(rhdg)<10 and dist=recovery.START then + -- Start time has passed. + + if time1 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 +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 + + -- Debug output. + local text=string.format("Starting aircraft recovery case %d.", 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) + + -- Message to all players in marshal stack. + -- TODO: maybe to all flights in CCA? + self:MessageToMarshal(text, "MARSHAL", "99") + + -- Switch to case. + self:RecoveryCase(Case, Offset) +end + +--- On after "RecoveryStop" event. Recovery of aircraft is stopped and carrier switches to state "Idle". +-- @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. Carrier goes to state idle.")) +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:UnHandleEvent(EVENTS.Birth) + self:UnHandleEvent(EVENTS.Land) + self:UnHandleEvent(EVENTS.Crash) + self:UnHandleEvent(EVENTS.Ejection) +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Parameter initialization +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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 + + -- If final waypoint reached, do route all over again. + if i==final and final>1 and airboss.adinfinitum then + airboss:_PatrolRoute() + end +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 + + +--- Patrol carrier +-- @param #AIRBOSS self +-- @return #AIRBOSS self +function AIRBOSS:_PatrolRoute() + + -- Get carrier group. + local CarrierGroup=self.carrier:GetGroup() + + -- Waypoints of group. + local Waypoints = CarrierGroup:GetTemplateRoutePoints() + + -- NOTE: This is only necessary, if the first waypoint would already be far way, i.e. when the script is started with a large delay. + -- Calculate the new Route. + --local wp0=CarrierGroup:GetCoordinate():WaypointGround(5.5*3.6) + -- Insert current coordinate as first waypoint + --table.insert(Waypoints, 1, wp0) + + for n=1,#Waypoints do + + -- Passing waypoint taskfunction + local TaskPassingWP=CarrierGroup:TaskFunction("AIRBOSS._PassingWaypoint", self, n, #Waypoints) + + -- Call task function when carrier arrives at waypoint. + CarrierGroup:SetTaskWaypoint(Waypoints[n], TaskPassingWP) + end + + -- Set waypoint table. + local i=1 + for _,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 + + -- Increase counter. + i=i+1 + end + + -- Current waypoint is 1. + self.currentwp=1 + + -- Route carrier group. + CarrierGroup:Route(Waypoints) +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() + + -- 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 + + +--- Estimated the carrier position at some point in the future given the current waypoints and speeds. +-- @param #AIRBOSS self +-- @param #number time Absolute mission time at which the carrier position is requested. +-- @return Core.Point#COORDINATE Coordinate of the carrier at the given time. +function AIRBOSS:_GetCarrierFuture(time) + + local nwp=self.currentwp + + local waypoints={} + local lastwp=nil --Core.Point#COORDINATE + for i=1,#self.waypoints do + + if i>nwp then + table.insert(waypoints, self.waypoints[i]) + elseif i==nwp then + lastwp=self.waypoints[i] + end + + end + + -- Current abs. time. + local tnow=timer.getAbsTime() + + local p=self:GetCoordinate() + local v=self.carrier:GetVelocityMPS() + + local s=p:Get2DDistance(self.waypoints[nwp+1]) + + -- v=s/t <==> t=s/v + local t=s/v + + local eta=UTILS.SecondsToClock(t+tnow) + + + for _,_wp in ipairs(waypoints) do + local wp=_wp --Core.Point#COORDINATE + + end + +end + +--- 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=-400 -- Not more than 400 m port of boat. Otherwise miss the zone. + 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= nil + self.Abeam.Xmax= nil + self.Abeam.Zmin=-UTILS.NMToMeters(2) -- Not more than 2 NM port. + self.Abeam.Zmax= 100 -- 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=-1000 -- Not more than 1 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 + +--- 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 + + -- 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 skyhawk then + -- A-4E-C Skyhawk parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390 + aoa.SLOW=19.0 + aoa.Slow=18.5 + aoa.OnSpeedMax=18.0 + aoa.OnSpeed=17.5 + aoa.OnSpeedMin=17.0 + aoa.Fast=16.5 + aoa.FAST=16.0 + 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 + +--- 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 + + -- 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 + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.ARCOUT then + + speed=UTILS.KnotsToMps(250) + + elseif step==AIRBOSS.PatternStep.DIRTYUP then + + alt=UTILS.FeetToMeters(1200) + + dist=UTILS.NMToMeters(12) + + 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 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 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 then + alt=UTILS.FeetToMeters(800) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + end + + elseif step==AIRBOSS.PatternStep.LATEBREAK then + + if hornet then + alt=UTILS.FeetToMeters(800) + elseif skyhawk then + alt=UTILS.FeetToMeters(600) + end + + elseif step==AIRBOSS.PatternStep.ABEAM then + + if hornet then + alt=UTILS.FeetToMeters(600) + elseif skyhawk then + alt=UTILS.FeetToMeters(500) + end + + aoa=aoaac.OnSpeed + + dist=UTILS.NMToMeters(1.2) + + elseif step==AIRBOSS.PatternStep.NINETY then + + if hornet then + alt=UTILS.FeetToMeters(500) + elseif skyhawk then + alt=UTILS.FeetToMeters(500) + end + + aoa=aoaac.OnSpeed + + elseif step==AIRBOSS.PatternStep.WAKE then + + if hornet then + alt=UTILS.FeetToMeters(370) + elseif skyhawk then + alt=UTILS.FeetToMeters(370) --? + end + + aoa=aoaac.OnSpeed + + elseif step==AIRBOSS.PatternStep.FINAL then + + if hornet then + alt=UTILS.FeetToMeters(300) + elseif skyhawk then + 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() + + -- Min 5 min in marshal before send to landing pattern. + local TmarshalMin=10*60 + + for _,_flight in pairs(self.Qmarshal) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Current stack. + local stack=flight.flag:Get() + + -- Marshal time. + local Tmarshal=timer.getAbsTime()-flight.time + + -- Check if conditions are right. + if stack==1 and flight.holding and Tmarshal>=TmarshalMin then + return flight + end + end + + return nil +end + + +--- Check marshal and pattern queues. +-- @param #AIRBOSS self +function AIRBOSS:_CheckQueue() + + -- Print queues. + self:_PrintQueue(self.flights, "All Flights") + self:_PrintQueue(self.Qmarshal, "Marshal") + self:_PrintQueue(self.Qpattern, "Pattern") + + -- Get number of aircraft units(!) currently in pattern. + local _,npattern=self:_GetQueueInfo(self.Qpattern) + + -- Get next marshal flight. + local marshalflight=self:_GetNextMarshalFight() + + -- Check if there are flights in marshal strack and if the pattern is free. + 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 aircraft in this group. + local npunits=patternflight.nunits + + -- 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=3*60*npunits --45*npunits -- 45 seconds interval per plane! + else + TpatternMin=3*60*npunits --120*npunits -- 120 seconds interval per plane! + end + + -- Check recovery window open and enough space to last pattern flight. + if self:IsRecovering() and Tpattern>TpatternMin then + self:_CheckCollapseMarshalStack(marshalflight) + 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 Rout=UTILS.NMToMeters(50) + 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 + + -- 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))) + + -- Send AI flight to marshal stack if group closes in more than 5 and has initial flag value. + if closein>UTILS.NMToMeters(5) and knownflight.flag:Get()==-100 then + + -- Check that we do not add a recovery tanker for marshaling. + if self.tanker and self.tanker.tanker:GetName()==groupname then + + -- Don't touch the recovery tanker! + + else + + -- Get the next free stack for current recovery case. + local stack=self:_GetFreeStack(self.case) + + -- Send AI to marshal stack. + self:_MarshalAI(knownflight, stack) + + -- Add group to marshal stack queue. + self:_AddMarshalGroup(knownflight, stack) + + end -- Tanker + end -- Closed in + end -- AI + else + -- Unknown new flight. Create a new flight group. + self:_CreateFlightGroup(group) + 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 not (flight.case>1 and self:_InQueue(self.Qmarshal, flight.group)) then + table.insert(remove, flight.group) + end + end + end + + -- Remove flight groups outside CCA. + for _,group in pairs(remove) do + self:_RemoveFlightGroup(group) + end + +end + + +--- Orbit at a specified position at a specified alititude with a specified speed. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_MarshalPlayer(playerData) + + -- Check if flight is known to the airboss already. + if playerData then + + -- Get free stack. + local mystack=self:_GetFreeStack(self.case) + + -- Add group to marshal stack. + self:_AddMarshalGroup(playerData, mystack) + + -- Set step to holding. + playerData.step=AIRBOSS.PatternStep.HOLDING + playerData.warning=nil + + -- 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 + flight.step=AIRBOSS.PatternStep.HOLDING + flight.holding=nil + flight.flag:Set(mystack) + end + + else + + -- Flight is not registered yet. + local text="you are not yet registered inside the CCA. Marshal request denied!" + self:MessageToPlayer(playerData, text, "MARSHAL") + + end + +end + +--- Command AI flight to orbit at a specified position at a specified alititude with a specified speed. +-- 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. This should be #self.Qmarshal+1 for new flight groups. +function AIRBOSS:_MarshalAI(flight, nstack) + + -- Flight group name. + local group=flight.group + local groupname=flight.groupname + + -- Get old/current stack. + local ostack=flight.flag:Get() + + -- Set new stack. + flight.flag:Set(nstack) + + -- Current carrier position. + local Carrier=self:GetCoordinate() + + -- Carrier heading. + local hdg=self:GetHeading() + + -- Recovery case. + local case=flight.case + + -- 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(400) + + 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. + -- TODO: Test and tune! + 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 + + -- Reinit waypoints. + group:WayPointInitialize(wp) + + -- Route group. + group:Route(wp, 0) + +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 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]=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) + + else + + -- CASE II/III: Holding at 6000 ft on a racetrack pattern astern the carrier. + angels0=6 + + -- Distance: d=n*angles0+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(7) + + -- 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 + +--- Add a flight group to a specific marshal stack and to the marshal queue. +-- @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:Set(stack) + + -- Set recovery case. + flight.case=self.case + + -- Pressure. + local P=UTILS.hPa2inHg(self:GetCoordinate():GetPressure()) + + -- Stack altitude. + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, flight.case)) + local brc=self:GetBRC() + + -- Marshal message. + -- TODO: Get charlie time estimate. + local text=string.format("Case %d, BRC is %03d, hold at %d. Expected Charlie Time XX.\n", flight.case, brc, alt) + text=text..string.format("Altimeter %.2f. Report see me.", 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) + text=text..string.format("\nSelect TACAN %d°, channel %d%s (%s)", radial, self.TACANchannel,self.TACANmode, self.TACANmorse) + end + + -- Message to all players. + self:MessageToAll(text, "MARSHAL", flight.onboard) + + -- Add to marshal queue. + table.insert(self.Qmarshal, flight) +end + +--- Check if marshal stack can be 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:_CheckCollapseMarshalStack(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:_CollapseMarshalStack(flight) + self:_LandAI(flight) + end + + -- Inform all flights. + local text=string.format("You are cleared for Case %d recovery.", flight.case) + self:MessageToAll(text, "MARSHAL", flight.onboard) + + -- Hint for human players. + if not flight.ai then + local playerData=flight --#AIRBOSS.PlayerData + + -- Hint for easy skill. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + self:MessageToPlayer(flight, string.format("Use F10 radio menu \"Request Commence\" command when ready!"), nil, "", 5) + end + 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:Get() + + -- 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 collaps stack of which the flight left. CASE II/III stack is the same. + if (case==1 and mflight.case==1) or (case>1 and mflight.case>1) then + + -- Get current flag/stack value. + local mstack=mflight.flag:Get() + + -- Only collapse stacks above the new pattern flight. + -- This will go wrong, if patternflight is not in marshal stack because it will have value -100 and all mstacks will be larger! + -- Maybe need to set the initial value to 1000? Or check stack>0 of pattern flight? + if stack>0 and mstack>stack then + + -- New stack is old stack minus one. + -- TODO: If we include the recovery tanker, this needs to be generalized. + local newstack=mstack-1 + + -- Debug info. + self:T(self.lid..string.format("Flight %s case %d is changing marshal stack %d --> %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:Set(newstack) + + -- Inform players. + if mflight.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Send message to all non-pros that they can descent. + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(newstack, case)) + local text=string.format("descent to next lower stack at %d ft", alt) + self:MessageToPlayer(mflight, text, "MARSHAL") + + end + + -- 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:Set(newstack) + + -- Inform section member. + if sec.difficulty~=AIRBOSS.Difficulty.HARD then + local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(newstack, case)) + local text=string.format("follow your lead to next lower stack at %d ft", alt) + self:MessageToPlayer(sec, text, "MARSHAL") + 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)) + + -- Set flag to -1. -1 is rather arbitrary. Should not be -100 or positive. + flight.flag:Set(-1) + + 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)) + + -- Decrease flag. + flight.flag:Set(stack-1) + + -- Add flight to pattern queue. + table.insert(self.Qpattern, flight) + + end + + -- New time stamp for time in pattern. + flight.time=timer.getAbsTime() + + + -- Remove flight from marshal queue. + self:_RemoveGroupFromQueue(self.Qmarshal, flight.group) + +end + +--- Get next free stack depending on recovery case. Note that here we assume one flight group per stack! +-- @param #AIRBOSS self +-- @param #number case Recovery case. Default current (self) case in progress. +-- @return #number Lowest free stack available for the given case. +function AIRBOSS:_GetFreeStack(case) + + -- Recovery case. + case=case or self.case + + -- Get stack + local nfull + if case==1 then + -- Lowest Case I stack. + nfull=self:_GetQueueInfo(self.Qmarshal, 1) + else + -- Lowest Case II or III stack. + nfull=self:_GetQueueInfo(self.Qmarshal, 23) + end + + -- Simple case without a recovery tanker for now. + local nfree=nfull+1 + + --[[ + -- Get recovery tanker stack. + local tankerstack=9999 + if self.tanker and case==1 then + tankerstack=self:_GetAngels(self.tanker.altitude) + end + + if nfull=1 + + -- No update if carrier is turning! + if turning then + self:T2(self.lid..string.format("Carrier is turning. Delta Heading = %.1f", deltaLast)) + return + end + + -- Check if orientation changed. + local Hchange=false + if math.abs(deltaHeading)>=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.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. Turning=%s.", UTILS.MetersToNM(dist), tostring(turning))) + Dchange=true + end + + -- 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:Get()) + end + + end + + -- Inform player about new final bearing. + if Hchange then + -- 99, new final bearing XXX + local FB=self:GetFinalBearing(true) + local text=string.format("new final bearing %d.", FB) + self:MessageToAll(text, "MARSHAL", "99", 10) + end + + -- Reset parameters for next update check. + self.Corientation=vNew + self.Cposition=pos + self.Tpupdate=timer.getTime() + 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 and in air. + if unit:IsAlive() then + + -- Display aircraft attitude and other parameters as message text. + if playerData.attitudemonitor then + self:_AttitudeMonitor(playerData) + end + + -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier). + if unit:IsInZone(self.zoneCCA) then + + -- Check if player is too close to another aircraft in the pattern. + -- TODO: At which steps is the really necessary. Case II/III? + if playerData.step==AIRBOSS.PatternStep.INITIAL or + playerData.step==AIRBOSS.PatternStep.BREAKENTRY or + playerData.step==AIRBOSS.PatternStep.EARLYBREAK or + playerData.step==AIRBOSS.PatternStep.LATEBREAK or + playerData.step==AIRBOSS.PatternStep.ABEAM or + playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_IM then + --self:_CheckPlayerPatternDistance(playerData) + end + + 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 + + -- Might still be better to stay in commencing? + + elseif playerData.step==AIRBOSS.PatternStep.HOLDING then + + -- CASE I/II/III: In holding pattern. + self:_Holding(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.COMMENCING then + + -- CASE I/II/III: New approach. + self:_Commencing(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.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_RB 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_IW then + + -- CASE I/II: In the groove. + self:_Groove(playerData) + + elseif playerData.step==AIRBOSS.PatternStep.DEBRIEF then + + -- Debriefing in 10 seconds. + SCHEDULER:New(self, self._Debrief, {playerData}, 10) + + -- Undefined status. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + else + + self:E(self.lid..string.format("ERROR: Unknown player step %s. Please report!", tostring(playerData.step))) + + end + + else + self:T(self.lid.."WARNING: Player left the CCA!") + end + + else + -- Unit not alive. + self:T(self.lid.."WARNING: Player unit is not alive!") + end + end + end + +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}) + + local _unitName=EventData.IniUnitName + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + self:T3(self.lid.."BIRTH: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."BIRTH: group = "..tostring(EventData.IniGroupName)) + self:T3(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("AIRBOSS: 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:T(self.lid..text) + return + end + + -- Add Menu commands. + self:_AddF10Commands(_unitName) + + -- Init new player data. + local playerData=self:_NewPlayer(_unitName) + + -- Init player data. + self.players[_playername]=playerData + + -- Welcome player message. + self:MessageToPlayer(playerData, string.format("Welcome, %s %s!", playerData.difficulty, playerData.name), "AIRBOSS", "", 5) + end +end + +--- Airboss event handler for event land. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventLand(EventData) + self:F3({eventland = EventData}) + + -- Get unit name that landed. + local _unitName=EventData.IniUnitName + + -- Check if this was a player. + local _unit, _playername=self:_GetPlayerUnitAndName(_unitName) + + -- Debug output. + self:T3(self.lid.."LAND: unit = "..tostring(EventData.IniUnitName)) + self:T3(self.lid.."LAND: group = "..tostring(EventData.IniGroupName)) + self:T3(self.lid.."LAND: player = "..tostring(_playername)) + + -- This would be the closest airbase. + local airbase=EventData.Place + 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() + + 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 that player landed on the carrier. + if _unit:IsInZone(zoneCarrier) then + + -- 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 + + -- 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 + + -- Get wire. We additionally shift the landing coord back because landing event for players is unfortunately delayed. + local wire=self:_GetWire(coord, 75) + + -- No wire ==> Bolter, Bolter radio call. + -- TODO: might need a better place for this. or check + if wire>4 then + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.BOLTER) + end + + -- Get time in the groove. + local gdataX0=playerData.groove.X0 --#AIRBOSS.GrooveData + if gdataX0 then + playerData.Tgroove=timer.getTime()-gdataX0.TGroove + else + playerData.Tgroove=999 + end + + -- Debug text. + local text=string.format("Player %s AC type %s landed at dist=%.1f m. Trapped wire=%d.", playerData.name, playerData.actype, dist, wire) + text=text..string.format(" X=%.1f m, Z=%.1f m, rho=%.1f m.", X, Z, rho) + self:T(self.lid..text) + + -- We did land. + playerData.landed=true + + -- Unkonwn step until we now more. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + -- Call trapped function in 1 second to make sure we did not bolter. + SCHEDULER:New(self, self._Trapped, {playerData}, 1) + + end + + else + -- TODO: Handle case where player did not land on the carrier. + 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 + + else + + -- AI unit landed. + + -- 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) + + -- AI always lands ==> remove unit from flight group and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) + + end + end +end + +--- Airboss event handler for event crash. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventCrash(EventData) + self:F3({eventland = EventData}) + + 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 + self:T(self.lid..string.format("Player %s crashed!",_playername)) + else + self:T2(self.lid..string.format("AI unit %s crashed!", EventData.IniUnitName)) + end + + -- Remove unit from flight and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) +end + +--- Airboss event handler for event Ejection. +-- @param #AIRBOSS self +-- @param Core.Event#EVENTDATA EventData +function AIRBOSS:OnEventEjection(EventData) + self:F3({eventland = EventData}) + + 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)) + else + self:T2(self.lid..string.format("AI unit %s ejected!", EventData.IniUnitName)) + end + + -- Remove unit from flight and queues. + self:_RemoveUnitFromFlight(EventData.IniUnit) +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- PATTERN functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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:Get() + + -- Pattern alitude. + 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) + + -- Check if player is in holding zone. + local inholdingzone=unit:IsInZone(zoneHolding) + + -- Altitude difference between player and assinged 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 +-100 ft. + altgood=UTILS.FeetToMeters(100) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- Normal guys should be within +-300 ft. + altgood=UTILS.FeetToMeters(300) + elseif playerData.difficulty==AIRBOSS.Difficulty.EASY then + -- Students should be within +-500 ft. + altgood=UTILS.FeetToMeters(500) + else + -- ERROR + end + + -- Check if stack just collapsed and give the player one minute to change the alitude. + local justcollapsed=false + if self.Tcollapse then + -- Time since last stack change. + local dT=timer.getTime()-self.Tcollapse + + -- Check if less then 60 seconds. + if dT<=60 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)<=altgood 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. + self:T("Player is back in the holding zone after leaving it.") + 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:T2("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 + + -- Debug output. + self:T("Player entered the holding zone for the first time.") + + -- 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 + + -- No info for the pros. + if playerData.difficulty==AIRBOSS.Difficulty.HARD then + text="" + end + + else + -- Player did not yet arrive in holding zone. + self:T2("Waiting for player to arrive in the holding zone.") + end + + end + + -- Send message. + self:MessageToPlayer(playerData, text, "MARSHAL", nil, 5) +end + + +--- Commence approach. This step initializes the player data. Next step depends on recovery case: +-- +-- * Case 1: Initial +-- * Case 2/3: Platform +-- +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data. +function AIRBOSS:_Commencing(playerData) + + -- Initialize player data for new approach. + self:_InitPlayer(playerData) + + -- Commencing message to player only. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + local text=string.format("Commencing. (Case %d)", playerData.case) + self:MessageToPlayer(playerData, text, playerData.onboard, "", 5) + end + + -- Next step: depends on case recovery. + if playerData.case==1 then + -- CASE I: Player has to fly to the initial which is 3 NM DME astern of the boat. + playerData.step=AIRBOSS.PatternStep.INITIAL + else + -- CASE II/III: Player has to start the descent at 4000 ft/min to the platform at 5k ft. + playerData.step=AIRBOSS.PatternStep.PLATFORM + end + + -- Next step hint. + self:_StepHint(playerData) + playerData.warning=nil +end + +--- Start pattern when player enters the initial zone in case I/II recoveries. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Initial(playerData) + + -- Check if player is in initial zone and entering the CASE I pattern. + local inzone=playerData.unit:IsInZone(self.zoneInitial) + + -- 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 + + -- Send message for normal and easy difficulty. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Inform player. + local hint=string.format("Initial") + + -- Hook down for students. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint.." - Hook down!" + end + + self:MessageToPlayer(playerData, hint, "MARSHAL") + end + + -- Next step: Break entry. + playerData.step=AIRBOSS.PatternStep.BREAKENTRY + playerData.warning=nil + self:_StepHint(playerData) + end + +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 valid approach corridor!", "MARSHAL") + playerData.warning=true + end + + -- Back in zone. + if (not invalid) and playerData.warning then + self:MessageToPlayer(playerData, "You're back in the approach corridor. Now stay there!", "MARSHAL") + 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 + + -- Debug message. + MESSAGE:New("Platform step reached", 5, "DEBUG"):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed =self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt=self:_AltitudeCheck(playerData, altitude) + + -- Get altitude hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Altitude and speed hint. + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: depends. + if math.abs(self.holdingoffset)>0 and playerData.case>1 then + -- Turn to BRC (case II) or FB (case III). + playerData.step=AIRBOSS.PatternStep.ARCIN + else + if playerData.case==2 then + -- Case II: Initial zone then Case I recovery. + playerData.step=AIRBOSS.PatternStep.INITIAL + elseif playerData.case==3 then + -- CASE III: Dirty up. + playerData.step=AIRBOSS.PatternStep.DIRTYUP + end + end + + -- Next step hint. + self:_StepHint(playerData) + playerData.warning=nil + 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 + + -- Debug message. + MESSAGE:New("Arc Turn In step reached", 5, "DEBUG"):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get speed hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint speed. + local hint=string.format("%s\n%s", playerData.step, hintSpeed) + + -- 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 %d°.", turn, radial) + end + + -- Message to player. + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: Arc Out Turn. + playerData.step=AIRBOSS.PatternStep.ARCOUT + playerData.warning=nil + self:_StepHint(playerData) + 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 self:_CheckLimits(X, Z, self.DirtyUp) then + if inzone then + + -- Debug message. + MESSAGE:New("Arc Turn Out step reached", 5, "DEBUG"):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get speed hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s", playerData.step, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: + if playerData.case==2 then + -- Case II: Initial. + playerData.step=AIRBOSS.PatternStep.INITIAL + elseif playerData.case==3 then + -- Case III: Dirty up. + playerData.step=AIRBOSS.PatternStep.DIRTYUP + else + -- ERROR! + end + + -- Next step hint. + self:_StepHint(playerData) + playerData.warning=nil + 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 + + -- Debug message. + MESSAGE:New("Dirty up step reached", 5, "DEBUG"):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt, debrief=self:_AltitudeCheck(playerData, altitude) + + -- Get speed hint. + -- TODO: Not sure if we already need to be onspeed AoA at this point? + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint alt and speed. + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + + -- Hint turn and set TACAN. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint.."\nDirty up! Hook, gear and flaps down." + end + + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Radio call "Say/Fly needles". Delayed by 10/15 seconds. + self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.SAYNEEDLES, false, 10) + self:RadioTransmission(self.MarshalRadio, AIRBOSS.MarshalCall.FLYNEEDLES, false, 15) + -- TODO: Make Fly Bullseye call if no automatic ICLS is active. + + -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS). + playerData.step=AIRBOSS.PatternStep.BULLSEYE + playerData.warning=nil + self:_StepHint(playerData) + end +end + +--- Intercept glide slop and follow ICLS, aka Bullseye for case III recovery. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +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 + + -- Debug message. + MESSAGE:New("Bullseye step reached", 5, "DEBUG"):ToAllIf(self.Debug) + + -- Get optimal altitiude. + local altitude, aoa, distance, speed=self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt=self:_AltitudeCheck(playerData, altitude) + + -- Get altitude hint. + local hintAoA=self:_AoACheck(playerData, aoa) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint alt and aoa. + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + + -- Hint follow the needles. + if playerData.difficulty==AIRBOSS.Difficulty.EASY then + hint=hint..string.format("Intercept glide slope and follow the needles.") + end + + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: Groove Call the ball. + playerData.step=AIRBOSS.PatternStep.GROOVE_XX + playerData.warning=nil + + -- Stephint should be empty. + self:_StepHint(playerData) + 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, rho, phi=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 + + -- Get optimal altitude, distance and speed. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + + -- Get altitude hint. + local hintAlt=self:_AltitudeCheck(playerData, alt) + + -- Get speed hint. + local hintSpeed=self:_SpeedCheck(playerData, speed) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintSpeed) + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Next step: Early Break. + playerData.step=AIRBOSS.PatternStep.EARLYBREAK + playerData.warning=nil + self:_StepHint(playerData) + 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, rho, phi=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 + + -- Check limits. + if self:_CheckLimits(X, Z, breakpoint) then + + -- Get optimal altitude, distance and speed. + local altitude=self:_GetAircraftParameters(playerData) + + -- Grade altitude. + local hint, debrief=self:_AltitudeCheck(playerData, altitude) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + + -- Hint alt. + local hint=string.format("%s %s", playerData.step, hint) + + -- Hint dirty up. + if playerData.difficult==AIRBOSS.Difficulty.EASY and part==AIRBOSS.PatternStep.LATEBREAK then + hint=hint.."\nDirty up! Gear down, flaps down. Check hook down." + end + + self:MessageToPlayer(playerData, hint, "MARSHAL", "") + end + + -- Debrief + self:_AddToDebrief(playerData, debrief) + + -- Next step: Late Break or Abeam. + if part==AIRBOSS.PatternStep.EARLYBREAK then + playerData.step=AIRBOSS.PatternStep.LATEBREAK + else + playerData.step=AIRBOSS.PatternStep.ABEAM + end + + playerData.warning=nil + self:_StepHint(playerData) + end +end + +--- Long downwind leg check. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_CheckForLongDownwind(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z=self:_GetDistances(playerData.unit) + + -- One NM from carrier is too far. + local limit=UTILS.NMToMeters(-1.5) + + -- Check we are not too far out w.r.t back of the boat. + if X90 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") + --TODO: pattern WO? + 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 + + -- Get optimal altitude, distance and speed. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + + -- Grade altitude. + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, alt) + + -- Grade AoA. + local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + self:MessageToPlayer(playerData, hint, "LSO", "") + end + + -- Debrief. + local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) + + -- Add to debrief. + self:_AddToDebrief(playerData, debrief) + + -- Next step: Final. + playerData.step=AIRBOSS.PatternStep.FINAL + playerData.warning=nil + self:_StepHint(playerData) + end +end + +--- Turn to final. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Final(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) + + -- In front of carrier or more than 4 km behind carrier. + if self:_CheckAbort(X, Z, self.Final) then + self:_AbortPattern(playerData, X, Z, self.Final, true) + return + end + + -- Relative heading 0=fly parallel +-90=fly perpendicular + local relhead=self:_GetRelativeHeading(playerData.unit, true) + + -- Line up wrt runway. + local lineup=self:_Lineup(playerData.unit, true) + + -- Player's angle of bank. + local roll=playerData.unit:GetRoll() + + -- Check if player is in +-5 deg cone and flying towards the runway. + if math.abs(lineup)<5 then --and math.abs(relhead)<5 then + + -- Get optimal altitude, distance and speed. + local alt, aoa, dist, speed=self:_GetAircraftParameters(playerData) + + -- Grade altitude. + local hintAlt, debriefAlt=self:_AltitudeCheck(playerData, alt) + + -- AoA feed back + local hintAoA, debriefAoA=self:_AoACheck(playerData, aoa) + + -- Message to player. + if playerData.difficulty~=AIRBOSS.Difficulty.HARD then + local hint=string.format("%s\n%s\n%s", playerData.step, hintAlt, hintAoA) + self:MessageToPlayer(playerData, hint, "LSO", "") + end + + -- Add to debrief. + local debrief=string.format("%s\n%s", debriefAlt, debriefAoA) + self:_AddToDebrief(playerData, debrief) + + -- Gather pilot data. + local groovedata={} --#AIRBOSS.GrooveData + groovedata.Step=playerData.step + groovedata.Alt=alt + groovedata.AoA=aoa + groovedata.GSE=self:_Glideslope(playerData.unit, 3.5) + groovedata.LUE=self:_Lineup(playerData.unit, true) + groovedata.Roll=roll + groovedata.Rhdg=relhead + groovedata.TGroove=timer.getTime() + + -- TODO: could add angled approach if lineup<5 and relhead>5. This would mean the player has not turned in correctly! + + -- Groove data. + playerData.groove.X0=groovedata + + -- Next step: X start & call the ball. + playerData.step=AIRBOSS.PatternStep.GROOVE_XX + playerData.warning=nil + self:_StepHint(playerData) + end + +end + + +--- In the groove. +-- @param #AIRBOSS self +-- @param #AIRBOSS.PlayerData playerData Player data table. +function AIRBOSS:_Groove(playerData) + + -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier) + local X, Z = self:_GetDistances(playerData.unit) + + -- Player altitude + local alt=playerData.unit:GetAltitude() + + -- Player group. + local player=playerData.unit:GetGroup() + + -- Check abort conditions. + if self:_CheckAbort(X, Z, self.Groove) then + self:_AbortPattern(playerData, X, Z, self.Groove, true) + return + end + + -- Stern position at the rundown. + local stern=self:_GetSternCoord() + + -- Distance from rundown to player aircraft. + local rho=stern:Get2DDistance(playerData.unit:GetCoordinate()) + + -- Lineup with runway centerline. + local lineupError=self:_Lineup(playerData.unit, true) + + -- Glide slope. + local glideslopeError=self:_Glideslope(playerData.unit, 3.5) + + -- Get AoA. + local AoA=playerData.unit:GetAoA() + + -- For debugging. + MESSAGE:New(string.format("%s: LineUp=%.1f GlideSlope=%.1f AoA=%.1f", playerData.step, lineupError, glideslopeError, AoA), 3, nil, true):ToAllIf(self.Debug) + + -- Ranges in the groove. + local RXX=UTILS.NMToMeters(0.750) -- Start of groove. 0.75 = 1389 m + local RRB=UTILS.NMToMeters(0.500) -- Roger Ball! call. 0.5 = 926 m + local RIM=UTILS.NMToMeters(0.375) -- In the Middle 0.75/2. 0.375 = 695 m + local RIC=UTILS.NMToMeters(0.100) -- In Close. 0.1 = 185 m + local RAR=UTILS.NMToMeters(0.000) -- At the Ramp. + + -- Data + local groovedata={} --#AIRBOSS.GrooveData + groovedata.Step=playerData.step + groovedata.Alt=alt + groovedata.AoA=AoA + groovedata.GSE=glideslopeError + groovedata.LUE=lineupError + groovedata.Roll=playerData.unit:GetRoll() + groovedata.Rhdg=self:_GetRelativeHeading(playerData.unit, true) + + if rho<=RXX and playerData.step==AIRBOSS.PatternStep.GROOVE_XX then + + -- LSO "Call the ball" call. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.CALLTHEBALL) + playerData.Tlso=timer.getTime() + + -- Pilot "405, Hornet Ball, 3.2" + -- Pilot output should come from pilot. + --local text=string.format("Hornet Ball, %.1f", self:_GetFuelState(playerData.unit)/1000) + --self:MessageToPlayer(playerData, text, playerData.onboard, "", 3, false, 3) + + -- Store data. + playerData.groove.XX=groovedata + + -- Next step: roger ball. + playerData.step=AIRBOSS.PatternStep.GROOVE_RB + playerData.warning=nil + + elseif rho<=RRB and playerData.step==AIRBOSS.PatternStep.GROOVE_RB then + + -- LSO "Roger ball" call. + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.ROGERBALL) + playerData.Tlso=timer.getTime()+1 + + -- Store data. + playerData.groove.RB=groovedata + + -- Next step: in the middle. + playerData.step=AIRBOSS.PatternStep.GROOVE_IM + playerData.warning=nil + + elseif rho<=RIM and playerData.step==AIRBOSS.PatternStep.GROOVE_IM then + + -- Debug. + local text=string.format("Groove IM=%d m", rho) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:T2(self.lid..text) + + -- Store data. + playerData.groove.IM=groovedata + + -- Next step: in close. + playerData.step=AIRBOSS.PatternStep.GROOVE_IC + playerData.warning=nil + + elseif rho<=RIC and playerData.step==AIRBOSS.PatternStep.GROOVE_IC then + + -- Check if player was already waved off. + if playerData.waveoff==false then + + -- Debug + local text=string.format("Groove IC=%d m", rho) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:T2(self.lid..text) + + -- Store data. + playerData.groove.IC=groovedata + + -- Check if player should wave off. + local waveoff=self:_CheckWaveOff(glideslopeError, lineupError, AoA, playerData) + + --local text=string.format("FF D=%.1f GLE=%.1f LUE=%.1f waveoff=%s", rho, glideslopeError, lineupError, tostring(waveoff)) + --env.info(text) + + -- Let's see.. + if waveoff then + + -- LSO Wave off! + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WAVEOFF) + playerData.Tlso=timer.getTime() + + -- Player was waved off! + playerData.waveoff=true + + return + else + -- Next step: AR at the ramp. + playerData.step=AIRBOSS.PatternStep.GROOVE_AR + playerData.warning=nil + end + + end + + elseif rho<=RAR and playerData.step==AIRBOSS.PatternStep.GROOVE_AR then + + -- Debug. + local text=string.format("Groove AR=%d m", rho) + MESSAGE:New(text, 5):ToAllIf(self.Debug) + self:T2(self.lid..text) + + -- Store data. + playerData.groove.AR=groovedata + + -- Next step: in the wires. + playerData.step=AIRBOSS.PatternStep.GROOVE_IW + playerData.warning=nil + end + + -- Time since last LSO call. + local deltaT=timer.getTime()-playerData.Tlso + + -- Check if we are beween 3/4 NM and end of ship. Only one call every 3 seconds. + if X=RAR and rho=3 and playerData.waveoff==false then + + -- LSO call if necessary. + self:_LSOadvice(playerData, glideslopeError, lineupError) + + 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:E("What? Player was not waved off but flew past the carrier without landing. Why did waveoff not kick in?") + + -- TODO: This is more like a pilot wave off then. + self:_AddToDebrief(playerData, "Pilot wave-off.") + + playerData.waveoff=true + + end + + -- Next step: debrief. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil + + end + +end + +--- LSO check if player needs to wave off. +-- Wave off conditions are: +-- +-- * Glide slope error > 1 degree. +-- * Line up error > 3 degrees. +-- * AoA check but only for TOPGUN graduates. +-- @param #AIRBOSS self +-- @param #number glideslopeError Glide slope 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 + + -- Too high or too low? + if math.abs(glideslopeError)>1 then + local text=string.format("Wave off due to glide slope error |%.1f| > 1 degree!", glideslopeError) + self:T(self.lid..string.format("%s: %s", playerData.name, text)) + self:_AddToDebrief(playerData, text) + waveoff=true + end + + -- Too far from centerline? + if math.abs(lineupError)>3 then + local text=string.format("Wave off due to line up error |%.1f| > 3 degrees!", lineupError) + 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("Wave off 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 + +--- 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():Translate(self.carrierparam.sterndist, hdg):Translate(7, FB+90) + + -- 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 + local wire=self:_GetWire(coord, dcorr) + + -- Debug. + local text=string.format("Player %s _Trapped: v=%.1f km/h, s=%.1f m ==> wire=%d (dcorr=%d)", playerData.name, v, s, 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 + SCHEDULER:New(self, self._Trapped, {playerData}, 0.1) + 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 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 Arc in 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 NM. + local radius=UTILS.NMToMeters(1) + + -- Distance = 12 NM + local distance=UTILS.NMToMeters(12) + + -- 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 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) + + -- 12+x NM from carrier + local x=12/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. +-- @return Core.Zone#ZONE_POLYGON_BASE Box zone. +function AIRBOSS:_GetZoneCorridor(case) + + -- Radial and offset. + local radial=self:GetRadial(case, false, false) + local offset=self:GetRadial(case, false, true) + + -- Angle between radial and offset in rad. + local alpha=math.rad(self.holdingoffset) + + -- Distance shift ahead of carrier to allow for some space to bolter + local dx=2 + + -- Width of the box in NM. + local w=2 + local w2=w/2 + + -- Distance from carrier to arc out zone. + local d=12 + + -- Length of the box in NM. + local x=(d+w/2)/math.cos(alpha) + local l=28-x + --local l=15 --/math.cos(alpha) + + -- Some math... + local y1=d-w2 + local x1=y1*math.tan(alpha) + local y2=d+w2 + local x2=y2*math.tan(alpha) + local b=w2*(1/math.cos(alpha)-1) + + -- This is what we need. + local P=x1+b + local Q=x2-b + + -- Debug output. + self:T3(string.format("FF case %d radial = %d", case, radial)) + self:T3(string.format("FF case %d offset = %d", case, offset)) + self:T3(string.format("FF w = %.1f NM", w)) + self:T3(string.format("FF l = %.1f NM", l)) + self:T3(string.format("FF d = %.1f NM", d)) + self:T3(string.format("FF y1 = %.1f NM", y1)) + self:T3(string.format("FF x1 = %.1f NM", x1)) + self:T3(string.format("FF y2 = %.1f NM", y2)) + self:T3(string.format("FF x2 = %.1f NM", x2)) + self:T3(string.format("FF b = %.1f NM", b)) + self:T3(string.format("FF P = %.1f NM", P)) + self:T3(string.format("FF Q = %.1f NM", Q)) + + local c={} + c[1]=self:GetCoordinate():Translate(-UTILS.NMToMeters(dx), radial) --Carrier coordinate translated 2 NM in direction of travel to allow for bolter space. + + if math.abs(self.holdingoffset)>1 then + -- Complicated case with an angle. + c[2]=c[1]:Translate( UTILS.NMToMeters(w2), radial-90) -- 1 Right of carrier. + c[3]=c[2]:Translate( UTILS.NMToMeters(d+dx+w2), radial) -- 13 "south" @ 1 right + c[4]=c[3]:Translate( UTILS.NMToMeters(Q), radial+90) -- + c[5]=c[4]:Translate( UTILS.NMToMeters(l), offset) + c[6]=c[5]:Translate( UTILS.NMToMeters(w), offset+90) -- Back wall (angled) + c[9]=c[1]:Translate( UTILS.NMToMeters(w2), radial+90) -- 1 left of carrier. + c[8]=c[9]:Translate( UTILS.NMToMeters(d+dx-w2), radial) -- 1 left and 11 behind of carrier. + c[7]=c[8]:Translate( UTILS.NMToMeters(P), 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(d+dx+w2+l), radial) -- 12+1+10 = 23 NM behind the carrier. 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 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, FB+90) + p[2]=p[1]:Translate(self.carrierparam.rwylength, FB) + p[3]=p[2]:Translate(self.carrierparam.rwywidth*2, 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 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 + return nil + end + + -- Pattern alitude. + local patternalt, c1, c2=self:_GetMarshalAltitude(stack, case) + + -- Select case. + if case==1 then + -- CASE I + + -- Get current carrier heading. + local hdg=self:GetHeading() + + -- Zone 2.5 NM port of carrier with a radius of 2.75 NM (holding pattern should be < 5 NM but we allow 10% error). + local R=UTILS.NMToMeters(2.5) + + -- Create zone. + local coord=self:GetCoordinate():Translate(R, hdg+270) + + zoneHolding=ZONE_RADIUS:New("CASE I Holding Zone", coord:GetVec2(), R*1.1) + + else + -- CASE II/II + + -- Get radial. + local radial=self:GetRadial(case, false, true) + + -- Create an array of a square! + 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 6 NM port of carrier. + p[4]=c2:Translate(UTILS.NMToMeters(7), radial+90):GetVec2() --p4 6 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 + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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) + + -- Relative heading Aircraft to Carrier. + local relhead=self:_GetRelativeHeading(playerData.unit) + + -- Output + local text=string.format("Pattern step: %s\n", playerData.step) + text=text..string.format("AoA=%.1f | |V|=%.1f knots\n", aoa, UTILS.MpsToKnots(vabs)) + text=text..string.format("Vx=%.1f Vy=%.1f Vz=%.1f m/s\n", velo.x, velo.y, velo.z) + text=text..string.format("Pitch=%.1f° | Roll=%.1f° | Yaw=%.1f°\n", pitch, roll, yaw) + text=text..string.format("Climb Angle=%.1f° | Rate=%d ft/min\n", unit:GetClimbAngle(), velo.y*196.85) + text=text..string.format("R=%.1f NM | X=%d Z=%d m\n", UTILS.MetersToNM(rho), dx, dz) + text=text..string.format("Gamma=%.1f°", relhead) + -- If in the groove, provide line up and glide slope error. + if playerData.step==AIRBOSS.PatternStep.GROOVE_XX or + playerData.step==AIRBOSS.PatternStep.GROOVE_RB 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_IW then + local lineup=self:_Lineup(playerData.unit, true) + local glideslope=self:_Glideslope(playerData.unit, 3.5) + text=text..string.format("\nLU Error = %.1f° (line up)", lineup) + text=text..string.format("\nGS Error = %.1f° (glide slope)", glideslope) + end + + -- Wind (for debugging). + --text=text..string.format("Wind Vx=%.1f Vy=%.1f Vz=%.1f\n", wind.x, wind.y, wind.z) + + MESSAGE:New(text, 3, 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) + + -- Default is 0. + optangle=optangle or 0 + + -- Stern coordinate. + local stern=self:_GetSternCoord() + + -- Ideally we want to land between 2nd and 3rd wire. + if self.carrierparam.wire3 then + local d23=self.carrierparam.wire2 --+0.5*(self.carrierparam.wire3-self.carrierparam.wire2) + stern=stern:Translate(d23, self:GetFinalBearing(false), true) + end + + -- Distance from stern to aircraft. + local x=unit:GetCoordinate():Get2DDistance(stern) + + -- Altitude of unit corrected by the deck height of the carrier. + local h=unit:GetAltitude()-self.carrierparam.deckheight + + -- Glide slope. + local glideslope=math.atan(h/x) + + -- Glide slope (error) in degrees. + local gs=math.deg(glideslope)-optangle + + --env.info(string.format("FF 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) + + -- Vector to carrier. + local A=self:_GetSternCoord():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 + + -- Orientation of carrier. + local X=self.carrier:GetOrientationX() + + -- 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() + + -- 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) + + --- + + -- Position of the aircraft in the new coordinate system. + local a={x=x, y=0, z=z} + + -- Stern position in the new coordinate system, which is simply the origin. + local b={x=0, y=0, z=0} + + -- Vector from plane to ref point on the boat. + local c=UTILS.VecSubstract(a, b) + + -- Current line up and error wrt to final heading of the runway. + local lineup=math.deg(math.atan2(c.z, c.x)) + + --env.info(string.format("FF lineup 2 = %.1f", lineup)) + + return lineup +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 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 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() + + -- 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 rad. + local rhdg=math.deg(math.acos(UTILS.VecDot(vC,vP)/UTILS.VecNorm(vC)/UTILS.VecNorm(vP))) + + -- Include runway angle. + if runway then + rhdg=rhdg-self.carrierparam.rwyangle + end + + -- Return heading in degrees. + return rhdg +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)) + if phi<0 then + phi=phi+360 + end + + -- phi=0 if the plane is directly behind the carrier, phi=180 if the plane is in front of the carrier + phi=phi-180 + + 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. + local text="" + if glideslopeError>1 then + -- "You're high!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.HIGH, true) + advice=advice+AIRBOSS.LSOCall.HIGH.duration + elseif glideslopeError>0.5 then + -- "You're a little high." + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.HIGH, false) + advice=advice+AIRBOSS.LSOCall.HIGH.duration + elseif glideslopeError<-1.0 then + -- "Power!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.POWER, true) + advice=advice+AIRBOSS.LSOCall.POWER.duration + elseif glideslopeError<-0.5 then + -- "You're a little low." + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.POWER, false) + advice=advice+AIRBOSS.LSOCall.POWER.duration + else + text="Good altitude." + end + + text=text..string.format(" Glideslope Error = %.2f°", glideslopeError) + text=text.."\n" + + -- Lineup left/right calls. + if lineupError<-3 then + -- "Come left!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.COMELEFT, true) + advice=advice+AIRBOSS.LSOCall.COMELEFT.duration + elseif lineupError<-1 then + -- "Come left." + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.COMELEFT, false) + advice=advice+AIRBOSS.LSOCall.COMELEFT.duration + elseif lineupError>3 then + -- "Right for lineup!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RIGHTFORLINEUP, true) + advice=advice+AIRBOSS.LSOCall.RIGHTFORLINEUP.duration + elseif lineupError>1 then + -- "Right for lineup." + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.RIGHTFORLINEUP, false) + advice=advice+AIRBOSS.LSOCall.RIGHTFORLINEUP.duration + else + text=text.."Good lineup." + end + + text=text..string.format(" Lineup Error = %.1f°\n", lineupError) + + -- Get current AoA. + local aoa=playerData.unit:GetAoA() + + -- Get aircraft AoA parameters. + local aircraftaoa=self:_GetAircraftAoA(playerData) + + -- Rate aoa. + if aoa>=aircraftaoa.Slow then + -- "Your're slow!" + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.SLOW, true) + advice=advice+AIRBOSS.LSOCall.SLOW.duration + elseif aoa>=aircraftaoa.OnSpeedMax and aoa=aircraftaoa.OnSpeedMin and aoa=aircraftaoa.Fast and 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 + + local grade + local points + if N==0 then + -- No deviations, should be REALLY RARE! + grade="_OK_" + points=5.0 + 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.patternwo then + -- Pattern Wave Off + grade="PWO" + if playerData.lig then + G="LIG" + elseif playerData.patternwo then + G="n/a" + end + points=1.0 + elseif playerData.waveoff then + -- Wave Off + 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 + + -- Data. + local fdata=playerData.groove[groovestep] + + -- No flight data ==> return empty string. + if fdata==nil then + self:T(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. + 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 AOA1 then + A=underline("H") + elseif GSE>0.5 then + A="H" + elseif GSE>0.25 then + A=little("H") + elseif GSE<-1 then + A=underline("LO") + elseif GSE<-0.5 then + A="LO" + elseif GSE<-0.25 then + A=little("LO") + end + + -- Line up. Good [-0.5, 0.5] + local D=nil + if LUE>3 then + D=underline("LUL") + elseif LUE>1 then + D="LUL" + elseif LUE>0.5 then + D=little("LUL") + elseif LUE<-3 then + D=underline("LUR") + elseif LUE<-1 then + D="LUR" + elseif LUE<-0.5 then + D=little("LUR") + end + + -- Compile. + local G="" + local n=0 + if S then + G=G..S + n=n+1 + end + if A then + G=G..A + n=n+1 + end + if D then + G=G..D + n=n+1 + end + + -- Add current step. + local step=self:_GS(step) + step=step:gsub("XX","X") + if G~="" then + G=G..step + end + + -- Debug info. + local text=string.format("LSO Grade at %s:\n", step) + text=text..string.format("AOA=%.1f\n",AOA) + text=text..string.format("GSE=%.1f\n",GSE) + text=text..string.format("LUE=%.1f\n",LUE) + text=text..string.format("ROL=%.1f\n",ROL) + text=text..G + self:T3(self.lid..text) + + return G,n +end + +--- Get short name of the grove step. +-- @param #AIRBOSS self +-- @param #number step Step +-- @return #string Shortcut name "X", "RB", "IM", "AR", "IW". +function AIRBOSS:_GS(step) + local gp + if step==AIRBOSS.PatternStep.FINAL then + gp="X0" -- Entering the groove. + elseif step==AIRBOSS.PatternStep.GROOVE_XX then + gp="X" -- Starting the groove. + elseif step==AIRBOSS.PatternStep.GROOVE_RB then + gp="RB" -- Roger ball call. + elseif step==AIRBOSS.PatternStep.GROOVE_IM then + gp="IM" -- In the middle. + elseif step==AIRBOSS.PatternStep.GROOVE_IC then + gp="IC" -- In close. + elseif step==AIRBOSS.PatternStep.GROOVE_AR then + gp="AR" -- At the ramp. + elseif step==AIRBOSS.PatternStep.GROOVE_IW then + gp="IW" -- In the wires. + end + return gp +end + +--- Check if a player is within the right area. +-- @param #AIRBOSS self +-- @param #number X X distance player to carrier. +-- @param #number Z Z distance player to carrier. +-- @param #AIRBOSS.Checkpoint pos Position data limits. +-- @return #boolean If true, approach should be aborted. +function AIRBOSS:_CheckAbort(X, Z, pos) + + local abort=false + if pos.Xmin and Xpos.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:E(self.lid..dtext) + + -- Message to player. + self:MessageToPlayer(playerData, text, "LSO", nil, 20) + + if patternwo then + + -- Pattern wave off! + playerData.patternwo=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, AIRBOSS.LSOCall.DEPARTANDREENTER, false, 3) + + -- Next step debrief. + playerData.step=AIRBOSS.PatternStep.DEBRIEF + playerData.warning=nil + 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 + + +--- 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. +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 + + local radiocall={} --#AIRBOSS.RadioCall + + local hint + if _error>badscore then + hint=string.format("You're high.") + radiocall=AIRBOSS.LSOCall.HIGH + radiocall.loud=true + radiocall.subtitle="" + elseif _error>lowscore then + hint= string.format("You're slightly high.") + radiocall=AIRBOSS.LSOCall.HIGH + radiocall.loud=false + radiocall.subtitle="" + elseif _error<-badscore then + hint=string.format("You're low. ") + radiocall=AIRBOSS.LSOCall.LOW + radiocall.loud=true + radiocall.subtitle="" + elseif _error<-lowscore then + hint=string.format("You're slightly low.") + radiocall=AIRBOSS.LSOCall.LOW + radiocall.loud=false + radiocall.subtitle="" + 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. + 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 +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. +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. + 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 +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. +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) + + -- Rate aoa. + local hint="" + if aoa>=aircraftaoa.SLOW then + hint="Your're slow!" + elseif aoa>=aircraftaoa.Slow then + hint="Your're slow." + 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." + else + hint="You're fast!" + 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.", optaoa) + elseif playerData.difficulty==AIRBOSS.Difficulty.NORMAL then + -- We keep is short normally. + 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.", aoa, _error, optaoa) + + return hint, debrief +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. +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 + + local hint + if _error>badscore then + hint=string.format("You're fast.") + elseif _error>lowscore then + hint= string.format("You're slightly fast.") + elseif _error<-badscore then + hint=string.format("You're low.") + elseif _error<-lowscore then + hint=string.format("You're slightly slow.") + 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. + 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 +end + +--- 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:F2(self.lid..string.format("Debriefing of player %s.", playerData.name)) + + -- LSO grade, points, and flight data analyis. + local grade, points, analysis=self:_LSOgrade(playerData) + + -- My LSO grade. + local mygrade={} --#AIRBOSS.LSOgrade + mygrade.grade=grade + mygrade.points=points + mygrade.details=analysis + mygrade.wire=playerData.wire + mygrade.Tgroove=playerData.Tgroove + + -- Add LSO grade to table. + table.insert(playerData.grades, mygrade) + + -- LSO grade: (OK) 3.0 PT - LURIM + local text=string.format("%s %.1f PT - %s", grade, points, analysis) + + -- Wire and Groove time only if not pattern WO. + if not playerData.patternwo 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<=60 and playerData.case<3 then + text=text..string.format("\nTime in the groove %d 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. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + + -- Check what happened? + if playerData.patternwo 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 + + -- Get heading and distance to initial zone ~3 NM astern. + heading=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) + distance=playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate()) + + 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 %d for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM(distance)) + self:MessageToPlayer(playerData, text, "LSO", nil, 10, false, 5) + + else + + -- Unit does not seem to be alive! + -- TODO: What now? + self:T2(self.lid..string.format("Player unit not alive!")) + + end + + elseif playerData.waveoff then + + -------------- + -- Wave Off -- + -------------- + + if playerData.unit:InAir() then + + if playerData.case<3 then + + -- Next step: Abeam + playerData.step=AIRBOSS.PatternStep.ABEAM + + else + + -- Next step? Taking Bullseye for now. + playerData.step=AIRBOSS.PatternStep.BULLSEYE + + end + + else + + -- 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, 10, false, 2) + + -- Next step undefined. Player landed. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + end + + elseif playerData.boltered then + + -------------- + -- Boltered -- + -------------- + + if playerData.unit:InAir() then + + if playerData.case<3 then + + -- Next step: Abeam + playerData.step=AIRBOSS.PatternStep.ABEAM + + else + + -- Next step? Taking Bullseye for now. + playerData.step=AIRBOSS.PatternStep.BULLSEYE + + end + + else + + -- Next step undefined. Player is not in air any more. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + end + + + elseif playerData.landed then + + ------------ + -- Landed -- + ------------ + + if not playerData.unit:InAir() then + + -- Remove player unit from flight and all queues. + self:_RemoveUnitFromFlight(playerData.unit) + + -- Welcome aboard! + self:RadioTransmission(self.LSORadio, AIRBOSS.LSOCall.WELCOMEABOARD) + + end + + else + + -- Message to player. + self:MessageToPlayer(playerData, "Undefined state after landing! Please report.", "ERROR", nil, 10) + + -- Next step. + playerData.step=AIRBOSS.PatternStep.UNDEFINED + + 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) +end + +--- Hind for flight students about the (next) step. +-- @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 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", 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 + + -- 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", "", 10, false, 2) + + end + + end +end + +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- MISC functions +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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 + +--- Check if aircraft is capable of landing on an 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) + local carrieraircraft=false + local aircrafttype=unit:GetTypeName() + for _,actype in pairs(AIRBOSS.AircraftCarrier) do + if actype==aircrafttype then + return true + end + end + 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) + + local angels=math.floor(UTILS.MetersToFeet(alt)/1000) + + return angels +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. 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 + + -- 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 + +--- 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 mission weather. +-- @param #AIRBOSS self +function AIRBOSS:_MissionWeather() + + -- Weather data from mission file. + local weather=env.mission.weather + + + --[[ + ["clouds"] = + { + ["thickness"] = 430, + ["density"] = 7, + ["base"] = 0, + ["iprecptns"] = 1, + }, -- end of ["clouds"] + ]] + local clouds=weather.clouds + + --[[ + ["fog"] = + { + ["thickness"] = 0, + ["visibility"] = 25, + }, -- end of ["fog"] + ]] + local fog=weather.fog + + -- Visibilty distance in meters. + local vis=weather.visibility.distance + +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- RADIO MESSAGE Functions +----------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- 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 #number prio Priority 0-100. +-- @field #boolean isplaying Currently playing. +-- @field Core.Beacon#RADIO radio Radio object. +-- @field #AIRBOSS.RadioCall call Radio call. +-- @field #boolean loud If true, play loud version of file. + +--- 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) + + -- Check if queue is empty. + if #radioqueue==0 then + return + end + + -- Get current abs time. + local time=timer.getAbsTime() + + -- Sort results table wrt times they have already been engaged. + local function _sort(a, b) + return (a.Tplay < b.Tplay) or (a.Tplay==b.Tplay and a.prio < b.prio) + end + --table.sort(radioqueue, _sort) + + 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 + --table.insert(remove, i) + + else -- still playing + + -- Transmission is still playing. + playing=true + + end + + else -- not playing yet + + -- Not playing ==> this will be next. + if next==nil then + next=transmission + end + + end + + else + + -- Transmission not due yet. + + end + end + + -- Found a new transmission. + if next~=nil and not playing then + self:RadioTransmit(next.radio, next.call, next.loud) + next.isplaying=true + next.Tstarted=time + end + + -- Remove completed calls from queue. + --for _,idx in pairs(remove) do + if remove then + table.remove(radioqueue, remove) + end + --end + +end + +--- Add Radio transmission to radio queue +-- @param #AIRBOSS self +-- @param Core.Radio#RADIO radio sending 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:RadioTransmission(radio, call, loud, delay) + self:F2({radio=radio, call=call, loud=loud, delay=delay}) + + -- Create a new radio transmission item. + local transmission={} --#AIRBOSS.Radioitem + + transmission.radio=radio + transmission.call=call + transmission.Tplay=timer.getAbsTime()+(delay or 0) + transmission.prio=50 + transmission.isplaying=false + transmission.Tstarted=nil + transmission.loud=loud and call.loud + + -- Add transmission to the right queue. + if radio:GetAlias()=="LSO" then + + table.insert(self.RQLSO, transmission) + + elseif radio:GetAlias()=="MARSHAL" then + + table.insert(self.RQMarshal, transmission) + + end +end + +--- Transmission radio message. +-- @param #AIRBOSS self +-- @param Core.Radio#RADIO radio sending 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:RadioTransmit(radio, call, loud, delay) + self:F2({radio=radio, call=call, loud=loud, delay=delay}) + + if (delay==nil) or (delay and delay==0) then + + -- Construct file name and subtitle. + local filename=call.file + local subtitle=call.subtitle + if loud then + if call.loud then + filename=filename.."_Loud" + end + if subtitle and subtitle~="" then + subtitle=subtitle.."!" + end + else + if subtitle and subtitle~="" then + subtitle=subtitle.."." + end + end + filename=filename.."."..(call.suffix or "ogg") + + -- New transmission. + radio:NewUnitTransmission(filename, call.subtitle, call.duration, radio.Frequency/1000000, radio.Modulation, false) + + -- Broadcast message. + radio:Broadcast(true) + + -- 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 + if playerData.actype==AIRBOSS.AircraftCarrier.A4EC then + USERSOUND:New(filename):ToGroup(playerData.group) + end + end + + -- Message "Subtitle" to all players. + self:MessageToAll(subtitle, radio:GetAlias(), "", call.duration) + + else + + -- Scheduled transmission. + SCHEDULER:New(self, self.RadioTransmission, {radio, call, loud}, delay) + + end +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. +-- @param #boolean soundoff If true, do not play boad number message. +function AIRBOSS:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) + + if playerData and message and message~="" then + + -- Default duration. + duration=duration or 10 + + -- 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(self, self.MessageToPlayer, {playerData, message, sender, receiver, duration, clear, 0, soundoff}, delay) + else + + if sender and not soundoff then + + if receiver=="99" then + + -- Radio message from LSO or MARSHAL to all. + if sender=="LSO" then + self:_Number2Radio(self.LSORadio, receiver, delay) + elseif sender=="MARSHAL" then + self:_Number2Radio(self.MarshalRadio, receiver, delay) + end + + elseif receiver==playerData.onboard then + + -- Sound only to player group. + if sender=="LSO" or sender=="MARSHAL" then + self:_Number2Sound(playerData, sender, receiver, delay) + end + + end + 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 CCA. +-- 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. +-- @param #boolean soundoff If true, do not play boad number message. +function AIRBOSS:MessageToAll(message, sender, receiver, duration, clear, delay, soundoff) + + -- Make sure the onboard number sound is played only once. + local soundoff=false + + for _,_player in pairs(self.players) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Message to all players in CCA. + if playerData.unit:IsInZone(self.zoneCCA) then + + -- Message to player. + self:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) + + -- Disable sound play of onboard number. + soundoff=true + 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. +-- @param #boolean soundoff If true, do not play boad number message. +function AIRBOSS:MessageToPattern(message, sender, receiver, duration, clear, delay, soundoff) + + -- Make sure the onboard number sound is played only once. + local soundoff=false + + -- Loop over all flights in the pattern queue. + for _,_player in pairs(self.Qpattern) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Message only to human pilots. + if not playerData.ai then + + -- Message to player. + self:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) + + -- Disable sound play of onboard number. + soundoff=true + end + end +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. +-- @param #boolean soundoff If true, do not play boad number message. +function AIRBOSS:MessageToMarshal(message, sender, receiver, duration, clear, delay, soundoff) + + -- Make sure the onboard number sound is played only once. + local soundoff=false + + -- Loop over all flights in the marshal queue. + for _,_player in pairs(self.Qmarshal) do + local playerData=_player --#AIRBOSS.PlayerData + + -- Message only to human pilots. + if not playerData.ai then + + -- Message to player. + self:MessageToPlayer(playerData, message, sender, receiver, duration, clear, delay, soundoff) + + -- Disable sound play of onboard number. + soundoff=true + end + end +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. +function AIRBOSS:_Number2Sound(playerData, sender, number, delay) + + --- 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 + + if delay and delay>0 then + -- Delayed call. + SCHEDULER:New(self, AIRBOSS._Number2Sound, {playerData, sender, number}, delay) + else + + -- Split string into characters. + local numbers=_split(number) + + local Sender + if sender=="LSO" then + Sender="LSOCall" + elseif sender=="MARSHAL" then + Sender="MarshalCall" + else + self:E(self.lid..string.format("ERROR: Unknown radio sender %s!", tostring(sender))) + return + end + + 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=AIRBOSS[Sender][N] --#AIRBOSS.RadioCall + + -- Create file name. + local filename=string.format("%s.%s", call.file, call.suffix) + + -- Play sound. + USERSOUND:New(filename):ToGroup(playerData.group, wait) + + -- Wait until this call is over before playing the next. + wait=wait+call.duration + end + + end +end + +--- Convert a number (as string) into a radio message. +-- E.g. for board number or headings. +-- @param #AIRBOSS self +-- @param Core.Radio#RADIO radio Radio used for transmission. +-- @param #string number Number string, e.g. "032" or "183". +-- @param #number delay Delay before transmission in seconds. +function AIRBOSS:_Number2Radio(radio, number, delay) + + --- 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 + + -- Get radio alias. + local alias=radio:GetAlias() + + local sender="" + if alias=="LSO" then + sender="LSOCall" + elseif alias=="MARSHAL" then + sender="MarshalCall" + else + self:E(self.lid.."ERROR: Unknown radio alias!") + end + + -- Split string into characters. + local numbers=_split(number) + + for i=1,#numbers do + + -- Current number + local n=numbers[i] + + if n=="0" then + self:RadioTransmission(radio, AIRBOSS[sender].N0, false, delay) + elseif n=="1" then + self:RadioTransmission(radio, AIRBOSS[sender].N1, false, delay) + elseif n=="2" then + self:RadioTransmission(radio, AIRBOSS[sender].N2, false, delay) + elseif n=="3" then + self:RadioTransmission(radio, AIRBOSS[sender].N3, false, delay) + elseif n=="4" then + self:RadioTransmission(radio, AIRBOSS[sender].N4, false, delay) + elseif n=="5" then + self:RadioTransmission(radio, AIRBOSS[sender].N5, false, delay) + elseif n=="6" then + self:RadioTransmission(radio, AIRBOSS[sender].N6, false, delay) + elseif n=="7" then + self:RadioTransmission(radio, AIRBOSS[sender].N7, false, delay) + elseif n=="8" then + self:RadioTransmission(radio, AIRBOSS[sender].N8, false, delay) + elseif n=="9" then + self:RadioTransmission(radio, AIRBOSS[sender].N9, false, delay) + else + self:E(self.lid..string.format("ERROR: Unknown number %s!", tostring(n))) + end + end + +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 + + -- Main F10 menu: F10/Airboss// + if AIRBOSS.MenuF10[gid]==nil then + AIRBOSS.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "Airboss") + end + + -- F10/Airboss/ + local _rootPath=missionCommands.addSubMenuForGroup(gid, self.alias, AIRBOSS.MenuF10[gid]) + + -------------------------------- + -- F10/Airboss//F1 Help + -------------------------------- + local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath) + -- F10/Airboss//F1 Help/F1 Mark Zones + local _markPath=missionCommands.addSubMenuForGroup(gid, "Mark Zones", _helpPath) + -- F10/Airboss//F1 Help/F1 Mark Zones/ + missionCommands.addCommandForGroup(gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false) -- F1 + missionCommands.addCommandForGroup(gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true) -- F2 + missionCommands.addCommandForGroup(gid, "Smoke Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false) -- F3 + missionCommands.addCommandForGroup(gid, "Flare Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true) -- F4 + -- 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, playername, AIRBOSS.Difficulty.EASY) -- F1 + missionCommands.addCommandForGroup(gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.NORMAL) -- F2 + missionCommands.addCommandForGroup(gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, playername, AIRBOSS.Difficulty.HARD) -- F3 + -- F10/Airboss//F1 Help/ + missionCommands.addCommandForGroup(gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName) -- F3 + missionCommands.addCommandForGroup(gid, "Attitude Monitor", _helpPath, self._AttitudeMonitor, self, playername) -- 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, "[Reset My Status]", _helpPath, self._ResetPlayerStatus, self, _unitName) -- F7 + + ------------------------------------- + -- 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// + ------------------------- + 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 + 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 + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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="Status reset executed! You have been removed from all queues." + self:MessageToPlayer(playerData, text, nil, "") + + -- Remove from marhal stack can collapse stack if necessary. + if self:_InQueue(self.Qmarshal, playerData.group) then + self:_CollapseMarshalStack(playerData, true) + end + + -- Remove flight from queues. + self:_RemoveFlight(playerData) + + -- Initialize player data. + self:_InitPlayer(playerData) + + 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, AIRBOSS.LSOCall.RADIOCHECK) + 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, AIRBOSS.MarshalCall.RADIOCHECK) + 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("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("you are already in the Pattern queue. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + elseif not _unit:InAir() then + + -- Flight group is already in pattern queue. + local text=string.format("you are not airborne. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + else + + -- Add flight to marshal stack. + self:_MarshalPlayer(playerData) + + end + + else + + -- Flight group is not in CCA yet. + local text=string.format("you are not inside CCA yet. Marshal request denied!") + self:MessageToPlayer(playerData, text, "MARSHAL") + + end + end + end +end + +--- Request to commence 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="" + if _unit:IsInZone(self.zoneCCA) then + + if self:_InQueue(self.Qpattern, playerData.group) then + + -- Flight group is already in pattern queue. + text=string.format("%s, you are already in the Pattern queue. Commence request denied!", playerData.name) + + elseif not _unit:InAir() then + + -- Flight group is already in pattern queue. + text=string.format("%s, you are not airborne. Commence request denied!", playerData.name) + + else + + -- Get stack value. + local stack=playerData.flag:Get() + + -- Check if player is in the lowest stack. + if stack>1 then + -- We are in a higher stack. + text="Negative ghostrider, it's not your turn yet!" + else + + -- Number of aircraft currently in pattern. + local _,npattern=self:_GetQueueInfo(self.Qpattern) + + -- Check if pattern is already full. + if npattern>=self.Nmaxpattern then + + -- Patern is full! + text=string.format("Negative ghostrider, pattern is full!\nThere are %d aircraft currently in the pattern.", npattern) + + else + + -- TODO: check if recovery window is open. + if not self:IsRecovering() then + text="Recovery window NOT open yet! However, you are cleared anyway.\n" + end + + -- Positive response. + if playerData.case==1 then + text=text.."Proceed to initial." + else + text=text.."Descent at 4k ft/min to platform at 5000 ft." + end + + -- Set player step. + playerData.step=AIRBOSS.PatternStep.COMMENCING + playerData.warning=nil + + -- Collaps marshal stack. + self:_CollapseMarshalStack(playerData, false) + end + + end + + end + else + -- This flight is not yet registered! + text="Negative ghostrider, you are not inside the CCA yet!" + end + + -- Debug + self:T(self.lid..text) + + -- Send message. + self:MessageToPlayer(playerData, text, "MARSHAL") + 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) + + -- Tanker is up and running. + text=string.format("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) + 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. + if self:_InQueue(self.Qmarshal, playerData.group) then + -- TODO: What if only the player and not his section wants to refuel?! + self:_CollapseMarshalStack(playerData, true) + end + elseif self.tanker:IsReturning() then + -- Tanker is RTB. + text="Tanker is RTB. Request denied!\nWait for the tanker to be back on station if you can." + end + + else + text="You are not registered inside the CCA yet. Request denied!" + end + else + text="No refueling tanker available. Request denied!" + end + + -- Send message. + self:MessageToPlayer(playerData, text, "MARSHAL") + end + end +end + +--- Set all flights within 200 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() + + -- Check if player is in Marshal or pattern queue already. + local text + if self:_InQueue(self.Qmarshal,playerData.group) then + text=string.format("You are already in the Marshal queue. Setting section not possible any more!") + elseif self:_InQueue(self.Qpattern, playerData.group) then + text=string.format("You are already in the Pattern queue. Setting section not possible any more!") + else + + -- Init array + playerData.section={} + + -- Loop over all registered flights. + for _,_flight in pairs(self.flights) do + local flight=_flight --#AIRBOSS.FlightGroup + + -- Only human flight groups excluding myself. + if flight.ai==false and flight.groupname~=playerData.groupname then + + -- Distance to other group. + local distance=flight.group:GetCoordinate():Get2DDistance(mycoord) + + if distance<200 then + table.insert(playerData.section, flight) + end + + end + end + + -- Info on section members. + if #playerData.section>0 then + text=string.format("Registered flight section:") + text=text..string.format("- %s (lead)", playerData.name) + for _,_flight in paris(playerData.section) do + local flight=_flight --#AIRBOSS.PlayerData + text=text..string.format("- %s", flight.name) + flight.seclead=playerData.name + + -- Inform player that he is now part of a section. + self:MessageToPlayer(flight, string.format("Your section lead is now %s.", playerData.name), "MARSHAL") + end + else + text="No other human flights found within radius of 200 meters!" + 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={} + + -- Player data of requestor. + local playerData=self.players[_playername] --#AIRBOSS.PlayerData + + -- Message text. + local text = string.format("Greenie Board:") + + for _playerName,_playerData in pairs(self.players) do + + local Paverage=0 + for _,_grade in pairs(_playerData.grades) do + Paverage=Paverage+_grade.points + end + _playerResults[_playerName]=Paverage + + end + + --Sort list! + local _sort=function(a, b) return a>b end + table.sort(_playerResults,_sort) + + local i=1 + for _playerName,_points in pairs(_playerResults) do + text=text..string.format("\n[%d] %.1f %s", i,_points,_playerName) + i=i+1 + end + + -- Send message. + 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 grades, %s:", _playername) + + local p=0 + for i,_grade in pairs(playerData.grades) do + local grade=_grade --#AIRBOSS.LSOgrade + + text=text..string.format("\n[%d] %s %.1f PT - %s", i, grade.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<=60 then + text=text..string.format(" Tgroove=%.1f s", grade.Tgroove) + end + + -- Add up points. + p=p+grade.points + end + + -- Number of grades. + local n=#playerData.grades + + 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:\n",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 + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- SKIL LEVEL MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set difficulty level. +-- @param #AIRBOSS self +-- @param #string playername Player name. +-- @param #AIRBOSS.Difficulty difficulty Difficulty level. +function AIRBOSS:_SetDifficulty(playername, difficulty) + self:T2({difficulty=difficulty, playername=playername}) + + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.difficulty=difficulty + local text=string.format("your difficulty 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 +end + +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- KNEEBOARD MENU +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Turn player's aircraft attitude display on or off. +-- @param #AIRBOSS self +-- @param #string playername Player name. +function AIRBOSS:_AttitudeMonitor(playername) + self:F2({playername=playername}) + + local playerData=self.players[playername] --#AIRBOSS.PlayerData + + if playerData then + playerData.attitudemonitor=not playerData.attitudemonitor + 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 + + -- Get groups, units in queues. + local Nmarshal,nmarshal=self:_GetQueueInfo(self.Qmarshal, playerData.case) + local Npattern,npattern=self:_GetQueueInfo(self.Qpattern) + + -- 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 + + -- Message text. + local text=string.format("%s info:\n", self.alias) + text=text..string.format("=============================================\n") + text=text..string.format("Carrier state %s\n", self:GetState()) + text=text..string.format("Case %d recovery\n", self.case) + text=text..string.format("BRC %03d°\n", self:GetBRC()) + text=text..string.format("FB %03d°\n", self:GetFinalBearing(true)) + text=text..string.format("Speed %d kts\n", carrierspeed) + 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) + text=text..string.format("# A/C total %d\n", #self.flights) + text=text..string.format("# A/C marshal %d (%d)\n", Nmarshal, nmarshal) + text=text..string.format("# A/C pattern %d (%d)\n", Npattern, npattern) + text=text..string.format(recoverytext) + self:T2(self.lid..text) + + -- Send message. + self:MessageToPlayer(playerData, text, nil, "", 20, 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() + local Wd,Ws=coord:GetWind() + + -- Get Beaufort wind scale. + local Bn,Bd=UTILS.BeaufortScale(Ws) + + local WD=string.format('%03d°', Wd) + local Ts=string.format("%d°C",T) + + local 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", UTILS.hPa2mmHg(P)) + if settings:IsImperial() then + tT=string.format("%d°F", UTILS.CelciusToFarenheit(T)) + tW=string.format("%.1f knots", UTILS.MpsToKnots(Ws)) + tP=string.format("%.2f inHg", UTILS.hPa2inHg(P)) + end + + -- 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("QFE %.1f hPa = %s", P, tP) + + -- 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 + + + +--- 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 + + -- Stack and stack altitude. + local stack=playerData.flag:Get() + local stackalt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack)) + + -- Fuel and fuel state. + local fuel=playerData.unit:GetFuel()*100 + local fuelstate=self:_GetFuelState(playerData.unit) + + -- 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("Current step: %s\n", playerData.step) + text=text..string.format("Skil level: %s\n", playerData.difficulty) + text=text..string.format("Aircraft: %s\n", playerData.actype) + text=text..string.format("Board number: %s\n", playerData.onboard) + text=text..string.format("Fuel state: %.1f lbs/1000 (%.1f %%)\n", fuelstate/1000, fuel) + text=text..string.format("Stack: %d alt=%d ft\n", stack, stackalt) + text=text..string.format("Group: %s\n", playerData.group:GetName()) + text=text..string.format("# units: %d (n=%d)\n", #playerData.group:GetUnits(), playerData.nunits) + text=text..string.format("Section Lead: %s\n", tostring(playerData.seclead)) + text=text..string.format("# section: %d", #playerData.section) + 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 + + -- Heading and distance to initial zone. + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self.zoneInitial:GetCoordinate()) + local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate())) + local brc=self:GetBRC() + + -- Help player to find its way to the initial zone. + text=text..string.format("\nFly heading %03d° for %.1f NM and turn to BRC %03d°.", flyhdg, flydist, brc) + + elseif playerData.step==AIRBOSS.PatternStep.PLATFORM then + + -- Heading and distance to platform zone. + local flyhdg=playerData.unit:GetCoordinate():HeadingTo(self:_GetZonePlatform(playerData.case):GetCoordinate()) + local flydist=UTILS.MetersToNM(playerData.unit:GetCoordinate():Get2DDistance(self.zoneInitial:GetCoordinate())) + local fb=self:GetFinalBearing(true) + + -- Help player to find its way to the initial zone. + text=text..string.format("\nFly heading %03d° for %.1f NM and turn to FB %03d°.", flyhdg, flydist, fb) + + end + + -- Send message. + self:MessageToPlayer(playerData, text, nil, "", 30, true) + end + 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:Get() + local case=playerData.case + + local text="" + if stack>0 then + + -- Get current holding zone. + local zone=self:_GetZoneHolding(case, stack) + + -- Pattern alitude. + local patternalt=self:_GetMarshalAltitude(stack, case) + + patternalt=0 + + if flare then + text="Marking marshal zone with WHITE flares." + zone:FlareZone(FLARECOLOR.White, 45, nil, patternalt) + else + text="Marking marshal zone with WHITE smoke." + zone:SmokeZone(SMOKECOLOR.White, 45, patternalt) + end + + else + text="You are currently not in a marshal stack. No zone to mark!" + end + + -- Send message to player. + self:MessageToPlayer(playerData, text, "MARSHAL", "") + 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("Marking CASE %d zones\n", case) + + -- Flare or smoke? + if flare then + + -- Case I/II: Initial + if case==1 or case==2 then + text=text.."* initial with WHITE flares\n" + self.zoneInitial:FlareZone(FLARECOLOR.White, 45) + end + + -- Case II/III: approach corridor + if case==2 or case==3 then + text=text.."* approach corridor with GREEN flares\n" + self:_GetZoneCorridor(case):FlareZone(FLARECOLOR.Green, 45) + end + + -- Case II/III: platform + if case==2 or case==3 then + text=text.."* platform with RED flares\n" + self:_GetZonePlatform(case):FlareZone(FLARECOLOR.Red, 45) + end + + -- Case III: dirty up + if case==3 then + text=text.."* dirty up with YELLOW flares\n" + 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.Yellow, 45) + text=text.."* arc turn in with YELLOW flares\n" + self:_GetZoneArcOut(case):FlareZone(FLARECOLOR.White, 45) + text=text.."* arc trun out with WHITE flares\n" + end + end + + -- Case III: bullseye + if case==3 then + text=text.."* bullseye with WHITE flares\n" + self:_GetZoneBullseye(case):FlareZone(FLARECOLOR.White, 45) + end + + else + + -- Case I/II: Initial + if case==1 or case==2 then + text=text.."* initial with WHITE smoke\n" + self.zoneInitial:SmokeZone(SMOKECOLOR.White, 45) + end + + -- Case II/III: Approach Corridor + if case==2 or case==3 then + text=text.."* approach corridor with GREEN smoke\n" + self:_GetZoneCorridor(case):SmokeZone(SMOKECOLOR.Green, 45) + end + + -- Case II/III: platform + if case==2 or case==3 then + text=text.."* platform with RED smoke\n" + 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.."* arc turn in with BLUE smoke\n" + self:_GetZoneArcOut(case):SmokeZone(SMOKECOLOR.Blue, 45) + text=text.."* arc trun out with BLUE smoke\n" + end + end + + -- Case III: dirty up + if case==3 then + text=text.."* dirty up with ORANGE smoke\n" + self:_GetZoneDirtyUp(case):SmokeZone(SMOKECOLOR.Orange, 45) + end + + -- Case III: bullseye + if case==3 then + text=text.."* bullseye with WHITE smoke\n" + self:_GetZoneBullseye(case):SmokeZone(SMOKECOLOR.White, 45) + end + + end + + -- Send message to player. + self:MessageToPlayer(playerData, text, "MARSHAL", "") + end + end + +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ diff --git a/Moose Development/Moose/Ops/RecoveryTanker.lua b/Moose Development/Moose/Ops/RecoveryTanker.lua new file mode 100644 index 000000000..cdec7ebad --- /dev/null +++ b/Moose Development/Moose/Ops/RecoveryTanker.lua @@ -0,0 +1,1279 @@ +--- **Ops** - (R2.5) - Recovery tanker for carrier operations. +-- +-- Tanker aircraft flying a racetrack pattern overhead an aircraft carrier. +-- +-- **Main Features:** +-- +-- * Regular pattern update with respect to carrier positon. +-- * 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 different 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 MOOSE.JPG + +--- 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 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 Positon of carrier. Used to monitor if carrier significantly changed its position and then update the tanker pattern. +-- @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 engies 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 realsitic 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. +-- +-- ## 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 contantly 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. +-- +-- # 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, + 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, +} + +--- Class version. +-- @field #string version +RECOVERYTANKER.version="1.0.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: 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 documenation. +-- 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 + + -- Save self in static object. Easier to retrieve later. + self.carrier:SetState(self.carrier, "RECOVERYTANKER", self) + + -- Debug log id. + self.lid=string.format("RECOVERYTANKER %s", self.carrier:GetName()) + + -- 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:SetPatternUpdateDistance() + self:SetPatternUpdateHeading() + self:SetPatternUpdateInterval() + + -- 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("*", "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. + + + --- 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 "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. +-- @return #RECOVERYTANKER self +function RECOVERYTANKER: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 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 + +--- 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 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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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.Refueling, self._RefuelingStart) --Need explcit functions sice OnEventRefueling and OnEventRefuelingStop did not hook. + self:HandleEvent(EVENTS.RefuelingStop, self._RefuelingStop) + + -- Set unique alias for spawn from tanker group name and carrier unit name. + local tankergroupalias=string.format("%s_%s", self.tankergroupname, self.carrier:GetName()) + + -- 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, tankergroupalias) + + -- 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.useuncontrolled 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) + + end + + end + + -- Initialize route. self.distStern<0! + SCHEDULER:New(self, self._InitRoute, {-self.distStern+UTILS.NMToMeters(3)}, 1) + --self:_InitRoute(-self.distStern+UTILS.NMToMeters(3), 1) + + -- Create tanker beacon. + if self.TACANon then + self:_ActivateTACAN(2) + 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() + + -- Get fuel of tanker. + local fuel=self.tanker:GetFuel()*100 + local text=string.format("Recovery tanker %s: state=%s fuel=%.1f", self.tanker:GetName(), self:GetState(), fuel) + self:T(self.lid..text) + + -- 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 + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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\") ') -- 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(self, self._ActivateTACAN, {}, delay) + + else + + -- Get tanker unit. + local unit=self.tanker:GetUnit(1) + + -- Check if unit is alive. + if unit:IsAlive() then + + -- Debug message. + local text=string.format("Activating recovery tanker 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("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..7a39d29d7 --- /dev/null +++ b/Moose Development/Moose/Ops/RescueHelo.lua @@ -0,0 +1,1093 @@ +--- **Ops** - (R2.5) - 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. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- ### Contributions: Flightcontrol (@{AI.AI_Formation} class being used here) +-- +-- @module Ops.RescueHelo +-- @image MOOSE.JPG + +--- 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. +-- @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 recue 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 engies 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 realsitic 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 aircaft 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 Positon +-- +-- 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 rescricted to aircraft of the same coaliton 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, +} + +--- Class version. +-- @field #string version +RESCUEHELO.version="1.0.0" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add messages for rescue mission. +-- TODO: Add option to stop carrier while rescue operation is in progress? Done but NOT working! +-- DONE: Write documenation. +-- DONE: Add option to deactivate the rescueing. +-- 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 + + -- Log ID. + self.lid=string.format("RESCUEHELO %s |", self.carrier:GetName()) + + -- 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:SetRescueStopBoatOff() + + -- Some more. + self.rtb=false + self.carrierstop=false + + -- 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") + self:AddTransition("Running", "Rescue", "Rescuing") + self:AddTransition("Running", "RTB", "Returning") + self:AddTransition("Rescuing", "RTB", "Returning") + self:AddTransition("*", "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. + + + --- 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 "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 orienation of carrier. +-- @param #RESCUEHELO self +-- @param #number distance Offset distance in meters. Default 200 m. +-- @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 100 m. +-- @return #RESCUEHELO self +function RESCUEHELO:SetOffsetZ(distance) + self.offsetZ=distance or 100 + 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 + +--- 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 + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- 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:IsAlive() then + + -- Group name that landed. + local groupname=group:GetName() + + -- Check that it was our helo that landed. + if groupname==self.helo:GetName() then + + -- Respawn the Helo. + local text=string.format("Respawning rescue helo group %s at home base.", groupname) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + if self:IsRescuing() then + + self:T(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 self:IsRescuing() then + + self:T(string.format("Rescue helo %s returned from rescue operation.", groupname)) + + -- Respawn helo at current airbase. + self.helo=group:RespawnAtCurrentAirbase() + + else + + self:T2(string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true unless a rescue operation finished.", groupname)) + + -- Respawn helo at current airbase anyway. + if self.respawn then + self.helo=group:RespawnAtCurrentAirbase() + end + + end + + else + + -- Respawn helo at current airbase. + if self.respawn then + self.helo=group:RespawnAtCurrentAirbase() + end + + end + + -- Restart the formation. + self:__Run(10) + + 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:T(self.lid..text) + + -- 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 + + self:E(self.lid..string.format("Rescue helo %s crashed!", unitname)) + + 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.Birth) + self:HandleEvent(EVENTS.Land) + self:HandleEvent(EVENTS.Crash, self._OnEventCrashOrEject) + self:HandleEvent(EVENTS.Ejection, self._OnEventCrashOrEject) + + -- Delay before formation is started. + local delay=120 + + -- Set unique alias for spawn. + local helogroupalias=string.format("%s_%s", self.helogroupname, self.carrier:GetName()) + + -- 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, helogroupalias) + + -- 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.useuncontrolled then + + -- Use an uncontrolled aircraft group. + self.helo=GROUP:FindByName(self.helogroupname) + + if 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) + + -- 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) + + -- 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() + + -- Get relative fuel wrt to initial fuel of helo (DCS bug https://forums.eagle.ru/showthread.php?t=223712) + local fuel=self.helo:GetFuel()/self.HeloFuel0*100 + + -- Report current fuel. + local text=string.format("Rescue Helo %s: state=%s fuel=%.1f", self.helo:GetName(), self:GetState(), fuel) + MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug) + self:T(self.lid..text) + + -- Check if helo is running and not RTBing already or rescuing. + if self:IsRunning() then + + -- Check if fuel is low. + if fuel Missions -- @extends Core.Base#BASE ---- # COMMANDCENTER class, extends @{Base#BASE} --- --- The COMMANDCENTER class governs multiple missions, the tasking and the reporting. +--- Governs multiple missions, the tasking and the reporting. -- --- The commandcenter communicates important messages between the various groups of human players executing tasks in missions. +-- Command centers govern missions, communicates the task assignments between human players of the coalition, and manages the menu flow. +-- It can assign a random task to a player when requested. +-- The commandcenter provides the facilitites to communicate between human players online, executing a task. -- --- ## COMMANDCENTER constructor +-- ## 1. Create a command center object. -- -- * @{#COMMANDCENTER.New}(): Creates a new COMMANDCENTER object. -- --- ## Mission Management +-- ## 2. Command center mission management. +-- +-- Command centers manage missions. These can be added, removed and provides means to retrieve missions. +-- These methods are heavily used by the task dispatcher classes. -- -- * @{#COMMANDCENTER.AddMission}(): Adds a mission to the commandcenter control. -- * @{#COMMANDCENTER.RemoveMission}(): Removes a mission to the commandcenter control. -- * @{#COMMANDCENTER.GetMissions}(): Retrieves the missions table controlled by the commandcenter. -- --- ## Reference Zones +-- ## 3. Communication management between players. +-- +-- Command center provide means of communication between players. +-- Because a command center is a central object governing multiple missions, +-- there are several levels at which communication needs to be done. +-- Within MOOSE, communication is facilitated using the message system within the DCS simulator. +-- +-- Messages can be sent between players at various levels: +-- +-- - On a global level, to all players. +-- - On a coalition level, only to the players belonging to the same coalition. +-- - On a group level, to the players belonging to the same group. +-- +-- Messages can be sent to **all players** by the command center using the method @{Tasking.CommandCenter#COMMANDCENTER.MessageToAll}(). +-- +-- To send messages to **the coalition of the command center**, there are two methods available: +-- +-- - Use the method @{Tasking.CommandCenter#COMMANDCENTER.MessageToCoalition}() to send a specific message to the coalition, with a given message display duration. +-- - You can send a specific type of message using the method @{Tasking.CommandCenter#COMMANDCENTER.MessageTypeToCoalition}(). +-- This will send a message of a specific type to the coalition, and as a result its display duration will be flexible according the message display time selection by the human player. +-- +-- To send messages **to the group** of human players, there are also two methods available: +-- +-- - Use the method @{Tasking.CommandCenter#COMMANDCENTER.MessageToGroup}() to send a specific message to a group, with a given message display duration. +-- - You can send a specific type of message using the method @{Tasking.CommandCenter#COMMANDCENTER.MessageTypeToGroup}(). +-- This will send a message of a specific type to the group, and as a result its display duration will be flexible according the message display time selection by the human player . +-- +-- Messages are considered to be sometimes disturbing for human players, therefore, the settings menu provides the means to activate or deactivate messages. +-- For more information on the message types and display timings that can be selected and configured using the menu, refer to the @{Core.Settings} menu description. +-- +-- ## 4. Command center detailed methods. +-- +-- Various methods are added to manage command centers. +-- +-- ### 4.1. Naming and description. +-- +-- There are 3 methods that can be used to retrieve the description of a command center: +-- +-- - Use the method @{Tasking.CommandCenter#COMMANDCENTER.GetName}() to retrieve the name of the command center. +-- This is the name given as part of the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor. +-- The returned name using this method, is not to be used for message communication. +-- +-- A textual description can be retrieved that provides the command center name to be used within message communication: +-- +-- - @{Tasking.CommandCenter#COMMANDCENTER.GetShortText}() returns the command center name as `CC [CommandCenterName]`. +-- - @{Tasking.CommandCenter#COMMANDCENTER.GetText}() returns the command center name as `Command Center [CommandCenterName]`. +-- +-- ### 4.2. The coalition of the command center. +-- +-- The method @{Tasking.CommandCenter#COMMANDCENTER.GetCoalition}() returns the coalition of the command center. +-- The return value is an enumeration of the type @{DCS#coalition.side}, which contains the RED, BLUE and NEUTRAL coalition. +-- +-- ### 4.3. The command center is a real object. +-- +-- The command center must be represented by a live object within the DCS simulator. As a result, the command center +-- can be a @{Wrapper.Unit}, a @{Wrapper.Group}, an @{Wrapper.Airbase} or a @{Wrapper.Static} object. +-- +-- Using the method @{Tasking.CommandCenter#COMMANDCENTER.GetPositionable}() you retrieve the polymorphic positionable object representing +-- the command center, but just be aware that you should be able to use the representable object derivation methods. +-- +-- ### 5. Command center reports. +-- +-- Because a command center giverns multiple missions, there are several reports available that are generated by command centers. +-- These reports are generated using the following methods: +-- +-- - @{Tasking.CommandCenter#COMMANDCENTER.ReportSummary}(): Creates a summary report of all missions governed by the command center. +-- - @{Tasking.CommandCenter#COMMANDCENTER.ReportDetails}(): Creates a detailed report of all missions governed by the command center. +-- - @{Tasking.CommandCenter#COMMANDCENTER.ReportMissionPlayers}(): Creates a report listing the players active at the missions governed by the command center. +-- +-- ## 6. Reference Zones. -- -- Command Centers may be aware of certain Reference Zones within the battleground. These Reference Zones can refer to -- known areas, recognizable buildings or sites, or any other point of interest. @@ -86,11 +163,13 @@ COMMANDCENTER = { -- @return #COMMANDCENTER function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) - local self = BASE:Inherit( self, BASE:New() ) + local self = BASE:Inherit( self, BASE:New() ) -- #COMMANDCENTER self.CommandCenterPositionable = CommandCenterPositionable self.CommandCenterName = CommandCenterName or CommandCenterPositionable:GetName() self.CommandCenterCoalition = CommandCenterPositionable:GetCoalition() + + self.AutoAssignTasks = false self.Missions = {} @@ -171,7 +250,7 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) end ) - -- Handle when a player leaves a slot and goes back to spectators ... + -- Handle when a player crashes ... -- The PlayerUnit will be UnAssigned from the Task. -- When there is no Unit left running the Task, the Task goes into Abort... self:HandleEvent( EVENTS.Crash, @@ -191,6 +270,8 @@ function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName ) self:SetMenu() _SETTINGS:SetSystemMenu( CommandCenterPositionable ) + + self:SetCommandMenu() return self end @@ -220,6 +301,14 @@ function COMMANDCENTER:GetShortText() end +--- Gets the coalition of the command center. +-- @param #COMMANDCENTER self +-- @return DCScoalition#coalition +function COMMANDCENTER:GetCoalition() + + return self.CommandCenterCoalition +end + --- Gets the POSITIONABLE of the HQ command center. -- @param #COMMANDCENTER self @@ -233,7 +322,7 @@ end -- @return #list function COMMANDCENTER:GetMissions() - return self.Missions + return self.Missions or {} end --- Add a MISSION to be governed by the HQ command center. @@ -322,7 +411,7 @@ end --- Sets the menu structure of the Missions governed by the HQ command center. -- @param #COMMANDCENTER self function COMMANDCENTER:SetMenu() - self:F() + self:F2() local MenuTime = timer.getTime() for MissionID, Mission in pairs( self:GetMissions() or {} ) do @@ -331,7 +420,7 @@ function COMMANDCENTER:SetMenu() end for MissionID, Mission in pairs( self:GetMissions() or {} ) do - local Mission = Mission -- Tasking.Mission#MISSION + Mission = Mission -- Tasking.Mission#MISSION Mission:RemoveMenu( MenuTime ) end @@ -339,14 +428,160 @@ end --- Gets the commandcenter menu structure governed by the HQ command center. -- @param #COMMANDCENTER self +-- @param Wrapper.Group#Group TaskGroup Task Group. -- @return Core.Menu#MENU_COALITION -function COMMANDCENTER:GetMenu() - return self.CommandCenterMenu +function COMMANDCENTER:GetMenu( TaskGroup ) + + local MenuTime = timer.getTime() + + self.CommandCenterMenus = self.CommandCenterMenus or {} + local CommandCenterMenu + + local CommandCenterText = self:GetText() + CommandCenterMenu = MENU_GROUP:New( TaskGroup, CommandCenterText ):SetTime(MenuTime) + self.CommandCenterMenus[TaskGroup] = CommandCenterMenu + + if self.AutoAssignTasks == false then + local AssignTaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Assign Task", CommandCenterMenu, self.AssignRandomTask, self, TaskGroup ):SetTime(MenuTime):SetTag("AutoTask") + end + CommandCenterMenu:Remove( MenuTime, "AutoTask" ) + + return self.CommandCenterMenus[TaskGroup] end ---- Checks of the COMMANDCENTER has a GROUP. + +--- Assigns a random task to a TaskGroup. -- @param #COMMANDCENTER self --- @param Wrapper.Group#GROUP +-- @return #COMMANDCENTER +function COMMANDCENTER:AssignRandomTask( TaskGroup ) + + local Tasks = {} + + for MissionID, Mission in pairs( self:GetMissions() ) do + local Mission = Mission -- Tasking.Mission#MISSION + local MissionTasks = Mission:GetGroupTasks( TaskGroup ) + for MissionTaskName, MissionTask in pairs( MissionTasks or {} ) do + Tasks[#Tasks+1] = MissionTask + end + end + + local Task = Tasks[ math.random( 1, #Tasks ) ] -- Tasking.Task#TASK + + Task:SetAssignMethod( ACT_ASSIGN_MENU_ACCEPT:New( Task.TaskBriefing ) ) + + Task:AssignToGroup( TaskGroup ) + +end + + +--- Sets the menu of the command center. +-- This command is called within the :New() method. +-- @param #COMMANDCENTER self +function COMMANDCENTER:SetCommandMenu() + + local MenuTime = timer.getTime() + + if self.CommandCenterPositionable and self.CommandCenterPositionable:IsInstanceOf(GROUP) then + local CommandCenterText = self:GetText() + local CommandCenterMenu = MENU_GROUP:New( self.CommandCenterPositionable, CommandCenterText ):SetTime(MenuTime) + + if self.AutoAssignTasks == false then + local AutoAssignTaskMenu = MENU_GROUP_COMMAND:New( self.CommandCenterPositionable, "Assign Task On", CommandCenterMenu, self.SetAutoAssignTasks, self, true ):SetTime(MenuTime):SetTag("AutoTask") + else + local AutoAssignTaskMenu = MENU_GROUP_COMMAND:New( self.CommandCenterPositionable, "Assign Task Off", CommandCenterMenu, self.SetAutoAssignTasks, self, false ):SetTime(MenuTime):SetTag("AutoTask") + end + CommandCenterMenu:Remove( MenuTime, "AutoTask" ) + end + +end + + + +--- Automatically assigns tasks to all TaskGroups. +-- @param #COMMANDCENTER self +-- @param #boolean AutoAssign true for ON and false or nil for OFF. +function COMMANDCENTER:SetAutoAssignTasks( AutoAssign ) + + self.AutoAssignTasks = AutoAssign or false + + local GroupSet = self:AddGroups() + + for GroupID, TaskGroup in pairs( GroupSet:GetSet() ) do + local TaskGroup = TaskGroup -- Wrapper.Group#GROUP + self:GetMenu( TaskGroup ) + end + + if self.AutoAssignTasks == true then + self:ScheduleRepeat( 10, 30, 0, nil, self.AssignTasks, self ) + else + self:ScheduleStop( self.AssignTasks ) + end + + self:SetCommandCenterMenu() + +end + + +--- Automatically assigns tasks to all TaskGroups. +-- @param #COMMANDCENTER self +function COMMANDCENTER:AssignTasks() + + local GroupSet = self:AddGroups() + + for GroupID, TaskGroup in pairs( GroupSet:GetSet() ) do + local TaskGroup = TaskGroup -- Wrapper.Group#GROUP + + if self:IsGroupAssigned( TaskGroup ) then + else + -- Only groups with planes or helicopters will receive automatic tasks. + -- TODO Workaround DCS-BUG-3 - https://github.com/FlightControl-Master/MOOSE/issues/696 + if TaskGroup:IsAir() then + self:AssignRandomTask( TaskGroup ) + end + end + end + +end + + +--- Get all the Groups active within the command center. +-- @param #COMMANDCENTER self +-- @return Core.Set#SET_GROUP The set of groups active within the command center. +function COMMANDCENTER:AddGroups() + + local GroupSet = SET_GROUP:New() + + for MissionID, Mission in pairs( self.Missions ) do + local Mission = Mission -- Tasking.Mission#MISSION + GroupSet = Mission:AddGroups( GroupSet ) + end + + return GroupSet +end + + +--- Checks of the TaskGroup has a Task. +-- @param #COMMANDCENTER self +-- @return #boolean When true, the TaskGroup has a Task, otherwise the returned value will be false. +function COMMANDCENTER:IsGroupAssigned( TaskGroup ) + + local Assigned = false + + for MissionID, Mission in pairs( self.Missions ) do + local Mission = Mission -- Tasking.Mission#MISSION + if Mission:IsGroupAssigned( TaskGroup ) then + Assigned = true + break + end + end + + return Assigned +end + + +--- Checks of the command center has the given MissionGroup. +-- @param #COMMANDCENTER self +-- @param Wrapper.Group#GROUP MissionGroup The group active within one of the missions governed by the command center. -- @return #boolean function COMMANDCENTER:HasGroup( MissionGroup ) @@ -363,64 +598,67 @@ function COMMANDCENTER:HasGroup( MissionGroup ) return Has end ---- Send a CC message to the coalition of the CC. +--- Let the command center send a Message to all players. -- @param #COMMANDCENTER self +-- @param #string Message The message text. function COMMANDCENTER:MessageToAll( Message ) self:GetPositionable():MessageToAll( Message, 20, self:GetName() ) end ---- Send a CC message to a GROUP. +--- Let the command center send a message to the MessageGroup. -- @param #COMMANDCENTER self --- @param #string Message --- @param Wrapper.Group#GROUP TaskGroup -function COMMANDCENTER:MessageToGroup( Message, TaskGroup ) +-- @param #string Message The message text. +-- @param Wrapper.Group#GROUP MessageGroup The group to receive the message. +function COMMANDCENTER:MessageToGroup( Message, MessageGroup ) - self:GetPositionable():MessageToGroup( Message, 15, TaskGroup, self:GetShortText() ) + self:GetPositionable():MessageToGroup( Message, 15, MessageGroup, self:GetShortText() ) end ---- Send a CC message of a specified type to a GROUP. +--- Let the command center send a message to the MessageGroup. -- @param #COMMANDCENTER self --- @param #string Message --- @param Wrapper.Group#GROUP TaskGroup +-- @param #string Message The message text. +-- @param Wrapper.Group#GROUP MessageGroup The group to receive the message. -- @param Core.Message#MESSAGE.MessageType MessageType The type of the message, resulting in automatic time duration and prefix of the message. -function COMMANDCENTER:MessageTypeToGroup( Message, TaskGroup, MessageType ) +function COMMANDCENTER:MessageTypeToGroup( Message, MessageGroup, MessageType ) - self:GetPositionable():MessageTypeToGroup( Message, MessageType, TaskGroup, self:GetShortText() ) + self:GetPositionable():MessageTypeToGroup( Message, MessageType, MessageGroup, self:GetShortText() ) end ---- Send a CC message to the coalition of the CC. +--- Let the command center send a message to the coalition of the command center. -- @param #COMMANDCENTER self +-- @param #string Message The message text. function COMMANDCENTER:MessageToCoalition( Message ) local CCCoalition = self:GetPositionable():GetCoalition() --TODO: Fix coalition bug! - self:GetPositionable():MessageToCoalition( Message, 15, CCCoalition ) + self:GetPositionable():MessageToCoalition( Message, 15, CCCoalition, self:GetShortText() ) end ---- Send a CC message of a specified type to the coalition of the CC. +--- Let the command center send a message of a specified type to the coalition of the command center. -- @param #COMMANDCENTER self --- @param #string Message The message. +-- @param #string Message The message text. -- @param Core.Message#MESSAGE.MessageType MessageType The type of the message, resulting in automatic time duration and prefix of the message. function COMMANDCENTER:MessageTypeToCoalition( Message, MessageType ) local CCCoalition = self:GetPositionable():GetCoalition() --TODO: Fix coalition bug! - self:GetPositionable():MessageTypeToCoalition( Message, MessageType, CCCoalition ) + self:GetPositionable():MessageTypeToCoalition( Message, MessageType, CCCoalition, self:GetShortText() ) end ---- Report the status of all MISSIONs to a GROUP. +--- Let the command center send a report of the status of all missions to a group. -- Each Mission is listed, with an indication how many Tasks are still to be completed. -- @param #COMMANDCENTER self +-- @param Wrapper.Group#GROUP ReportGroup The group to receive the report. function COMMANDCENTER:ReportSummary( ReportGroup ) self:F( ReportGroup ) @@ -432,15 +670,16 @@ function COMMANDCENTER:ReportSummary( ReportGroup ) for MissionID, Mission in pairs( self.Missions ) do local Mission = Mission -- Tasking.Mission#MISSION - Report:Add( " - " .. Mission:ReportSummary() ) + Report:Add( " - " .. Mission:ReportSummary( ReportGroup ) ) end self:MessageToGroup( Report:Text(), ReportGroup ) end ---- Report the players of all MISSIONs to a GROUP. +--- Let the command center send a report of the players of all missions to a group. -- Each Mission is listed, with an indication how many Tasks are still to be completed. -- @param #COMMANDCENTER self +-- @param Wrapper.Group#GROUP ReportGroup The group to receive the report. function COMMANDCENTER:ReportMissionsPlayers( ReportGroup ) self:F( ReportGroup ) @@ -448,17 +687,19 @@ function COMMANDCENTER:ReportMissionsPlayers( ReportGroup ) Report:Add( "Players active in all missions." ) - for MissionID, Mission in pairs( self.Missions ) do - local Mission = Mission -- Tasking.Mission#MISSION - Report:Add( " - " .. Mission:ReportPlayers() ) + for MissionID, MissionData in pairs( self.Missions ) do + local Mission = MissionData -- Tasking.Mission#MISSION + Report:Add( " - " .. Mission:ReportPlayersPerTask(ReportGroup) ) end self:MessageToGroup( Report:Text(), ReportGroup ) end ---- Report the status of a Task to a Group. +--- Let the command center send a report of the status of a task to a group. -- Report the details of a Mission, listing the Mission, and all the Task details. -- @param #COMMANDCENTER self +-- @param Wrapper.Group#GROUP ReportGroup The group to receive the report. +-- @param Tasking.Task#TASK Task The task to be reported. function COMMANDCENTER:ReportDetails( ReportGroup, Task ) self:F( ReportGroup ) diff --git a/Moose Development/Moose/Tasking/DetectionManager.lua b/Moose Development/Moose/Tasking/DetectionManager.lua index 6252dc0a4..9e4532e39 100644 --- a/Moose Development/Moose/Tasking/DetectionManager.lua +++ b/Moose Development/Moose/Tasking/DetectionManager.lua @@ -1,38 +1,36 @@ ---- This module contains the DETECTION_MANAGER class and derived classes. +--- **Tasking** - This module contains the DETECTION_MANAGER class and derived classes. -- -- === -- --- 1) @{DetectionManager#DETECTION_MANAGER} class, extends @{Fsm#FSM} --- === --- The @{DetectionManager#DETECTION_MANAGER} class defines the core functions to report detected objects to groups. +-- The @{#DETECTION_MANAGER} class defines the core functions to report detected objects to groups. -- Reportings can be done in several manners, and it is up to the derived classes if DETECTION_MANAGER to model the reporting behaviour. -- -- 1.1) DETECTION_MANAGER constructor: -- ----------------------------------- --- * @{DetectionManager#DETECTION_MANAGER.New}(): Create a new DETECTION_MANAGER instance. +-- * @{#DETECTION_MANAGER.New}(): Create a new DETECTION_MANAGER instance. -- -- 1.2) DETECTION_MANAGER reporting: -- --------------------------------- --- Derived DETECTION_MANAGER classes will reports detected units using the method @{DetectionManager#DETECTION_MANAGER.ReportDetected}(). This method implements polymorphic behaviour. +-- Derived DETECTION_MANAGER classes will reports detected units using the method @{#DETECTION_MANAGER.ReportDetected}(). This method implements polymorphic behaviour. -- --- The time interval in seconds of the reporting can be changed using the methods @{DetectionManager#DETECTION_MANAGER.SetRefreshTimeInterval}(). --- To control how long a reporting message is displayed, use @{DetectionManager#DETECTION_MANAGER.SetReportDisplayTime}(). --- Derived classes need to implement the method @{DetectionManager#DETECTION_MANAGER.GetReportDisplayTime}() to use the correct display time for displayed messages during a report. +-- The time interval in seconds of the reporting can be changed using the methods @{#DETECTION_MANAGER.SetRefreshTimeInterval}(). +-- To control how long a reporting message is displayed, use @{#DETECTION_MANAGER.SetReportDisplayTime}(). +-- Derived classes need to implement the method @{#DETECTION_MANAGER.GetReportDisplayTime}() to use the correct display time for displayed messages during a report. -- --- Reporting can be started and stopped using the methods @{DetectionManager#DETECTION_MANAGER.StartReporting}() and @{DetectionManager#DETECTION_MANAGER.StopReporting}() respectively. --- If an ad-hoc report is requested, use the method @{DetectionManager#DETECTION_MANAGER#ReportNow}(). +-- Reporting can be started and stopped using the methods @{#DETECTION_MANAGER.StartReporting}() and @{#DETECTION_MANAGER.StopReporting}() respectively. +-- If an ad-hoc report is requested, use the method @{#DETECTION_MANAGER#ReportNow}(). -- -- The default reporting interval is every 60 seconds. The reporting messages are displayed 15 seconds. -- -- === -- --- 2) @{DetectionManager#DETECTION_REPORTING} class, extends @{DetectionManager#DETECTION_MANAGER} +-- 2) @{#DETECTION_REPORTING} class, extends @{#DETECTION_MANAGER} -- === --- The @{DetectionManager#DETECTION_REPORTING} class implements detected units reporting. Reporting can be controlled using the reporting methods available in the @{DetectionManager#DETECTION_MANAGER} class. +-- The @{#DETECTION_REPORTING} class implements detected units reporting. Reporting can be controlled using the reporting methods available in the @{Tasking.DetectionManager#DETECTION_MANAGER} class. -- -- 2.1) DETECTION_REPORTING constructor: -- ------------------------------- --- The @{DetectionManager#DETECTION_REPORTING.New}() method creates a new DETECTION_REPORTING instance. +-- The @{#DETECTION_REPORTING.New}() method creates a new DETECTION_REPORTING instance. -- -- -- === @@ -40,15 +38,18 @@ -- ### Contributions: Mechanist, Prof_Hilactic, FlightControl - Concept & Testing -- ### Author: FlightControl - Framework Design & Programming -- --- @module DetectionManager +-- @module Tasking.DetectionManager +-- @image Task_Detection_Manager.JPG do -- DETECTION MANAGER - --- DETECTION_MANAGER class. - -- @type DETECTION_MANAGER - -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. + --- @type DETECTION_MANAGER + -- @field Core.Set#SET_GROUP SetGroup The groups to which the FAC will report to. -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. -- @extends Core.Fsm#FSM + + --- DETECTION_MANAGER class. + -- @field #DETECTION_MANAGER DETECTION_MANAGER = { ClassName = "DETECTION_MANAGER", SetGroup = nil, @@ -57,7 +58,7 @@ do -- DETECTION MANAGER --- FAC constructor. -- @param #DETECTION_MANAGER self - -- @param Set#SET_GROUP SetGroup + -- @param Core.Set#SET_GROUP SetGroup -- @param Functional.Detection#DETECTION_BASE Detection -- @return #DETECTION_MANAGER self function DETECTION_MANAGER:New( SetGroup, Detection ) @@ -122,6 +123,48 @@ do -- DETECTION MANAGER -- @function [parent=#DETECTION_MANAGER] __Stop -- @param #DETECTION_MANAGER self -- @param #number Delay + + self:AddTransition( "Started", "Success", "Started" ) + + --- Success Handler OnAfter for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] OnAfterSuccess + -- @param #DETECTION_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task + + + self:AddTransition( "Started", "Failed", "Started" ) + + --- Failed Handler OnAfter for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] OnAfterFailed + -- @param #DETECTION_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task + + + self:AddTransition( "Started", "Aborted", "Started" ) + + --- Aborted Handler OnAfter for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] OnAfterAborted + -- @param #DETECTION_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task + + self:AddTransition( "Started", "Cancelled", "Started" ) + + --- Cancelled Handler OnAfter for DETECTION_MANAGER + -- @function [parent=#DETECTION_MANAGER] OnAfterCancelled + -- @param #DETECTION_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task self:AddTransition( "Started", "Report", "Started" ) @@ -175,7 +218,7 @@ do -- DETECTION MANAGER return self._ReportDisplayTime end - --- Reports the detected items to the @{Set#SET_GROUP}. + --- Reports the detected items to the @{Core.Set#SET_GROUP}. -- @param #DETECTION_MANAGER self -- @param Functional.Detection#DETECTION_BASE Detection -- @return #DETECTION_MANAGER self @@ -191,7 +234,7 @@ do -- DETECTION_REPORTING --- DETECTION_REPORTING class. -- @type DETECTION_REPORTING - -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. + -- @field Core.Set#SET_GROUP SetGroup The groups to which the FAC will report to. -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. -- @extends #DETECTION_MANAGER DETECTION_REPORTING = { @@ -201,7 +244,7 @@ do -- DETECTION_REPORTING --- DETECTION_REPORTING constructor. -- @param #DETECTION_REPORTING self - -- @param Set#SET_GROUP SetGroup + -- @param Core.Set#SET_GROUP SetGroup -- @param Functional.Detection#DETECTION_AREAS Detection -- @return #DETECTION_REPORTING self function DETECTION_REPORTING:New( SetGroup, Detection ) @@ -215,7 +258,7 @@ do -- DETECTION_REPORTING --- Creates a string of the detected items in a @{Detection}. -- @param #DETECTION_MANAGER self - -- @param Set#SET_UNIT DetectedSet The detected Set created by the @{Detection#DETECTION_BASE} object. + -- @param Core.Set#SET_UNIT DetectedSet The detected Set created by the @{Functional.Detection#DETECTION_BASE} object. -- @return #DETECTION_MANAGER self function DETECTION_REPORTING:GetDetectedItemsText( DetectedSet ) self:F2() @@ -245,10 +288,10 @@ do -- DETECTION_REPORTING - --- Reports the detected items to the @{Set#SET_GROUP}. + --- Reports the detected items to the @{Core.Set#SET_GROUP}. -- @param #DETECTION_REPORTING self - -- @param Wrapper.Group#GROUP Group The @{Group} object to where the report needs to go. - -- @param Functional.Detection#DETECTION_AREAS Detection The detection created by the @{Detection#DETECTION_BASE} object. + -- @param Wrapper.Group#GROUP Group The @{Wrapper.Group} object to where the report needs to go. + -- @param Functional.Detection#DETECTION_AREAS Detection The detection created by the @{Functional.Detection#DETECTION_BASE} object. -- @return #boolean Return true if you want the reporting to continue... false will cancel the reporting loop. function DETECTION_REPORTING:ProcessDetected( Group, Detection ) self:F2( Group ) diff --git a/Moose Development/Moose/Tasking/Mission.lua b/Moose Development/Moose/Tasking/Mission.lua index 06a7e7d9b..2d6ac4660 100644 --- a/Moose Development/Moose/Tasking/Mission.lua +++ b/Moose Development/Moose/Tasking/Mission.lua @@ -1,4 +1,12 @@ ---- **Tasking** -- A MISSION is the main owner of a Mission orchestration within MOOSE. +--- **Tasking** -- A mission models a goal to be achieved through the execution and completion of tasks by human players. +-- +-- **Features:** +-- +-- * A mission has a goal to be achieved, through the execution and completion of tasks of different categories by human players. +-- * A mission manages these tasks. +-- * A mission has a state, that indicates the fase of the mission. +-- * A mission has a menu structure, that facilitates mission reports and tasking menus. +-- * A mission can assign a task to a player. -- -- === -- @@ -8,14 +16,109 @@ -- -- === -- --- @module Mission +-- @module Tasking.Mission +-- @image Task_Mission.JPG ---- The MISSION class --- @type MISSION +--- @type MISSION -- @field #MISSION.Clients _Clients -- @field Core.Menu#MENU_COALITION MissionMenu -- @field #string MissionBriefing -- @extends Core.Fsm#FSM + +--- Models goals to be achieved and can contain multiple tasks to be executed to achieve the goals. +-- +-- A mission contains multiple tasks and can be of different task types. +-- These tasks need to be assigned to human players to be executed. +-- +-- A mission can have multiple states, which will evolve as the mission progresses during the DCS simulation. +-- +-- - **IDLE**: The mission is defined, but not started yet. No task has yet been joined by a human player as part of the mission. +-- - **ENGAGED**: The mission is ongoing, players have joined tasks to be executed. +-- - **COMPLETED**: The goals of the mission has been successfully reached, and the mission is flagged as completed. +-- - **FAILED**: For a certain reason, the goals of the mission has not been reached, and the mission is flagged as failed. +-- - **HOLD**: The mission was enaged, but for some reason it has been put on hold. +-- +-- Note that a mission goals need to be checked by a goal check trigger: @{#MISSION.OnBeforeMissionGoals}(), which may return false if the goal has not been reached. +-- This goal is checked automatically by the mission object every x seconds. +-- +-- - @{#MISSION.Start}() or @{#MISSION.__Start}() will start the mission, and will bring it from **IDLE** state to **ENGAGED** state. +-- - @{#MISSION.Stop}() or @{#MISSION.__Stop}() will stop the mission, and will bring it from **ENGAGED** state to **IDLE** state. +-- - @{#MISSION.Complete}() or @{#MISSION.__Complete}() will complete the mission, and will bring the mission state to **COMPLETED**. +-- Note that the mission must be in state **ENGAGED** to be able to complete the mission. +-- - @{#MISSION.Fail}() or @{#MISSION.__Fail}() will fail the mission, and will bring the mission state to **FAILED**. +-- Note that the mission must be in state **ENGAGED** to be able to fail the mission. +-- - @{#MISSION.Hold}() or @{#MISSION.__Hold}() will hold the mission, and will bring the mission state to **HOLD**. +-- Note that the mission must be in state **ENGAGED** to be able to hold the mission. +-- Re-engage the mission using the engage trigger. +-- +-- The following sections provide an overview of the most important methods that can be used as part of a mission object. +-- Note that the @{Tasking.CommandCenter} system is using most of these methods to manage the missions in its system. +-- +-- ## 1. Create a mission object. +-- +-- - @{#MISSION.New}(): Creates a new MISSION object. +-- +-- ## 2. Mission task management. +-- +-- Missions maintain tasks, which can be added or removed, or enquired. +-- +-- - @{#MISSION.AddTask}(): Adds a task to the mission. +-- - @{#MISSION.RemoveTask}(): Removes a task from the mission. +-- +-- ## 3. Mission detailed methods. +-- +-- Various methods are added to manage missions. +-- +-- ### 3.1. Naming and description. +-- +-- There are several methods that can be used to retrieve the properties of a mission: +-- +-- - Use the method @{#MISSION.GetName}() to retrieve the name of the mission. +-- This is the name given as part of the @{#MISSION.New}() constructor. +-- +-- A textual description can be retrieved that provides the mission name to be used within message communication: +-- +-- - @{#MISSION.GetShortText}() returns the mission name as `Mission "MissionName"`. +-- - @{#MISSION.GetText}() returns the mission name as `Mission "MissionName (MissionPriority)"`. A longer version including the priority text of the mission. +-- +-- ### 3.2. Get task information. +-- +-- - @{#MISSION.GetTasks}(): Retrieves a list of the tasks controlled by the mission. +-- - @{#MISSION.GetTask}(): Retrieves a specific task controlled by the mission. +-- - @{#MISSION.GetTasksRemaining}(): Retrieve a list of the tasks that aren't finished or failed, and are governed by the mission. +-- - @{#MISSION.GetGroupTasks}(): Retrieve a list of the tasks that can be asigned to a @{Wrapper.Group}. +-- - @{#MISSION.GetTaskTypes}(): Retrieve a list of the different task types governed by the mission. +-- +-- ### 3.3. Get the command center. +-- +-- - @{#MISSION.GetCommandCenter}(): Retrieves the @{Tasking.CommandCenter} governing the mission. +-- +-- ### 3.4. Get the groups active in the mission as a @{Core.Set}. +-- +-- - @{#MISSION.GetGroups}(): Retrieves a @{Core.Set#SET_GROUP} of all the groups active in the mission (as part of the tasks). +-- +-- ### 3.5. Get the names of the players. +-- +-- - @{#MISSION.GetPlayerNames}(): Retrieves the list of the players that were active within th mission.. +-- +-- ## 4. Menu management. +-- +-- A mission object is able to manage its own menu structure. Use the @{#MISSION.GetMenu}() and @{#MISSION.SetMenu}() to manage the underlying submenu +-- structure managing the tasks of the mission. +-- +-- ## 5. Reporting management. +-- +-- Several reports can be generated for a mission, and will return a text string that can be used to display using the @{Core.Message} system. +-- +-- - @{#MISSION.ReportBriefing}(): Generates the briefing for the mission. +-- - @{#MISSION.ReportOverview}(): Generates an overview of the tasks and status of the mission. +-- - @{#MISSION.ReportDetails}(): Generates a detailed report of the tasks of the mission. +-- - @{#MISSION.ReportSummary}(): Generates a summary report of the tasks of the mission. +-- - @{#MISSION.ReportPlayersPerTask}(): Generates a report showing the active players per task. +-- - @{#MISSION.ReportPlayersProgress}(): Generates a report showing the task progress per player. +-- +-- +-- @field #MISSION MISSION = { ClassName = "MISSION", Name = "", @@ -26,10 +129,10 @@ MISSION = { --- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc. -- @param #MISSION self -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter --- @param #string MissionName is the name of the mission. This name will be used to reference the status of each mission by the players. --- @param #string MissionPriority is a string indicating the "priority" of the Mission. f.e. "Primary", "Secondary" or "First", "Second". It is free format and up to the Mission designer to choose. There are no rules behind this field. --- @param #string MissionBriefing is a string indicating the mission briefing to be shown when a player joins a @{CLIENT}. --- @param Dcs.DCSCoalitionWrapper.Object#coalition MissionCoalition is a string indicating the coalition or party to which this mission belongs to. It is free format and can be chosen freely by the mission designer. Note that this field is not to be confused with the coalition concept of the ME. Examples of a Mission Coalition could be "NATO", "CCCP", "Intruders", "Terrorists"... +-- @param #string MissionName Name of the mission. This name will be used to reference the status of each mission by the players. +-- @param #string MissionPriority String indicating the "priority" of the Mission. e.g. "Primary", "Secondary". It is free format and up to the Mission designer to choose. There are no rules behind this field. +-- @param #string MissionBriefing String indicating the mission briefing to be shown when a player joins a @{CLIENT}. +-- @param DCS#coaliton.side MissionCoalition Side of the coalition, i.e. and enumerator @{#DCS.coalition.side} corresponding to RED, BLUE or NEUTRAL. -- @return #MISSION self function MISSION:New( CommandCenter, MissionName, MissionPriority, MissionBriefing, MissionCoalition ) @@ -265,6 +368,8 @@ function MISSION:New( CommandCenter, MissionName, MissionPriority, MissionBriefi end + + --- FSM function for a MISSION -- @param #MISSION self -- @param #string From @@ -375,24 +480,30 @@ function MISSION:GetScoring() return self.Scoring end ---- Get the groups for which TASKS are given in the mission +--- Gets the groups for which TASKS are given in the mission -- @param #MISSION self +-- @param Core.Set#SET_GROUP GroupSet -- @return Core.Set#SET_GROUP function MISSION:GetGroups() - local SetGroup = SET_GROUP:New() + return self:AddGroups() + +end + +--- Adds the groups for which TASKS are given in the mission +-- @param #MISSION self +-- @param Core.Set#SET_GROUP GroupSet +-- @return Core.Set#SET_GROUP +function MISSION:AddGroups( GroupSet ) + + GroupSet = GroupSet or SET_GROUP:New() for TaskID, Task in pairs( self:GetTasks() ) do local Task = Task -- Tasking.Task#TASK - local GroupSet = Task:GetGroups() - GroupSet:ForEachGroup( - function( TaskGroup ) - SetGroup:Add( TaskGroup, TaskGroup ) - end - ) + GroupSet = Task:AddGroups( GroupSet ) end - return SetGroup + return GroupSet end @@ -441,16 +552,16 @@ do -- Group Assignment local MissionGroupName = MissionGroup:GetName() if self.AssignedGroups[MissionGroupName] == MissionGroup then - self:T( { "Mission is assigned to:", MissionGroup:GetName() } ) + self:T2( { "Mission is assigned to:", MissionGroup:GetName() } ) return true end - self:T( { "Mission is not assigned to:", MissionGroup:GetName() } ) + self:T2( { "Mission is not assigned to:", MissionGroup:GetName() } ) return false end - --- Set @{Group} assigned to the @{Mission}. + --- Set @{Wrapper.Group} assigned to the @{Mission}. -- @param #MISSION self -- @param Wrapper.Group#GROUP MissionGroup -- @return #MISSION @@ -465,7 +576,7 @@ do -- Group Assignment return self end - --- Clear the @{Group} assignment from the @{Mission}. + --- Clear the @{Wrapper.Group} assignment from the @{Mission}. -- @param #MISSION self -- @param Wrapper.Group#GROUP MissionGroup -- @return #MISSION @@ -500,13 +611,14 @@ function MISSION:RemoveTaskMenu( Task ) end ---- Gets the root mission menu for the TaskGroup. +--- Gets the root mission menu for the TaskGroup. Obsolete?! Originally no reference to TaskGroup parameter! -- @param #MISSION self +-- @param Wrapper.Group#GROUP TaskGroup Task group. -- @return Core.Menu#MENU_COALITION self function MISSION:GetRootMenu( TaskGroup ) -- R2.2 local CommandCenter = self:GetCommandCenter() - local CommandCenterMenu = CommandCenter:GetMenu() + local CommandCenterMenu = CommandCenter:GetMenu( TaskGroup ) local MissionName = self:GetText() --local MissionMenu = CommandCenterMenu:GetMenu( MissionName ) @@ -518,22 +630,18 @@ end --- Gets the mission menu for the TaskGroup. -- @param #MISSION self +-- @param Wrapper.Group#GROUP TaskGroup Task group. -- @return Core.Menu#MENU_COALITION self function MISSION:GetMenu( TaskGroup ) -- R2.1 -- Changed Menu Structure local CommandCenter = self:GetCommandCenter() - local CommandCenterMenu = CommandCenter:GetMenu() + local CommandCenterMenu = CommandCenter:GetMenu( TaskGroup ) - --local MissionMenu = CommandCenterMenu:GetMenu( MissionName ) - self.MissionGroupMenu = self.MissionGroupMenu or {} self.MissionGroupMenu[TaskGroup] = self.MissionGroupMenu[TaskGroup] or {} local GroupMenu = self.MissionGroupMenu[TaskGroup] - local CommandCenterText = CommandCenter:GetText() - CommandCenterMenu = MENU_GROUP:New( TaskGroup, CommandCenterText ) - local MissionText = self:GetText() self.MissionMenu = MENU_GROUP:New( TaskGroup, MissionText, CommandCenterMenu ) @@ -562,7 +670,7 @@ end -- @param #string TaskName The Name of the @{Task} within the @{Mission}. -- @return Tasking.Task#TASK The Task -- @return #nil Returns nil if no task was found. -function MISSION:GetTask( TaskName ) +function MISSION:GetTask( TaskName ) self:F( { TaskName } ) return self.Tasks[TaskName] @@ -1003,9 +1111,28 @@ end -- env.info( "Task 2 Completion = " .. Tasks[2]:GetGoalPercentage() .. "%" ) function MISSION:GetTasks() - return self.Tasks + return self.Tasks or {} end +--- Get the relevant tasks of a TaskGroup. +-- @param #MISSION +-- @param Wrapper.Group#GROUP TaskGroup +-- @return #list +function MISSION:GetGroupTasks( TaskGroup ) + + local Tasks = {} + + for TaskID, Task in pairs( self:GetTasks() ) do + local Task = Task -- Tasking.Task#TASK + if Task:HasGroup( TaskGroup ) then + Tasks[#Tasks+1] = Task + end + end + + return Tasks +end + + --- Reports the briefing. -- @param #MISSION self -- @param Wrapper.Group#GROUP ReportGroup The group to which the report needs to be sent. diff --git a/Moose Development/Moose/Tasking/Task.lua b/Moose Development/Moose/Tasking/Task.lua index e8ae381a1..0f4ec262d 100644 --- a/Moose Development/Moose/Tasking/Task.lua +++ b/Moose Development/Moose/Tasking/Task.lua @@ -1,4 +1,208 @@ ---- **Tasking** -- This module contains the TASK class, the main engine to run human taskings. +--- **Tasking** -- A task object governs the main engine to administer human taskings. +-- +-- **Features:** +-- +-- * A base class for other task classes filling in the details and making a concrete task process. +-- * Manage the overall task execution, following-up the progression made by the pilots and actors. +-- * Provide a mechanism to set a task status, depending on the progress made within the task. +-- * Manage a task briefing. +-- * Manage the players executing the task. +-- * Manage the task menu system. +-- * Manage the task goal and scoring. +-- +-- === +-- +-- # 1) Tasking from a player perspective. +-- +-- Tasking can be controlled by using the "other" menu in the radio menu of the player group. +-- +-- ![Other Menu](../Tasking/Menu_Main.JPG) +-- +-- ## 1.1) Command Centers govern multiple Missions. +-- +-- Depending on the tactical situation, your coalition may have one (or multiple) command center(s). +-- These command centers govern one (or multiple) mission(s). +-- +-- For each command center, there will be a separate **Command Center Menu** that focuses on the missions governed by that command center. +-- +-- ![Command Center](../Tasking/Menu_CommandCenter.JPG) +-- +-- In the above example menu structure, there is one command center with the name **`[Lima]`**. +-- The command center has one @{Tasking.Mission}, named **`"Overlord"`** with **`High`** priority. +-- +-- ## 1.2) Missions govern multiple Tasks. +-- +-- A mission has a mission goal to be achieved by the players within the coalition. +-- The mission goal is actually dependent on the tactical situation of the overall battlefield and the conditions set to achieve the goal. +-- So a mission can be much more than just shoot stuff ... It can be a combination of different conditions or events to complete a mission goal. +-- +-- A mission can be in a specific state during the simulation run. For more information about these states, please check the @{Tasking.Mission} section. +-- +-- To achieve the mission goal, a mission administers @{Tasking.Task}s that are set to achieve the mission goal by the human players. +-- Each of these tasks can be **dynamically created** using a task dispatcher, or **coded** by the mission designer. +-- Each mission has a separate **Mission Menu**, that focuses on the administration of these tasks. +-- +-- On top, a mission has a mission briefing, can help to allocate specific points of interest on the map, and provides various reports. +-- +-- ![Mission](../Tasking/Menu_Mission.JPG) +-- +-- The above shows a mission menu in detail of **`"Overlord"`**. +-- +-- The two other menus are related to task assignment. Which will be detailed later. +-- +-- ### 1.2.1) Mission briefing. +-- +-- The task briefing will show a message containing a description of the mission goal, and other tactical information. +-- +-- ![Mission](../Tasking/Report_Briefing.JPG) +-- +-- ### 1.2.2) Mission Map Locations. +-- +-- Various points of interest as part of the mission can be indicated on the map using the *Mark Task Locations on Map* menu. +-- As a result, the map will contain various points of interest for the player (group). +-- +-- ![Mission](../Tasking/Report_Mark_Task_Location.JPG) +-- +-- ### 1.2.3) Mission Task Reports. +-- +-- Various reports can be generated on the status of each task governed within the mission. +-- +-- ![Mission](../Tasking/Report_Task_Summary.JPG) +-- +-- The Task Overview Report will show each task, with its task status and a short coordinate information. +-- +-- ![Mission](../Tasking/Report_Tasks_Planned.JPG) +-- +-- The other Task Menus will show for each task more details, for example here the planned tasks report. +-- Note that the order of the tasks are shortest distance first to the unit position seated by the player. +-- +-- ### 1.2.4) Mission Statistics. +-- +-- Various statistics can be displayed regarding the mission. +-- +-- ![Mission](../Tasking/Report_Statistics_Progress.JPG) +-- +-- A statistic report on the progress of the mission. Each task achievement will increase the %-tage to 100% as a goal to complete the task. +-- +-- ## 1.3) Join a Task. +-- +-- The mission menu contains a very important option, that is to join a task governed within the mission. +-- In order to join a task, select the **Join Planned Task** menu, and a new menu will be given. +-- +-- ![Mission](../Tasking/Menu_Join_Planned_Tasks.JPG) +-- +-- A mission governs multiple tasks, as explained earlier. Each task is of a certain task type. +-- This task type was introduced to have some sort of task classification system in place for the player. +-- A short acronym is shown that indicates the task type. The meaning of each acronym can be found in the task types explanation. +-- +-- ![Mission](../Tasking/Menu_Join_Tasks.JPG) +-- +-- When the player selects a task type, a list of the available tasks of that type are listed... +-- In this case the **`SEAD`** task type was selected and a list of available **`SEAD`** tasks can be selected. +-- +-- ![Mission](../Tasking/Menu_Join_Planned_Task.JPG) +-- +-- A new list of menu options are now displayed that allow to join the task selected, but also to obtain first some more information on the task. +-- +-- ### 1.3.1) Report Task Details. +-- +-- ![Mission](../Tasking/Report_Task_Detailed.JPG) +-- +-- When selected, a message is displayed that shows detailed information on the task, like the coordinate, enemy target information, threat level etc. +-- +-- ### 1.3.2) Mark Task Location on Map. +-- +-- ![Mission](../Tasking/Report_Task_Detailed.JPG) +-- +-- When selected, the target location on the map is indicated with specific information on the task. +-- +-- ### 1.3.3) Join Task. +-- +-- ![Mission](../Tasking/Report_Task_Detailed.JPG) +-- +-- By joining a task, the player will indicate that the task is assigned to him, and the task is started. +-- The Command Center will communicate several task details to the player and the coalition of the player. +-- +-- ## 1.4) Task Control and Actions. +-- +-- ![Mission](../Tasking/Menu_Main_Task.JPG) +-- +-- When a player has joined a task, a **Task Action Menu** is available to be used by the player. +-- +-- ![Mission](../Tasking/Menu_Task.JPG) +-- +-- The task action menu contains now menu items specific to the task, but also one generic menu item, which is to control the task. +-- This **Task Control Menu** allows to display again the task details and the task map location information. +-- But it also allows to abort a task! +-- +-- Depending on the task type, the task action menu can contain more menu items which are specific to the task. +-- For example, cargo transportation tasks will contain various additional menu items to select relevant cargo coordinates, +-- or to load/unload cargo. +-- +-- ## 1.5) Automatic task assignment. +-- +-- ![Command Center](../Tasking/Menu_CommandCenter.JPG) +-- +-- When we take back the command center menu, you see two addtional **Assign Task** menu items. +-- The menu **Assign Task On** will automatically allocate a task to the player. +-- After the selection of this menu, the menu will change into **Assign Task Off**, +-- and will need to be selected again by the player to switch of the automatic task assignment. +-- +-- The other option is to select **Assign Task**, which will assign a new random task to the player. +-- +-- When a task is automatically assigned to a player, the task needs to be confirmed as accepted within 30 seconds. +-- If this is not the case, the task will be cancelled automatically, and a new random task will be assigned to the player. +-- This will continue to happen until the player accepts the task or switches off the automatic task assignment process. +-- +-- The player can accept the task using the menu **Confirm Task Acceptance** ... +-- +-- ## 1.6) Task states. +-- +-- A task has a state, reflecting the progress or completion status of the task: +-- +-- - **Planned**: Expresses that the task is created, but not yet in execution and is not assigned yet to a pilot. +-- - **Assigned**: Expresses that the task is assigned to a group of pilots, and that the task is in execution mode. +-- - **Success**: Expresses the successful execution and finalization of the task. +-- - **Failed**: Expresses the failure of a task. +-- - **Abort**: Expresses that the task is aborted by by the player using the abort menu. +-- - **Cancelled**: Expresses that the task is cancelled by HQ or through a logical situation where a cancellation of the task is required. +-- +-- ### 1.6.1) Task progress. +-- +-- The task governor takes care of the **progress** and **completion** of the task **goal(s)**. +-- Tasks are executed by **human pilots** and actors within a DCS simulation. +-- Pilots can use a **menu system** to engage or abort a task, and provides means to +-- understand the **task briefing** and goals, and the relevant **task locations** on the map and +-- obtain **various reports** related to the task. +-- +-- ### 1.6.2) Task completion. +-- +-- As the task progresses, the **task status** will change over time, from Planned state to Completed state. +-- **Multiple pilots** can execute the same task, as such, the tasking system provides a **co-operative model** for joint task execution. +-- Depending on the task progress, a **scoring** can be allocated to award pilots of the achievements made. +-- The scoring is fully flexible, and different levels of awarding can be provided depending on the task type and complexity. +-- +-- A normal flow of task status would evolve from the **Planned** state, to the **Assigned** state ending either in a **Success** or a **Failed** state. +-- +-- Planned -> Assigned -> Success +-- -> Failed +-- -> Cancelled +-- +-- The state completion is by default set to **Success**, if the goals of the task have been reached, but can be overruled by a goal method. +-- +-- Depending on the tactical situation, a task can be **Cancelled** by the mission governer. +-- It is actually the mission designer who has the flexibility to decide at which conditions a task would be set to **Success**, **Failed** or **Cancelled**. +-- This decision all depends on the task goals, and the phase/evolution of the task conditions that would accomplish the goals. +-- +-- For example, if the task goal is to merely destroy a target, and the target is mid-mission destroyed by another event than the pilot destroying the target, +-- the task goal could be set to **Failed**, or .. **Cancelled** ... +-- However, it could very well be also acceptable that the task would be flagged as **Success**. +-- +-- The tasking mechanism governs beside the progress also a scoring mechanism, and in case of goal completion without any active pilot involved +-- in the execution of the task, could result in a **Success** task completion status, but no score would be awared, as there were no players involved. +-- +-- These different completion states are important for the mission designer to reflect scoring to a player. +-- A success could mean a positive score to be given, while a failure could mean a negative score or penalties to be awarded. -- -- === -- @@ -8,7 +212,8 @@ -- -- === -- --- @module Task +-- @module Tasking.Task +-- @image MOOSE.JPG --- @type TASK -- @field Core.Scheduler#SCHEDULER TaskScheduler @@ -20,50 +225,106 @@ -- @field Tasking.TaskInfo#TASKINFO TaskInfo -- @extends Core.Fsm#FSM_TASK ---- --- # TASK class, extends @{Base#BASE} +--- Governs the main engine to administer human taskings. -- --- ## The TASK class implements the methods for task orchestration within MOOSE. +-- A task is governed by a @{Tasking.Mission} object. Tasks are of different types. +-- The @{#TASK} object is used or derived by more detailed tasking classes that will implement the task execution mechanisms +-- and goals. -- --- The class provides a couple of methods to: +-- # 1) Derived task classes. -- --- * @{#TASK.AssignToGroup}():Assign a task to a group (of players). --- * @{#TASK.AddProcess}():Add a @{Process} to a task. --- * @{#TASK.RemoveProcesses}():Remove a running @{Process} from a running task. --- * @{#TASK.SetStateMachine}():Set a @{Fsm} to a task. --- * @{#TASK.RemoveStateMachine}():Remove @{Fsm} from a task. --- * @{#TASK.HasStateMachine}():Enquire if the task has a @{Fsm} --- * @{#TASK.AssignToUnit}(): Assign a task to a unit. (Needs to be implemented in the derived classes from @{#TASK}. --- * @{#TASK.UnAssignFromUnit}(): Unassign the task from a unit. --- * @{#TASK.SetTimeOut}(): Set timer in seconds before task gets cancelled if not assigned. +-- The following TASK_ classes are derived from @{#TASK}. +-- +-- TASK +-- TASK_A2A +-- TASK_A2A_ENGAGE +-- TASK_A2A_INTERCEPT +-- TASK_A2A_SWEEP +-- TASK_A2G +-- TASK_A2G_SEAD +-- TASK_A2G_CAS +-- TASK_A2G_BAI +-- TASK_CARGO +-- TASK_CARGO_TRANSPORT +-- TASK_CARGO_CSAR +-- +-- ## 1.1) A2A Tasks +-- +-- - @{Tasking.Task_A2A#TASK_A2A_ENGAGE} - Models an A2A engage task of a target group of airborne intruders mid-air. +-- - @{Tasking.Task_A2A#TASK_A2A_INTERCEPT} - Models an A2A ground intercept task of a target group of airborne intruders mid-air. +-- - @{Tasking.Task_A2A#TASK_A2A_SWEEP} - Models an A2A sweep task to clean an area of previously detected intruders mid-air. +-- +-- ## 1.2) A2G Tasks +-- +-- - @{Tasking.Task_A2G#TASK_A2G_SEAD} - Models an A2G Suppression or Extermination of Air Defenses task to clean an area of air to ground defense threats. +-- - @{Tasking.Task_A2G#TASK_A2G_CAS} - Models an A2G Close Air Support task to provide air support to nearby friendlies near the front-line. +-- - @{Tasking.Task_A2G#TASK_A2G_BAI} - Models an A2G Battlefield Air Interdiction task to provide air support to nearby friendlies near the front-line. +-- +-- ## 1.3) Cargo Tasks +-- +-- - @{Tasking.Task_Cargo#TASK_CARGO_TRANSPORT} - Models the transportation of cargo to deployment zones. +-- - @{Tasking.Task_Cargo#TASK_CARGO_CSAR} - Models the rescue of downed friendly pilots from behind enemy lines. +-- +-- +-- # 2) Task status events. +-- +-- The task statuses can be set by using the following methods: +-- +-- - @{#TASK.Success}() - Set the task to **Success** state. +-- - @{#TASK.Fail}() - Set the task to **Failed** state. +-- - @{#TASK.Hold}() - Set the task to **Hold** state. +-- - @{#TASK.Abort}() - Set the task to **Aborted** state, aborting the task. The task may be replanned. +-- - @{#TASK.Cancel}() - Set the task to **Cancelled** state, cancelling the task. +-- +-- The mentioned derived TASK_ classes are implementing the task status transitions out of the box. +-- So no extra logic needs to be written. -- --- ## 1.2) Set and enquire task status (beyond the task state machine processing). +-- # 3) Goal conditions for a task. -- --- A task needs to implement as a minimum the following task states: +-- Every 30 seconds, a @{#Task.Goal} trigger method is fired. +-- You as a mission designer, can capture the **Goal** event trigger to check your own task goal conditions and take action! -- --- * **Success**: Expresses the successful execution and finalization of the task. --- * **Failed**: Expresses the failure of a task. --- * **Planned**: Expresses that the task is created, but not yet in execution and is not assigned yet. --- * **Assigned**: Expresses that the task is assigned to a Group of players, and that the task is in execution mode. +-- ## 3.1) Goal event handler `OnAfterGoal()`. -- --- A task may also implement the following task states: --- --- * **Rejected**: Expresses that the task is rejected by a player, who was requested to accept the task. --- * **Cancelled**: Expresses that the task is cancelled by HQ or through a logical situation where a cancellation of the task is required. --- --- A task can implement more statusses than the ones outlined above. Please consult the documentation of the specific tasks to understand the different status modelled. --- --- The status of tasks can be set by the methods **State** followed by the task status. An example is `StateAssigned()`. --- The status of tasks can be enquired by the methods **IsState** followed by the task status name. An example is `if IsStateAssigned() then`. +-- And this is a really great feature! Imagine a task which has **several conditions to check** before the task can move into **Success** state. +-- You can do this with the OnAfterGoal method. -- --- ## 1.3) Add scoring when reaching a certain task status: +-- The following code provides an example of such a goal condition check implementation. +-- +-- function Task:OnAfterGoal() +-- if condition == true then +-- self:Success() -- This will flag the task to Succcess when the condition is true. +-- else +-- if condition2 == true and condition3 == true then +-- self:Fail() -- This will flag the task to Failed, when condition2 and condition3 would be true. +-- end +-- end +-- end +-- +-- So the @{#TASK.OnAfterGoal}() event handler would be called every 30 seconds automatically, +-- and within this method, you can now check the conditions and take respective action. +-- +-- ## 3.2) Goal event trigger `Goal()`. +-- +-- If you would need to check a goal at your own defined event timing, then just call the @{#TASK.Goal}() method within your logic. +-- The @{#TASK.OnAfterGoal}() event handler would then directly be called and would execute the logic. +-- Note that you can also delay the goal check by using the delayed event trigger syntax `:__Goal( Delay )`. +-- +-- +-- # 4) Score task completion. -- -- Upon reaching a certain task status in a task, additional scoring can be given. If the Mission has a scoring system attached, the scores will be added to the mission scoring. -- Use the method @{#TASK.AddScore}() to add scores when a status is reached. -- --- ## 1.4) Task briefing: +-- # 5) Task briefing. +-- +-- A task briefing is a text that is shown to the player when he is assigned to the task. +-- The briefing is broadcasted by the command center owning the mission. +-- +-- The briefing is part of the parameters in the @{#TASK.New}() constructor, +-- but can separately be modified later in your mission using the +-- @{#TASK.SetBriefing}() method. -- --- A task briefing can be given that is shown to the player when he is assigned to the task. -- -- @field #TASK TASK -- @@ -161,7 +422,7 @@ TASK = { -- @return #TASK self function TASK:New( Mission, SetGroupAssign, TaskName, TaskType, TaskBriefing ) - local self = BASE:Inherit( self, FSM_TASK:New() ) -- Tasking.Task#TASK + local self = BASE:Inherit( self, FSM_TASK:New( TaskName ) ) -- Tasking.Task#TASK self:SetStartState( "Planned" ) self:AddTransition( "Planned", "Assign", "Assigned" ) @@ -169,17 +430,24 @@ function TASK:New( Mission, SetGroupAssign, TaskName, TaskType, TaskBriefing ) self:AddTransition( "Assigned", "Success", "Success" ) self:AddTransition( "Assigned", "Hold", "Hold" ) self:AddTransition( "Assigned", "Fail", "Failed" ) - self:AddTransition( "Assigned", "Abort", "Aborted" ) + self:AddTransition( { "Planned", "Assigned" }, "Abort", "Aborted" ) self:AddTransition( "Assigned", "Cancel", "Cancelled" ) self:AddTransition( "Assigned", "Goal", "*" ) + self.Fsm = {} + + local Fsm = self:GetUnitProcess() + Fsm:SetStartState( "Planned" ) + Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "Assigned", Rejected = "Reject" } ) + Fsm:AddTransition( "Assigned", "Assigned", "*" ) + --- Goal Handler OnBefore for TASK -- @function [parent=#TASK] OnBeforeGoal -- @param #TASK self -- @param #string From -- @param #string Event -- @param #string To - -- @param Wrapper.Unit#UNIT PlayerUnit The @{Unit} of the player. + -- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the player. -- @param #string PlayerName The name of the player. -- @return #boolean @@ -189,26 +457,27 @@ function TASK:New( Mission, SetGroupAssign, TaskName, TaskType, TaskBriefing ) -- @param #string From -- @param #string Event -- @param #string To - -- @param Wrapper.Unit#UNIT PlayerUnit The @{Unit} of the player. + -- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the player. -- @param #string PlayerName The name of the player. --- Goal Trigger for TASK -- @function [parent=#TASK] Goal -- @param #TASK self - -- @param Wrapper.Unit#UNIT PlayerUnit The @{Unit} of the player. + -- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the player. -- @param #string PlayerName The name of the player. --- Goal Asynchronous Trigger for TASK -- @function [parent=#TASK] __Goal -- @param #TASK self -- @param #number Delay - -- @param Wrapper.Unit#UNIT PlayerUnit The @{Unit} of the player. + -- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the player. -- @param #string PlayerName The name of the player. self:AddTransition( "*", "PlayerCrashed", "*" ) self:AddTransition( "*", "PlayerAborted", "*" ) + self:AddTransition( "*", "PlayerRejected", "*" ) self:AddTransition( "*", "PlayerDead", "*" ) self:AddTransition( { "Failed", "Aborted", "Cancelled" }, "Replan", "Planned" ) self:AddTransition( "*", "TimeOut", "Cancelled" ) @@ -216,7 +485,6 @@ function TASK:New( Mission, SetGroupAssign, TaskName, TaskType, TaskBriefing ) self:F( "New TASK " .. TaskName ) self.Processes = {} - self.Fsm = {} self.Mission = Mission self.CommandCenter = Mission:GetCommandCenter() @@ -229,7 +497,6 @@ function TASK:New( Mission, SetGroupAssign, TaskName, TaskType, TaskBriefing ) self:SetBriefing( TaskBriefing ) - self.FsmTemplate = self.FsmTemplate or FSM_PROCESS:New() self.TaskInfo = TASKINFO:New( self ) @@ -246,7 +513,8 @@ function TASK:GetUnitProcess( TaskUnit ) if TaskUnit then return self:GetStateMachine( TaskUnit ) else - return self.FsmTemplate + self.FsmTemplate = self.FsmTemplate or FSM_PROCESS:New() + return self.FsmTemplate end end @@ -295,34 +563,61 @@ function TASK:JoinUnit( PlayerUnit, PlayerGroup ) return PlayerUnitAdded end ---- Abort a PlayerUnit from a Task. --- If the Unit was not part of the Task, false is returned. --- If the Unit is part of the Task, true is returned. +--- A group rejecting a planned task. -- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player aborting the Task. +-- @param Wrapper.Group#GROUP PlayerGroup The group rejecting the task. -- @return #TASK -function TASK:AbortGroup( PlayerGroup ) - self:F( { PlayerGroup = PlayerGroup } ) +function TASK:RejectGroup( PlayerGroup ) local PlayerGroups = self:GetGroups() -- Is the PlayerGroup part of the PlayerGroups? if PlayerGroups:IsIncludeObject( PlayerGroup ) then - -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is aborted from the Task. + -- Check if the PlayerGroup is already assigned or is planned to be assigned to the Task. + -- If yes, the PlayerGroup is aborted from the Task. -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group. - if self:IsStateAssigned() then + if self:IsStatePlanned() then + + local IsGroupAssigned = self:IsGroupAssigned( PlayerGroup ) + if IsGroupAssigned then + local PlayerName = PlayerGroup:GetUnit(1):GetPlayerName() + self:GetMission():GetCommandCenter():MessageToGroup( "Task " .. self:GetName() .. " has been rejected! We will select another task.", PlayerGroup ) + self:UnAssignFromGroup( PlayerGroup ) + + self:PlayerRejected( PlayerGroup:GetUnit(1) ) + end + + end + end + + return self +end + + +--- A group aborting the task. +-- @param #TASK self +-- @param Wrapper.Group#GROUP PlayerGroup The group aborting the task. +-- @return #TASK +function TASK:AbortGroup( PlayerGroup ) + + local PlayerGroups = self:GetGroups() + + -- Is the PlayerGroup part of the PlayerGroups? + if PlayerGroups:IsIncludeObject( PlayerGroup ) then + + -- Check if the PlayerGroup is already assigned or is planned to be assigned to the Task. + -- If yes, the PlayerGroup is aborted from the Task. + -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group. + if self:IsStateAssigned() then + local IsGroupAssigned = self:IsGroupAssigned( PlayerGroup ) - self:F( { IsGroupAssigned = IsGroupAssigned } ) if IsGroupAssigned then local PlayerName = PlayerGroup:GetUnit(1):GetPlayerName() - --self:MessageToGroups( PlayerName .. " aborted Task " .. self:GetName() ) self:UnAssignFromGroup( PlayerGroup ) - --self:Abort() -- Now check if the task needs to go to hold... -- It will go to hold, if there are no players in the mission... - PlayerGroups:Flush( self ) local IsRemaining = false for GroupName, AssignedGroup in pairs( PlayerGroups:GetSet() or {} ) do @@ -347,11 +642,10 @@ function TASK:AbortGroup( PlayerGroup ) return self end ---- A PlayerUnit crashed in a Task. Abort the Player. --- If the Unit was not part of the Task, false is returned. --- If the Unit is part of the Task, true is returned. + +--- A group crashing and thus aborting from the task. -- @param #TASK self --- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player aborting the Task. +-- @param Wrapper.Group#GROUP PlayerGroup The group aborting the task. -- @return #TASK function TASK:CrashGroup( PlayerGroup ) self:F( { PlayerGroup = PlayerGroup } ) @@ -413,9 +707,29 @@ end -- @param #TASK self -- @return Core.Set#SET_GROUP function TASK:GetGroups() + return self.SetGroup end + +--- Gets the SET_GROUP assigned to the TASK. +-- @param #TASK self +-- @param Core.Set#SET_GROUP GroupSet +-- @return Core.Set#SET_GROUP +function TASK:AddGroups( GroupSet ) + + GroupSet = GroupSet or SET_GROUP:New() + + self.SetGroup:ForEachGroup( + --- @param Wrapper.Group#GROUP GroupSet + function( GroupItem ) + GroupSet:Add( GroupItem:GetName(), GroupItem) + end + ) + + return GroupSet +end + do -- Group Assignment --- Returns if the @{Task} is assigned to the Group. @@ -436,7 +750,7 @@ do -- Group Assignment end - --- Set @{Group} assigned to the @{Task}. + --- Set @{Wrapper.Group} assigned to the @{Task}. -- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup -- @return #TASK @@ -466,7 +780,7 @@ do -- Group Assignment return self end - --- Clear the @{Group} assignment from the @{Task}. + --- Clear the @{Wrapper.Group} assignment from the @{Task}. -- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup -- @return #TASK @@ -500,7 +814,17 @@ end do -- Group Assignment - --- Assign the @{Task} to a @{Group}. + --- @param #TASK self + -- @param Actions.Act_Assign#ACT_ASSIGN AcceptClass + function TASK:SetAssignMethod( AcceptClass ) + + local ProcessTemplate = self:GetUnitProcess() + + ProcessTemplate:SetProcess( "Planned", "Accept", AcceptClass ) -- Actions.Act_Assign#ACT_ASSIGN + end + + + --- Assign the @{Task} to a @{Wrapper.Group}. -- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup -- @return #TASK @@ -535,7 +859,7 @@ do -- Group Assignment return self end - --- UnAssign the @{Task} from a @{Group}. + --- UnAssign the @{Task} from a @{Wrapper.Group}. -- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup function TASK:UnAssignFromGroup( TaskGroup ) @@ -570,7 +894,7 @@ function TASK:HasGroup( FindGroup ) end ---- Assign the @{Task} to an alive @{Unit}. +--- Assign the @{Task} to an alive @{Wrapper.Unit}. -- @param #TASK self -- @param Wrapper.Unit#UNIT TaskUnit -- @return #TASK self @@ -589,7 +913,7 @@ function TASK:AssignToUnit( TaskUnit ) return self end ---- UnAssign the @{Task} from an alive @{Unit}. +--- UnAssign the @{Task} from an alive @{Wrapper.Unit}. -- @param #TASK self -- @param Wrapper.Unit#UNIT TaskUnit -- @return #TASK self @@ -597,7 +921,9 @@ function TASK:UnAssignFromUnit( TaskUnit ) self:F( TaskUnit:GetName() ) self:RemoveStateMachine( TaskUnit ) - + + -- If a Task Control Menu had been set, then this will be removed. + self:RemoveTaskControlMenu( TaskUnit ) return self end @@ -612,7 +938,7 @@ function TASK:SetTimeOut ( Timer ) return self end ---- Send a message of the @{Task} to the assigned @{Group}s. +--- Send a message of the @{Task} to the assigned @{Wrapper.Group}s. -- @param #TASK self function TASK:MessageToGroups( Message ) self:F( { Message = Message } ) @@ -620,35 +946,40 @@ function TASK:MessageToGroups( Message ) local Mission = self:GetMission() local CC = Mission:GetCommandCenter() - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetAliveSet() ) do - local TaskGroup = TaskGroup -- Wrapper.Group#GROUP - CC:MessageToGroup( Message, TaskGroup, TaskGroup:GetName() ) - end -end - - ---- Send the briefng message of the @{Task} to the assigned @{Group}s. --- @param #TASK self -function TASK:SendBriefingToAssignedGroups() - self:F2() - - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetAliveSet() ) do - - if self:IsGroupAssigned( TaskGroup ) then - TaskGroup:Message( self.TaskBriefing, 60 ) + for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do + TaskGroup = TaskGroup -- Wrapper.Group#GROUP + if TaskGroup:IsAlive() == true then + CC:MessageToGroup( Message, TaskGroup, TaskGroup:GetName() ) end end end ---- UnAssign the @{Task} from the @{Group}s. +--- Send the briefng message of the @{Task} to the assigned @{Wrapper.Group}s. +-- @param #TASK self +function TASK:SendBriefingToAssignedGroups() + self:F2() + + for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if TaskGroup:IsAlive() then + if self:IsGroupAssigned( TaskGroup ) then + TaskGroup:Message( self.TaskBriefing, 60 ) + end + end + end +end + + +--- UnAssign the @{Task} from the @{Wrapper.Group}s. -- @param #TASK self function TASK:UnAssignFromGroups() self:F2() - for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetAliveSet() ) do - if self:IsGroupAssigned(TaskGroup) then - self:UnAssignFromGroup( TaskGroup ) + for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if TaskGroup:IsAlive() == true then + if self:IsGroupAssigned(TaskGroup) then + self:UnAssignFromGroup( TaskGroup ) + end end end end @@ -661,13 +992,15 @@ end function TASK:HasAliveUnits() self:F() - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetAliveSet() ) do - if self:IsStateAssigned() then - if self:IsGroupAssigned( TaskGroup ) then - for TaskUnitID, TaskUnit in pairs( TaskGroup:GetUnits() ) do - if TaskUnit:IsAlive() then - self:T( { HasAliveUnits = true } ) - return true + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if TaskGroup:IsAlive() == true then + if self:IsStateAssigned() then + if self:IsGroupAssigned( TaskGroup ) then + for TaskUnitID, TaskUnit in pairs( TaskGroup:GetUnits() ) do + if TaskUnit:IsAlive() then + self:T( { HasAliveUnits = true } ) + return true + end end end end @@ -686,7 +1019,8 @@ function TASK:SetMenu( MenuTime ) --R2.1 Mission Reports and Task Reports added. self:F( { self:GetName(), MenuTime } ) --self.SetGroup:Flush() - for TaskGroupID, TaskGroupData in pairs( self.SetGroup:GetAliveSet() ) do + --for TaskGroupID, TaskGroupData in pairs( self.SetGroup:GetAliveSet() ) do + for TaskGroupID, TaskGroupData in pairs( self.SetGroup:GetSet() ) do local TaskGroup = TaskGroupData -- Wrapper.Group#GROUP if TaskGroup:IsAlive() == true and TaskGroup:GetPlayerNames() then @@ -729,21 +1063,14 @@ function TASK:SetPlannedMenuForGroup( TaskGroup, MenuTime ) local Mission = self:GetMission() local MissionName = Mission:GetName() - local CommandCenter = Mission:GetCommandCenter() - local CommandCenterMenu = CommandCenter:GetMenu() + local MissionMenu = Mission:GetMenu( TaskGroup ) local TaskType = self:GetType() local TaskPlayerCount = self:GetPlayerCount() local TaskPlayerString = string.format( " (%dp)", TaskPlayerCount ) --- local TaskText = string.format( "%s%s", self:GetName(), TaskPlayerString ) --, TaskThreatLevelString ) local TaskText = string.format( "%s", self:GetName() ) local TaskName = string.format( "%s", self:GetName() ) - local MissionMenu = Mission:GetMenu( TaskGroup ) - --local MissionMenu = MENU_GROUP:New( TaskGroup, MissionName, CommandCenterMenu ):SetTime( MenuTime ) - - --local MissionMenu = Mission:GetMenu( TaskGroup ) - self.MenuPlanned = self.MenuPlanned or {} self.MenuPlanned[TaskGroup] = MENU_GROUP_DELAYED:New( TaskGroup, "Join Planned Task", MissionMenu, Mission.MenuReportTasksPerStatus, Mission, TaskGroup, "Planned" ):SetTime( MenuTime ):SetTag( "Tasking" ) local TaskTypeMenu = MENU_GROUP_DELAYED:New( TaskGroup, TaskType, self.MenuPlanned[TaskGroup] ):SetTime( MenuTime ):SetTag( "Tasking" ) @@ -768,26 +1095,24 @@ end function TASK:SetAssignedMenuForGroup( TaskGroup, MenuTime ) self:F( { TaskGroup:GetName(), MenuTime } ) - local Mission = self:GetMission() - local MissionName = Mission:GetName() - local CommandCenter = Mission:GetCommandCenter() - local CommandCenterMenu = CommandCenter:GetMenu() - local TaskType = self:GetType() local TaskPlayerCount = self:GetPlayerCount() local TaskPlayerString = string.format( " (%dp)", TaskPlayerCount ) local TaskText = string.format( "%s%s", self:GetName(), TaskPlayerString ) --, TaskThreatLevelString ) local TaskName = string.format( "%s", self:GetName() ) - local MissionMenu = Mission:GetMenu( TaskGroup ) --- local MissionMenu = MENU_GROUP:New( TaskGroup, MissionName, CommandCenterMenu ):SetTime( MenuTime ) --- local MissionMenu = Mission:GetMenu( TaskGroup ) - - self.MenuAssigned = self.MenuAssigned or {} - self.MenuAssigned[TaskGroup] = MENU_GROUP_DELAYED:New( TaskGroup, string.format( "Assigned Task %s", TaskName ), MissionMenu ):SetTime( MenuTime ):SetTag( "Tasking" ) - local TaskMenu = MENU_GROUP_COMMAND_DELAYED:New( TaskGroup, string.format( "Abort Task" ), self.MenuAssigned[TaskGroup], self.MenuTaskAbort, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ) - local MarkMenu = MENU_GROUP_COMMAND_DELAYED:New( TaskGroup, string.format( "Mark Task Location on Map" ), self.MenuAssigned[TaskGroup], self.MenuMarkToGroup, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ) - local TaskTypeMenu = MENU_GROUP_COMMAND_DELAYED:New( TaskGroup, string.format( "Report Task Details" ), self.MenuAssigned[TaskGroup], self.MenuTaskStatus, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ) + for UnitName, TaskUnit in pairs( TaskGroup:GetPlayerUnits() ) do + local TaskUnit = TaskUnit -- Wrapper.Unit#UNIT + if TaskUnit then + local MenuControl = self:GetTaskControlMenu( TaskUnit ) + local TaskControl = MENU_GROUP:New( TaskGroup, "Control Task", MenuControl ):SetTime( MenuTime ):SetTag( "Tasking" ) + if self:IsStateAssigned() then + local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Abort Task" ), TaskControl, self.MenuTaskAbort, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ) + end + local MarkMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Mark Task Location on Map" ), TaskControl, self.MenuMarkToGroup, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ) + local TaskTypeMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Report Task Details" ), TaskControl, self.MenuTaskStatus, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" ) + end + end return self end @@ -799,16 +1124,18 @@ end function TASK:RemoveMenu( MenuTime ) self:F( { self:GetName(), MenuTime } ) - for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetAliveSet() ) do - local TaskGroup = TaskGroup -- Wrapper.Group#GROUP - if TaskGroup:IsAlive() == true and TaskGroup:GetPlayerNames() then - self:RefreshMenus( TaskGroup, MenuTime ) + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if TaskGroup:IsAlive() == true then + local TaskGroup = TaskGroup -- Wrapper.Group#GROUP + if TaskGroup:IsAlive() == true and TaskGroup:GetPlayerNames() then + self:RefreshMenus( TaskGroup, MenuTime ) + end end end end ---- Remove the menu option of the @{Task} for a @{Group}. +--- Remove the menu option of the @{Task} for a @{Wrapper.Group}. -- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup -- @param #number MenuTime @@ -818,9 +1145,6 @@ function TASK:RefreshMenus( TaskGroup, MenuTime ) local Mission = self:GetMission() local MissionName = Mission:GetName() - local CommandCenter = Mission:GetCommandCenter() - local CommandCenterMenu = CommandCenter:GetMenu() - local MissionMenu = Mission:GetMenu( TaskGroup ) local TaskName = self:GetName() @@ -842,7 +1166,7 @@ function TASK:RefreshMenus( TaskGroup, MenuTime ) end ---- Remove the assigned menu option of the @{Task} for a @{Group}. +--- Remove the assigned menu option of the @{Task} for a @{Wrapper.Group}. -- @param #TASK self -- @param Wrapper.Group#GROUP TaskGroup -- @param #number MenuTime @@ -852,7 +1176,6 @@ function TASK:RemoveAssignedMenuForGroup( TaskGroup ) local Mission = self:GetMission() local MissionName = Mission:GetName() - local MissionMenu = Mission:GetMenu( TaskGroup ) if MissionMenu then @@ -877,15 +1200,27 @@ function TASK:MenuMarkToGroup( TaskGroup ) self:UpdateTaskInfo( self.DetectedItem ) - local Report = REPORT:New():SetIndent( 0 ) - - self.TaskInfo:Report( Report, "M", TaskGroup ) - - local TargetCoordinate = self.TaskInfo:GetData( "Coordinate" ) -- Core.Point#COORDINATE - local MarkText = Report:Text( ", " ) - self:F( { Coordinate = TargetCoordinate, MarkText = MarkText } ) - TargetCoordinate:MarkToGroup( MarkText, TaskGroup ) - --Coordinate:MarkToAll( Briefing ) + local TargetCoordinates = self.TaskInfo:GetData( "Coordinates" ) -- Core.Point#COORDINATE + if TargetCoordinates then + for TargetCoordinateID, TargetCoordinate in pairs( TargetCoordinates ) do + local Report = REPORT:New():SetIndent( 0 ) + self.TaskInfo:Report( Report, "M", TaskGroup, self ) + local MarkText = Report:Text( ", " ) + self:F( { Coordinate = TargetCoordinate, MarkText = MarkText } ) + TargetCoordinate:MarkToGroup( MarkText, TaskGroup ) + --Coordinate:MarkToAll( Briefing ) + end + else + local TargetCoordinate = self.TaskInfo:GetData( "Coordinate" ) -- Core.Point#COORDINATE + if TargetCoordinate then + local Report = REPORT:New():SetIndent( 0 ) + self.TaskInfo:Report( Report, "M", TaskGroup, self ) + local MarkText = Report:Text( ", " ) + self:F( { Coordinate = TargetCoordinate, MarkText = MarkText } ) + TargetCoordinate:MarkToGroup( MarkText, TaskGroup ) + end + end + end --- Report the task status. @@ -939,7 +1274,7 @@ end -- TODO: Obscolete? ---- Fail processes from @{Task} with key @{Unit} +--- Fail processes from @{Task} with key @{Wrapper.Unit} -- @param #TASK self -- @param #string TaskUnitName -- @return #TASK self @@ -951,7 +1286,7 @@ function TASK:FailProcesses( TaskUnitName ) end end ---- Add a FiniteStateMachine to @{Task} with key Task@{Unit} +--- Add a FiniteStateMachine to @{Task} with key Task@{Wrapper.Unit} -- @param #TASK self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Core.Fsm#FSM_PROCESS Fsm @@ -964,7 +1299,7 @@ function TASK:SetStateMachine( TaskUnit, Fsm ) return Fsm end ---- Gets the FiniteStateMachine of @{Task} with key Task@{Unit} +--- Gets the FiniteStateMachine of @{Task} with key Task@{Wrapper.Unit} -- @param #TASK self -- @param Wrapper.Unit#UNIT TaskUnit -- @return Core.Fsm#FSM_PROCESS @@ -974,7 +1309,7 @@ function TASK:GetStateMachine( TaskUnit ) return self.Fsm[TaskUnit] end ---- Remove FiniteStateMachines from @{Task} with key Task@{Unit} +--- Remove FiniteStateMachines from @{Task} with key Task@{Wrapper.Unit} -- @param #TASK self -- @param Wrapper.Unit#UNIT TaskUnit -- @return #TASK self @@ -998,7 +1333,7 @@ function TASK:RemoveStateMachine( TaskUnit ) end ---- Checks if there is a FiniteStateMachine assigned to Task@{Unit} for @{Task} +--- Checks if there is a FiniteStateMachine assigned to Task@{Wrapper.Unit} for @{Task} -- @param #TASK self -- @param Wrapper.Unit#UNIT TaskUnit -- @return #TASK self @@ -1210,12 +1545,16 @@ function TASK:onenterAssigned( From, Event, To, PlayerUnit, PlayerName ) --- This test is required, because the state transition will be fired also when the state does not change in case of an event. if From ~= "Assigned" then - self:F( { From, Event, To, PlayerUnit:GetName(), PlayerName } ) - self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " is assigned." ) - + local PlayerNames = self:GetPlayerNames() + local PlayerText = REPORT:New() + for PlayerName, TaskName in pairs( PlayerNames ) do + PlayerText:Add( PlayerName ) + end + + self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " is assigned to players " .. PlayerText:Text(",") .. ". Good Luck!" ) + -- Set the total Progress to be achieved. - self:SetGoalTotal() -- Polymorphic to set the initial goal total! if self.Dispatcher then @@ -1231,7 +1570,7 @@ function TASK:onenterAssigned( From, Event, To, PlayerUnit, PlayerName ) self:SetMenu() self:F( { "--> Task Assigned", TaskName = self:GetName(), Mission = self:GetMission():GetName() } ) - self:F( { "--> Task Player Names", PlayerNames = self:GetPlayerNames() } ) + self:F( { "--> Task Player Names", PlayerNames = PlayerNames } ) end end @@ -1273,6 +1612,7 @@ function TASK:onenterAborted( From, Event, To ) end + --- FSM function for a TASK -- @param #TASK self -- @param #string From @@ -1411,7 +1751,7 @@ function TASK:ReportSummary( ReportGroup ) -- Determine the status of the Task. Report:Add( "State: <" .. self:GetState() .. ">" ) - self.TaskInfo:Report( Report, "S", ReportGroup ) + self.TaskInfo:Report( Report, "S", ReportGroup, self ) return Report:Text( ', ' ) end @@ -1428,7 +1768,7 @@ function TASK:ReportOverview( ReportGroup ) local TaskName = self:GetName() local Report = REPORT:New() - self.TaskInfo:Report( Report, "O", ReportGroup ) + self.TaskInfo:Report( Report, "O", ReportGroup, self ) return Report:Text() end @@ -1441,11 +1781,13 @@ function TASK:GetPlayerCount() --R2.1 Get a count of the players. local PlayerCount = 0 -- Loop each Unit active in the Task, and find Player Names. - for TaskGroupID, PlayerGroup in pairs( self:GetGroups():GetAliveSet() ) do + for TaskGroupID, PlayerGroup in pairs( self:GetGroups():GetSet() ) do local PlayerGroup = PlayerGroup -- Wrapper.Group#GROUP - if self:IsGroupAssigned( PlayerGroup ) then - local PlayerNames = PlayerGroup:GetPlayerNames() - PlayerCount = PlayerCount + #PlayerNames + if PlayerGroup:IsAlive() == true then + if self:IsGroupAssigned( PlayerGroup ) then + local PlayerNames = PlayerGroup:GetPlayerNames() + PlayerCount = PlayerCount + #PlayerNames + end end end @@ -1461,12 +1803,14 @@ function TASK:GetPlayerNames() --R2.1 Get a map of the players. local PlayerNameMap = {} -- Loop each Unit active in the Task, and find Player Names. - for TaskGroupID, PlayerGroup in pairs( self:GetGroups():GetAliveSet() ) do + for TaskGroupID, PlayerGroup in pairs( self:GetGroups():GetSet() ) do local PlayerGroup = PlayerGroup -- Wrapper.Group#GROUP - if self:IsGroupAssigned( PlayerGroup ) then - local PlayerNames = PlayerGroup:GetPlayerNames() - for PlayerNameID, PlayerName in pairs( PlayerNames ) do - PlayerNameMap[PlayerName] = PlayerGroup + if PlayerGroup:IsAlive() == true then + if self:IsGroupAssigned( PlayerGroup ) then + local PlayerNames = PlayerGroup:GetPlayerNames() + for PlayerNameID, PlayerName in pairs( PlayerNames ) do + PlayerNameMap[PlayerName] = PlayerGroup + end end end end @@ -1499,7 +1843,7 @@ function TASK:ReportDetails( ReportGroup ) local PlayerReport = REPORT:New() for PlayerName, PlayerGroup in pairs( PlayerNames ) do - PlayerReport:Add( "Group " .. PlayerGroup:GetCallsign() .. ": " .. PlayerName ) + PlayerReport:Add( "Players group " .. PlayerGroup:GetCallsign() .. ": " .. PlayerName ) end local Players = PlayerReport:Text() @@ -1508,7 +1852,7 @@ function TASK:ReportDetails( ReportGroup ) Report:AddIndent( Players ) end - self.TaskInfo:Report( Report, "D", ReportGroup ) + self.TaskInfo:Report( Report, "D", ReportGroup, self ) return Report:Text() end @@ -1595,3 +1939,65 @@ do -- Additional Task Scoring and Task Progress end end + +do -- Task Control Menu + + -- The Task Control Menu is a menu attached to the task at the main menu to quickly be able to do actions in the task. + -- The Task Control Menu can only be shown when the task is assigned to the player. + -- The Task Control Menu is linked to the process executing the task, so no task menu can be set to the main static task definition. + + --- Init Task Control Menu + -- @param #TASK self + -- @param Wrapper.Unit#UNIT TaskUnit The @{Wrapper.Unit} that contains a player. + -- @return Task Control Menu Refresh ID + function TASK:InitTaskControlMenu( TaskUnit ) + + self.TaskControlMenuTime = timer.getTime() + + return self.TaskControlMenuTime + end + + --- Get Task Control Menu + -- @param #TASK self + -- @param Wrapper.Unit#UNIT TaskUnit The @{Wrapper.Unit} that contains a player. + -- @return Core.Menu#MENU_GROUP TaskControlMenu The Task Control Menu + function TASK:GetTaskControlMenu( TaskUnit, TaskName ) + + TaskName = TaskName or "" + + local TaskGroup = TaskUnit:GetGroup() + local TaskPlayerCount = TaskGroup:GetPlayerCount() + + if TaskPlayerCount <= 1 then + self.TaskControlMenu = MENU_GROUP:New( TaskUnit:GetGroup(), "Task " .. self:GetName() .. " control" ):SetTime( self.TaskControlMenuTime ) + else + self.TaskControlMenu = MENU_GROUP:New( TaskUnit:GetGroup(), "Task " .. self:GetName() .. " control for " .. TaskUnit:GetPlayerName() ):SetTime( self.TaskControlMenuTime ) + end + + return self.TaskControlMenu + end + + --- Remove Task Control Menu + -- @param #TASK self + -- @param Wrapper.Unit#UNIT TaskUnit The @{Wrapper.Unit} that contains a player. + function TASK:RemoveTaskControlMenu( TaskUnit ) + + if self.TaskControlMenu then + self.TaskControlMenu:Remove() + self.TaskControlMenu = nil + end + end + + --- Refresh Task Control Menu + -- @param #TASK self + -- @param Wrapper.Unit#UNIT TaskUnit The @{Wrapper.Unit} that contains a player. + -- @param MenuTime The refresh time that was used to refresh the Task Control Menu items. + -- @param MenuTag The tag. + function TASK:RefreshTaskControlMenu( TaskUnit, MenuTime, MenuTag ) + + if self.TaskControlMenu then + self.TaskControlMenu:Remove( MenuTime, MenuTag ) + end + end + +end diff --git a/Moose Development/Moose/Tasking/TaskInfo.lua b/Moose Development/Moose/Tasking/TaskInfo.lua index bf75bcf5a..d3fc88350 100644 --- a/Moose Development/Moose/Tasking/TaskInfo.lua +++ b/Moose Development/Moose/Tasking/TaskInfo.lua @@ -8,13 +8,14 @@ -- -- === -- --- @module TaskInfo +-- @module Tasking.TaskInfo +-- @image MOOSE.JPG --- @type TASKINFO -- @extends Core.Base#BASE --- --- # TASKINFO class, extends @{Base#BASE} +-- # TASKINFO class, extends @{Core.Base#BASE} -- -- ## The TASKINFO class implements the methods to contain information and display information of a task. -- @@ -84,7 +85,7 @@ end -- @return Data The data of the info. function TASKINFO:GetData( Key ) local Object = self.Info:Get( Key ) - return Object.Data + return Object and Object.Data end @@ -129,6 +130,19 @@ function TASKINFO:AddCoordinate( Coordinate, Order, Detail, Keep ) end +--- Add Coordinates. +-- @param #TASKINFO self +-- @param #list Coordinates +-- @param #number Order The display order, which is a number from 0 to 100. +-- @param #TASKINFO.Detail Detail The detail Level. +-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports. +-- @return #TASKINFO self +function TASKINFO:AddCoordinates( Coordinates, Order, Detail, Keep ) + self:AddInfo( "Coordinates", Coordinates, Order, Detail, Keep ) + return self +end + + --- Add Threat. -- @param #TASKINFO self @@ -249,17 +263,16 @@ end function TASKINFO:AddCargoSet( SetCargo, Order, Detail, Keep ) local CargoReport = REPORT:New() + CargoReport:Add( "" ) SetCargo:ForEachCargo( - --- @param Core.Cargo#CARGO Cargo + --- @param Cargo.Cargo#CARGO Cargo function( Cargo ) - local CargoType = Cargo:GetType() - local CargoName = Cargo:GetName() - local CargoCoordinate = Cargo:GetCoordinate() - CargoReport:Add( string.format( '"%s" (%s) at %s', CargoName, CargoType, CargoCoordinate:ToStringMGRS() ) ) + CargoReport:Add( string.format( ' - %s (%s) %s - status %s ', Cargo:GetName(), Cargo:GetType(), Cargo:GetTransportationMethod(), Cargo:GetCurrentState() ) ) end ) - self:AddInfo( "CargoSet", CargoReport:Text(), Order, Detail, Keep ) + self:AddInfo( "Cargo", CargoReport:Text(), Order, Detail, Keep ) + return self end @@ -271,8 +284,9 @@ end -- @param Core.Report#REPORT Report -- @param #TASKINFO.Detail Detail The detail Level. -- @param Wrapper.Group#GROUP ReportGroup +-- @param Tasking.Task#TASK Task -- @return #TASKINFO self -function TASKINFO:Report( Report, Detail, ReportGroup ) +function TASKINFO:Report( Report, Detail, ReportGroup, Task ) local Line = 0 local LineReport = REPORT:New() @@ -293,7 +307,7 @@ function TASKINFO:Report( Report, Detail, ReportGroup ) end if Key == "Coordinate" then local Coordinate = Data.Data -- Core.Point#COORDINATE - Text = Coordinate:ToString( ReportGroup:GetUnit(1), nil, self ) + Text = Coordinate:ToString( ReportGroup:GetUnit(1), nil, Task ) end if Key == "Threat" then local DataText = Data.Data -- #string @@ -309,17 +323,17 @@ function TASKINFO:Report( Report, Detail, ReportGroup ) end if Key == "QFE" then local Coordinate = Data.Data -- Core.Point#COORDINATE - Text = Coordinate:ToStringPressure( ReportGroup:GetUnit(1), nil, self ) + Text = Coordinate:ToStringPressure( ReportGroup:GetUnit(1), nil, Task ) end if Key == "Temperature" then local Coordinate = Data.Data -- Core.Point#COORDINATE - Text = Coordinate:ToStringTemperature( ReportGroup:GetUnit(1), nil, self ) + Text = Coordinate:ToStringTemperature( ReportGroup:GetUnit(1), nil, Task ) end if Key == "Wind" then local Coordinate = Data.Data -- Core.Point#COORDINATE - Text = Coordinate:ToStringWind( ReportGroup:GetUnit(1), nil, self ) + Text = Coordinate:ToStringWind( ReportGroup:GetUnit(1), nil, Task ) end - if Key == "CargoSet" then + if Key == "Cargo" then local DataText = Data.Data -- #string Text = DataText end diff --git a/Moose Development/Moose/Tasking/TaskZoneCapture.lua b/Moose Development/Moose/Tasking/TaskZoneCapture.lua index fd5934dfd..fb1ab81c5 100644 --- a/Moose Development/Moose/Tasking/TaskZoneCapture.lua +++ b/Moose Development/Moose/Tasking/TaskZoneCapture.lua @@ -8,7 +8,8 @@ -- -- === -- --- @module TaskZoneCapture +-- @module Tasking.TaskZoneCapture +-- @image MOOSE.JPG do -- TASK_ZONE_GOAL @@ -17,14 +18,14 @@ do -- TASK_ZONE_GOAL -- @field Core.ZoneGoal#ZONE_GOAL ZoneGoal -- @extends Tasking.Task#TASK - --- # TASK_ZONE_GOAL class, extends @{Task#TASK} + --- # TASK_ZONE_GOAL class, extends @{Tasking.Task#TASK} -- -- The TASK_ZONE_GOAL class defines the task to protect or capture a protection zone. - -- The TASK_ZONE_GOAL is implemented using a @{Fsm#FSM_TASK}, and has the following statuses: + -- The TASK_ZONE_GOAL is implemented using a @{Core.Fsm#FSM_TASK}, and has the following statuses: -- -- * **None**: Start of the process -- * **Planned**: The A2G task is planned. - -- * **Assigned**: The A2G task is assigned to a @{Group#GROUP}. + -- * **Assigned**: The A2G task is assigned to a @{Wrapper.Group#GROUP}. -- * **Success**: The A2G task is successfully completed. -- * **Failed**: The A2G task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. -- @@ -44,7 +45,7 @@ do -- TASK_ZONE_GOAL --- Instantiates a new TASK_ZONE_GOAL. -- @param #TASK_ZONE_GOAL self -- @param Tasking.Mission#MISSION Mission - -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @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 -- @return #TASK_ZONE_GOAL self @@ -166,12 +167,12 @@ do -- TASK_ZONE_CAPTURE -- @field Core.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoal -- @extends #TASK_ZONE_GOAL - --- # TASK_ZONE_CAPTURE class, extends @{TaskZoneGoal#TASK_ZONE_GOAL} + --- # TASK_ZONE_CAPTURE 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. -- 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 @{Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create SEAD tasks + -- The TASK_ZONE_CAPTURE 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 diff --git a/Moose Development/Moose/Tasking/Task_A2A.lua b/Moose Development/Moose/Tasking/Task_A2A.lua index 4db992c40..bd12b7e43 100644 --- a/Moose Development/Moose/Tasking/Task_A2A.lua +++ b/Moose Development/Moose/Tasking/Task_A2A.lua @@ -8,24 +8,23 @@ -- -- === -- --- @module Task_A2A +-- @module Tasking.Task_A2A +-- @image MOOSE.JPG do -- TASK_A2A --- The TASK_A2A class -- @type TASK_A2A - -- @field Set#SET_UNIT TargetSetUnit + -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends Tasking.Task#TASK - --- # TASK_A2A class, extends @{Task#TASK} - -- - -- The TASK_A2A class defines Air To Air tasks for a @{Set} of Target Units, - -- based on the tasking capabilities defined in @{Task#TASK}. - -- The TASK_A2A is implemented using a @{Fsm#FSM_TASK}, and has the following statuses: + --- Defines Air To Air tasks for a @{Set} of Target Units, + -- based on the tasking capabilities defined in @{Tasking.Task#TASK}. + -- The TASK_A2A is implemented using a @{Core.Fsm#FSM_TASK}, and has the following statuses: -- -- * **None**: Start of the process -- * **Planned**: The A2A task is planned. - -- * **Assigned**: The A2A task is assigned to a @{Group#GROUP}. + -- * **Assigned**: The A2A task is assigned to a @{Wrapper.Group#GROUP}. -- * **Success**: The A2A task is successfully completed. -- * **Failed**: The A2A task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. -- @@ -45,9 +44,9 @@ do -- TASK_A2A --- Instantiates a new TASK_A2A. -- @param #TASK_A2A self -- @param Tasking.Mission#MISSION Mission - -- @param Set#SET_GROUP SetAttack The set of groups for which the Task can be assigned. + -- @param Core.Set#SET_GROUP SetAttack The set of groups for which the Task can be assigned. -- @param #string TaskName The name of the Task. - -- @param Set#SET_UNIT UnitSetTargets + -- @param Core.Set#SET_UNIT UnitSetTargets -- @param #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range. -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known. -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be. @@ -62,8 +61,6 @@ do -- TASK_A2A local Fsm = self:GetUnitProcess() - Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "RouteToRendezVous", Rejected = "Reject" } ) - Fsm:AddTransition( "Assigned", "RouteToRendezVous", "RoutingToRendezVous" ) Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtRendezVous" } ) Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtRendezVous" } ) @@ -84,6 +81,15 @@ do -- TASK_A2A Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) Fsm:AddTransition( "Failed", "Fail", "Failed" ) + + ---- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #TASK_CARGO Task + function Fsm:OnLeaveAssigned( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + self:SelectAction() + end --- Test -- @param #FSM_PROCESS self @@ -352,18 +358,16 @@ do -- TASK_A2A_INTERCEPT --- The TASK_A2A_INTERCEPT class -- @type TASK_A2A_INTERCEPT - -- @field Set#SET_UNIT TargetSetUnit + -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends Tasking.Task#TASK - --- # TASK_A2A_INTERCEPT class, extends @{Task_A2A#TASK_A2A} - -- - -- The TASK_A2A_INTERCEPT class defines an intercept task for a human player to be executed. + --- Defines an intercept task for a human player to be executed. -- When enemy planes need to be intercepted by human players, use this task type to urgen the players to get out there! -- - -- The TASK_A2A_INTERCEPT is used by the @{Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create intercept tasks + -- The TASK_A2A_INTERCEPT is used by the @{Tasking.Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create intercept tasks -- based on detected airborne enemy targets intruding friendly airspace. -- - -- The task is defined for a @{Mission#MISSION}, where a friendly @{Set#SET_GROUP} consisting of GROUPs with one human players each, is intercepting the targets. + -- The task is defined for a @{Tasking.Mission#MISSION}, where a friendly @{Core.Set#SET_GROUP} consisting of GROUPs with one human players each, is intercepting the targets. -- The task is given a name and a briefing, that is used in the menu structure and in the reporting. -- -- @field #TASK_A2A_INTERCEPT @@ -451,20 +455,18 @@ do -- TASK_A2A_SWEEP --- The TASK_A2A_SWEEP class -- @type TASK_A2A_SWEEP - -- @field Set#SET_UNIT TargetSetUnit + -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends Tasking.Task#TASK - --- # TASK_A2A_SWEEP class, extends @{Task_A2A#TASK_A2A} - -- - -- The TASK_A2A_SWEEP class defines a sweep task for a human player to be executed. + --- Defines a sweep task for a human player to be executed. -- A sweep task needs to be given when targets were detected but somehow the detection was lost. -- Most likely, these enemy planes are hidden in the mountains or are flying under radar. -- These enemy planes need to be sweeped by human players, and use this task type to urge the players to get out there and find those enemy fighters. -- - -- The TASK_A2A_SWEEP is used by the @{Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create sweep tasks + -- The TASK_A2A_SWEEP is used by the @{Tasking.Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create sweep tasks -- based on detected airborne enemy targets intruding friendly airspace, for which the detection has been lost for more than 60 seconds. -- - -- The task is defined for a @{Mission#MISSION}, where a friendly @{Set#SET_GROUP} consisting of GROUPs with one human players each, is sweeping the targets. + -- The task is defined for a @{Tasking.Mission#MISSION}, where a friendly @{Core.Set#SET_GROUP} consisting of GROUPs with one human players each, is sweeping the targets. -- The task is given a name and a briefing, that is used in the menu structure and in the reporting. -- -- @field #TASK_A2A_SWEEP @@ -562,18 +564,16 @@ do -- TASK_A2A_ENGAGE --- The TASK_A2A_ENGAGE class -- @type TASK_A2A_ENGAGE - -- @field Set#SET_UNIT TargetSetUnit + -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends Tasking.Task#TASK - --- # TASK_A2A_ENGAGE class, extends @{Task_A2A#TASK_A2A} - -- - -- The TASK_A2A_ENGAGE class defines an engage task for a human player to be executed. + --- Defines an engage task for a human player to be executed. -- When enemy planes are close to human players, use this task type is used urge the players to get out there! -- - -- The TASK_A2A_ENGAGE is used by the @{Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create engage tasks + -- The TASK_A2A_ENGAGE is used by the @{Tasking.Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create engage tasks -- based on detected airborne enemy targets intruding friendly airspace. -- - -- The task is defined for a @{Mission#MISSION}, where a friendly @{Set#SET_GROUP} consisting of GROUPs with one human players each, is engaging the targets. + -- The task is defined for a @{Tasking.Mission#MISSION}, where a friendly @{Core.Set#SET_GROUP} consisting of GROUPs with one human players each, is engaging the targets. -- The task is given a name and a briefing, that is used in the menu structure and in the reporting. -- -- @field #TASK_A2A_ENGAGE diff --git a/Moose Development/Moose/Tasking/Task_A2A_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_A2A_Dispatcher.lua index a3fc20c88..44db945c7 100644 --- a/Moose Development/Moose/Tasking/Task_A2A_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_A2A_Dispatcher.lua @@ -1,7 +1,17 @@ ---- **Tasking** - The TASK_A2A_DISPATCHER creates and manages player TASK_A2A tasks based on detected targets. +--- **Tasking** - Dynamically allocates A2A tasks to human players, based on detected airborne targets through an EWR network. -- --- The @{#TASK_A2A_DISPATCHER} classes implement the dynamic dispatching of tasks upon groups of detected units determined a @{Set} of EWR installation groups. +-- **Features:** -- +-- * Dynamically assign tasks to human players based on detected targets. +-- * Dynamically change the tasks as the tactical situation evolves during the mission. +-- * Dynamically assign (CAP) Control Air Patrols tasks for human players to perform CAP. +-- * Dynamically assign (GCI) Ground Control Intercept tasks for human players to perform GCI. +-- * Dynamically assign Engage tasks for human players to engage on close-by airborne bogeys. +-- * Define and use an EWR (Early Warning Radar) network. +-- * Define different ranges to engage upon intruders. +-- * Keep task achievements. +-- * Score task achievements. +-- -- === -- -- ### Author: **FlightControl** @@ -10,7 +20,8 @@ -- -- === -- --- @module Task_A2A_Dispatcher +-- @module Tasking.Task_A2A_Dispatcher +-- @image Task_A2A_Dispatcher.JPG do -- TASK_A2A_DISPATCHER @@ -18,11 +29,7 @@ do -- TASK_A2A_DISPATCHER -- @type TASK_A2A_DISPATCHER -- @extends Tasking.DetectionManager#DETECTION_MANAGER - --- # TASK_A2A_DISPATCHER class, extends @{Tasking#DETECTION_MANAGER} - -- - -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia1.JPG) - -- - -- The @{#TASK_A2A_DISPATCHER} class implements the dynamic dispatching of tasks upon groups of detected units determined a @{Set} of EWR installation groups. + --- Orchestrates the dynamic dispatching of tasks upon groups of detected units determined a @{Set} of EWR installation groups. -- -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia3.JPG) -- @@ -83,7 +90,7 @@ do -- TASK_A2A_DISPATCHER -- 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_BASE} object that is given as the input parameter of the TASK\_A2A\_DISPATCHER class. + -- EWR networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection#DETECTION_BASE} object that is given as the input parameter of the TASK\_A2A\_DISPATCHER class. -- By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network, -- increasing or decreasing the radar coverage of the Early Warning System. -- @@ -182,7 +189,7 @@ do -- TASK_A2A_DISPATCHER --- TASK_A2A_DISPATCHER constructor. -- @param #TASK_A2A_DISPATCHER self -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done. - -- @param Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission. + -- @param Core.Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission. -- @param Functional.Detection#DETECTION_BASE Detection The detection results that are used to dynamically assign new tasks to human players. -- @return #TASK_A2A_DISPATCHER self function TASK_A2A_DISPATCHER:New( Mission, SetGroup, Detection ) @@ -200,6 +207,7 @@ do -- TASK_A2A_DISPATCHER self.Detection:SetRefreshTimeInterval( 30 ) self:AddTransition( "Started", "Assign", "Started" ) + --- OnAfter Transition Handler for Event Assign. -- @function [parent=#TASK_A2A_DISPATCHER] OnAfterAssign @@ -210,7 +218,7 @@ do -- TASK_A2A_DISPATCHER -- @param Tasking.Task_A2A#TASK_A2A Task -- @param Wrapper.Unit#UNIT TaskUnit -- @param #string PlayerName - + self:__Start( 5 ) return self @@ -250,7 +258,7 @@ do -- TASK_A2A_DISPATCHER --- Creates an INTERCEPT task when there are targets for it. -- @param #TASK_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. -- @return #nil If there are no targets to be set. function TASK_A2A_DISPATCHER:EvaluateINTERCEPT( DetectedItem ) self:F( { DetectedItem.ItemID } ) @@ -277,7 +285,7 @@ do -- TASK_A2A_DISPATCHER --- Creates an SWEEP task when there are targets for it. -- @param #TASK_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. -- @return #nil If there are no targets to be set. function TASK_A2A_DISPATCHER:EvaluateSWEEP( DetectedItem ) self:F( { DetectedItem.ItemID } ) @@ -303,7 +311,7 @@ do -- TASK_A2A_DISPATCHER --- Creates an ENGAGE task when there are human friendlies airborne near the targets. -- @param #TASK_A2A_DISPATCHER self -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem - -- @return Set#SET_UNIT TargetSetUnit: The target set of units. + -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units. -- @return #nil If there are no targets to be set. function TASK_A2A_DISPATCHER:EvaluateENGAGE( DetectedItem ) self:F( { DetectedItem.ItemID } ) @@ -336,7 +344,7 @@ do -- TASK_A2A_DISPATCHER -- @param #TASK_A2A_DISPATCHER self -- @param Tasking.Mission#MISSION Mission -- @param Tasking.Task#TASK Task - -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Detection#DETECTION_BASE} derived object. + -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. -- @param #boolean DetectedItemID -- @param #boolean DetectedItemChange -- @return Tasking.Task#TASK @@ -484,9 +492,9 @@ do -- TASK_A2A_DISPATCHER end - --- Assigns tasks in relation to the detected items to the @{Set#SET_GROUP}. + --- Assigns tasks in relation to the detected items to the @{Core.Set#SET_GROUP}. -- @param #TASK_A2A_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Detection#DETECTION_BASE} derived object. + -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. function TASK_A2A_DISPATCHER:ProcessDetected( Detection ) self:F() @@ -561,6 +569,22 @@ do -- TASK_A2A_DISPATCHER Task:SetTargetZone( DetectedZone, DetectedItem.Coordinate.y, DetectedItem.Coordinate.Heading ) Task:SetDispatcher( self ) Mission:AddTask( Task ) + + function Task.OnEnterSuccess( Task, From, Event, To ) + self:Success( Task ) + end + + function Task.OnEnterCancelled( Task, From, Event, To ) + self:Cancelled( Task ) + end + + function Task.OnEnterFailed( Task, From, Event, To ) + self:Failed( Task ) + end + + function Task.OnEnterAborted( Task, From, Event, To ) + self:Aborted( Task ) + end TaskReport:Add( Task:GetName() ) else diff --git a/Moose Development/Moose/Tasking/Task_A2G.lua b/Moose Development/Moose/Tasking/Task_A2G.lua index 6ecb4ce78..634234654 100644 --- a/Moose Development/Moose/Tasking/Task_A2G.lua +++ b/Moose Development/Moose/Tasking/Task_A2G.lua @@ -8,24 +8,23 @@ -- -- === -- --- @module Task_A2G +-- @module Tasking.Task_A2G +-- @image MOOSE.JPG do -- TASK_A2G --- The TASK_A2G class -- @type TASK_A2G - -- @field Set#SET_UNIT TargetSetUnit + -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends Tasking.Task#TASK - --- # TASK_A2G class, extends @{Task#TASK} - -- - -- The TASK_A2G class defines Air To Ground tasks for a @{Set} of Target Units, - -- based on the tasking capabilities defined in @{Task#TASK}. - -- The TASK_A2G is implemented using a @{Fsm#FSM_TASK}, and has the following statuses: + --- The TASK_A2G class defines Air To Ground tasks for a @{Set} of Target Units, + -- based on the tasking capabilities defined in @{Tasking.Task#TASK}. + -- The TASK_A2G is implemented using a @{Core.Fsm#FSM_TASK}, and has the following statuses: -- -- * **None**: Start of the process -- * **Planned**: The A2G task is planned. - -- * **Assigned**: The A2G task is assigned to a @{Group#GROUP}. + -- * **Assigned**: The A2G task is assigned to a @{Wrapper.Group#GROUP}. -- * **Success**: The A2G task is successfully completed. -- * **Failed**: The A2G task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. -- @@ -45,9 +44,9 @@ do -- TASK_A2G --- Instantiates a new TASK_A2G. -- @param #TASK_A2G self -- @param Tasking.Mission#MISSION Mission - -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @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 Set#SET_UNIT UnitSetTargets + -- @param Core.Set#SET_UNIT UnitSetTargets -- @param #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range. -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known. -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be. @@ -61,9 +60,6 @@ do -- TASK_A2G local Fsm = self:GetUnitProcess() - - Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "RouteToRendezVous", Rejected = "Reject" } ) - Fsm:AddTransition( "Assigned", "RouteToRendezVous", "RoutingToRendezVous" ) Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtRendezVous" } ) Fsm:AddProcess ( "RoutingToRendezVous", "RouteToRendezVousZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtRendezVous" } ) @@ -84,6 +80,18 @@ do -- TASK_A2G Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) Fsm:AddTransition( "Failed", "Fail", "Failed" ) + + + --- Test + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_A2G#TASK_A2G Task + function Fsm:onafterAssigned( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + -- Determine the first Unit from the self.RendezVousSetUnit + + self:RouteToRendezVous() + end --- Test -- @param #FSM_PROCESS self @@ -140,7 +148,7 @@ do -- TASK_A2G local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT if TargetUnit then local Coordinate = TargetUnit:GetPointVec3() - self:T( { TargetCoordinate = Coordinate, Coordinate:GetX(), Coordinate:GetAlt(), Coordinate:GetZ() } ) + self:T( { TargetCoordinate = Coordinate, Coordinate:GetX(), Coordinate:GetY(), Coordinate:GetZ() } ) Task:SetTargetCoordinate( Coordinate, TaskUnit ) end self:__RouteToTargetPoint( 0.1 ) @@ -355,15 +363,13 @@ do -- TASK_A2G_SEAD --- The TASK_A2G_SEAD class -- @type TASK_A2G_SEAD - -- @field Set#SET_UNIT TargetSetUnit + -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends Tasking.Task#TASK - --- # TASK_A2G_SEAD class, extends @{Task_A2G#TASK_A2G} - -- - -- The TASK_A2G_SEAD class defines an Suppression or Extermination of Air Defenses task for a human player to be executed. + --- Defines an Suppression or Extermination of Air Defenses task for a human player to be executed. -- These tasks are important to be executed as they will help to achieve air superiority at the vicinity. -- - -- The TASK_A2G_SEAD is used by the @{Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create SEAD tasks + -- The TASK_A2G_SEAD is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create SEAD tasks -- based on detected enemy ground targets. -- -- @field #TASK_A2G_SEAD @@ -448,16 +454,14 @@ do -- TASK_A2G_BAI --- The TASK_A2G_BAI class -- @type TASK_A2G_BAI - -- @field Set#SET_UNIT TargetSetUnit + -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends Tasking.Task#TASK - --- # TASK_A2G_BAI class, extends @{Task_A2G#TASK_A2G} - -- - -- The TASK_A2G_BAI class defines an Battlefield Air Interdiction task for a human player to be executed. + --- Defines a Battlefield Air Interdiction task for a human player to be executed. -- These tasks are more strategic in nature and are most of the time further away from friendly forces. -- BAI tasks can also be used to express the abscence of friendly forces near the vicinity. -- - -- The TASK_A2G_BAI is used by the @{Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create BAI tasks + -- The TASK_A2G_BAI is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create BAI tasks -- based on detected enemy ground targets. -- -- @field #TASK_A2G_BAI @@ -544,15 +548,13 @@ do -- TASK_A2G_CAS --- The TASK_A2G_CAS class -- @type TASK_A2G_CAS - -- @field Set#SET_UNIT TargetSetUnit + -- @field Core.Set#SET_UNIT TargetSetUnit -- @extends Tasking.Task#TASK - --- # TASK_A2G_CAS class, extends @{Task_A2G#TASK_A2G} - -- - -- The TASK_A2G_CAS class defines an Close Air Support task for a human player to be executed. + --- Defines an Close Air Support task for a human player to be executed. -- Friendly forces will be in the vicinity within 6km from the enemy. -- - -- The TASK_A2G_CAS is used by the @{Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create CAS tasks + -- The TASK_A2G_CAS is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create CAS tasks -- based on detected enemy ground targets. -- -- @field #TASK_A2G_CAS diff --git a/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua index 256ca2db2..499efcf89 100644 --- a/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua +++ b/Moose Development/Moose/Tasking/Task_A2G_Dispatcher.lua @@ -1,5 +1,16 @@ ---- **Tasking** - The TASK\_A2G\_DISPATCHER dispatches A2G Tasks to Players based on enemy location detection. +--- **Tasking** -- Dynamically allocates A2G tasks to human players, based on detected ground targets through reconnaissance. -- +-- **Features:** +-- +-- * Dynamically assign tasks to human players based on detected targets. +-- * Dynamically change the tasks as the tactical situation evolves during the mission. +-- * Dynamically assign (CAS) Close Air Support tasks for human players. +-- * Dynamically assign (BAI) Battlefield Air Interdiction tasks for human players. +-- * Dynamically assign (SEAD) Supression of Enemy Air Defense tasks for human players to eliminate G2A missile threats. +-- * Define and use an EWR (Early Warning Radar) network. +-- * Define different ranges to engage upon intruders. +-- * Keep task achievements. +-- * Score task achievements.-- -- === -- -- ### Author: **FlightControl** @@ -8,31 +19,29 @@ -- -- === -- --- @module Task_A2G_Dispatcher +-- @module Tasking.Task_A2G_Dispatcher +-- @image Task_A2G_Dispatcher.JPG do -- TASK_A2G_DISPATCHER --- TASK\_A2G\_DISPATCHER class. -- @type TASK_A2G_DISPATCHER - -- @field Set#SET_GROUP SetGroup The groups to which the FAC will report to. + -- @field Core.Set#SET_GROUP SetGroup The groups to which the FAC will report to. -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects. -- @field Tasking.Mission#MISSION Mission -- @extends Tasking.DetectionManager#DETECTION_MANAGER - --- # TASK\_A2G\_DISPATCHER class, extends @{DetectionManager#DETECTION_MANAGER} + --- Orchestrates dynamic **A2G Task Dispatching** based on the detection results of a linked @{Detection} object. -- - -- ![](..\Presentations\TASK\_A2G\_DISPATCHER\Dia1.JPG) - -- - -- The TASK\_A2G\_DISPATCHER class orchestrates dynamic **A2G Task Dispatching** based on the detection results of a linked @{Detection} object. -- It uses the Tasking System within the MOOSE framework, which is a multi-player Tasking Orchestration system. -- It provides a truly dynamic battle environment for pilots and ground commanders to engage upon, -- in a true co-operation environment wherein **Multiple Teams** will collaborate in Missions to **achieve a common Mission Goal**. -- - -- The A2G dispatcher will dispatch the A2G Tasks to a defined @{Set} of @{Group}s that will be manned by **Players**. - -- We call this the **AttackSet** of the A2G dispatcher. So, the Players are seated in the @{Client}s of the @{Group} @{Set}. + -- The A2G dispatcher will dispatch the A2G Tasks to a defined @{Set} of @{Wrapper.Group}s that will be manned by **Players**. + -- We call this the **AttackSet** of the A2G dispatcher. So, the Players are seated in the @{Client}s of the @{Wrapper.Group} @{Set}. -- -- Depending on the actions of the enemy, preventive tasks are dispatched to the players to orchestrate the engagement in a true co-operation. - -- The detection object will group the detected targets by its grouping method, and integrates a @{Set} of @{Group}s that are Recce vehicles or air units. + -- The detection object will group the detected targets by its grouping method, and integrates a @{Set} of @{Wrapper.Group}s that are Recce vehicles or air units. -- We call this the **RecceSet** of the A2G dispatcher. -- -- Depending on the current detected tactical situation, different task types will be dispatched to the Players seated in the AttackSet.. @@ -379,8 +388,8 @@ do -- TASK_A2G_DISPATCHER -- - A @{Mission} object. Each task belongs to a Mission. -- - A @{Detection} object. There are several detection grouping methods to choose from. -- - A @{Task_A2G_Dispatcher} object. The master A2G task dispatcher. - -- - A @{Set} of @{Group} objects that will detect the emeny, the RecceSet. This is attached to the @{Detection} object. - -- - A @{Set} ob @{Group} objects that will attack the enemy, the AttackSet. This is attached to the @{Task_A2G_Dispatcher} object. + -- - A @{Set} of @{Wrapper.Group} objects that will detect the emeny, the RecceSet. This is attached to the @{Detection} object. + -- - A @{Set} ob @{Wrapper.Group} objects that will attack the enemy, the AttackSet. This is attached to the @{Task_A2G_Dispatcher} object. -- -- Below an example mission declaration that is defines a Task A2G Dispatcher object. -- @@ -432,7 +441,7 @@ do -- TASK_A2G_DISPATCHER --- TASK_A2G_DISPATCHER constructor. -- @param #TASK_A2G_DISPATCHER self -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done. - -- @param Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission. + -- @param Core.Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission. -- @param Functional.Detection#DETECTION_BASE Detection The detection results that are used to dynamically assign new tasks to human players. -- @return #TASK_A2G_DISPATCHER self function TASK_A2G_DISPATCHER:New( Mission, SetGroup, Detection ) @@ -582,9 +591,9 @@ do -- TASK_A2G_DISPATCHER end - --- Assigns tasks in relation to the detected items to the @{Set#SET_GROUP}. + --- Assigns tasks in relation to the detected items to the @{Core.Set#SET_GROUP}. -- @param #TASK_A2G_DISPATCHER self - -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Detection#DETECTION_BASE} derived object. + -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object. -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. function TASK_A2G_DISPATCHER:ProcessDetected( Detection ) self:F() @@ -761,6 +770,23 @@ do -- TASK_A2G_DISPATCHER Task:SetDispatcher( self ) Task:UpdateTaskInfo( DetectedItem ) Mission:AddTask( Task ) + + function Task.OnEnterSuccess( Task, From, Event, To ) + self:Success( Task ) + end + + function Task.OnEnterCancelled( Task, From, Event, To ) + self:Cancelled( Task ) + end + + function Task.OnEnterFailed( Task, From, Event, To ) + self:Failed( Task ) + end + + function Task.OnEnterAborted( Task, From, Event, To ) + self:Aborted( Task ) + end + TaskReport:Add( Task:GetName() ) else diff --git a/Moose Development/Moose/Tasking/Task_CARGO.lua b/Moose Development/Moose/Tasking/Task_CARGO.lua index 7d11cae8d..97a067220 100644 --- a/Moose Development/Moose/Tasking/Task_CARGO.lua +++ b/Moose Development/Moose/Tasking/Task_CARGO.lua @@ -1,18 +1,396 @@ ---- **Tasking** -- The TASK_CARGO models tasks for players to transport @{Cargo}. +--- **Tasking** -- Base class to model tasks for players to transport cargo. -- --- ![Banner Image](..\Presentations\TASK_CARGO\Dia1.JPG) +-- ## Features: +-- +-- * TASK_CARGO is the **base class** for: +-- +-- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT} +-- * @{Tasking.Task_Cargo_CSAR#TASK_CARGO_CSAR} +-- +-- +-- === +-- +-- ## Test Missions: +-- +-- Test missions can be located on the main GITHUB site. +-- +-- [FlightControl-Master/MOOSE_MISSIONS/TAD - Task Dispatching/CGO - Cargo Dispatching/](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/TAD%20-%20Task%20Dispatching/CGO%20-%20Cargo%20Dispatching) -- -- === -- --- The Moose framework provides various CARGO classes that allow DCS phisical or logical objects to be transported or sling loaded by Carriers. --- The CARGO_ classes, as part of the moose core, are able to Board, Load, UnBoard and UnLoad cargo between Carrier units. +-- ## Tasking system. -- --- This collection of classes in this module define tasks for human players to handle these cargo objects. --- Cargo can be transported, picked-up, deployed and sling-loaded from and to other places. +-- #### If you are not yet aware what the MOOSE tasking system is about, read FIRST the explanation on the @{Tasking.Task} module. -- --- The following classes are important to consider: +-- === -- --- * @{#TASK_CARGO_TRANSPORT}: Defines a task for a human player to transport a set of cargo between various zones. +-- ## Context of cargo tasking. +-- +-- The Moose framework provides various CARGO classes that allow DCS physical or logical objects to be transported or sling loaded by Carriers. +-- The CARGO_ classes, as part of the MOOSE core, are able to Board, Load, UnBoard and UnLoad cargo between Carrier units. +-- +-- The TASK_CARGO class is not meant to use within your missions as a mission designer. It is a base class, and other classes are derived from it. +-- +-- The following TASK_CARGO_ classes are important, as they implement the CONCRETE tasks: +-- +-- * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT}: Defines a task for a human player to transport a set of cargo between various zones. +-- * @{Tasking.Task_Cargo_CSAR#TASK_CARGO_CSAR}: Defines a task for a human player to Search and Rescue wounded pilots. +-- +-- However! The menu system and basic usage of the TASK_CARGO classes is explained in the @{#TASK_CARGO} class description. +-- So please browse further below to understand how to use it from a player perspective! +-- +-- === +-- +-- ## Cargo tasking from a player perspective. +-- +-- A human player can join the battle field in a client airborne slot or a ground vehicle within the CA module (ALT-J). +-- The player needs to accept the task from the task overview list within the mission, using the menus. +-- +-- Once the task is assigned to the player and accepted by the player, the player will obtain +-- an extra **Cargo (Radio) Menu** that contains the CARGO objects that need to be transported. +-- +-- Each @{Cargo.Cargo} object has a certain state: +-- +-- * **UnLoaded**: The cargo is located within the battlefield. It may still need to be transported. +-- * **Loaded**: The cargo is loaded within a Carrier. This can be your air unit, or another air unit, or even a vehicle. +-- * **Boarding**: The cargo is running or moving towards your Carrier for loading. +-- * **UnBoarding**: The cargo is driving or jumping out of your Carrier and moves to a location in the Deployment Zone. +-- +-- Cargo must be transported towards different Deployment @{Core.Zone}s. +-- +-- The Cargo Menu system allows to execute **various actions** to transport the cargo. +-- In the menu, you'll find for each CARGO, that is part of the scope of the task, various actions that can be completed. +-- Depending on the location of your Carrier unit, the menu options will vary. +-- +-- ### Joining a Cargo Transport Task +-- +-- Once you've joined a task, using the **Join Planned Task Menu**, +-- you can Pickup cargo from a pickup location and Deploy cargo in deployment zones, using the **Task Action Menu**. +-- +-- ### Task Action Menu. +-- +-- When a player has joined a **`CARGO`** task (type), for that player only, +-- it's **Task Action Menu** will show an additional menu options. +-- +-- From within this menu, you will be able to route to a cargo location, deploy zone, and load/unload cargo. +-- +-- ### Pickup cargo by Boarding, Loading and Sling Loading. +-- +-- There are three different ways how cargo can be picked up: +-- +-- - **Boarding**: Moveable cargo (like infantry or vehicles), can be boarded, that means, the cargo will move towards your carrier to board. +-- However, it can only execute the boarding actions if it is within the foreseen **Reporting Range**. +-- Therefore, it is important that you steer your Carrier within the Reporting Range around the cargo, +-- so that boarding actions can be executed on the cargo. The reporting range is set by the mission designer. +-- Fortunately, the cargo is reporting to you when it is within reporting range. +-- +-- - **Loading**: Stationary cargo (like crates), which are heavy, can only be loaded or sling loaded, meaning, +-- your carrier must be close enough to the cargo to be able to load the cargo within the carrier bays. +-- Moose provides you with an additional menu system to load stationary cargo into your carrier bays using the menu. +-- These menu options will become available, when the carrier is within loading range. +-- The Moose cargo will report to the carrier when the range is close enough. The load range is set by the mission designer. +-- +-- - **Sling Loading**: Stationary cargo (like crates), which are heavy, can only be loaded or sling loaded, meaning, +-- your carrier must be close enough to the cargo to be able to load the cargo within the carrier bays. +-- Sling loading cargo is done using the default DCS menu system. However, Moose cargo will report to the carrier that +-- it is within sling loading range. +-- +-- In order to be able to pickup cargo, you'll need to know where the cargo is located, right? +-- +-- Fortunately, if your Carrier is not within the reporting range of the cargo, +-- **the HQ can help to route you to the locations of cargo**. +-- +-- ![Task_Types](../Tasking/Task_Cargo_Main_Menu.JPG) +-- +-- Use the task action menu to receive HQ help for this. +-- +-- ![Task_Types](../Tasking/Task_Cargo_Action_Menu.JPG) +-- +-- Depending on the location within the battlefield, the task action menu will contain **Route options** that can be selected +-- to start the HQ sending you routing messages. +-- The **route options will vary**, depending on the position of your carrier, and the location of the cargo and the deploy zones. +-- Note that the route options will **only be created** for cargo that is **in scope of your cargo transportation task**, +-- so there may be other cargo objects within the DCS simulation, but if those belong to other cargo transportations tasks, +-- then no routing options will be shown for these cargo. +-- This is done to ensure that **different teams** have a **defined scope** for defined cargo, and that **multiple teams** can join +-- **multiple tasks**, transporting cargo **simultaneously** in a **cooperation**. +-- +-- In this example, there is a menu option to **Route to pickup cargo...**. +-- Use this menu to route towards cargo locations for pickup into your carrier. +-- +-- ![Task_Types](../Tasking/Task_Cargo_Types_Menu.JPG) +-- +-- When you select this menu, you'll see a new menu listing the different cargo types that are out there in the dcs simulator. +-- These cargo types are symbolic names that are assigned by the mission designer, like oil, liquid, engineers, food, workers etc. +-- MOOSE has introduced this concept to allow mission designers to make different cargo types for different purposes. +-- Only the creativity of the mission designer limits now the things that can be done with cargo ... +-- Okay, let's continue ..., and let's select Oil ... +-- +-- When selected, the HQ will send you routing messages. +-- +-- ![Task_Types](../Tasking/Task_Cargo_Routing_BR.JPG) +-- +-- An example of routing in BR mode. +-- +-- Note that the coordinate display format in the message can be switched between LL DMS, LL DDM, MGRS and BR. +-- +-- ![Task_Types](../Tasking/Main_Settings.JPG) +-- +-- Use the @{Core.Settings} menu to change your display format preferences. +-- +-- ![Task_Types](../Tasking/Settings_A2G_Coordinate.JPG) +-- +-- There you can change the display format to another format that suits your need. +-- Because cargo transportation is Air 2 Ground oriented, you need to select the A2G coordinate format display options. +-- Note that the main settings menu contains much more +-- options to control your display formats, like switch to metric and imperial, or change the duration of the display messages. +-- +-- ![Task_Types](../Tasking/Task_Cargo_Routing_LL.JPG) +-- +-- Here I changed the routing display format to LL DMS. +-- +-- One important thing to know, is that the routing messages will flash at regular time intervals. +-- When using BR coordinate display format, the **distance and angle will change accordingly** from your carrier position and the location of the cargo. +-- +-- Another important note is the routing towards deploy zones. +-- These routing options will only be shown, when your carrier bays have cargo loaded. +-- So, only when there is something to be deployed from your carrier, the deploy options will be shown. +-- +-- #### Pickup Cargo. +-- +-- In order to pickup cargo, use the **task action menu** to **route to a specific cargo**. +-- When a cargo route is selected, the HQ will send you routing messages indicating the location of the cargo. +-- +-- Upon arrival at the cargo, and when the cargo is within **reporting range**, the cargo will contact you and **further instructions will be given**. +-- +-- - When your Carrier is airborne, you will receive instructions to land your Carrier. +-- The action will not be completed until you've landed your Carrier. +-- +-- - For ground carriers, you can just drive to the optimal cargo board or load position. +-- +-- It takes a bit of skill to land a helicopter near a cargo to be loaded, but that is part of the game, isn't it? +-- Expecially when you are landing in a "hot" zone, so when cargo is under immediate threat of fire. +-- +-- #### Board Cargo (infantry). +-- +-- ![](../Tasking/Boarding_Ready.png) +-- +-- If your Carrier is within the **Reporting Range of the cargo**, and the cargo is **moveable**, the **cargo can be boarded**! +-- This type of cargo will be most of the time be infantry. +-- +-- ![](../Tasking/Boarding_Menu.png) +-- +-- A **Board cargo...** sub menu has appeared, because your carrier is in boarding range of the cargo (infantry). +-- Select the **Board cargo...** menu. +-- +-- ![](../Tasking/Boarding_Menu_Engineers.png) +-- +-- Any cargo that can be boarded (thus movable cargo), within boarding range of the carrier, will be listed here! +-- In this example, the cargo **Engineers** can be boarded, by selecting the menu option. +-- +-- ![](../Tasking/Boarding_Started.png) +-- +-- After the menu option to board the cargo has been selected, the boarding process is started. +-- A message from the cargo is communicated to the pilot, that boarding is started. +-- +-- ![](../Tasking/Boarding_Ongoing.png) +-- +-- **The pilot must wait at the exact position until all cargo has been boarded!** +-- +-- The moveable cargo will run in formation to your carrier, and will board one by one, depending on the near range set by the mission designer. +-- The near range as added because carriers can be large or small, depending on the object size of the carrier. +-- +-- ![](../Tasking/Boarding_In_Progress.png) +-- +-- ![](../Tasking/Boarding_Almost_Done.png) +-- +-- Note that multiple units may need to board your Carrier, so it is required to await the full boarding process. +-- +-- ![](../Tasking/Boarding_Done.png) +-- +-- Once the cargo is fully boarded within your Carrier, you will be notified of this. +-- +-- **Remarks:** +-- +-- * For airborne Carriers, it is required to land first before the Boarding process can be initiated. +-- If during boarding the Carrier gets airborne, the boarding process will be cancelled. +-- * The carrier must remain stationary when the boarding sequence has started until further notified. +-- +-- #### Load Cargo. +-- +-- Cargo can be loaded into vehicles or helicopters or airplanes, as long as the carrier is sufficiently near to the cargo object. +-- +-- ![](../Tasking/Loading_Ready.png) +-- +-- If your Carrier is within the **Loading Range of the cargo**, thus, sufficiently near to the cargo, and the cargo is **stationary**, the **cargo can be loaded**, but not boarded! +-- +-- ![](../Tasking/Loading_Menu.png) +-- +-- Select the task action menu and now a **Load cargo...** sub menu will be listed. +-- Select the **Load cargo...** sub menu, and a further detailed menu will be shown. +-- +-- ![](../Tasking/Loading_Menu_Crate.png) +-- +-- For each non-moveable cargo object (crates etc), **within loading range of the carrier**, the cargo will be listed and can be loaded into the carrier! +-- +-- ![](../Tasking/Loading_Cargo_Loaded.png) +-- +-- Once the cargo is loaded within your Carrier, you will be notified of this. +-- +-- **Remarks:** +-- +-- * For airborne Carriers, it is required to **land first right near the cargo**, before the loading process can be initiated. +-- As stated, this requires some pilot skills :-) +-- +-- #### Sling Load Cargo (helicopters only). +-- +-- If your Carrier is within the **Loading Range of the cargo**, and the cargo is **stationary**, the **cargo can also be sling loaded**! +-- Note that this is only possible for helicopters. +-- +-- To sling load cargo, there is no task action menu required. Just follow the normal sling loading procedure and the cargo will report. +-- Use the normal DCS sling loading menu system to hook the cargo you the cable attached on your helicopter. +-- +-- Again note that you may land firstly right next to the cargo, before the loading process can be initiated. +-- As stated, this requires some pilot skills :-) +-- +-- +-- ### Deploy cargo by Unboarding, Unloading and Sling Deploying. +-- +-- #### **Deploying the relevant cargo within deploy zones, will make you achieve cargo transportation tasks!!!** +-- +-- There are two different ways how cargo can be deployed: +-- +-- - **Unboarding**: Moveable cargo (like infantry or vehicles), can be unboarded, that means, +-- the cargo will step out of the carrier and will run to a group location. +-- Moose provides you with an additional menu system to unload stationary cargo from the carrier bays, +-- using the menu. These menu options will become available, when the carrier is within the deploy zone. +-- +-- - **Unloading**: Stationary cargo (like crates), which are heavy, can only be unloaded or sling loaded. +-- Moose provides you with an additional menu system to unload stationary cargo from the carrier bays, +-- using the menu. These menu options will become available, when the carrier is within the deploy zone. +-- +-- - **Sling Deploying**: Stationary cargo (like crates), which are heavy, can also be sling deployed. +-- Once the cargo is within the deploy zone, the cargo can be deployed from the sling onto the ground. +-- +-- In order to be able to deploy cargo, you'll need to know where the deploy zone is located, right? +-- Fortunately, the HQ can help to route you to the locations of deploy zone. +-- Use the task action menu to receive HQ help for this. +-- +-- ![](../Tasking/Routing_Deploy_Zone_Menu.png) +-- +-- Depending on the location within the battlefield, the task action menu will contain **Route options** that can be selected +-- to start the HQ sending you routing messages. Also, if the carrier cargo bays contain cargo, +-- then beside **Route options** there will also be **Deploy options** listed. +-- These **Deploy options** are meant to route you to the deploy zone locations. +-- +-- ![](../Tasking/Routing_Deploy_Zone_Menu_Workplace.png) +-- +-- Depending on the task that you have selected, the deploy zones will be listed. +-- **There may be multiple deploy zones within the mission, but only the deploy zones relevant for your task will be available in the menu!** +-- +-- ![](../Tasking/Routing_Deploy_Zone_Message.png) +-- +-- When a routing option is selected, you are sent routing messages in a selected coordinate format. +-- Possible routing coordinate formats are: Bearing Range (BR), Lattitude Longitude (LL) or Military Grid System (MGRS). +-- Note that for LL, there are two sub formats. (See pickup). +-- +-- ![](../Tasking/Routing_Deploy_Zone_Arrived.png) +-- +-- When you are within the range of the deploy zone (can be also a polygon!), a message is communicated by HQ that you have arrived within the zone! +-- +-- The routing messages are formulated in the coordinate format that is currently active as configured in your settings profile. +-- ![Task_Types](../Tasking/Task_Cargo_Settings.JPG) +-- Use the **Settings Menu** to select the coordinate format that you would like to use for location determination. +-- +-- #### Unboard Cargo. +-- +-- If your carrier contains cargo, and the cargo is **moveable**, the **cargo can be unboarded**! +-- You can only unload cargo if there is cargo within your cargo bays within the carrier. +-- +-- ![](../Tasking/Unboarding_Menu.png) +-- +-- Select the task action menu and now an **Unboard cargo...** sub menu will be listed! +-- Again, this option will only be listed if there is a non moveable cargo within your cargo bays. +-- +-- ![](../Tasking/Unboarding_Menu_Engineers.png) +-- +-- Now you will see a menu option to unload the non-moveable cargo. +-- In this example, you can unload the **Engineers** that was loaded within your carrier cargo bays. +-- Depending on the cargo loaded within your cargo bays, you will see other options here! +-- Select the relevant menu option from the cargo unload menu, and the cargo will unloaded from your carrier. +-- +-- ![](../Tasking/Unboarding_Started.png) +-- +-- **The cargo will step out of your carrier and will move towards a grouping point.** +-- When the unboarding process has started, you will be notified by a message to your carrier. +-- +-- ![](../Tasking/Unboarding_In_Progress.png) +-- +-- The moveable cargo will unboard one by one, so note that multiple units may need to unboard your Carrier, +-- so it is required to await the full completion of the unboarding process. +-- +-- ![](../Tasking/Unboarding_Done.png) +-- +-- Once the cargo is fully unboarded from your carrier, you will be notified of this. +-- +-- **Remarks:** +-- +-- * For airborne carriers, it is required to land first before the unboarding process can be initiated. +-- If during unboarding the Carrier gets airborne, the unboarding process will be cancelled. +-- * Once the moveable cargo is unboarded, they will start moving towards a specified gathering point. +-- * The moveable cargo will send a message to your carrier with unboarding status updates. +-- +-- **Deploying a cargo within a deployment zone, may complete a deployment task! So ensure that you deploy the right cargo at the right deployment zone!** +-- +-- #### Unload Cargo. +-- +-- If your carrier contains cargo, and the cargo is **stationary**, the **cargo can be unloaded**, but not unboarded! +-- You can only unload cargo if there is cargo within your cargo bays within the carrier. +-- +-- ![](../Tasking/Unloading_Menu.png) +-- +-- Select the task action menu and now an **Unload cargo...** sub menu will be listed! +-- Again, this option will only be listed if there is a non moveable cargo within your cargo bays. +-- +-- ![](../Tasking/Unloading_Menu_Crate.png) +-- +-- Now you will see a menu option to unload the non-moveable cargo. +-- In this example, you can unload the **Crate** that was loaded within your carrier cargo bays. +-- Depending on the cargo loaded within your cargo bays, you will see other options here! +-- Select the relevant menu option from the cargo unload menu, and the cargo will unloaded from your carrier. +-- +-- ![](../Tasking/Unloading_Done.png) +-- +-- Once the cargo is unloaded fom your Carrier, you may be notified of this, when there is a truck near to the cargo. +-- If there is no truck near to the unload area, no message will be sent to your carrier! +-- +-- **Remarks:** +-- +-- * For airborne Carriers, it is required to land first, before the unloading process can be initiated. +-- * A truck must be near the unload area to get messages to your carrier of the unload event! +-- * Unloading is only for non-moveable cargo. +-- * The non-moveable cargo must be within your cargo bays, or no unload option will be available. +-- +-- **Deploying a cargo within a deployment zone, may complete a deployment task! So ensure that you deploy the right cargo at the right deployment zone!** +-- +-- +-- #### Sling Deploy Cargo (helicopters only). +-- +-- If your Carrier is within the **deploy zone**, and the cargo is **stationary**, the **cargo can also be sling deploying**! +-- Note that this is only possible for helicopters. +-- +-- To sling deploy cargo, there is no task action menu required. Just follow the normal sling deploying procedure. +-- +-- **Deploying a cargo within a deployment zone, may complete a deployment task! So ensure that you deploy the right cargo at the right deployment zone!** +-- +-- ## Cargo tasking from a mission designer perspective. +-- +-- Please consult the documentation how to implement the derived classes of SET_CARGO in: +-- +-- - @{Tasking.Task_Cargo#TASK_CARGO}: Documents the main methods how to handle the cargo tasking from a mission designer perspective. +-- - @{Tasking.Task_Cargo#TASK_CARGO_TRANSPORT}: Documents the specific methods how to handle the cargo transportation tasking from a mission designer perspective. +-- - @{Tasking.Task_Cargo#TASK_CARGO_CSAR}: Documents the specific methods how to handle the cargo CSAR tasking from a mission designer perspective. +-- -- -- === -- @@ -22,124 +400,81 @@ -- -- === -- --- @module Task_Cargo +-- @module Tasking.Task_Cargo +-- @image MOOSE.JPG do -- TASK_CARGO --- @type TASK_CARGO -- @extends Tasking.Task#TASK - --- - -- # TASK_CARGO class, extends @{Task#TASK} + --- Model tasks for players to transport Cargo. -- - -- ## A flexible tasking system + -- This models the process of a flexible transporation tasking system of cargo. + -- + -- # 1) A flexible tasking system. -- -- The TASK_CARGO classes provide you with a flexible tasking sytem, -- that allows you to transport cargo of various types between various locations -- and various dedicated deployment zones. -- - -- The cargo in scope of the TASK_CARGO classes must be explicitly given, and is of type SET_CARGO. + -- The cargo in scope of the TASK\_CARGO classes must be explicitly given, and is of type SET\_CARGO. -- The SET_CARGO contains a collection of CARGO objects that must be handled by the players in the mission. -- + -- # 2) Cargo Tasking from a mission designer perspective. -- - -- ## Task execution experience from the player perspective + -- A cargo task is governed by a @{Tasking.Mission} object. Tasks are of different types. + -- The @{#TASK} object is used or derived by more detailed tasking classes that will implement the task execution mechanisms + -- and goals. -- - -- A human player can join the battle field in a client airborne slot or a ground vehicle within the CA module (ALT-J). - -- The player needs to accept the task from the task overview list within the mission, using the radio menus. + -- ## 2.1) Derived cargo task classes. -- - -- Once the TASK_CARGO is assigned to the player and accepted by the player, the player will obtain - -- an extra **Cargo Handling Radio Menu** that contains the CARGO objects that need to be transported. + -- The following TASK_CARGO classes are derived from @{#TASK}. -- - -- Each CARGO object has a certain state: + -- TASK + -- TASK_CARGO + -- TASK_CARGO_TRANSPORT + -- TASK_CARGO_CSAR -- - -- * **UnLoaded**: The CARGO is located within the battlefield. It may still need to be transported. - -- * **Loaded**: The CARGO is loaded within a Carrier. This can be your air unit, or another air unit, or even a vehicle. - -- * **Boarding**: The CARGO is running or moving towards your Carrier for loading. - -- * **UnBoarding**: The CARGO is driving or jumping out of your Carrier and moves to a location in the Deployment Zone. + -- ### 2.1.1) Cargo Tasks -- - -- Cargo must be transported towards different **Deployment @{Zone}s**. - -- - -- The Cargo Handling Radio Menu system allows to execute **various actions** to handle the cargo. - -- In the menu, you'll find for each CARGO, that is part of the scope of the task, various actions that can be completed. - -- Depending on the location of your Carrier unit, the menu options will vary. - -- - -- - -- ## Cargo Pickup and Boarding - -- - -- For cargo boarding, a cargo can only execute the boarding actions if it is within the foreseen **Reporting Range**. - -- Therefore, it is important that you steer your Carrier within the Reporting Range, - -- so that boarding actions can be executed on the cargo. - -- To Pickup and Board cargo, the following menu items will be shown in your carrier radio menu: - -- - -- ### Board Cargo - -- - -- If your Carrier is within the Reporting Range of the cargo, it will allow to pickup the cargo by selecting this menu option. - -- Depending on the Cargo type, the cargo will either move to your Carrier or you will receive instructions how to handle the cargo - -- pickup. If the cargo moves to your carrier, it will indicate the boarding status. - -- Note that multiple units need to board your Carrier, so it is required to await the full boarding process. - -- Once the cargo is fully boarded within your Carrier, you will be notified of this. - -- - -- Note that for airborne Carriers, it is required to land first before the Boarding process can be initiated. - -- If during boarding the Carrier gets airborne, the boarding process will be cancelled. - -- - -- ## Pickup Cargo - -- - -- If your Carrier is not within the Reporting Range of the cargo, the HQ will guide you to its location. - -- Routing information is shown in flight that directs you to the cargo within Reporting Range. - -- Upon arrival, the Cargo will contact you and further instructions will be given. - -- When your Carrier is airborne, you will receive instructions to land your Carrier. - -- The action will not be completed until you've landed your Carrier. - -- - -- - -- ## Cargo Deploy and UnBoarding - -- - -- Various Deployment Zones can be foreseen in the scope of the Cargo transportation. Each deployment zone can be of varying @{Zone} type. - -- The Cargo Handling Radio Menu provides with menu options to execute an action to steer your Carrier to a specific Zone. - -- - -- ### UnBoard Cargo - -- - -- If your Carrier is already within a Deployment Zone, - -- then the Cargo Handling Radio Menu allows to **UnBoard** a specific cargo that is - -- loaded within your Carrier group into the Deployment Zone. - -- Note that the Unboarding process takes a while, as the cargo units (infantry or vehicles) must unload from your Carrier. - -- Ensure that you stay at the position or stay on the ground while Unboarding. - -- If any unforeseen manoeuvre is done by the Carrier, then the Unboarding will be cancelled. - -- - -- ### Deploy Cargo - -- - -- If your Carrier is not within a Deployment Zone, you'll need to fly towards one. - -- Fortunately, the Cargo Handling Radio Menu provides you with menu options to select a specific Deployment Zone to fly towards. - -- Once a Deployment Zone has been selected, your Carrier will receive routing information from HQ towards the Deployment Zone center. - -- Upon arrival, the HQ will provide you with further instructions. - -- When your Carrier is airborne, you will receive instructions to land your Carrier. - -- The action will not be completed until you've landed your Carrier! - -- - -- ## Handle TASK_CARGO Events ... + -- - @{Tasking.Task_Cargo#TASK_CARGO_TRANSPORT} - Models the transportation of cargo to deployment zones. + -- - @{Tasking.Task_Cargo#TASK_CARGO_CSAR} - Models the rescue of downed friendly pilots from behind enemy lines. + -- + -- ## 2.2) Handle TASK_CARGO Events ... -- -- The TASK_CARGO classes define @{Cargo} transport tasks, - -- based on the tasking capabilities defined in @{Task#TASK}. + -- based on the tasking capabilities defined in @{Tasking.Task#TASK}. -- - -- ### Specific TASK_CARGO Events + -- ### 2.2.1) Boarding events. -- - -- Specific Cargo Handling event can be captured, that allow to trigger specific actions! + -- Specific Cargo event can be captured, that allow to trigger specific actions! -- -- * **Boarded**: Triggered when the Cargo has been Boarded into your Carrier. -- * **UnBoarded**: Triggered when the cargo has been Unboarded from your Carrier and has arrived at the Deployment Zone. -- - -- ### Standard TASK_CARGO Events + -- ### 2.2.2) Loading events. -- - -- The TASK_CARGO is implemented using a @{Statemachine#FSM_TASK}, and has the following standard statuses: + -- Specific Cargo event can be captured, that allow to trigger specific actions! + -- + -- * **Loaded**: Triggered when the Cargo has been Loaded into your Carrier. + -- * **UnLoaded**: Triggered when the cargo has been Unloaded from your Carrier and has arrived at the Deployment Zone. + -- + -- ### 2.2.2) Standard TASK_CARGO Events + -- + -- The TASK_CARGO is implemented using a @{Core.Fsm#FSM_TASK}, and has the following standard statuses: -- -- * **None**: Start of the process. -- * **Planned**: The cargo task is planned. - -- * **Assigned**: The cargo task is assigned to a @{Group#GROUP}. + -- * **Assigned**: The cargo task is assigned to a @{Wrapper.Group#GROUP}. -- * **Success**: The cargo task is successfully completed. -- * **Failed**: The cargo task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. -- + -- + -- -- === -- -- @field #TASK_CARGO - -- TASK_CARGO = { ClassName = "TASK_CARGO", } @@ -147,7 +482,7 @@ do -- TASK_CARGO --- Instantiates a new TASK_CARGO. -- @param #TASK_CARGO self -- @param Tasking.Mission#MISSION Mission - -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. -- @param #string TaskName The name of the Task. -- @param Core.Set#SET_CARGO SetCargo The scope of the cargo to be transported. -- @param #string TaskType The type of Cargo task. @@ -166,24 +501,98 @@ do -- TASK_CARGO self.DeployZones = {} -- setmetatable( {}, { __mode = "v" } ) -- weak table on value + self:AddTransition( "*", "CargoDeployed", "*" ) + + --- CargoDeployed Handler OnBefore for TASK_CARGO + -- @function [parent=#TASK_CARGO] OnBeforeCargoDeployed + -- @param #TASK_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc. + -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. + -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded. + -- @return #boolean + + --- CargoDeployed Handler OnAfter for TASK_CARGO + -- @function [parent=#TASK_CARGO] OnAfterCargoDeployed + -- @param #TASK_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc. + -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. + -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded. + -- @usage + -- + -- -- Add a Transport task to transport cargo of different types to a Transport Deployment Zone. + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups ) + -- + -- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() + -- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) + -- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) + -- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) + -- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) + -- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) + -- + -- -- Here we add the task. We name the task "Build a Workplace". + -- -- We provide the CargoSetWorkmaterials, and a briefing as the 2nd and 3rd parameter. + -- -- The :AddTransportTask() returns a Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT object, which we keep as a reference for further actions. + -- -- The WorkplaceTask holds the created and returned Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT object. + -- local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." ) + -- + -- -- Here we set a TransportDeployZone. We use the WorkplaceTask as the reference, and provide a ZONE object. + -- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) + -- + -- Helos = { SPAWN:New( "Helicopters 1" ), SPAWN:New( "Helicopters 2" ), SPAWN:New( "Helicopters 3" ), SPAWN:New( "Helicopters 4" ), SPAWN:New( "Helicopters 5" ) } + -- EnemyHelos = { SPAWN:New( "Enemy Helicopters 1" ), SPAWN:New( "Enemy Helicopters 2" ), SPAWN:New( "Enemy Helicopters 3" ) } + -- + -- -- This is our worker method! So when a cargo is deployed within a deployment zone, this method will be called. + -- -- By example we are spawning here a random friendly helicopter and a random enemy helicopter. + -- function WorkplaceTask:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) + -- Helos[ math.random(1,#Helos) ]:Spawn() + -- EnemyHelos[ math.random(1,#EnemyHelos) ]:Spawn() + -- end + + self:AddTransition( "*", "CargoPickedUp", "*" ) + + --- CargoPickedUp Handler OnBefore for TASK_CARGO + -- @function [parent=#TASK_CARGO] OnBeforeCargoPickedUp + -- @param #TASK_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc. + -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. + -- @return #boolean + + --- CargoPickedUp Handler OnAfter for TASK_CARGO + -- @function [parent=#TASK_CARGO] OnAfterCargoPickedUp + -- @param #TASK_CARGO self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc. + -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. + local Fsm = self:GetUnitProcess() - Fsm:SetStartState( "Planned" ) - - Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "SelectAction", Rejected = "Reject" } ) +-- Fsm:SetStartState( "Planned" ) +-- +-- Fsm:AddProcess ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "SelectAction", Rejected = "Reject" } ) - Fsm:AddTransition( { "Planned", "Assigned", "WaitingForCommand", "ArrivedAtPickup", "ArrivedAtDeploy", "Boarded", "UnBoarded", "Landed", "Boarding" }, "SelectAction", "*" ) + Fsm:AddTransition( { "Planned", "Assigned", "Cancelled", "WaitingForCommand", "ArrivedAtPickup", "ArrivedAtDeploy", "Boarded", "UnBoarded", "Loaded", "UnLoaded", "Landed", "Boarding" }, "SelectAction", "*" ) Fsm:AddTransition( "*", "RouteToPickup", "RoutingToPickup" ) Fsm:AddProcess ( "RoutingToPickup", "RouteToPickupPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtPickup", Cancelled = "CancelRouteToPickup" } ) Fsm:AddTransition( "Arrived", "ArriveAtPickup", "ArrivedAtPickup" ) - Fsm:AddTransition( "Cancelled", "CancelRouteToPickup", "WaitingForCommand" ) + Fsm:AddTransition( "Cancelled", "CancelRouteToPickup", "Cancelled" ) Fsm:AddTransition( "*", "RouteToDeploy", "RoutingToDeploy" ) Fsm:AddProcess ( "RoutingToDeploy", "RouteToDeployZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtDeploy", Cancelled = "CancelRouteToDeploy" } ) Fsm:AddTransition( "Arrived", "ArriveAtDeploy", "ArrivedAtDeploy" ) - Fsm:AddTransition( "Cancelled", "CancelRouteToDeploy", "WaitingForCommand" ) + Fsm:AddTransition( "Cancelled", "CancelRouteToDeploy", "Cancelled" ) Fsm:AddTransition( { "ArrivedAtPickup", "ArrivedAtDeploy", "Landing" }, "Land", "Landing" ) Fsm:AddTransition( "Landing", "Landed", "Landed" ) @@ -191,10 +600,14 @@ do -- TASK_CARGO Fsm:AddTransition( "*", "PrepareBoarding", "AwaitBoarding" ) Fsm:AddTransition( "AwaitBoarding", "Board", "Boarding" ) Fsm:AddTransition( "Boarding", "Boarded", "Boarded" ) + + Fsm:AddTransition( "*", "Load", "Loaded" ) Fsm:AddTransition( "*", "PrepareUnBoarding", "AwaitUnBoarding" ) Fsm:AddTransition( "AwaitUnBoarding", "UnBoard", "UnBoarding" ) Fsm:AddTransition( "UnBoarding", "UnBoarded", "UnBoarded" ) + + Fsm:AddTransition( "*", "Unload", "Unloaded" ) Fsm:AddTransition( "*", "Planned", "Planned" ) @@ -202,30 +615,31 @@ do -- TASK_CARGO Fsm:AddTransition( "Deployed", "Success", "Success" ) Fsm:AddTransition( "Rejected", "Reject", "Aborted" ) Fsm:AddTransition( "Failed", "Fail", "Failed" ) + + ---- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #TASK_CARGO Task + function Fsm:OnAfterAssigned( TaskUnit, Task ) + self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + + self:SelectAction() + end + --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_CARGO#TASK_CARGO Task + -- @param #TASK_CARGO Task function Fsm:onafterSelectAction( TaskUnit, Task ) local TaskUnitName = TaskUnit:GetName() - - self:F( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } ) - - local MenuTime = timer.getTime() - - TaskUnit.Menu = MENU_GROUP:New( TaskUnit:GetGroup(), Task:GetName() .. " @ " .. TaskUnit:GetName() ) - - local CargoItemCount = TaskUnit:CargoItemCount() - - --Task:GetMission():GetCommandCenter():MessageToGroup( "Cargo in carrier: " .. CargoItemCount, TaskUnit:GetGroup() ) - + local MenuTime = Task:InitTaskControlMenu( TaskUnit ) + local MenuControl = Task:GetTaskControlMenu( TaskUnit ) Task.SetCargo:ForEachCargo( - --- @param Core.Cargo#CARGO Cargo + --- @param Cargo.Cargo#CARGO Cargo function( Cargo ) if Cargo:IsAlive() then @@ -234,18 +648,26 @@ do -- TASK_CARGO -- MENU_GROUP_COMMAND:New( -- TaskUnit:GetGroup(), -- "Cancel Route " .. Cargo.Name, --- TaskUnit.Menu, +-- MenuControl, -- self.MenuRouteToPickupCancel, -- self, -- Cargo -- ):SetTime(MenuTime) -- end - self:F( { CargoUnloaded = Cargo:IsUnLoaded(), CargoLoaded = Cargo:IsLoaded(), CargoItemCount = CargoItemCount } ) + --self:F( { CargoUnloaded = Cargo:IsUnLoaded(), CargoLoaded = Cargo:IsLoaded(), CargoItemCount = CargoItemCount } ) + local TaskGroup = TaskUnit:GetGroup() + if Cargo:IsUnLoaded() then - if CargoItemCount <= Task.CargoLimit then - if Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then + local CargoBayFreeWeight = TaskUnit:GetCargoBayFreeWeight() + local CargoWeight = Cargo:GetWeight() + + self:F({CargoBayFreeWeight=CargoBayFreeWeight}) + + -- Only when there is space within the bay to load the next cargo item! + if CargoBayFreeWeight > CargoWeight then + if Cargo:IsInReportRadius( TaskUnit:GetPointVec2() ) then local NotInDeployZones = true for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do if Cargo:IsInZone( DeployZone ) then @@ -254,28 +676,106 @@ do -- TASK_CARGO end if NotInDeployZones then if not TaskUnit:InAir() then - MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), "Board cargo " .. Cargo.Name, TaskUnit.Menu, self.MenuBoardCargo, self, Cargo ):SetTime(MenuTime) - TaskUnit.Menu:SetTime( MenuTime ) + if Cargo:CanBoard() == true then + if Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then + Cargo:Report( "Ready for boarding.", "board", TaskUnit:GetGroup() ) + local BoardMenu = MENU_GROUP:New( TaskGroup, "Board cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, BoardMenu, self.MenuBoardCargo, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + else + Cargo:Report( "Board at " .. Cargo:GetCoordinate():ToString( TaskUnit:GetGroup() .. "." ), "reporting", TaskUnit:GetGroup() ) + end + else + if Cargo:CanLoad() == true then + if Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then + Cargo:Report( "Ready for loading.", "load", TaskUnit:GetGroup() ) + local LoadMenu = MENU_GROUP:New( TaskGroup, "Load cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, LoadMenu, self.MenuLoadCargo, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + else + Cargo:Report( "Load at " .. Cargo:GetCoordinate():ToString( TaskUnit:GetGroup() ) .. " within " .. Cargo.NearRadius .. ".", "reporting", TaskUnit:GetGroup() ) + end + else + --local Cargo = Cargo -- Cargo.CargoSlingload#CARGO_SLINGLOAD + if Cargo:CanSlingload() == true then + if Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then + Cargo:Report( "Ready for sling loading.", "slingload", TaskUnit:GetGroup() ) + local SlingloadMenu = MENU_GROUP:New( TaskGroup, "Slingload cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, SlingloadMenu, self.MenuLoadCargo, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + else + Cargo:Report( "Slingload at " .. Cargo:GetCoordinate():ToString( TaskUnit:GetGroup() ) .. ".", "reporting", TaskUnit:GetGroup() ) + end + end + end + end + else + Cargo:ReportResetAll( TaskUnit:GetGroup() ) end end else - MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), "Route to Pickup cargo " .. Cargo.Name, TaskUnit.Menu, self.MenuRouteToPickup, self, Cargo ):SetTime(MenuTime) - TaskUnit.Menu:SetTime( MenuTime ) + if not Cargo:IsDeployed() == true then + local RouteToPickupMenu = MENU_GROUP:New( TaskGroup, "Route to pickup cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" ) + --MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, RouteToPickupMenu, self.MenuRouteToPickup, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + Cargo:ReportResetAll( TaskUnit:GetGroup() ) + if Cargo:CanBoard() == true then + if not Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then + local BoardMenu = MENU_GROUP:New( TaskGroup, "Board cargo", RouteToPickupMenu ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, BoardMenu, self.MenuRouteToPickup, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + end + else + if Cargo:CanLoad() == true then + if not Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then + local LoadMenu = MENU_GROUP:New( TaskGroup, "Load cargo", RouteToPickupMenu ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, LoadMenu, self.MenuRouteToPickup, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + end + else + --local Cargo = Cargo -- Cargo.CargoSlingload#CARGO_SLINGLOAD + if Cargo:CanSlingload() == true then + if not Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then + local SlingloadMenu = MENU_GROUP:New( TaskGroup, "Slingload cargo", RouteToPickupMenu ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, SlingloadMenu, self.MenuRouteToPickup, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + end + end + end + end + end + end + end + + -- Cargo in deployzones are flagged as deployed. + for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do + if Cargo:IsInZone( DeployZone ) then + Task:I( { CargoIsDeployed = Task.CargoDeployed and "true" or "false" } ) + if Cargo:IsDeployed() == false then + Cargo:SetDeployed( true ) + -- Now we call a callback method to handle the CargoDeployed event. + Task:I( { CargoIsAlive = Cargo:IsAlive() and "true" or "false" } ) + if Cargo:IsAlive() then + Task:CargoDeployed( TaskUnit, Cargo, DeployZone ) + end + end + end + end + + end + + if Cargo:IsLoaded() == true and Cargo:IsLoadedInCarrier( TaskUnit ) == true then + if not TaskUnit:InAir() then + if Cargo:CanUnboard() == true then + local UnboardMenu = MENU_GROUP:New( TaskGroup, "Unboard cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, UnboardMenu, self.MenuUnboardCargo, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + else + if Cargo:CanUnload() == true then + local UnloadMenu = MENU_GROUP:New( TaskGroup, "Unload cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, UnloadMenu, self.MenuUnloadCargo, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() + end end end end - - if Cargo:IsLoaded() then - if not TaskUnit:InAir() then - MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), "Unboard cargo " .. Cargo.Name, TaskUnit.Menu, self.MenuUnBoardCargo, self, Cargo ):SetTime(MenuTime) - TaskUnit.Menu:SetTime( MenuTime ) - end - -- Deployzones are optional zones that can be selected to request routing information. - for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do - if not Cargo:IsInZone( DeployZone ) then - MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), "Route to Deploy cargo at " .. DeployZoneName, TaskUnit.Menu, self.MenuRouteToDeploy, self, DeployZone ):SetTime(MenuTime) - TaskUnit.Menu:SetTime( MenuTime ) - end + + -- Deployzones are optional zones that can be selected to request routing information. + for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do + if not Cargo:IsInZone( DeployZone ) then + local RouteToDeployMenu = MENU_GROUP:New( TaskGroup, "Route to deploy cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" ) + MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), "Zone " .. DeployZoneName, RouteToDeployMenu, self.MenuRouteToDeploy, self, DeployZone ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent() end end end @@ -283,10 +783,9 @@ do -- TASK_CARGO end ) - TaskUnit.Menu:Remove( MenuTime ) + Task:RefreshTaskControlMenu( TaskUnit, MenuTime, "Cargo" ) - - self:__SelectAction( -15 ) + self:__SelectAction( -1 ) end @@ -294,21 +793,31 @@ do -- TASK_CARGO --- -- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit - -- @param Tasking.Task_Cargo#TASK_CARGO Task + -- @param #TASK_CARGO Task function Fsm:OnLeaveWaitingForCommand( TaskUnit, Task ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - TaskUnit.Menu:Remove() + --local MenuControl = Task:GetTaskControlMenu( TaskUnit ) + + --MenuControl:Remove() end function Fsm:MenuBoardCargo( Cargo ) self:__PrepareBoarding( 1.0, Cargo ) end - function Fsm:MenuUnBoardCargo( Cargo, DeployZone ) + function Fsm:MenuLoadCargo( Cargo ) + self:__Load( 1.0, Cargo ) + end + + function Fsm:MenuUnboardCargo( Cargo, DeployZone ) self:__PrepareUnBoarding( 1.0, Cargo, DeployZone ) end + function Fsm:MenuUnloadCargo( Cargo, DeployZone ) + self:__Unload( 1.0, Cargo, DeployZone ) + end + function Fsm:MenuRouteToPickup( Cargo ) self:__RouteToPickup( 1.0, Cargo ) end @@ -335,7 +844,7 @@ do -- TASK_CARGO self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) if Cargo:IsAlive() then - self.Cargo = Cargo -- Core.Cargo#CARGO + self.Cargo = Cargo -- Cargo.Cargo#CARGO Task:SetCargoPickup( self.Cargo, TaskUnit ) self:__RouteToPickupPoint( -0.1 ) end @@ -350,7 +859,6 @@ do -- TASK_CARGO function Fsm:onafterArriveAtPickup( TaskUnit, Task ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) if self.Cargo:IsAlive() then - self.Cargo:Smoke( Task:GetSmokeColor(), 15 ) if TaskUnit:IsAir() then Task:GetMission():GetCommandCenter():MessageToGroup( "Land", TaskUnit:GetGroup() ) self:__Land( -0.1, "Pickup" ) @@ -367,6 +875,7 @@ do -- TASK_CARGO function Fsm:onafterCancelRouteToPickup( TaskUnit, Task ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + Task:GetMission():GetCommandCenter():MessageToGroup( "Cancelled routing to Cargo " .. self.Cargo:GetName(), TaskUnit:GetGroup() ) self:__SelectAction( -0.1 ) end @@ -404,6 +913,7 @@ do -- TASK_CARGO function Fsm:onafterCancelRouteToDeploy( TaskUnit, Task ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) + Task:GetMission():GetCommandCenter():MessageToGroup( "Cancelled routing to deploy zone " .. self.DeployZone:GetName(), TaskUnit:GetGroup() ) self:__SelectAction( -0.1 ) end @@ -415,19 +925,30 @@ do -- TASK_CARGO function Fsm:onafterLand( TaskUnit, Task, From, Event, To, Action ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - if self.Cargo:IsAlive() then - if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then - if TaskUnit:InAir() then - self:__Land( -10, Action ) + if Action == "Pickup" then + if self.Cargo:IsAlive() then + if self.Cargo:IsInReportRadius( TaskUnit:GetPointVec2() ) then + if TaskUnit:InAir() then + self:__Land( -10, Action ) + else + Task:GetMission():GetCommandCenter():MessageToGroup( "Landed at pickup location...", TaskUnit:GetGroup() ) + self:__Landed( -0.1, Action ) + end else - Task:GetMission():GetCommandCenter():MessageToGroup( "Landed ...", TaskUnit:GetGroup() ) - self:__Landed( -0.1, Action ) + self:__RouteToPickup( -0.1, self.Cargo ) end - else - if Action == "Pickup" then - self:__RouteToPickupZone( -0.1 ) + end + else + if TaskUnit:IsAlive() then + if TaskUnit:IsInZone( self.DeployZone ) then + if TaskUnit:InAir() then + self:__Land( -10, Action ) + else + Task:GetMission():GetCommandCenter():MessageToGroup( "Landed at deploy zone " .. self.DeployZone:GetName(), TaskUnit:GetGroup() ) + self:__Landed( -0.1, Action ) + end else - self:__RouteToDeployZone( -0.1 ) + self:__RouteToDeploy( -0.1, self.Cargo ) end end end @@ -439,18 +960,28 @@ do -- TASK_CARGO function Fsm:onafterLanded( TaskUnit, Task, From, Event, To, Action ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - if self.Cargo:IsAlive() then - if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then - if TaskUnit:InAir() then - self:__Land( -0.1, Action ) + if Action == "Pickup" then + if self.Cargo:IsAlive() then + if self.Cargo:IsInReportRadius( TaskUnit:GetPointVec2() ) then + if TaskUnit:InAir() then + self:__Land( -0.1, Action ) + else + self:__SelectAction( -0.1 ) + end else - self:__SelectAction( -0.1 ) + self:__RouteToPickup( -0.1, self.Cargo ) end - else - if Action == "Pickup" then - self:__RouteToPickupZone( -0.1 ) + end + else + if TaskUnit:IsAlive() then + if TaskUnit:IsInZone( self.DeployZone ) then + if TaskUnit:InAir() then + self:__Land( -10, Action ) + else + self:__SelectAction( -0.1 ) + end else - self:__RouteToDeployZone( -0.1 ) + self:__RouteToDeploy( -0.1, self.Cargo ) end end end @@ -463,30 +994,30 @@ do -- TASK_CARGO self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) if Cargo and Cargo:IsAlive() then - self.Cargo = Cargo -- Core.Cargo#CARGO_GROUP - self:__Board( -0.1 ) + self:__Board( -0.1, Cargo ) end end + --- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:onafterBoard( TaskUnit, Task ) + function Fsm:onafterBoard( TaskUnit, Task, From, Event, To, Cargo ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } ) - function self.Cargo:OnEnterLoaded( From, Event, To, TaskUnit, TaskProcess ) + function Cargo:OnEnterLoaded( From, Event, To, TaskUnit, TaskProcess ) self:F({From, Event, To, TaskUnit, TaskProcess }) - TaskProcess:__Boarded( 0.1 ) + TaskProcess:__Boarded( 0.1, self ) end - if self.Cargo:IsAlive() then - if self.Cargo:IsInRadius( TaskUnit:GetPointVec2() ) then + if Cargo:IsAlive() then + if Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then if TaskUnit:InAir() then --- ABORT the boarding. Split group if any and go back to select action. else - self.Cargo:MessageToGroup( "Boarding ...", TaskUnit:GetGroup() ) - if not self.Cargo:IsBoarding() then - self.Cargo:Board( TaskUnit, 20, self ) + Cargo:MessageToGroup( "Boarding ...", TaskUnit:GetGroup() ) + if not Cargo:IsBoarding() then + Cargo:Board( TaskUnit, nil, self ) end end else @@ -499,24 +1030,36 @@ do -- TASK_CARGO --- @param #FSM_PROCESS self -- @param Wrapper.Unit#UNIT TaskUnit -- @param Tasking.Task_Cargo#TASK_CARGO Task - function Fsm:onafterBoarded( TaskUnit, Task ) + function Fsm:onafterBoarded( TaskUnit, Task, From, Event, To, Cargo ) + + local TaskUnitName = TaskUnit:GetName() + self:F( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } ) + + Cargo:MessageToGroup( "Boarded cargo " .. Cargo:GetName(), TaskUnit:GetGroup() ) + + self:__Load( -0.1, Cargo ) + + end + + + --- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:onafterLoad( TaskUnit, Task, From, Event, To, Cargo ) local TaskUnitName = TaskUnit:GetName() self:F( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } ) - self.Cargo:MessageToGroup( "Boarded ...", TaskUnit:GetGroup() ) - - TaskUnit:AddCargo( self.Cargo ) - - self:__SelectAction( 1 ) - - -- TODO:I need to find a more decent solution for this. - Task:E( { CargoPickedUp = Task.CargoPickedUp } ) - if self.Cargo:IsAlive() then - if Task.CargoPickedUp then - Task:CargoPickedUp( TaskUnit, self.Cargo ) - end + if not Cargo:IsLoaded() then + Cargo:Load( TaskUnit ) end + + Cargo:MessageToGroup( "Loaded cargo " .. Cargo:GetName(), TaskUnit:GetGroup() ) + TaskUnit:AddCargo( Cargo ) + + Task:CargoPickedUp( TaskUnit, Cargo ) + + self:SelectAction( -1 ) end @@ -530,7 +1073,7 @@ do -- TASK_CARGO -- @param To -- @param Cargo -- @param Core.Zone#ZONE_BASE DeployZone - function Fsm:onafterPrepareUnBoarding( TaskUnit, Task, From, Event, To, Cargo ) + function Fsm:onafterPrepareUnBoarding( TaskUnit, Task, From, Event, To, Cargo ) self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID(), From, Event, To, Cargo } ) self.Cargo = Cargo @@ -568,9 +1111,9 @@ do -- TASK_CARGO if self.Cargo:IsAlive() then self.Cargo:MessageToGroup( "UnBoarding ...", TaskUnit:GetGroup() ) if DeployZone then - self.Cargo:UnBoard( DeployZone:GetPointVec2(), 400, self ) + self.Cargo:UnBoard( DeployZone:GetCoordinate():GetRandomCoordinateInRadius( 25, 10 ), 400, self ) else - self.Cargo:UnBoard( TaskUnit:GetPointVec2():AddX(60), 400, self ) + self.Cargo:UnBoard( TaskUnit:GetCoordinate():GetRandomCoordinateInRadius( 25, 10 ), 400, self ) end end end @@ -585,34 +1128,34 @@ do -- TASK_CARGO local TaskUnitName = TaskUnit:GetName() self:F( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } ) - self.Cargo:MessageToGroup( "UnBoarded ...", TaskUnit:GetGroup() ) - - TaskUnit:RemoveCargo( self.Cargo ) - - local NotInDeployZones = true - for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do - if self.Cargo:IsInZone( DeployZone ) then - NotInDeployZones = false - end - end + self.Cargo:MessageToGroup( "UnBoarded cargo " .. self.Cargo:GetName(), TaskUnit:GetGroup() ) - if NotInDeployZones == false then - self.Cargo:SetDeployed( true ) - end - - -- TODO:I need to find a more decent solution for this. - Task:E( { CargoDeployed = Task.CargoDeployed and "true" or "false" } ) - Task:E( { CargoIsAlive = self.Cargo:IsAlive() and "true" or "false" } ) - if self.Cargo:IsAlive() then - if Task.CargoDeployed then - Task:CargoDeployed( TaskUnit, self.Cargo, self.DeployZone ) - end - end + self:Unload( self.Cargo ) + end + + --- + -- @param #FSM_PROCESS self + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param Tasking.Task_Cargo#TASK_CARGO Task + function Fsm:onafterUnload( TaskUnit, Task, From, Event, To, Cargo, DeployZone ) + + local TaskUnitName = TaskUnit:GetName() + self:F( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } ) + if not Cargo:IsUnLoaded() then + if DeployZone then + Cargo:UnLoad( DeployZone:GetCoordinate():GetRandomCoordinateInRadius( 25, 10 ), 400, self ) + else + Cargo:UnLoad( TaskUnit:GetCoordinate():GetRandomCoordinateInRadius( 25, 10 ), 400, self ) + end + end + TaskUnit:RemoveCargo( Cargo ) + + Cargo:MessageToGroup( "Unloaded cargo " .. Cargo:GetName(), TaskUnit:GetGroup() ) + self:Planned() self:__SelectAction( 1 ) end - return self @@ -675,13 +1218,17 @@ do -- TASK_CARGO self:F({Cargo, TaskUnit}) local ProcessUnit = self:GetUnitProcess( TaskUnit ) + + local MenuTime = self:InitTaskControlMenu( TaskUnit ) + local MenuControl = self:GetTaskControlMenu( TaskUnit ) local ActRouteCargo = ProcessUnit:GetProcess( "RoutingToPickup", "RouteToPickupPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT ActRouteCargo:Reset() ActRouteCargo:SetCoordinate( Cargo:GetCoordinate() ) - ActRouteCargo:SetRange( Cargo:GetBoardingRange() ) - ActRouteCargo:SetMenuCancel( TaskUnit:GetGroup(), "Cancel Routing to Cargo " .. Cargo:GetName(), TaskUnit.Menu ) + ActRouteCargo:SetRange( Cargo:GetLoadRadius() ) + ActRouteCargo:SetMenuCancel( TaskUnit:GetGroup(), "Cancel Routing to Cargo " .. Cargo:GetName(), MenuControl, MenuTime, "Cargo" ) ActRouteCargo:Start() + return self end @@ -694,11 +1241,15 @@ do -- TASK_CARGO local ProcessUnit = self:GetUnitProcess( TaskUnit ) + local MenuTime = self:InitTaskControlMenu( TaskUnit ) + local MenuControl = self:GetTaskControlMenu( TaskUnit ) + local ActRouteDeployZone = ProcessUnit:GetProcess( "RoutingToDeploy", "RouteToDeployZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE ActRouteDeployZone:Reset() ActRouteDeployZone:SetZone( DeployZone ) - ActRouteDeployZone:SetMenuCancel( TaskUnit:GetGroup(), "Cancel Routing to Deploy Zone" .. DeployZone:GetName(), TaskUnit.Menu ) + ActRouteDeployZone:SetMenuCancel( TaskUnit:GetGroup(), "Cancel Routing to Deploy Zone" .. DeployZone:GetName(), MenuControl, MenuTime, "Cargo" ) ActRouteDeployZone:Start() + return self end @@ -726,12 +1277,12 @@ do -- TASK_CARGO end --- @param #TASK_CARGO self - -- @param @list DeployZones + -- @param #list DeployZones -- @param Wrapper.Unit#UNIT TaskUnit -- @return #TASK_CARGO function TASK_CARGO:SetDeployZones( DeployZones, TaskUnit ) - for DeployZoneID, DeployZone in pairs( DeployZones ) do + for DeployZoneID, DeployZone in pairs( DeployZones or {} ) do self.DeployZones[DeployZone:GetName()] = DeployZone end @@ -810,11 +1361,19 @@ do -- TASK_CARGO end --- @param #TASK_CARGO self - function TASK_CARGO:UpdateTaskInfo( DetectedItem ) + function TASK_CARGO:UpdateTaskInfo() if self:IsStatePlanned() or self:IsStateAssigned() then self.TaskInfo:AddTaskName( 0, "MSOD" ) self.TaskInfo:AddCargoSet( self.SetCargo, 10, "SOD", true ) + local Coordinates = {} + for CargoName, Cargo in pairs( self.SetCargo:GetSet() ) do + local Cargo = Cargo -- Cargo.Cargo#CARGO + if not Cargo:IsLoaded() then + Coordinates[#Coordinates+1] = Cargo:GetCoordinate() + end + end + self.TaskInfo:AddCoordinates( Coordinates, 1, "M" ) end end @@ -823,186 +1382,8 @@ do -- TASK_CARGO return 0 end + + end -do -- TASK_CARGO_TRANSPORT - - --- The TASK_CARGO_TRANSPORT class - -- @type TASK_CARGO_TRANSPORT - -- @extends #TASK_CARGO - TASK_CARGO_TRANSPORT = { - ClassName = "TASK_CARGO_TRANSPORT", - } - - --- Instantiates a new TASK_CARGO_TRANSPORT. - -- @param #TASK_CARGO_TRANSPORT self - -- @param Tasking.Mission#MISSION Mission - -- @param Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param Core.Set#SET_CARGO SetCargo The scope of the cargo to be transported. - -- @param #string TaskBriefing The Cargo Task briefing. - -- @return #TASK_CARGO_TRANSPORT self - function TASK_CARGO_TRANSPORT:New( Mission, SetGroup, TaskName, SetCargo, TaskBriefing ) - local self = BASE:Inherit( self, TASK_CARGO:New( Mission, SetGroup, TaskName, SetCargo, "Transport", TaskBriefing ) ) -- #TASK_CARGO_TRANSPORT - self:F() - - Mission:AddTask( self ) - - - -- Events - - self:AddTransition( "*", "CargoPickedUp", "*" ) - self:AddTransition( "*", "CargoDeployed", "*" ) - - self:F( { CargoDeployed = self.CargoDeployed ~= nil and "true" or "false" } ) - - --- OnBefore Transition Handler for Event CargoPickedUp. - -- @function [parent=#TASK_CARGO_TRANSPORT] OnBeforeCargoPickedUp - -- @param #TASK_CARGO_TRANSPORT self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event CargoPickedUp. - -- @function [parent=#TASK_CARGO_TRANSPORT] OnAfterCargoPickedUp - -- @param #TASK_CARGO_TRANSPORT self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - - --- Synchronous Event Trigger for Event CargoPickedUp. - -- @function [parent=#TASK_CARGO_TRANSPORT] CargoPickedUp - -- @param #TASK_CARGO_TRANSPORT self - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - - --- Asynchronous Event Trigger for Event CargoPickedUp. - -- @function [parent=#TASK_CARGO_TRANSPORT] __CargoPickedUp - -- @param #TASK_CARGO_TRANSPORT self - -- @param #number Delay The delay in seconds. - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - - --- OnBefore Transition Handler for Event CargoDeployed. - -- @function [parent=#TASK_CARGO_TRANSPORT] OnBeforeCargoDeployed - -- @param #TASK_CARGO_TRANSPORT self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded. - -- @return #boolean Return false to cancel Transition. - - --- OnAfter Transition Handler for Event CargoDeployed. - -- @function [parent=#TASK_CARGO_TRANSPORT] OnAfterCargoDeployed - -- @param #TASK_CARGO_TRANSPORT self - -- @param #string From The From State string. - -- @param #string Event The Event string. - -- @param #string To The To State string. - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded. - - --- Synchronous Event Trigger for Event CargoDeployed. - -- @function [parent=#TASK_CARGO_TRANSPORT] CargoDeployed - -- @param #TASK_CARGO_TRANSPORT self - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded. - - --- Asynchronous Event Trigger for Event CargoDeployed. - -- @function [parent=#TASK_CARGO_TRANSPORT] __CargoDeployed - -- @param #TASK_CARGO_TRANSPORT self - -- @param #number Delay The delay in seconds. - -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc. - -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. - -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded. - - local Fsm = self:GetUnitProcess() - - local CargoReport = REPORT:New( "Transport Cargo. The following cargo needs to be transported including initial positions:") - - SetCargo:ForEachCargo( - --- @param Core.Cargo#CARGO Cargo - function( Cargo ) - local CargoType = Cargo:GetType() - local CargoName = Cargo:GetName() - local CargoCoordinate = Cargo:GetCoordinate() - CargoReport:Add( string.format( '- "%s" (%s) at %s', CargoName, CargoType, CargoCoordinate:ToStringMGRS() ) ) - end - ) - - self:SetBriefing( - TaskBriefing or - CargoReport:Text() - ) - - - return self - end - - function TASK_CARGO_TRANSPORT:ReportOrder( ReportGroup ) - - return 0 - end - - - --- - -- @param #TASK_CARGO_TRANSPORT self - -- @return #boolean - function TASK_CARGO_TRANSPORT:IsAllCargoTransported() - - local CargoSet = self:GetCargoSet() - local Set = CargoSet:GetSet() - - local DeployZones = self:GetDeployZones() - - local CargoDeployed = true - - -- Loop the CargoSet (so evaluate each Cargo in the SET_CARGO ). - for CargoID, CargoData in pairs( Set ) do - local Cargo = CargoData -- Core.Cargo#CARGO - - self:F( { Cargo = Cargo:GetName(), CargoDeployed = Cargo:IsDeployed() } ) - - if Cargo:IsDeployed() then - --- -- Loop the DeployZones set for the TASK_CARGO_TRANSPORT. --- for DeployZoneID, DeployZone in pairs( DeployZones ) do --- --- -- If all cargo is in one of the deploy zones, then all is good. --- self:T( { Cargo.CargoObject } ) --- if Cargo:IsInZone( DeployZone ) == false then --- CargoDeployed = false --- end --- end - else - CargoDeployed = false - end - end - - self:F( { CargoDeployed = CargoDeployed } ) - - return CargoDeployed - end - - --- @param #TASK_CARGO_TRANSPORT self - function TASK_CARGO_TRANSPORT:onafterGoal( TaskUnit, From, Event, To ) - local CargoSet = self.CargoSet - - if self:IsAllCargoTransported() then - self:Success() - end - - self:__Goal( -10 ) - end - -end - diff --git a/Moose Development/Moose/Tasking/Task_Cargo_CSAR.lua b/Moose Development/Moose/Tasking/Task_Cargo_CSAR.lua new file mode 100644 index 000000000..7b099980f --- /dev/null +++ b/Moose Development/Moose/Tasking/Task_Cargo_CSAR.lua @@ -0,0 +1,393 @@ +--- **Tasking** -- Orchestrates the task for players to execute CSAR for downed pilots. +-- +-- **Specific features:** +-- +-- * Creates a task to retrieve a pilot @{Cargo.Cargo} from behind enemy lines. +-- * Derived from the TASK_CARGO class, which is derived from the TASK class. +-- * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled. +-- * Co-operation tasking, so a player joins a group of players executing the same task. +-- +-- +-- **A complete task menu system to allow players to:** +-- +-- * Join the task, abort the task. +-- * Mark the task location on the map. +-- * Provide details of the target. +-- * Route to the cargo. +-- * Route to the deploy zones. +-- * Load/Unload cargo. +-- * Board/Unboard cargo. +-- * Slingload cargo. +-- * Display the task briefing. +-- +-- +-- **A complete mission menu system to allow players to:** +-- +-- * Join a task, abort the task. +-- * Display task reports. +-- * Display mission statistics. +-- * Mark the task locations on the map. +-- * Provide details of the targets. +-- * Display the mission briefing. +-- * Provide status updates as retrieved from the command center. +-- * Automatically assign a random task as part of a mission. +-- * Manually assign a specific task as part of a mission. +-- +-- +-- **A settings system, using the settings menu:** +-- +-- * Tweak the duration of the display of messages. +-- * Switch between metric and imperial measurement system. +-- * Switch between coordinate formats used in messages: BR, BRA, LL DMS, LL DDM, MGRS. +-- * Different settings modes for A2G and A2A operations. +-- * Various other options. +-- +-- === +-- +-- Please read through the @{Tasking.Task_Cargo} process to understand the mechanisms of tasking and cargo tasking and handling. +-- +-- The cargo will be a downed pilot, which is located somwhere on the battlefield. Use the menus system and facilities to +-- join the CSAR task, and retrieve the pilot from behind enemy lines. The menu system is generic, there is nothing +-- specific on a CSAR task that requires further explanation, than reading the generic TASK_CARGO explanations. +-- +-- Enjoy! +-- FC +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.Task_Cargo_CSAR +-- @image Task_Cargo_CSAR.JPG + + +do -- TASK_CARGO_CSAR + + --- @type TASK_CARGO_CSAR + -- @extends Tasking.Task_Cargo#TASK_CARGO + + --- Orchestrates the task for players to execute CSAR for downed pilots. + -- + -- CSAR tasks are suited to govern the process of return downed pilots behind enemy lines back to safetly. + -- Typically, this task is executed by helicopter pilots, but it can also be executed by ground forces! + -- + -- === + -- + -- A CSAR task can be created manually, but actually, it is better to **GENERATE** these tasks using the + -- @{Tasking.Task_Cargo_Dispatcher} module. + -- + -- Using the dispatcher, CSAR tasks will be created **automatically** when a pilot ejects from a damaged AI aircraft. + -- When this happens, the pilot actually will survive, but needs to be retrieved from behind enemy lines. + -- + -- # 1) Create a CSAR task manually (code it). + -- + -- Although it is recommended to use the dispatcher, you can create a CSAR task yourself as a mission designer. + -- It is easy, as it works just like any other task setup. + -- + -- ## 1.1) Create a command center. + -- + -- First you need to create a command center using the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor. + -- + -- local CommandCenter = COMMANDCENTER + -- :New( HQ, "Lima" ) -- Create the CommandCenter. + -- + -- ## 1.2) Create a mission. + -- + -- Tasks work in a mission, which groups these tasks to achieve a joint mission goal. + -- A command center can govern multiple missions. + -- Create a new mission, using the @{Tasking.Mission#MISSION.New}() constructor. + -- + -- -- Declare the Mission for the Command Center. + -- local Mission = MISSION + -- :New( CommandCenter, + -- "Overlord", + -- "High", + -- "Retrieve the downed pilots.", + -- coalition.side.RED + -- ) + -- + -- ## 1.3) Create the CSAR cargo task. + -- + -- So, now that we have a command center and a mission, we now create the CSAR task. + -- We create the CSAR task using the @{#TASK_CARGO_CSAR.New}() constructor. + -- + -- Because a CSAR task will not generate the cargo itself, you'll need to create it first. + -- The cargo in this case will be the downed pilot! + -- + -- -- Here we define the "cargo set", which is a collection of cargo objects. + -- -- The cargo set will be the input for the cargo transportation task. + -- -- So a transportation object is handling a cargo set, which is automatically refreshed when new cargo is added/deleted. + -- local CargoSet = SET_CARGO:New():FilterTypes( "Pilots" ):FilterStart() + -- + -- -- Now we add cargo into the battle scene. + -- local PilotGroup = GROUP:FindByName( "Pilot" ) + -- + -- -- CARGO_GROUP can be used to setup cargo with a GROUP object underneath. + -- -- We name this group Engineers. + -- -- Note that the name of the cargo is "Engineers". + -- -- The cargoset "CargoSet" will embed all defined cargo of type "Pilots" (prefix) into its set. + -- local CargoGroup = CARGO_GROUP:New( PilotGroup, "Pilots", "Downed Pilot", 500 ) + -- + -- What is also needed, is to have a set of @{Core.Group}s defined that contains the clients of the players. + -- + -- -- Allocate the Transport, which are the helicopter to retrieve the pilot, that can be manned by players. + -- local GroupSet = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart() + -- + -- Now that we have a CargoSet and a GroupSet, we can now create the CSARTask manually. + -- + -- -- Declare the CSAR task. + -- local CSARTask = TASK_CARGO_CSAR + -- :New( Mission, + -- GroupSet, + -- "CSAR Pilot", + -- CargoSet, + -- "Fly behind enemy lines, and retrieve the downed pilot." + -- ) + -- + -- So you can see, setting up a CSAR task manually is a lot of work. + -- It is better you use the cargo dispatcher to generate CSAR tasks and it will work as it is intended. + -- By doing this, CSAR tasking will become a dynamic experience. + -- + -- # 2) Create a task using the @{Tasking.Task_Cargo_Dispatcher} module. + -- + -- Actually, it is better to **GENERATE** these tasks using the @{Tasking.Task_Cargo_Dispatcher} module. + -- Using the dispatcher module, transport tasks can be created much more easy. + -- + -- Find below an example how to use the TASK_CARGO_DISPATCHER class: + -- + -- + -- -- Find the HQ group. + -- HQ = GROUP:FindByName( "HQ", "Bravo" ) + -- + -- -- Create the command center with the name "Lima". + -- CommandCenter = COMMANDCENTER + -- :New( HQ, "Lima" ) + -- + -- -- Create the mission, for the command center, with the name "CSAR Mission", a "Tactical" mission, with the mission briefing "Rescue downed pilots.", for the RED coalition. + -- Mission = MISSION + -- :New( CommandCenter, "CSAR Mission", "Tactical", "Rescue downed pilots.", coalition.side.RED ) + -- + -- -- Create the SET of GROUPs containing clients (players) that will transport the cargo. + -- -- These are have a name that start with "Rescue" and are of the "red" coalition. + -- AttackGroups = SET_GROUP:New():FilterCoalitions( "red" ):FilterPrefixes( "Rescue" ):FilterStart() + -- + -- + -- -- Here we create the TASK_CARGO_DISPATCHER object! This is where we assign the dispatcher to generate tasks in the Mission for the AttackGroups. + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, AttackGroups ) + -- + -- + -- -- Here the task dispatcher will generate automatically CSAR tasks once a pilot ejects. + -- TaskDispatcher:StartCSARTasks( + -- "CSAR", + -- { ZONE_UNIT:New( "Hospital", STATIC:FindByName( "Hospital" ), 100 ) }, + -- "One of our pilots has ejected. Go out to Search and Rescue our pilot!\n" .. + -- "Use the radio menu to let the command center assist you with the CSAR tasking." + -- ) + -- + -- # 3) Handle cargo task events. + -- + -- When a player is picking up and deploying cargo using his carrier, events are generated by the tasks. These events can be captured and tailored with your own code. + -- + -- In order to properly capture the events and avoid mistakes using the documentation, it is advised that you execute the following actions: + -- + -- * **Copy / Paste** the code section into your script. + -- * **Change** the CLASS literal to the task object name you have in your script. + -- * Within the function, you can now **write your own code**! + -- * **IntelliSense** will recognize the type of the variables provided by the function. Note: the From, Event and To variables can be safely ignored, + -- but you need to declare them as they are automatically provided by the event handling system of MOOSE. + -- + -- You can send messages or fire off any other events within the code section. The sky is the limit! + -- + -- NOTE: CSAR tasks are actually automatically created by the TASK_CARGO_DISPATCHER. So the underlying is not really applicable for mission designers as they will use the dispatcher instead + -- of capturing these events from manually created CSAR tasks! + -- + -- ## 3.1) Handle the **CargoPickedUp** event. + -- + -- Find below an example how to tailor the **CargoPickedUp** event, generated by the CSARTask: + -- + -- function CSARTask:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo ) + -- + -- MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has picked up cargo.", MESSAGE.Type.Information ):ToAll() + -- + -- end + -- + -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has picked up a cargo object in the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- + -- --- CargoPickedUp event handler OnAfter for CLASS. + -- -- @param #CLASS self + -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has picked up the cargo. + -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been picked up. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! + -- function CLASS:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo ) + -- + -- -- Write here your own code. + -- + -- end + -- + -- + -- ## 3.2) Handle the **CargoDeployed** event. + -- + -- Find below an example how to tailor the **CargoDeployed** event, generated by the CSARTask: + -- + -- function CSARTask:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) + -- + -- MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has deployed cargo at zone " .. DeployZone:GetName(), MESSAGE.Type.Information ):ToAll() + -- + -- end + -- + -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has deployed a cargo object from the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- + -- + -- --- CargoDeployed event handler OnAfter for CLASS. + -- -- @param #CLASS self + -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has deployed the cargo. + -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been deployed. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! + -- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + -- function CLASS:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) + -- + -- -- Write here your own code. + -- + -- end + -- + -- === + -- + -- @field #TASK_CARGO_CSAR + TASK_CARGO_CSAR = { + ClassName = "TASK_CARGO_CSAR", + } + + --- Instantiates a new TASK_CARGO_CSAR. + -- @param #TASK_CARGO_CSAR self + -- @param Tasking.Mission#MISSION Mission + -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Core.Set#SET_CARGO SetCargo The scope of the cargo to be transported. + -- @param #string TaskBriefing The Cargo Task briefing. + -- @return #TASK_CARGO_CSAR self + function TASK_CARGO_CSAR:New( Mission, SetGroup, TaskName, SetCargo, TaskBriefing ) + local self = BASE:Inherit( self, TASK_CARGO:New( Mission, SetGroup, TaskName, SetCargo, "CSAR", TaskBriefing ) ) -- #TASK_CARGO_CSAR + self:F() + + Mission:AddTask( self ) + + + -- Events + + self:AddTransition( "*", "CargoPickedUp", "*" ) + self:AddTransition( "*", "CargoDeployed", "*" ) + + self:F( { CargoDeployed = self.CargoDeployed ~= nil and "true" or "false" } ) + + --- OnAfter Transition Handler for Event CargoPickedUp. + -- @function [parent=#TASK_CARGO_CSAR] OnAfterCargoPickedUp + -- @param #TASK_CARGO_CSAR self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc. + -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. + + --- OnAfter Transition Handler for Event CargoDeployed. + -- @function [parent=#TASK_CARGO_CSAR] OnAfterCargoDeployed + -- @param #TASK_CARGO_CSAR self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc. + -- @param Core.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status. + -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded. + + local Fsm = self:GetUnitProcess() + + local CargoReport = REPORT:New( "Rescue a downed pilot from the following position:") + + SetCargo:ForEachCargo( + --- @param Core.Cargo#CARGO Cargo + function( Cargo ) + local CargoType = Cargo:GetType() + local CargoName = Cargo:GetName() + local CargoCoordinate = Cargo:GetCoordinate() + CargoReport:Add( string.format( '- "%s" (%s) at %s', CargoName, CargoType, CargoCoordinate:ToStringMGRS() ) ) + end + ) + + self:SetBriefing( + TaskBriefing or + CargoReport:Text() + ) + + + return self + end + + + + function TASK_CARGO_CSAR:ReportOrder( ReportGroup ) + + return 0 + end + + + --- + -- @param #TASK_CARGO_CSAR self + -- @return #boolean + function TASK_CARGO_CSAR:IsAllCargoTransported() + + local CargoSet = self:GetCargoSet() + local Set = CargoSet:GetSet() + + local DeployZones = self:GetDeployZones() + + local CargoDeployed = true + + -- Loop the CargoSet (so evaluate each Cargo in the SET_CARGO ). + for CargoID, CargoData in pairs( Set ) do + local Cargo = CargoData -- Core.Cargo#CARGO + + self:F( { Cargo = Cargo:GetName(), CargoDeployed = Cargo:IsDeployed() } ) + + if Cargo:IsDeployed() then + +-- -- Loop the DeployZones set for the TASK_CARGO_CSAR. +-- for DeployZoneID, DeployZone in pairs( DeployZones ) do +-- +-- -- If all cargo is in one of the deploy zones, then all is good. +-- self:T( { Cargo.CargoObject } ) +-- if Cargo:IsInZone( DeployZone ) == false then +-- CargoDeployed = false +-- end +-- end + else + CargoDeployed = false + end + end + + self:F( { CargoDeployed = CargoDeployed } ) + + return CargoDeployed + end + + --- @param #TASK_CARGO_CSAR self + function TASK_CARGO_CSAR:onafterGoal( TaskUnit, From, Event, To ) + local CargoSet = self.CargoSet + + if self:IsAllCargoTransported() then + self:Success() + end + + self:__Goal( -10 ) + end + +end + diff --git a/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua b/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua new file mode 100644 index 000000000..6833f197c --- /dev/null +++ b/Moose Development/Moose/Tasking/Task_Cargo_Dispatcher.lua @@ -0,0 +1,910 @@ +--- **Tasking** - Creates and manages player TASK_CARGO tasks. +-- +-- The **TASK_CARGO_DISPATCHER** allows you to setup various tasks for let human +-- players transport cargo as part of a task. +-- +-- The cargo dispatcher will implement for you mechanisms to create cargo transportation tasks: +-- +-- * As setup by the mission designer. +-- * Dynamically create CSAR missions (when a pilot is downed as part of a downed plane). +-- * Dynamically spawn new cargo and create cargo taskings! +-- +-- +-- +-- **Specific features:** +-- +-- * Creates a task to transport @{Cargo.Cargo} to and between deployment zones. +-- * Derived from the TASK_CARGO class, which is derived from the TASK class. +-- * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled. +-- * Co-operation tasking, so a player joins a group of players executing the same task. +-- +-- +-- **A complete task menu system to allow players to:** +-- +-- * Join the task, abort the task. +-- * Mark the task location on the map. +-- * Provide details of the target. +-- * Route to the cargo. +-- * Route to the deploy zones. +-- * Load/Unload cargo. +-- * Board/Unboard cargo. +-- * Slingload cargo. +-- * Display the task briefing. +-- +-- +-- **A complete mission menu system to allow players to:** +-- +-- * Join a task, abort the task. +-- * Display task reports. +-- * Display mission statistics. +-- * Mark the task locations on the map. +-- * Provide details of the targets. +-- * Display the mission briefing. +-- * Provide status updates as retrieved from the command center. +-- * Automatically assign a random task as part of a mission. +-- * Manually assign a specific task as part of a mission. +-- +-- +-- **A settings system, using the settings menu:** +-- +-- * Tweak the duration of the display of messages. +-- * Switch between metric and imperial measurement system. +-- * Switch between coordinate formats used in messages: BR, BRA, LL DMS, LL DDM, MGRS. +-- * Different settings modes for A2G and A2A operations. +-- * Various other options. +-- +-- === +-- +-- ### Author: **FlightControl** +-- +-- ### Contributions: +-- +-- === +-- +-- @module Tasking.Task_Cargo_Dispatcher +-- @image Task_Cargo_Dispatcher.JPG + +do -- TASK_CARGO_DISPATCHER + + --- TASK_CARGO_DISPATCHER class. + -- @type TASK_CARGO_DISPATCHER + -- @extends Tasking.Task_Manager#TASK_MANAGER + -- @field TASK_CARGO_DISPATCHER.CSAR CSAR + -- @field Core.Set#SET_ZONE SetZonesCSAR + + --- @type TASK_CARGO_DISPATCHER.CSAR + -- @field Wrapper.Unit#UNIT PilotUnit + -- @field Tasking.Task#TASK Task + + + --- Implements the dynamic dispatching of cargo tasks. + -- + -- The **TASK_CARGO_DISPATCHER** allows you to setup various tasks for let human + -- players transport cargo as part of a task. + -- + -- There are currently **two types of tasks** that can be constructed: + -- + -- * A **normal cargo transport** task, which tasks humans to transport cargo from a location towards a deploy zone. + -- * A **CSAR** cargo transport task. CSAR tasks are **automatically generated** when a friendly (AI) plane is downed and the friendly pilot ejects... + -- You as a player (the helo pilot) can go out in the battlefield, fly behind enemy lines, and rescue the pilot (back to a deploy zone). + -- + -- Let's explore **step by step** how to setup the task cargo dispatcher. + -- + -- # 1. Setup a mission environment. + -- + -- It is easy, as it works just like any other task setup, so setup a command center and a mission. + -- + -- ## 1.1. Create a command center. + -- + -- First you need to create a command center using the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor. + -- + -- local CommandCenter = COMMANDCENTER + -- :New( HQ, "Lima" ) -- Create the CommandCenter. + -- + -- ## 1.2. Create a mission. + -- + -- Tasks work in a mission, which groups these tasks to achieve a joint mission goal. + -- A command center can govern multiple missions. + -- Create a new mission, using the @{Tasking.Mission#MISSION.New}() constructor. + -- + -- -- Declare the Mission for the Command Center. + -- local Mission = MISSION + -- :New( CommandCenter, + -- "Overlord", + -- "High", + -- "Transport the cargo.", + -- coalition.side.RED + -- ) + -- + -- + -- # 2. Dispatch a **transport cargo** task. + -- + -- So, now that we have a command center and a mission, we now create the transport task. + -- We create the transport task using the @{#TASK_CARGO_DISPATCHER.AddTransportTask}() constructor. + -- + -- ## 2.1. Create the cargo in the mission. + -- + -- Because a transport task will not generate the cargo itself, you'll need to create it first. + -- + -- -- Here we define the "cargo set", which is a collection of cargo objects. + -- -- The cargo set will be the input for the cargo transportation task. + -- -- So a transportation object is handling a cargo set, which is automatically updated when new cargo is added/deleted. + -- local WorkmaterialsCargoSet = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() + -- + -- -- Now we add cargo into the battle scene. + -- local PilotGroup = GROUP:FindByName( "Engineers" ) + -- + -- -- CARGO_GROUP can be used to setup cargo with a GROUP object underneath. + -- -- We name the type of this group "Workmaterials", so that this cargo group will be included within the WorkmaterialsCargoSet. + -- -- Note that the name of the cargo is "Engineer Team 1". + -- local CargoGroup = CARGO_GROUP:New( PilotGroup, "Workmaterials", "Engineer Team 1", 500 ) + -- + -- What is also needed, is to have a set of @{Core.Group}s defined that contains the clients of the players. + -- + -- -- Allocate the Transport, which are the helicopters to retrieve the pilot, that can be manned by players. + -- -- The name of these helicopter groups containing one client begins with "Transport", as modelled within the mission editor. + -- local PilotGroupSet = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart() + -- + -- ## 2.2. Setup the cargo transport task. + -- + -- First, we need to create a TASK_CARGO_DISPATCHER object. + -- + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, PilotGroupSet ) + -- + -- So, the variable `TaskDispatcher` will contain the object of class TASK_CARGO_DISPATCHER, which will allow you to dispatch cargo transport tasks: + -- + -- * for mission `Mission`. + -- * for the group set `PilotGroupSet`. + -- + -- Now that we have `TaskDispatcher` object, we can now **create the TransportTask**, using the @{#TASK_CARGO_DISPATCHER.AddTransportTask}() method! + -- + -- local TransportTask = TaskDispatcher:AddTransportTask( + -- "Transport workmaterials", + -- WorkmaterialsCargoSet, + -- "Transport the workers, engineers and the equipment near the Workplace." ) + -- + -- As a result of this code, the `TransportTask` (returned) variable will contain an object of @{#TASK_CARGO_TRANSPORT}! + -- We pass to the method the title of the task, and the `WorkmaterialsCargoSet`, which is the set of cargo groups to be transported! + -- This object can also be used to setup additional things, or to control this specific task with special actions. + -- + -- And you're done! As you can see, it is a bit of work, but the reward is great. + -- And, because all this is done using program interfaces, you can build a mission with a **dynamic cargo transport task mechanism** yourself! + -- Based on events happening within your mission, you can use the above methods to create new cargo, and setup a new task for cargo transportation to a group of players! + -- + -- + -- # 3. Dispatch CSAR tasks. + -- + -- CSAR tasks can be dynamically created when a friendly pilot ejects, or can be created manually. + -- We'll explore both options. + -- + -- ## 3.1. CSAR task dynamic creation. + -- + -- Because there is an "event" in a running simulation that creates CSAR tasks, the method @{#TASK_CARGO_DISPATCHER.StartCSARTasks}() will create automatically: + -- + -- 1. a new downed pilot at the location where the plane was shot + -- 2. declare that pilot as cargo + -- 3. creates a CSAR task automatically to retrieve that pilot + -- 4. requires deploy zones to be specified where to transport the downed pilot to, in order to complete that task. + -- + -- You create a CSAR task dynamically in a very easy way: + -- + -- TaskDispatcher:StartCSARTasks( + -- "CSAR", + -- { ZONE_UNIT:New( "Hospital", STATIC:FindByName( "Hospital" ), 100 ) }, + -- "One of our pilots has ejected. Go out to Search and Rescue our pilot!\n" .. + -- "Use the radio menu to let the command center assist you with the CSAR tasking." + -- ) + -- + -- The method @{#TASK_CARGO_DISPATCHER.StopCSARTasks}() will automatically stop with the creation of CSAR tasks when friendly pilots eject. + -- + -- **Remarks:** + -- + -- * the ZONE_UNIT can also be a ZONE, or a ZONE_POLYGON object, or any other ZONE_ object! + -- * you can declare the array of zones in another variable, or course! + -- + -- + -- ## 3.2. CSAR task manual creation. + -- + -- We create the CSAR task using the @{#TASK_CARGO_DISPATCHER.AddCSARTask}() constructor. + -- + -- The method will create a new CSAR task, and will generate the pilots cargo itself, at the specified coordinate. + -- + -- What is first needed, is to have a set of @{Core.Group}s defined that contains the clients of the players. + -- + -- -- Allocate the Transport, which are the helicopter to retrieve the pilot, that can be manned by players. + -- local GroupSet = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart() + -- + -- We need to create a TASK_CARGO_DISPATCHER object. + -- + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, GroupSet ) + -- + -- So, the variable `TaskDispatcher` will contain the object of class TASK_CARGO_DISPATCHER, which will allow you to dispatch cargo CSAR tasks: + -- + -- * for mission `Mission`. + -- * for the group of players (pilots) captured within the `GroupSet` (those groups with a name starting with `"Transport"`). + -- + -- Now that we have a PilotsCargoSet and a GroupSet, we can now create the CSAR task manually. + -- + -- -- Declare the CSAR task. + -- local CSARTask = TaskDispatcher:AddCSARTask( + -- "CSAR Task", + -- Coordinate, + -- 270, + -- "Bring the pilot back!" + -- ) + -- + -- As a result of this code, the `CSARTask` (returned) variable will contain an object of @{#TASK_CARGO_CSAR}! + -- We pass to the method the title of the task, and the `WorkmaterialsCargoSet`, which is the set of cargo groups to be transported! + -- This object can also be used to setup additional things, or to control this specific task with special actions. + -- Note that when you declare a CSAR task manually, you'll still need to specify a deployment zone! + -- + -- # 4. Setup the deploy zone(s). + -- + -- The task cargo dispatcher also foresees methods to setup the deployment zones to where the cargo needs to be transported! + -- + -- There are two levels on which deployment zones can be configured: + -- + -- * Default deploy zones: The TASK_CARGO_DISPATCHER object can have default deployment zones, which will apply over all tasks active in the task dispatcher. + -- * Task specific deploy zones: The TASK_CARGO_DISPATCHER object can have specific deployment zones which apply to a specific task only! + -- + -- Note that for Task specific deployment zones, there are separate deployment zone creation methods per task type! + -- + -- ## 4.1. Setup default deploy zones. + -- + -- Use the @{#TASK_CARGO_DISPATCHER.SetDefaultDeployZone}() to setup one deployment zone, and @{#TASK_CARGO_DISPATCHER.SetDefaultDeployZones}() to setup multiple default deployment zones in one call. + -- + -- ## 4.2. Setup task specific deploy zones for a **transport task**. + -- + -- Use the @{#TASK_CARGO_DISPATCHER.SetTransportDeployZone}() to setup one deployment zone, and @{#TASK_CARGO_DISPATCHER.SetTransportDeployZones}() to setup multiple default deployment zones in one call. + -- + -- ## 4.3. Setup task specific deploy zones for a **CSAR task**. + -- + -- Use the @{#TASK_CARGO_DISPATCHER.SetCSARDeployZone}() to setup one deployment zone, and @{#TASK_CARGO_DISPATCHER.SetCSARDeployZones}() to setup multiple default deployment zones in one call. + -- + -- ## 4.4. **CSAR ejection zones**. + -- + -- Setup a set of zones where the pilots will only eject and a task is created for CSAR. When such a set of zones is given, any ejection outside those zones will not result in a pilot created for CSAR! + -- + -- Use the @{#TASK_CARGO_DISPATCHER.SetCSARZones}() to setup the set of zones. + -- + -- ## 4.5. **CSAR ejection maximum**. + -- + -- Setup how many pilots will eject the maximum. This to avoid an overload of CSAR tasks being created :-) The default is endless CSAR tasks. + -- + -- Use the @{#TASK_CARGO_DISPATCHER.SetMaxCSAR}() to setup the maximum of pilots that will eject for CSAR. + -- + -- + -- # 5) Handle cargo task events. + -- + -- When a player is picking up and deploying cargo using his carrier, events are generated by the dispatcher. These events can be captured and tailored with your own code. + -- + -- In order to properly capture the events and avoid mistakes using the documentation, it is advised that you execute the following actions: + -- + -- * **Copy / Paste** the code section into your script. + -- * **Change** the CLASS literal to the task object name you have in your script. + -- * Within the function, you can now **write your own code**! + -- * **IntelliSense** will recognize the type of the variables provided by the function. Note: the From, Event and To variables can be safely ignored, + -- but you need to declare them as they are automatically provided by the event handling system of MOOSE. + -- + -- You can send messages or fire off any other events within the code section. The sky is the limit! + -- + -- First, we need to create a TASK_CARGO_DISPATCHER object. + -- + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, PilotGroupSet ) + -- + -- Second, we create a new cargo transport task for the transportation of workmaterials. + -- + -- TaskDispatcher:AddTransportTask( + -- "Transport workmaterials", + -- WorkmaterialsCargoSet, + -- "Transport the workers, engineers and the equipment near the Workplace." ) + -- + -- Note that we don't really need to keep the resulting task, it is kept internally also in the dispatcher. + -- + -- Using the `TaskDispatcher` object, we can now cpature the CargoPickedUp and CargoDeployed events. + -- + -- ## 5.1) Handle the **CargoPickedUp** event. + -- + -- Find below an example how to tailor the **CargoPickedUp** event, generated by the `TaskDispatcher`: + -- + -- function TaskDispatcher:OnAfterCargoPickedUp( From, Event, To, Task, TaskPrefix, TaskUnit, Cargo ) + -- + -- MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has picked up cargo for task " .. Task:GetName() .. ".", MESSAGE.Type.Information ):ToAll() + -- + -- end + -- + -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has picked up a cargo object in the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- + -- --- CargoPickedUp event handler OnAfter for CLASS. + -- -- @param #CLASS self + -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- -- @param Tasking.Task_Cargo#TASK_CARGO Task The cargo task for which the cargo has been picked up. Note that this will be a derived TAKS_CARGO object! + -- -- @param #string TaskPrefix The prefix of the task that was provided when the task was created. + -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has picked up the cargo. + -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been picked up. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! + -- function CLASS:OnAfterCargoPickedUp( From, Event, To, Task, TaskPrefix, TaskUnit, Cargo ) + -- + -- -- Write here your own code. + -- + -- end + -- + -- + -- ## 5.2) Handle the **CargoDeployed** event. + -- + -- Find below an example how to tailor the **CargoDeployed** event, generated by the `TaskDispatcher`: + -- + -- function WorkplaceTask:OnAfterCargoDeployed( From, Event, To, Task, TaskPrefix, TaskUnit, Cargo, DeployZone ) + -- + -- MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has deployed cargo at zone " .. DeployZone:GetName() .. " for task " .. Task:GetName() .. ".", MESSAGE.Type.Information ):ToAll() + -- + -- Helos[ math.random(1,#Helos) ]:Spawn() + -- EnemyHelos[ math.random(1,#EnemyHelos) ]:Spawn() + -- end + -- + -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has deployed a cargo object from the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- + -- + -- --- CargoDeployed event handler OnAfter for CLASS. + -- -- @param #CLASS self + -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- -- @param Tasking.Task_Cargo#TASK_CARGO Task The cargo task for which the cargo has been deployed. Note that this will be a derived TAKS_CARGO object! + -- -- @param #string TaskPrefix The prefix of the task that was provided when the task was created. + -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has deployed the cargo. + -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been deployed. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! + -- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + -- function CLASS:OnAfterCargoDeployed( From, Event, To, Task, TaskPrefix, TaskUnit, Cargo, DeployZone ) + -- + -- -- Write here your own code. + -- + -- end + -- + -- + -- + -- @field #TASK_CARGO_DISPATCHER + TASK_CARGO_DISPATCHER = { + ClassName = "TASK_CARGO_DISPATCHER", + Mission = nil, + Tasks = {}, + CSAR = {}, + CSARSpawned = 0, + + Transport = {}, + TransportCount = 0, + } + + + --- TASK_CARGO_DISPATCHER constructor. + -- @param #TASK_CARGO_DISPATCHER self + -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done. + -- @param Core.Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission. + -- @return #TASK_CARGO_DISPATCHER self + function TASK_CARGO_DISPATCHER:New( Mission, SetGroup ) + + -- Inherits from DETECTION_MANAGER + local self = BASE:Inherit( self, TASK_MANAGER:New( SetGroup ) ) -- #TASK_CARGO_DISPATCHER + + self.Mission = Mission + + self:AddTransition( "Started", "Assign", "Started" ) + self:AddTransition( "Started", "CargoPickedUp", "Started" ) + self:AddTransition( "Started", "CargoDeployed", "Started" ) + + --- OnAfter Transition Handler for Event Assign. + -- @function [parent=#TASK_CARGO_DISPATCHER] OnAfterAssign + -- @param #TASK_CARGO_DISPATCHER self + -- @param #string From The From State string. + -- @param #string Event The Event string. + -- @param #string To The To State string. + -- @param Tasking.Task_A2A#TASK_A2A Task + -- @param Wrapper.Unit#UNIT TaskUnit + -- @param #string PlayerName + + self:SetCSARRadius() + self:__StartTasks( 5 ) + + self.MaxCSAR = nil + self.CountCSAR = 0 + + -- For CSAR missions, we process the event when a pilot ejects. + + self:HandleEvent( EVENTS.Ejection ) + + return self + end + + + --- Sets the set of zones were pilots will only be spawned (eject) when the planes crash. + -- Note that because this is a set of zones, the MD can create the zones dynamically within his mission! + -- Just provide a set of zones, see usage, but find the tactical situation here: + -- + -- ![CSAR Zones](../Tasking/CSAR_Zones.JPG) + -- + -- @param #TASK_CARGO_DISPATCHER self + -- @param Core.Set#SET_ZONE SetZonesCSAR The set of zones where pilots will only be spawned for CSAR when they eject. + -- @usage + -- + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, AttackGroups ) + -- + -- -- Use this call to pass the set of zones. + -- -- Note that you can create the set of zones inline, because the FilterOnce method (and other SET_ZONE methods return self). + -- -- So here the zones can be created as normal trigger zones (MOOSE creates a collection of ZONE objects when teh mission starts of all trigger zones). + -- -- Just name them as CSAR zones here. + -- TaskDispatcher:SetCSARZones( SET_ZONE:New():FilterPrefixes("CSAR"):FilterOnce() ) + -- + function TASK_CARGO_DISPATCHER:SetCSARZones( SetZonesCSAR ) + + self.SetZonesCSAR = SetZonesCSAR + + end + + + --- Sets the maximum of pilots that will be spawned (eject) when the planes crash. + -- @param #TASK_CARGO_DISPATCHER self + -- @param #number MaxCSAR The maximum of pilots that will eject for CSAR. + -- @usage + -- + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, AttackGroups ) + -- + -- -- Use this call to the maximum of CSAR to 10. + -- TaskDispatcher:SetMaxCSAR( 10 ) + -- + function TASK_CARGO_DISPATCHER:SetMaxCSAR( MaxCSAR ) + + self.MaxCSAR = MaxCSAR + + end + + + + --- Handle the event when a pilot ejects. + -- @param #TASK_CARGO_DISPATCHER self + -- @param Core.Event#EVENTDATA EventData + function TASK_CARGO_DISPATCHER:OnEventEjection( EventData ) + self:F( { EventData = EventData } ) + + if self.CSARTasks == true then + + local CSARCoordinate = EventData.IniUnit:GetCoordinate() + local CSARCoalition = EventData.IniUnit:GetCoalition() + local CSARCountry = EventData.IniUnit:GetCountry() + local CSARHeading = EventData.IniUnit:GetHeading() + + -- Only add a CSAR task if the coalition of the mission is equal to the coalition of the ejected unit. + if CSARCoalition == self.Mission:GetCommandCenter():GetCoalition() then + -- And only add if the eject is in one of the zones, if defined. + if not self.SetZonesCSAR or ( self.SetZonesCSAR and self.SetZonesCSAR:IsCoordinateInZone( CSARCoordinate ) ) then + -- And only if the maximum of pilots is not reached that ejected! + if not self.MaxCSAR or ( self.MaxCSAR and self.CountCSAR < self.MaxCSAR ) then + local CSARTaskName = self:AddCSARTask( self.CSARTaskName, CSARCoordinate, CSARHeading, CSARCountry, self.CSARBriefing ) + self:SetCSARDeployZones( CSARTaskName, self.CSARDeployZones ) + self.CountCSAR = self.CountCSAR + 1 + end + end + end + end + + return self + end + + + --- Define one default deploy zone for all the cargo tasks. + -- @param #TASK_CARGO_DISPATCHER self + -- @param DefaultDeployZone A default deploy zone. + -- @return #TASK_CARGO_DISPATCHER + function TASK_CARGO_DISPATCHER:SetDefaultDeployZone( DefaultDeployZone ) + + self.DefaultDeployZones = { DefaultDeployZone } + + return self + end + + + --- Define the deploy zones for all the cargo tasks. + -- @param #TASK_CARGO_DISPATCHER self + -- @param DefaultDeployZones A list of the deploy zones. + -- @return #TASK_CARGO_DISPATCHER + -- + function TASK_CARGO_DISPATCHER:SetDefaultDeployZones( DefaultDeployZones ) + + self.DefaultDeployZones = DefaultDeployZones + + return self + end + + + --- Start the generation of CSAR tasks to retrieve a downed pilots. + -- You need to specify a task briefing, a task name, default deployment zone(s). + -- This method can only be used once! + -- @param #TASK_CARGO_DISPATCHER self + -- @param #string CSARTaskName The CSAR task name. + -- @param #string CSARDeployZones The zones to where the CSAR deployment should be directed. + -- @param #string CSARBriefing The briefing of the CSAR tasks. + -- @return #TASK_CARGO_DISPATCHER + function TASK_CARGO_DISPATCHER:StartCSARTasks( CSARTaskName, CSARDeployZones, CSARBriefing) + + if not self.CSARTasks then + self.CSARTasks = true + self.CSARTaskName = CSARTaskName + self.CSARDeployZones = CSARDeployZones + self.CSARBriefing = CSARBriefing + else + error( "TASK_CARGO_DISPATCHER: The generation of CSAR tasks has already started." ) + end + + return self + end + + + --- Stop the generation of CSAR tasks to retrieve a downed pilots. + -- @param #TASK_CARGO_DISPATCHER self + -- @return #TASK_CARGO_DISPATCHER + function TASK_CARGO_DISPATCHER:StopCSARTasks() + + if self.CSARTasks then + self.CSARTasks = nil + self.CSARTaskName = nil + self.CSARDeployZones = nil + self.CSARBriefing = nil + else + error( "TASK_CARGO_DISPATCHER: The generation of CSAR tasks was not yet started." ) + end + + return self + end + + + --- Add a CSAR task to retrieve a downed pilot. + -- You need to specify a coordinate from where the pilot will be spawned to be rescued. + -- @param #TASK_CARGO_DISPATCHER self + -- @param #string CSARTaskPrefix (optional) The prefix of the CSAR task. + -- @param Core.Point#COORDINATE CSARCoordinate The coordinate where a downed pilot will be spawned. + -- @param #number CSARHeading The heading of the pilot in degrees. + -- @param DCSCountry#Country CSARCountry The country ID of the pilot that will be spawned. + -- @param #string CSARBriefing The briefing of the CSAR task. + -- @return #string The CSAR Task Name as a string. The Task Name is the main key and is shown in the task list of the Mission Tasking menu. + -- @usage + -- + -- -- Add a CSAR task to rescue a downed pilot from within a coordinate. + -- local Coordinate = PlaneUnit:GetPointVec2() + -- TaskA2ADispatcher:AddCSARTask( "CSAR Task", Coordinate ) + -- + -- -- Add a CSAR task to rescue a downed pilot from within a coordinate of country RUSSIA, which is pointing to the west (270°). + -- local Coordinate = PlaneUnit:GetPointVec2() + -- TaskA2ADispatcher:AddCSARTask( "CSAR Task", Coordinate, 270, Country.RUSSIA ) + -- + function TASK_CARGO_DISPATCHER:AddCSARTask( CSARTaskPrefix, CSARCoordinate, CSARHeading, CSARCountry, CSARBriefing ) + + local CSARCoalition = self.Mission:GetCommandCenter():GetCoalition() + + CSARHeading = CSARHeading or 0 + CSARCountry = CSARCountry or self.Mission:GetCommandCenter():GetCountry() + + self.CSARSpawned = self.CSARSpawned + 1 + + local CSARTaskName = string.format( ( CSARTaskPrefix or "CSAR" ) .. ".%03d", self.CSARSpawned ) + + -- Create the CSAR Pilot SPAWN object. + -- Let us create the Template for the replacement Pilot :-) + local Template = { + ["visible"] = false, + ["hidden"] = false, + ["task"] = "Ground Nothing", + ["name"] = string.format( "CSAR Pilot#%03d", self.CSARSpawned ), + ["x"] = CSARCoordinate.x, + ["y"] = CSARCoordinate.z, + ["units"] = + { + [1] = + { + ["type"] = ( CSARCoalition == coalition.side.BLUE ) and "Soldier M4" or "Infantry AK", + ["name"] = string.format( "CSAR Pilot#%03d-01", self.CSARSpawned ), + ["skill"] = "Excellent", + ["playerCanDrive"] = false, + ["x"] = CSARCoordinate.x, + ["y"] = CSARCoordinate.z, + ["heading"] = CSARHeading, + }, -- end of [1] + }, -- end of ["units"] + } + + local CSARGroup = GROUP:NewTemplate( Template, CSARCoalition, Group.Category.GROUND, CSARCountry ) + + self.CSAR[CSARTaskName] = {} + self.CSAR[CSARTaskName].PilotGroup = CSARGroup + self.CSAR[CSARTaskName].Briefing = CSARBriefing + self.CSAR[CSARTaskName].Task = nil + self.CSAR[CSARTaskName].TaskPrefix = CSARTaskPrefix + + return CSARTaskName + end + + + --- Define the radius to when a CSAR task will be generated for any downed pilot within range of the nearest CSAR airbase. + -- @param #TASK_CARGO_DISPATCHER self + -- @param #number CSARRadius (Optional, Default = 50000) The radius in meters to decide whether a CSAR needs to be created. + -- @return #TASK_CARGO_DISPATCHER + -- @usage + -- + -- -- Set 20km as the radius to CSAR any downed pilot within range of the nearest CSAR airbase. + -- TaskA2ADispatcher:SetEngageRadius( 20000 ) + -- + -- -- Set 50km as the radius to to CSAR any downed pilot within range of the nearest CSAR airbase. + -- TaskA2ADispatcher:SetEngageRadius() -- 50000 is the default value. + -- + function TASK_CARGO_DISPATCHER:SetCSARRadius( CSARRadius ) + + self.CSARRadius = CSARRadius or 50000 + + return self + end + + + --- Define one deploy zone for the CSAR tasks. + -- @param #TASK_CARGO_DISPATCHER self + -- @param #string CSARTaskName (optional) The name of the CSAR task. + -- @param CSARDeployZone A CSAR deploy zone. + -- @return #TASK_CARGO_DISPATCHER + function TASK_CARGO_DISPATCHER:SetCSARDeployZone( CSARTaskName, CSARDeployZone ) + + if CSARTaskName then + self.CSAR[CSARTaskName].DeployZones = { CSARDeployZone } + end + + return self + end + + + --- Define the deploy zones for the CSAR tasks. + -- @param #TASK_CARGO_DISPATCHER self + -- @param #string CSARTaskName (optional) The name of the CSAR task. + -- @param CSARDeployZones A list of the CSAR deploy zones. + -- @return #TASK_CARGO_DISPATCHER + -- + function TASK_CARGO_DISPATCHER:SetCSARDeployZones( CSARTaskName, CSARDeployZones ) + + if CSARTaskName and self.CSAR[CSARTaskName] then + self.CSAR[CSARTaskName].DeployZones = CSARDeployZones + end + + return self + end + + + --- Add a Transport task to transport cargo from fixed locations to a deployment zone. + -- @param #TASK_CARGO_DISPATCHER self + -- @param #string TaskPrefix (optional) The prefix of the transport task. + -- This prefix will be appended with a . + a number of 3 digits. + -- If no TaskPrefix is given, then "Transport" will be used as the prefix. + -- @param Core.SetCargo#SET_CARGO SetCargo The SetCargo to be transported. + -- @param #string Briefing The briefing of the task transport to be shown to the player. + -- @return Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT + -- @usage + -- + -- -- Add a Transport task to transport cargo of different types to a Transport Deployment Zone. + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups ) + -- + -- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() + -- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) + -- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) + -- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) + -- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) + -- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) + -- + -- -- Here we add the task. We name the task "Build a Workplace". + -- -- We provide the CargoSetWorkmaterials, and a briefing as the 2nd and 3rd parameter. + -- -- The :AddTransportTask() returns a Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT object, which we keep as a reference for further actions. + -- -- The WorkplaceTask holds the created and returned Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT object. + -- local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." ) + -- + -- -- Here we set a TransportDeployZone. We use the WorkplaceTask as the reference, and provide a ZONE object. + -- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) + -- + function TASK_CARGO_DISPATCHER:AddTransportTask( TaskPrefix, SetCargo, Briefing ) + + self.TransportCount = self.TransportCount + 1 + + local TaskName = string.format( ( TaskPrefix or "Transport" ) .. ".%03d", self.TransportCount ) + + self.Transport[TaskName] = {} + self.Transport[TaskName].SetCargo = SetCargo + self.Transport[TaskName].Briefing = Briefing + self.Transport[TaskName].Task = nil + self.Transport[TaskName].TaskPrefix = TaskPrefix + + self:ManageTasks() + + return self.Transport[TaskName] and self.Transport[TaskName].Task + end + + + --- Define one deploy zone for the Transport tasks. + -- @param #TASK_CARGO_DISPATCHER self + -- @param Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT Task The name of the Transport task. + -- @param TransportDeployZone A Transport deploy zone. + -- @return #TASK_CARGO_DISPATCHER + -- @usage + -- + -- + function TASK_CARGO_DISPATCHER:SetTransportDeployZone( Task, TransportDeployZone ) + + if self.Transport[Task.TaskName] then + self.Transport[Task.TaskName].DeployZones = { TransportDeployZone } + else + error( "Task does not exist" ) + end + + self:ManageTasks() + + return self + end + + + --- Define the deploy zones for the Transport tasks. + -- @param #TASK_CARGO_DISPATCHER self + -- @param Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT Task The name of the Transport task. + -- @param TransportDeployZones A list of the Transport deploy zones. + -- @return #TASK_CARGO_DISPATCHER + -- + function TASK_CARGO_DISPATCHER:SetTransportDeployZones( Task, TransportDeployZones ) + + if self.Transport[Task.TaskName] then + self.Transport[Task.TaskName].DeployZones = TransportDeployZones + else + error( "Task does not exist" ) + end + + self:ManageTasks() + + return self + end + + --- Evaluates of a CSAR task needs to be started. + -- @param #TASK_CARGO_DISPATCHER self + -- @return Core.Set#SET_CARGO The SetCargo to be rescued. + -- @return #nil If there is no CSAR task required. + function TASK_CARGO_DISPATCHER:EvaluateCSAR( CSARUnit ) + + local CSARCargo = CARGO_GROUP:New( CSARUnit, "Pilot", CSARUnit:GetName(), 80, 1500, 10 ) + + local SetCargo = SET_CARGO:New() + SetCargo:AddCargosByName( CSARUnit:GetName() ) + + SetCargo:Flush(self) + + return SetCargo + + end + + + + --- Assigns tasks to the @{Core.Set#SET_GROUP}. + -- @param #TASK_CARGO_DISPATCHER self + -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop. + function TASK_CARGO_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. +-- local DetectedItem = Detection:GetDetectedItemByIndex( TaskIndex ) +-- if not DetectedItem then +-- local TaskText = Task:GetName() +-- for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do +-- Mission:GetCommandCenter():MessageToGroup( string.format( "Obsolete A2A task %s for %s removed.", TaskText, Mission:GetShortText() ), TaskGroup ) +-- end +-- Task = self:RemoveTask( TaskIndex ) +-- end + end + end + + -- Now that all obsolete tasks are removed, loop through the CSAR pilots. + for CSARName, CSAR in pairs( self.CSAR ) do + + if not CSAR.Task then + -- New CSAR Task + local SetCargo = self:EvaluateCSAR( CSAR.PilotGroup ) + CSAR.Task = TASK_CARGO_CSAR:New( Mission, self.SetGroup, CSARName, SetCargo, CSAR.Briefing ) + CSAR.Task.TaskPrefix = CSAR.TaskPrefix -- We keep the TaskPrefix for further reference! + Mission:AddTask( CSAR.Task ) + TaskReport:Add( CSARName ) + if CSAR.DeployZones then + CSAR.Task:SetDeployZones( CSAR.DeployZones or {} ) + else + CSAR.Task:SetDeployZones( self.DefaultDeployZones or {} ) + end + + -- Now broadcast the onafterCargoPickedUp event to the Task Cargo Dispatcher. + function CSAR.Task.OnAfterCargoPickedUp( Task, From, Event, To, TaskUnit, Cargo ) + self:CargoPickedUp( Task, Task.TaskPrefix, TaskUnit, Cargo ) + end + + -- Now broadcast the onafterCargoDeployed event to the Task Cargo Dispatcher. + function CSAR.Task.OnAfterCargoDeployed( Task, From, Event, To, TaskUnit, Cargo, DeployZone ) + self:CargoDeployed( Task, Task.TaskPrefix, TaskUnit, Cargo, DeployZone ) + end + + end + end + + + -- Now that all obsolete tasks are removed, loop through the Transport tasks. + for TransportName, Transport in pairs( self.Transport ) do + + if not Transport.Task then + -- New Transport Task + Transport.Task = TASK_CARGO_TRANSPORT:New( Mission, self.SetGroup, TransportName, Transport.SetCargo, Transport.Briefing ) + Transport.Task.TaskPrefix = Transport.TaskPrefix -- We keep the TaskPrefix for further reference! + Mission:AddTask( Transport.Task ) + TaskReport:Add( TransportName ) + function Transport.Task.OnEnterSuccess( Task, From, Event, To ) + self:Success( Task ) + end + + function Transport.Task.OnEnterCancelled( Task, From, Event, To ) + self:Cancelled( Task ) + end + + function Transport.Task.OnEnterFailed( Task, From, Event, To ) + self:Failed( Task ) + end + + function Transport.Task.OnEnterAborted( Task, From, Event, To ) + self:Aborted( Task ) + end + + -- Now broadcast the onafterCargoPickedUp event to the Task Cargo Dispatcher. + function Transport.Task.OnAfterCargoPickedUp( Task, From, Event, To, TaskUnit, Cargo ) + self:CargoPickedUp( Task, Task.TaskPrefix, TaskUnit, Cargo ) + end + + -- Now broadcast the onafterCargoDeployed event to the Task Cargo Dispatcher. + function Transport.Task.OnAfterCargoDeployed( Task, From, Event, To, TaskUnit, Cargo, DeployZone ) + self:CargoDeployed( Task, Task.TaskPrefix, TaskUnit, Cargo, DeployZone ) + end + + end + + if Transport.DeployZones then + Transport.Task:SetDeployZones( Transport.DeployZones or {} ) + else + Transport.Task:SetDeployZones( self.DefaultDeployZones or {} ) + end + + end + + + -- TODO set menus using the HQ coordinator + Mission:GetCommandCenter():SetMenu() + + local TaskText = TaskReport:Text(", ") + + for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do + if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" 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/Task_Cargo_Transport.lua b/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua new file mode 100644 index 000000000..484363978 --- /dev/null +++ b/Moose Development/Moose/Tasking/Task_Cargo_Transport.lua @@ -0,0 +1,360 @@ +--- **Tasking** -- Models tasks for players to transport cargo. +-- +-- **Specific features:** +-- +-- * Creates a task to transport @{Cargo.Cargo} to and between deployment zones. +-- * Derived from the TASK_CARGO class, which is derived from the TASK class. +-- * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled. +-- * Co-operation tasking, so a player joins a group of players executing the same task. +-- +-- +-- **A complete task menu system to allow players to:** +-- +-- * Join the task, abort the task. +-- * Mark the task location on the map. +-- * Provide details of the target. +-- * Route to the cargo. +-- * Route to the deploy zones. +-- * Load/Unload cargo. +-- * Board/Unboard cargo. +-- * Slingload cargo. +-- * Display the task briefing. +-- +-- +-- **A complete mission menu system to allow players to:** +-- +-- * Join a task, abort the task. +-- * Display task reports. +-- * Display mission statistics. +-- * Mark the task locations on the map. +-- * Provide details of the targets. +-- * Display the mission briefing. +-- * Provide status updates as retrieved from the command center. +-- * Automatically assign a random task as part of a mission. +-- * Manually assign a specific task as part of a mission. +-- +-- +-- **A settings system, using the settings menu:** +-- +-- * Tweak the duration of the display of messages. +-- * Switch between metric and imperial measurement system. +-- * Switch between coordinate formats used in messages: BR, BRA, LL DMS, LL DDM, MGRS. +-- * Different settings modes for A2G and A2A operations. +-- * Various other options. +-- +-- === +-- +-- Please read through the @{Tasking.Task_Cargo} process to understand the mechanisms of tasking and cargo tasking and handling. +-- +-- Enjoy! +-- FC +-- +-- === +-- +-- @module Tasking.Task_Cargo_Transport +-- @image Task_Cargo_Transport.JPG + + +do -- TASK_CARGO_TRANSPORT + + --- @type TASK_CARGO_TRANSPORT + -- @extends Tasking.Task_CARGO#TASK_CARGO + + --- Orchestrates the task for players to transport cargo to or between deployment zones. + -- + -- Transport tasks are suited to govern the process of transporting cargo to specific deployment zones. + -- Typically, this task is executed by helicopter pilots, but it can also be executed by ground forces! + -- + -- === + -- + -- A transport task can be created manually. + -- + -- # 1) Create a transport task manually (code it). + -- + -- Although it is recommended to use the dispatcher, you can create a transport task yourself as a mission designer. + -- It is easy, as it works just like any other task setup. + -- + -- ## 1.1) Create a command center. + -- + -- First you need to create a command center using the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor. + -- + -- local CommandCenter = COMMANDCENTER + -- :New( HQ, "Lima" ) -- Create the CommandCenter. + -- + -- ## 1.2) Create a mission. + -- + -- Tasks work in a mission, which groups these tasks to achieve a joint mission goal. + -- A command center can govern multiple missions. + -- Create a new mission, using the @{Tasking.Mission#MISSION.New}() constructor. + -- + -- -- Declare the Mission for the Command Center. + -- local Mission = MISSION + -- :New( CommandCenter, + -- "Overlord", + -- "High", + -- "Transport the cargo to the deploy zones.", + -- coalition.side.RED + -- ) + -- + -- ## 1.3) Create the transport cargo task. + -- + -- So, now that we have a command center and a mission, we now create the transport task. + -- We create the transport task using the @{#TASK_CARGO_TRANSPORT.New}() constructor. + -- + -- Because a transport task will not generate the cargo itself, you'll need to create it first. + -- The cargo in this case will be the downed pilot! + -- + -- -- Here we define the "cargo set", which is a collection of cargo objects. + -- -- The cargo set will be the input for the cargo transportation task. + -- -- So a transportation object is handling a cargo set, which is automatically refreshed when new cargo is added/deleted. + -- local CargoSet = SET_CARGO:New():FilterTypes( "Cargo" ):FilterStart() + -- + -- -- Now we add cargo into the battle scene. + -- local PilotGroup = GROUP:FindByName( "Engineers" ) + -- + -- -- CARGO_GROUP can be used to setup cargo with a GROUP object underneath. + -- -- We name this group Engineers. + -- -- Note that the name of the cargo is "Engineers". + -- -- The cargoset "CargoSet" will embed all defined cargo of type "Pilots" (prefix) into its set. + -- local CargoGroup = CARGO_GROUP:New( PilotGroup, "Cargo", "Engineer Team 1", 500 ) + -- + -- What is also needed, is to have a set of @{Core.Group}s defined that contains the clients of the players. + -- + -- -- Allocate the Transport, which are the helicopter to retrieve the pilot, that can be manned by players. + -- local GroupSet = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart() + -- + -- Now that we have a CargoSet and a GroupSet, we can now create the TransportTask manually. + -- + -- -- Declare the transport task. + -- local TransportTask = TASK_CARGO_TRANSPORT + -- :New( Mission, + -- GroupSet, + -- "Transport Engineers", + -- CargoSet, + -- "Fly behind enemy lines, and retrieve the downed pilot." + -- ) + -- + -- So you can see, setting up a transport task manually is a lot of work. + -- It is better you use the cargo dispatcher to create transport tasks and it will work as it is intended. + -- By doing this, cargo transport tasking will become a dynamic experience. + -- + -- + -- # 2) Create a task using the @{Tasking.Task_Cargo_Dispatcher} module. + -- + -- Actually, it is better to **GENERATE** these tasks using the @{Tasking.Task_Cargo_Dispatcher} module. + -- Using the dispatcher module, transport tasks can be created much more easy. + -- + -- Find below an example how to use the TASK_CARGO_DISPATCHER class: + -- + -- + -- -- Find the HQ group. + -- HQ = GROUP:FindByName( "HQ", "Bravo" ) + -- + -- -- Create the command center with the name "Lima". + -- CommandCenter = COMMANDCENTER + -- :New( HQ, "Lima" ) + -- + -- -- Create the mission, for the command center, with the name "Operation Cargo Fun", a "Tactical" mission, with the mission briefing "Transport Cargo", for the BLUE coalition. + -- Mission = MISSION + -- :New( CommandCenter, "Operation Cargo Fun", "Tactical", "Transport Cargo", coalition.side.BLUE ) + -- + -- -- Create the SET of GROUPs containing clients (players) that will transport the cargo. + -- -- These are have a name that start with "Transport" and are of the "blue" coalition. + -- TransportGroups = SET_GROUP:New():FilterCoalitions( "blue" ):FilterPrefixes( "Transport" ):FilterStart() + -- + -- + -- -- Here we create the TASK_CARGO_DISPATCHER object! This is where we assign the dispatcher to generate tasks in the Mission for the TransportGroups. + -- TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups ) + -- + -- + -- -- Here we declare the SET of CARGOs called "Workmaterials". + -- local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart() + -- + -- -- Here we declare (add) CARGO_GROUP objects of various types, that are filtered and added in the CargoSetworkmaterials cargo set. + -- -- These cargo objects have the type "Workmaterials" which is exactly the type of cargo the CargoSetworkmaterials is filtering on. + -- local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 ) + -- local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 ) + -- local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 ) + -- local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 ) + -- local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 ) + -- + -- -- And here we create a new WorkplaceTask, using the :AddTransportTask method of the TaskDispatcher. + -- local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." ) + -- TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) ) + -- + -- # 3) Handle cargo task events. + -- + -- When a player is picking up and deploying cargo using his carrier, events are generated by the tasks. These events can be captured and tailored with your own code. + -- + -- In order to properly capture the events and avoid mistakes using the documentation, it is advised that you execute the following actions: + -- + -- * **Copy / Paste** the code section into your script. + -- * **Change** the CLASS literal to the task object name you have in your script. + -- * Within the function, you can now **write your own code**! + -- * **IntelliSense** will recognize the type of the variables provided by the function. Note: the From, Event and To variables can be safely ignored, + -- but you need to declare them as they are automatically provided by the event handling system of MOOSE. + -- + -- You can send messages or fire off any other events within the code section. The sky is the limit! + -- + -- + -- ## 3.1) Handle the CargoPickedUp event. + -- + -- Find below an example how to tailor the **CargoPickedUp** event, generated by the WorkplaceTask: + -- + -- function WorkplaceTask:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo ) + -- + -- MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has picked up cargo.", MESSAGE.Type.Information ):ToAll() + -- + -- end + -- + -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has picked up a cargo object in the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- + -- --- CargoPickedUp event handler OnAfter for CLASS. + -- -- @param #CLASS self + -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has picked up the cargo. + -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been picked up. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! + -- function CLASS:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo ) + -- + -- -- Write here your own code. + -- + -- end + -- + -- + -- ## 3.2) Handle the CargoDeployed event. + -- + -- Find below an example how to tailor the **CargoDeployed** event, generated by the WorkplaceTask: + -- + -- function WorkplaceTask:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) + -- + -- MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has deployed cargo at zone " .. DeployZone:GetName(), MESSAGE.Type.Information ):ToAll() + -- + -- Helos[ math.random(1,#Helos) ]:Spawn() + -- EnemyHelos[ math.random(1,#EnemyHelos) ]:Spawn() + -- end + -- + -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has deployed a cargo object from the CarrierGroup. + -- You can use this event handler to post messages to players, or provide status updates etc. + -- + -- + -- --- CargoDeployed event handler OnAfter for CLASS. + -- -- @param #CLASS self + -- -- @param #string From A string that contains the "*from state name*" when the event was triggered. + -- -- @param #string Event A string that contains the "*event name*" when the event was triggered. + -- -- @param #string To A string that contains the "*to state name*" when the event was triggered. + -- -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has deployed the cargo. + -- -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been deployed. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object! + -- -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE. + -- function CLASS:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone ) + -- + -- -- Write here your own code. + -- + -- end + -- + -- + -- + -- === + -- + -- @field #TASK_CARGO_TRANSPORT + TASK_CARGO_TRANSPORT = { + ClassName = "TASK_CARGO_TRANSPORT", + } + + --- Instantiates a new TASK_CARGO_TRANSPORT. + -- @param #TASK_CARGO_TRANSPORT self + -- @param Tasking.Mission#MISSION Mission + -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned. + -- @param #string TaskName The name of the Task. + -- @param Core.Set#SET_CARGO SetCargo The scope of the cargo to be transported. + -- @param #string TaskBriefing The Cargo Task briefing. + -- @return #TASK_CARGO_TRANSPORT self + function TASK_CARGO_TRANSPORT:New( Mission, SetGroup, TaskName, SetCargo, TaskBriefing ) + local self = BASE:Inherit( self, TASK_CARGO:New( Mission, SetGroup, TaskName, SetCargo, "Transport", TaskBriefing ) ) -- #TASK_CARGO_TRANSPORT + self:F() + + Mission:AddTask( self ) + + local Fsm = self:GetUnitProcess() + + local CargoReport = REPORT:New( "Transport Cargo. The following cargo needs to be transported including initial positions:") + + SetCargo:ForEachCargo( + --- @param Core.Cargo#CARGO Cargo + function( Cargo ) + local CargoType = Cargo:GetType() + local CargoName = Cargo:GetName() + local CargoCoordinate = Cargo:GetCoordinate() + CargoReport:Add( string.format( '- "%s" (%s) at %s', CargoName, CargoType, CargoCoordinate:ToStringMGRS() ) ) + end + ) + + self:SetBriefing( + TaskBriefing or + CargoReport:Text() + ) + + + return self + end + + function TASK_CARGO_TRANSPORT:ReportOrder( ReportGroup ) + + return 0 + end + + + --- + -- @param #TASK_CARGO_TRANSPORT self + -- @return #boolean + function TASK_CARGO_TRANSPORT:IsAllCargoTransported() + + local CargoSet = self:GetCargoSet() + local Set = CargoSet:GetSet() + + local DeployZones = self:GetDeployZones() + + local CargoDeployed = true + + -- Loop the CargoSet (so evaluate each Cargo in the SET_CARGO ). + for CargoID, CargoData in pairs( Set ) do + local Cargo = CargoData -- Core.Cargo#CARGO + + self:F( { Cargo = Cargo:GetName(), CargoDeployed = Cargo:IsDeployed() } ) + + if Cargo:IsDeployed() then + +-- -- Loop the DeployZones set for the TASK_CARGO_TRANSPORT. +-- for DeployZoneID, DeployZone in pairs( DeployZones ) do +-- +-- -- If all cargo is in one of the deploy zones, then all is good. +-- self:T( { Cargo.CargoObject } ) +-- if Cargo:IsInZone( DeployZone ) == false then +-- CargoDeployed = false +-- end +-- end + else + CargoDeployed = false + end + end + + self:F( { CargoDeployed = CargoDeployed } ) + + return CargoDeployed + end + + --- @param #TASK_CARGO_TRANSPORT self + function TASK_CARGO_TRANSPORT:onafterGoal( TaskUnit, From, Event, To ) + local CargoSet = self.CargoSet + + if self:IsAllCargoTransported() then + self:Success() + end + + self:__Goal( -10 ) + end + +end + diff --git a/Moose Development/Moose/Tasking/Task_Manager.lua b/Moose Development/Moose/Tasking/Task_Manager.lua new file mode 100644 index 000000000..8c2d76406 --- /dev/null +++ b/Moose Development/Moose/Tasking/Task_Manager.lua @@ -0,0 +1,193 @@ +--- This module contains the TASK_MANAGER class and derived classes. +-- +-- === +-- +-- 1) @{Tasking.Task_Manager#TASK_MANAGER} class, extends @{Core.Fsm#FSM} +-- === +-- The @{Tasking.Task_Manager#TASK_MANAGER} class defines the core functions to report tasks to groups. +-- Reportings can be done in several manners, and it is up to the derived classes if TASK_MANAGER to model the reporting behaviour. +-- +-- 1.1) TASK_MANAGER constructor: +-- ----------------------------------- +-- * @{Tasking.Task_Manager#TASK_MANAGER.New}(): Create a new TASK_MANAGER instance. +-- +-- 1.2) TASK_MANAGER reporting: +-- --------------------------------- +-- Derived TASK_MANAGER classes will manage tasks using the method @{Tasking.Task_Manager#TASK_MANAGER.ManageTasks}(). This method implements polymorphic behaviour. +-- +-- The time interval in seconds of the task management can be changed using the methods @{Tasking.Task_Manager#TASK_MANAGER.SetRefreshTimeInterval}(). +-- To control how long a reporting message is displayed, use @{Tasking.Task_Manager#TASK_MANAGER.SetReportDisplayTime}(). +-- Derived classes need to implement the method @{Tasking.Task_Manager#TASK_MANAGER.GetReportDisplayTime}() to use the correct display time for displayed messages during a report. +-- +-- Task management can be started and stopped using the methods @{Tasking.Task_Manager#TASK_MANAGER.StartTasks}() and @{Tasking.Task_Manager#TASK_MANAGER.StopTasks}() respectively. +-- If an ad-hoc report is requested, use the method @{Tasking.Task_Manager#TASK_MANAGER#ManageTasks}(). +-- +-- The default task management interval is every 60 seconds. +-- +-- === +-- +-- ### Contributions: Mechanist, Prof_Hilactic, FlightControl - Concept & Testing +-- ### Author: FlightControl - Framework Design & Programming +-- +-- @module Tasking.Task_Manager +-- @image MOOSE.JPG + +do -- TASK_MANAGER + + --- TASK_MANAGER class. + -- @type TASK_MANAGER + -- @field Core.Set#SET_GROUP SetGroup The set of group objects containing players for which tasks are managed. + -- @extends Core.Fsm#FSM + TASK_MANAGER = { + ClassName = "TASK_MANAGER", + SetGroup = nil, + } + + --- TASK\_MANAGER constructor. + -- @param #TASK_MANAGER self + -- @param Core.Set#SET_GROUP SetGroup The set of group objects containing players for which tasks are managed. + -- @return #TASK_MANAGER self + function TASK_MANAGER:New( SetGroup ) + + -- Inherits from BASE + local self = BASE:Inherit( self, FSM:New() ) -- #TASK_MANAGER + + self.SetGroup = SetGroup + + self:SetStartState( "Stopped" ) + self:AddTransition( "Stopped", "StartTasks", "Started" ) + + --- StartTasks Handler OnBefore for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnBeforeStartTasks + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- StartTasks Handler OnAfter for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnAfterStartTasks + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- StartTasks Trigger for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] StartTasks + -- @param #TASK_MANAGER self + + --- StartTasks Asynchronous Trigger for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] __StartTasks + -- @param #TASK_MANAGER self + -- @param #number Delay + + + + self:AddTransition( "Started", "StopTasks", "Stopped" ) + + --- StopTasks Handler OnBefore for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnBeforeStopTasks + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @return #boolean + + --- StopTasks Handler OnAfter for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnAfterStopTasks + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + + --- StopTasks Trigger for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] StopTasks + -- @param #TASK_MANAGER self + + --- StopTasks Asynchronous Trigger for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] __StopTasks + -- @param #TASK_MANAGER self + -- @param #number Delay + + + self:AddTransition( "Started", "Manage", "Started" ) + + self:AddTransition( "Started", "Success", "Started" ) + + --- Success Handler OnAfter for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnAfterSuccess + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task + + + self:AddTransition( "Started", "Failed", "Started" ) + + --- Failed Handler OnAfter for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnAfterFailed + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task + + + self:AddTransition( "Started", "Aborted", "Started" ) + + --- Aborted Handler OnAfter for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnAfterAborted + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task + + self:AddTransition( "Started", "Cancelled", "Started" ) + + --- Cancelled Handler OnAfter for TASK_MANAGER + -- @function [parent=#TASK_MANAGER] OnAfterCancelled + -- @param #TASK_MANAGER self + -- @param #string From + -- @param #string Event + -- @param #string To + -- @param Tasking.Task#TASK Task + + self:SetRefreshTimeInterval( 30 ) + + return self + end + + function TASK_MANAGER:onafterStartTasks( From, Event, To ) + self:Manage() + end + + function TASK_MANAGER:onafterManage( From, Event, To ) + + self:__Manage( -self._RefreshTimeInterval ) + + self:ManageTasks() + end + + --- Set the refresh time interval in seconds when a new task management action needs to be done. + -- @param #TASK_MANAGER self + -- @param #number RefreshTimeInterval The refresh time interval in seconds when a new task management action needs to be done. + -- @return #TASK_MANAGER self + function TASK_MANAGER:SetRefreshTimeInterval( RefreshTimeInterval ) + self:F2() + + self._RefreshTimeInterval = RefreshTimeInterval + end + + + --- Manages the tasks for the @{Core.Set#SET_GROUP}. + -- @param #TASK_MANAGER self + -- @return #TASK_MANAGER self + function TASK_MANAGER:ManageTasks() + self:E() + + end + +end + diff --git a/Moose Development/Moose/Tasking/Task_Pickup.lua b/Moose Development/Moose/Tasking/Task_Pickup.lua deleted file mode 100644 index 459f9f4a2..000000000 --- a/Moose Development/Moose/Tasking/Task_Pickup.lua +++ /dev/null @@ -1,132 +0,0 @@ ---- This module contains the TASK_PICKUP classes. --- --- 1) @{#TASK_PICKUP} class, extends @{Task#TASK} --- === --- The @{#TASK_PICKUP} class defines a pickup task of a @{Set} of @{CARGO} objects defined within the mission. --- based on the tasking capabilities defined in @{Task#TASK}. --- The TASK_PICKUP is implemented using a @{Statemachine#FSM_TASK}, and has the following statuses: --- --- * **None**: Start of the process --- * **Planned**: The SEAD task is planned. Upon Planned, the sub-process @{Process_Fsm.Assign#ACT_ASSIGN_ACCEPT} is started to accept the task. --- * **Assigned**: The SEAD task is assigned to a @{Group#GROUP}. Upon Assigned, the sub-process @{Process_Fsm.Route#ACT_ROUTE} is started to route the active Units in the Group to the attack zone. --- * **Success**: The SEAD task is successfully completed. Upon Success, the sub-process @{Process_SEAD#PROCESS_SEAD} is started to follow-up successful SEADing of the targets assigned in the task. --- * **Failed**: The SEAD task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ. --- --- === --- --- ### Authors: FlightControl - Design and Programming --- --- @module Task_PICKUP - - -do -- TASK_PICKUP - - --- The TASK_PICKUP class - -- @type TASK_PICKUP - -- @extends Tasking.Task#TASK - TASK_PICKUP = { - ClassName = "TASK_PICKUP", - } - - --- Instantiates a new TASK_PICKUP. - -- @param #TASK_PICKUP self - -- @param Tasking.Mission#MISSION Mission - -- @param Set#SET_GROUP AssignedSetGroup The set of groups for which the Task can be assigned. - -- @param #string TaskName The name of the Task. - -- @param #string TaskType BAI or CAS - -- @param Set#SET_UNIT UnitSetTargets - -- @param Core.Zone#ZONE_BASE TargetZone - -- @return #TASK_PICKUP self - function TASK_PICKUP:New( Mission, AssignedSetGroup, TaskName, TaskType ) - local self = BASE:Inherit( self, TASK:New( Mission, AssignedSetGroup, TaskName, TaskType, "PICKUP" ) ) - self:F() - - return self - end - - --- Removes a TASK_PICKUP. - -- @param #TASK_PICKUP self - -- @return #nil - function TASK_PICKUP:CleanUp() - - self:GetParent( self ):CleanUp() - - return nil - end - - - --- Assign the @{Task} to a @{Unit}. - -- @param #TASK_PICKUP self - -- @param Wrapper.Unit#UNIT TaskUnit - -- @return #TASK_PICKUP self - function TASK_PICKUP:AssignToUnit( TaskUnit ) - self:F( TaskUnit:GetName() ) - - local ProcessAssign = self:AddProcess( TaskUnit, ACT_ASSIGN_ACCEPT:New( self, TaskUnit, self.TaskBriefing ) ) - local ProcessPickup = self:AddProcess( TaskUnit, PROCESS_PICKUP:New( self, self.TaskType, TaskUnit ) ) - - local Process = self:AddStateMachine( TaskUnit, FSM_TASK:New( self, TaskUnit, { - initial = 'None', - events = { - { name = 'Next', from = 'None', to = 'Planned' }, - { name = 'Next', from = 'Planned', to = 'Assigned' }, - { name = 'Next', from = 'Assigned', to = 'Success' }, - { name = 'Fail', from = 'Assigned', to = 'Failed' }, - }, - callbacks = { - onNext = self.OnNext, - }, - subs = { - Assign = { onstateparent = 'Planned', oneventparent = 'Next', fsm = ProcessAssign.Fsm, event = 'Start', returnevents = { 'Next', 'Reject' } }, - Pickup = { onstateparent = 'Assigned', oneventparent = 'Next', fsm = ProcessDestroy.Fsm, event = 'Start', returnevents = { 'Next' } }, - } - } ) ) - - ProcessRoute:AddScore( "Failed", "failed to destroy a ground unit", -100 ) - ProcessDestroy:AddScore( "Pickup", "Picked-Up a Cargo", 25 ) - ProcessDestroy:AddScore( "Failed", "failed to destroy a ground unit", -100 ) - - Process:Next() - - return self - end - - --- StateMachine callback function for a TASK - -- @param #TASK_PICKUP self - -- @param Core.Fsm#FSM_TASK Fsm - -- @param #string Event - -- @param #string From - -- @param #string To - -- @param Event#EVENTDATA Event - function TASK_PICKUP:OnNext( Fsm, From, Event, To, Event ) - - self:SetState( self, "State", To ) - - end - - --- @param #TASK_PICKUP self - function TASK_PICKUP:GetPlannedMenuText() - return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )" - end - - - --- @param #TASK_PICKUP self - function TASK_PICKUP:_Schedule() - self:F2() - - self.TaskScheduler = SCHEDULER:New( self, _Scheduler, {}, 15, 15 ) - return self - end - - - --- @param #TASK_PICKUP self - function TASK_PICKUP._Scheduler() - self:F2() - - return true - end - -end - - - diff --git a/Moose Development/Moose/Utilities/Routines.lua b/Moose Development/Moose/Utilities/Routines.lua index 508a35ed5..c63359c2e 100644 --- a/Moose Development/Moose/Utilities/Routines.lua +++ b/Moose Development/Moose/Utilities/Routines.lua @@ -1,6 +1,6 @@ --- Various routines -- @module routines --- @author Flightcontrol +-- @image MOOSE.JPG env.setErrorMessageBoxEnabled(false) @@ -2290,7 +2290,7 @@ end function MessageToBlue( MsgText, MsgTime, MsgName ) --trace.f() - MESSAGE:New( MsgText, MsgTime, "To Blue Coalition" ):ToCoalition( coalition.side.RED ) + MESSAGE:New( MsgText, MsgTime, "To Blue Coalition" ):ToCoalition( coalition.side.BLUE ) end function getCarrierHeight( CarrierGroup ) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index e7f730ad1..ca20c1b7f 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -10,6 +10,7 @@ -- * FlightControl : Rework to OO framework -- -- @module Utils +-- @image MOOSE.JPG --- @type SMOKECOLOR @@ -29,6 +30,32 @@ SMOKECOLOR = trigger.smokeColor -- #SMOKECOLOR 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, +} + +--- 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" +} + --- Utilities static class. -- @type UTILS UTILS = { @@ -236,7 +263,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 ) @@ -267,7 +298,26 @@ 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. @@ -481,4 +531,301 @@ function UTILS.BeaufortScale(speed) bd="Hurricane" end return bn,bd -end \ No newline at end of file +end + +--- Split string at seperators. C.f. http://stackoverflow.com/questions/1426954/split-string-in-lua +-- @param #string str Sting to split. +-- @param #string sep Speparator for split. +-- @return #table Split text. +function UTILS.Split(str, sep) + local result = {} + local regex = ("([^%s]+)"):format(sep) + for each in str:gmatch(regex) do + table.insert(result, each) + end + return result +end + +--- Convert time in seconds to hours, minutes and seconds. +-- @param #number seconds Time in seconds, e.g. from timer.getAbsTime() function. +-- @return #string Time in format Hours:Minutes:Seconds+Days (HH:MM:SS+D). +function UTILS.SecondsToClock(seconds) + + -- Nil check. + if seconds==nil then + return nil + end + + -- Seconds + local seconds = tonumber(seconds) + + -- Seconds of this day. + local _seconds=seconds%(60*60*24) + + if seconds<0 then + return nil + else + local hours = string.format("%02.f", math.floor(_seconds/3600)) + local mins = string.format("%02.f", math.floor(_seconds/60 - (hours*60))) + local secs = string.format("%02.f", math.floor(_seconds - hours*3600 - mins *60)) + local days = string.format("%d", seconds/(60*60*24)) + return hours..":"..mins..":"..secs.."+"..days + 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. +-- @return #number Seconds. Corresponds to what you cet from timer.getAbsTime() function. +function UTILS.ClockToSeconds(clock) + + -- Nil check. + if clock==nil then + return nil + end + + -- Seconds init. + local seconds=0 + + -- Split additional days. + local dsplit=UTILS.Split(clock, "+") + + -- Convert days to seconds. + if #dsplit>1 then + seconds=seconds+tonumber(dsplit[2])*60*60*24 + end + + -- Split hours, minutes, seconds + local tsplit=UTILS.Split(dsplit[1], ":") + + -- Get time in seconds + local i=1 + for _,time in ipairs(tsplit) do + if i==1 then + -- Hours + seconds=seconds+tonumber(time)*60*60 + elseif i==2 then + -- Minutes + seconds=seconds+tonumber(time)*60 + elseif i==3 then + -- Seconds + seconds=seconds+tonumber(time) + end + i=i+1 + end + + return seconds +end + +--- Display clock and mission time on screen as a message to all. +-- @param #number duration Duration in seconds how long the time is displayed. Default is 5 seconds. +function UTILS.DisplayMissionTime(duration) + duration=duration or 5 + local Tnow=timer.getAbsTime() + local mission_time=Tnow-timer.getTime0() + local mission_time_minutes=mission_time/60 + local mission_time_seconds=mission_time%60 + local local_time=UTILS.SecondsToClock(Tnow) + local text=string.format("Time: %s - %02d:%02d", local_time, mission_time_minutes, mission_time_seconds) + MESSAGE:New(text, duration):ToAll() +end + + +--- Generate a Gaussian pseudo-random number. +-- @param #number x0 Expectation value of distribution. +-- @param #number sigma (Optional) Standard deviation. Default 10. +-- @param #number xmin (Optional) Lower cut-off value. +-- @param #number xmax (Optional) Upper cut-off value. +-- @param #number imax (Optional) Max number of tries to get a value between xmin and xmax (if specified). Default 100. +-- @return #number Gaussian random number. +function UTILS.RandomGaussian(x0, sigma, xmin, xmax, imax) + + -- Standard deviation. Default 10 if not given. + sigma=sigma or 10 + + -- Max attempts. + imax=imax or 100 + + local r + local gotit=false + local i=0 + while not gotit do + + -- Uniform numbers in [0,1). We need two. + local x1=math.random() + local x2=math.random() + + -- Transform to Gaussian exp(-(x-x0)²/(2*sigma²). + r = math.sqrt(-2*sigma*sigma * math.log(x1)) * math.cos(2*math.pi * x2) + x0 + + i=i+1 + if (r>=xmin and r<=xmax) or i>imax then + gotit=true + end + end + + return r +end + +--- Randomize a value by a certain amount. +-- @param #number value The value which should be randomized +-- @param #number fac Randomization factor. +-- @param #number lower (Optional) Lower limit of the returned value. +-- @param #number upper (Optional) Upper limit of the returned value. +-- @return #number Randomized value. +-- @usage UTILS.Randomize(100, 0.1) returns a value between 90 and 110, i.e. a plus/minus ten percent variation. +-- @usage UTILS.Randomize(100, 0.5, nil, 120) returns a value between 50 and 120, i.e. a plus/minus fivty percent variation with upper bound 120. +function UTILS.Randomize(value, fac, lower, upper) + local min + if lower then + min=math.max(value-value*fac, lower) + else + min=value-value*fac + end + local max + if upper then + max=math.min(value+value*fac, upper) + else + max=value+value*fac + end + + local r=math.random(min, max) + + return r +end + +--- Calculate the [dot product](https://en.wikipedia.org/wiki/Dot_product) of two vectors. The result is a number. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return #number Scalar product of the two vectors a*b. +function UTILS.VecDot(a, b) + return a.x*b.x + a.y*b.y + a.z*b.z +end + +--- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 3D vector. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @return #number Norm of the vector. +function UTILS.VecNorm(a) + return math.sqrt(UTILS.VecDot(a, a)) +end + +--- Calculate the [cross product](https://en.wikipedia.org/wiki/Cross_product) of two 3D vectors. The result is a 3D vector. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return DCS#Vec3 Vector +function UTILS.VecCross(a, b) + return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x} +end + +--- Calculate the difference between two 3D vectors by substracting the x,y,z components from each other. +-- @param DCS#Vec3 a Vector in 3D with x, y, z components. +-- @param DCS#Vec3 b Vector in 3D with x, y, z components. +-- @return DCS#Vec3 Vector c=a-b with c(i)=a(i)-b(i), i=x,y,z. +function UTILS.VecSubstract(a, b) + return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z} +end + +--- Calculate the 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 alpha=math.acos(UTILS.VecDot(a,b)/(UTILS.VecNorm(a)*UTILS.VecNorm(b))) + return math.deg(alpha) +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 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 + + diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index e6db9284c..04fd9e93f 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -4,19 +4,18 @@ -- -- ### Author: **FlightControl** -- --- ### Contributions: +-- ### Contributions: **funkyfranky** -- -- === -- --- @module Airbase +-- @module Wrapper.Airbase +-- @image Wrapper_Airbase.JPG --- @type AIRBASE -- @extends Wrapper.Positionable#POSITIONABLE ---- # AIRBASE class, extends @{Positionable#POSITIONABLE} --- --- AIRBASE is a wrapper class to handle the DCS Airbase objects: +--- Wrapper class to handle the DCS Airbase objects: -- -- * Support all DCS Airbase APIs. -- * Enhance with Airbase specific APIs not in the DCS Airbase API set. @@ -107,10 +106,8 @@ AIRBASE.Caucasus = { ["Mozdok"] = "Mozdok", ["Beslan"] = "Beslan", } - ---- @field Nevada --- --- These are all airbases of Nevada: + +--- These are all airbases of Nevada: -- -- * AIRBASE.Nevada.Creech_AFB -- * AIRBASE.Nevada.Groom_Lake_AFB @@ -130,7 +127,7 @@ AIRBASE.Caucasus = { -- * AIRBASE.Nevada.Pahute_Mesa_Airstrip -- * AIRBASE.Nevada.Tonopah_Airport -- * AIRBASE.Nevada.Tonopah_Test_Range_Airfield --- +-- @field Nevada AIRBASE.Nevada = { ["Creech_AFB"] = "Creech AFB", ["Groom_Lake_AFB"] = "Groom Lake AFB", @@ -152,9 +149,7 @@ AIRBASE.Nevada = { ["Tonopah_Test_Range_Airfield"] = "Tonopah Test Range Airfield", } ---- @field Normandy --- --- These are all airbases of Normandy: +--- These are all airbases of Normandy: -- -- * AIRBASE.Normandy.Saint_Pierre_du_Mont -- * AIRBASE.Normandy.Lignerolles @@ -187,6 +182,7 @@ AIRBASE.Nevada = { -- * AIRBASE.Normandy.Funtington -- * AIRBASE.Normandy.Tangmere -- * AIRBASE.Normandy.Ford +-- @field Normandy AIRBASE.Normandy = { ["Saint_Pierre_du_Mont"] = "Saint Pierre du Mont", ["Lignerolles"] = "Lignerolles", @@ -221,6 +217,97 @@ AIRBASE.Normandy = { ["Ford"] = "Ford", } +--- 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_Musa_Island_Airport +-- * 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.Khasab +-- * AIRBASE.PersianGulf.Al_Minhad_AB +-- * AIRBASE.PersianGulf.Sharjah_Intl +-- * AIRBASE.PersianGulf.Shiraz_International_Airport +-- * AIRBASE.PersianGulf.Kerman_Airport +-- * AIRBASE.PersianGulf.Jiroft_Airport +-- * AIRBASE.PersianGulf.Lavan_Island_Airport +-- @field PersianGulf +AIRBASE.PersianGulf = { + ["Fujairah_Intl"] = "Fujairah Intl", + ["Qeshm_Island"] = "Qeshm Island", + ["Sir_Abu_Nuayr"] = "Sir Abu Nuayr", + ["Abu_Musa_Island_Airport"] = "Abu Musa Island Airport", + ["Bandar_Abbas_Intl"] = "Bandar Abbas Intl", + ["Bandar_Lengeh"] = "Bandar Lengeh", + ["Tunb_Island_AFB"] = "Tunb Island AFB", + ["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", + ["Khasab"] = "Khasab", + ["Al_Minhad_AB"] = "Al Minhad AB", + ["Sharjah_Intl"] = "Sharjah Intl", + ["Shiraz_International_Airport"] = "Shiraz International Airport", + ["Kerman_Airport"] = "Kerman Airport", + ["Jiroft_Airport"] = "Jiroft Airport", + ["Lavan_Island_Airport"] = "Lavan Island Airport", + } + +--- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". +-- @type AIRBASE.ParkingSpot +-- @field Core.Point#COORDINATE Coordinate Coordinate of the parking spot. +-- @field #number TerminalID Terminal ID of the spot. Generally, this is not the same number as displayed in the mission editor. +-- @field #AIRBASE.TerminalType TerminalType Type of the spot, i.e. for which type of aircraft it can be used. +-- @field #boolean TOAC Takeoff or landing aircarft. I.e. this stop is occupied currently by an aircraft until it took of or until it landed. +-- @field #boolean Free This spot is currently free, i.e. there is no alive aircraft on it at the present moment. +-- @field #number TerminalID0 Unknown what this means. If you know, please tell us! +-- @field #number DistToRwy Distance to runway in meters. Currently bugged and giving the same number as the TerminalID. + +--- Terminal Types of parking spots. See also https://wiki.hoggitworld.com/view/DCS_func_getParking +-- +-- Supported types are: +-- +-- * AIRBASE.TerminalType.Runway = 16: Valid spawn points on runway. +-- * AIRBASE.TerminalType.HelicopterOnly = 40: Special spots for Helicopers. +-- * AIRBASE.TerminalType.Shelter = 68: Hardened Air Shelter. Currently only on Caucaus map. +-- * AIRBASE.TerminalType.OpenMed = 72: Open/Shelter air airplane only. +-- * AIRBASE.TerminalType.OpenBig = 104: Open air spawn points. Generally larger but does not guarantee large aircraft are capable of spawning there. +-- * AIRBASE.TerminalType.OpenMedOrBig = 176: Combines OpenMed and OpenBig spots. +-- * AIRBASE.TerminalType.HelicopterUnsable = 216: Combines HelicopterOnly, OpenMed and OpenBig. +-- * AIRBASE.TerminalType.FighterAircraft = 244: Combines Shelter. OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. +-- +-- @type AIRBASE.TerminalType +-- @field #number Runway 16: Valid spawn points on runway. +-- @field #number HelicopterOnly 40: Special spots for Helicopers. +-- @field #number Shelter 68: Hardened Air Shelter. Currently only on Caucaus map. +-- @field #number OpenMed 72: Open/Shelter air airplane only. +-- @field #number OpenBig 104: Open air spawn points. Generally larger but does not guarantee large aircraft are capable of spawning there. +-- @field #number OpenMedOrBig 176: Combines OpenMed and OpenBig spots. +-- @field #number HelicopterUnsable 216: Combines HelicopterOnly, OpenMed and OpenBig. +-- @field #number FighterAircraft 244: Combines Shelter. OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft. +AIRBASE.TerminalType = { + Runway=16, + HelicopterOnly=40, + Shelter=68, + OpenMed=72, + OpenBig=104, + OpenMedOrBig=176, + HelicopterUsable=216, + FighterAircraft=244, +} + -- Registration. --- Create a new AIRBASE from DCSAirbase. @@ -239,7 +326,7 @@ end --- Finds a AIRBASE from the _DATABASE using a DCSAirbase object. -- @param #AIRBASE self --- @param Dcs.DCSWrapper.Airbase#Airbase DCSAirbase An existing DCS Airbase object reference. +-- @param DCS#Airbase DCSAirbase An existing DCS Airbase object reference. -- @return Wrapper.Airbase#AIRBASE self function AIRBASE:Find( DCSAirbase ) @@ -251,13 +338,16 @@ end --- Find a AIRBASE in the _DATABASE using the name of an existing DCS Airbase. -- @param #AIRBASE self -- @param #string AirbaseName The Airbase Name. --- @return Wrapper.Airbase#AIRBASE self +-- @return #AIRBASE self function AIRBASE:FindByName( AirbaseName ) local AirbaseFound = _DATABASE:FindAirbase( AirbaseName ) return AirbaseFound end +--- Get the DCS object of an airbase +-- @param #AIRBASE self +-- @return DCS#Airbase DCS airbase object. function AIRBASE:GetDCSObject() local DCSAirbase = Airbase.getByName( self.AirbaseName ) @@ -275,5 +365,565 @@ function AIRBASE:GetZone() return self.AirbaseZone end +--- Get all airbases of the current map. This includes ships and FARPS. +-- @param DCS#Coalition coalition (Optional) Return only airbases belonging to the specified coalition. By default, all airbases of the map are returned. +-- @return #table Table containing all airbase objects of the current map. +function AIRBASE.GetAllAirbases(coalition) + + local airbases={} + for _,airbase in pairs(_DATABASE.AIRBASES) do + if (coalition~=nil and airbase:GetCoalition()==coalition) or coalition==nil then + table.insert(airbases, airbase) + end + end + + return airbases +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: +-- +-- * 16 : Valid spawn points on runway +-- * 40 : Helicopter only spawn +-- * 68 : Hardened Air Shelter +-- * 72 : Open/Shelter air airplane only +-- * 104: Open air spawn +-- +-- Note that only Caucuses will return 68 as it is the only map currently with hardened air shelters. +-- 104 are also generally larger, but does not guarantee a large aircraft like the B-52 or a C-130 are capable of spawning there. +-- +-- Table entries: +-- +-- * Term_index is the id for the parking +-- * vTerminal pos is its vec3 position in the world +-- * fDistToRW is the distance to the take-off position for the active runway from the parking. +-- +-- @param #AIRBASE self +-- @param #boolean available If true, only available parking spots will be returned. +-- @return #table Table with parking data. See https://wiki.hoggitworld.com/view/DCS_func_getParking +function AIRBASE:GetParkingData(available) + self:F2(available) + + -- Get DCS airbase object. + local DCSAirbase=self:GetDCSObject() + + -- Get parking data. + local parkingdata=nil + if DCSAirbase then + parkingdata=DCSAirbase:getParking(available) + end + + self:T2({parkingdata=parkingdata}) + return parkingdata +end + +--- Get number of parking spots at an airbase. Optionally, a specific terminal type can be requested. +-- @param #AIRBASE self +-- @param #AIRBASE.TerminalType termtype Terminal type of which the number of spots is counted. Default all spots but spawn points on runway. +-- @return #number Number of parking spots at this airbase. +function AIRBASE:GetParkingSpotsNumber(termtype) + + -- Get free parking spots data. + local parkingdata=self:GetParkingData(false) + + local nspots=0 + for _,parkingspot in pairs(parkingdata) do + if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then + nspots=nspots+1 + end + end + + return nspots +end + +--- Get number of free parking spots at an airbase. +-- @param #AIRBASE self +-- @param #AIRBASE.TerminalType termtype Terminal type. +-- @param #boolean allowTOAC If true, spots are considered free even though TO_AC is true. Default is off which is saver to avoid spawning aircraft on top of each other. Option might be enabled for FARPS and ships. +-- @return #number Number of free parking spots at this airbase. +function AIRBASE:GetFreeParkingSpotsNumber(termtype, allowTOAC) + + -- Get free parking spots data. + local parkingdata=self:GetParkingData(true) + + local nfree=0 + for _,parkingspot in pairs(parkingdata) do + -- Spots on runway are not counted unless explicitly requested. + if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then + if (allowTOAC and allowTOAC==true) or parkingspot.TO_AC==false then + nfree=nfree+1 + end + end + end + + return nfree +end + +--- Get the coordinates of free parking spots at an airbase. +-- @param #AIRBASE self +-- @param #AIRBASE.TerminalType termtype Terminal type. +-- @param #boolean allowTOAC If true, spots are considered free even though TO_AC is true. Default is off which is saver to avoid spawning aircraft on top of each other. Option might be enabled for FARPS and ships. +-- @return #table Table of coordinates of the free parking spots. +function AIRBASE:GetFreeParkingSpotsCoordinates(termtype, allowTOAC) + + -- Get free parking spots data. + local parkingdata=self:GetParkingData(true) + + -- Put coordinates of free spots into table. + local spots={} + for _,parkingspot in pairs(parkingdata) do + -- Coordinates on runway are not returned unless explicitly requested. + if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then + if (allowTOAC and allowTOAC==true) or parkingspot.TO_AC==false then + table.insert(spots, COORDINATE:NewFromVec3(parkingspot.vTerminalPos)) + end + end + end + + return spots +end + +--- Get the coordinates of all parking spots at an airbase. Optionally only those of a specific terminal type. Spots on runways are excluded if not explicitly requested by terminal type. +-- @param #AIRBASE self +-- @param #AIRBASE.TerminalType termtype (Optional) Terminal type. Default all. +-- @return #table Table of coordinates of parking spots. +function AIRBASE:GetParkingSpotsCoordinates(termtype) + + -- Get all parking spots data. + local parkingdata=self:GetParkingData(false) + + -- Put coordinates of free spots into table. + local spots={} + for _,parkingspot in pairs(parkingdata) do + + -- Coordinates on runway are not returned unless explicitly requested. + if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then + + -- Get coordinate from Vec3 terminal position. + local _coord=COORDINATE:NewFromVec3(parkingspot.vTerminalPos) + + -- Add to table. + table.insert(spots, _coord) + end + + end + + return spots +end + + +--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase. +-- @param #AIRBASE self +-- @param #AIRBASE.TerminalType termtype Terminal type. +-- @return #table Table free parking spots. Table has the elements ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". +function AIRBASE:GetParkingSpotsTable(termtype) + + -- Get parking data of all spots (free or occupied) + local parkingdata=self:GetParkingData(false) + -- Get parking data of all free spots. + local parkingfree=self:GetParkingData(true) + + -- Function to ckeck if any parking spot is free. + local function _isfree(_tocheck) + for _,_spot in pairs(parkingfree) do + if _spot.Term_Index==_tocheck.Term_Index then + return true + end + end + return false + end + + -- Put coordinates of parking spots into table. + local spots={} + for _,_spot in pairs(parkingdata) do + if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) then + local _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 + + return spots +end + +--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase. +-- @param #AIRBASE self +-- @param #AIRBASE.TerminalType termtype Terminal type. +-- @param #boolean allowTOAC If true, spots are considered free even though TO_AC is true. Default is off which is saver to avoid spawning aircraft on top of each other. Option might be enabled for FARPS and ships. +-- @return #table Table free parking spots. Table has the elements ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". +function AIRBASE:GetFreeParkingSpotsTable(termtype, allowTOAC) + + -- Get parking data of all free spots. + local parkingfree=self:GetParkingData(true) + + -- Put coordinates of free spots into table. + local freespots={} + for _,_spot in pairs(parkingfree) do + if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) 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}) + end + end + end + + return freespots +end + +--- Place markers of parking spots on the F10 map. +-- @param #AIRBASE self +-- @param #AIRBASE.TerminalType termtype Terminal type for which marks should be placed. +-- @param #boolean mark If false, do not place markers but only give output to DCS.log file. Default true. +function AIRBASE:MarkParkingSpots(termtype, mark) + + -- Default is true. + if mark==nil then + mark=true + end + + -- Get parking data from getParking() wrapper function. + local parkingdata=self:GetParkingSpotsTable(termtype) + + -- Get airbase name. + local airbasename=self:GetName() + self:E(string.format("Parking spots at %s for termial type %s:", airbasename, tostring(termtype))) + + for _,_spot in pairs(parkingdata) do + + -- Mark text. + local _text=string.format("Term Index=%d, Term Type=%d, Free=%s, TOAC=%s, Term ID0=%d, Dist2Rwy=%.1f m", + _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy) + + -- Create mark on the F10 map. + if mark then + _spot.Coordinate:MarkToAll(_text) + end + + -- Info to DCS.log file. + local _text=string.format("%s, Term Index=%3d, Term Type=%03d, Free=%5s, TOAC=%5s, Term ID0=%3d, Dist2Rwy=%.1f m", + airbasename, _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy) + self:E(_text) + end +end + +--- Seach unoccupied parking spots at the airbase for a specific group of aircraft. The routine also optionally checks for other unit, static and scenery options in a certain radius around the parking spot. +-- The dimension of the spawned aircraft and of the potential obstacle are taken into account. Note that the routine can only return so many spots that are free. +-- @param #AIRBASE self +-- @param Wrapper.Group#GROUP group Aircraft group for which the parking spots are requested. +-- @param #AIRBASE.TerminalType terminaltype (Optional) Only search spots at a specific terminal type. Default is all types execpt on runway. +-- @param #number scanradius (Optional) Radius in meters around parking spot to scan for obstacles. Default 50 m. +-- @param #boolean scanunits (Optional) Scan for units as obstacles. Default true. +-- @param #boolean scanstatics (Optional) Scan for statics as obstacles. Default true. +-- @param #boolean scanscenery (Optional) Scan for scenery as obstacles. Default false. Can cause problems with e.g. shelters. +-- @param #boolean verysafe (Optional) If true, wait until an aircraft has taken off until the parking spot is considered to be free. Defaul false. +-- @param #number nspots (Optional) Number of freeparking spots requested. Default is the number of aircraft in the group. +-- @param #table parkingdata (Optional) Parking spots data table. If not given it is automatically derived from the GetParkingSpotsTable() function. +-- @return #table Table of coordinates and terminal IDs of free parking spots. Each table entry has the elements .Coordinate and .TerminalID. +function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nspots, parkingdata) + + -- Init default + scanradius=scanradius or 50 + if scanunits==nil then + scanunits=true + end + if scanstatics==nil then + scanstatics=true + end + if scanscenery==nil then + scanscenery=false + end + if verysafe==nil then + verysafe=false + end + + -- 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 + end + + -- Get airport name. + local airport=self:GetName() + + -- Get parking spot data table. This contains free and "non-free" spots. + -- Note that there are three major issues with the DCS getParking() function: + -- 1. A spot is considered as NOT free until an aircraft that is present has finally taken off. This might be a bit long especiall at smaller airports. + -- 2. A "free" spot does not take the aircraft size into accound. So if two big aircraft are spawned on spots next to each other, they might overlap and get destroyed. + -- 3. The routine return a free spot, if there a static objects placed on the spot. + parkingdata=parkingdata or self:GetParkingSpotsTable(terminaltype) + + -- Get the aircraft size, i.e. it's longest side of x,z. + local aircraft=group:GetUnit(1) + local _aircraftsize, ax,ay,az=_GetObjectSize(aircraft, true) + + -- Number of spots we are looking for. Note that, e.g. grouping can require a number different from the group size! + local _nspots=nspots or group:GetSize() + + -- Debug info. + self:E(string.format("%s: Looking for %d parking spot(s) for aircraft of size %.1f m (x=%.1f,y=%.1f,z=%.1f) at termial type %s.", airport, _nspots, _aircraftsize, ax, ay, az, tostring(terminaltype))) + + -- Table of valid spots. + local validspots={} + local nvalid=0 + + -- Test other stuff if no parking spot is available. + local _test=false + if _test then + return validspots + end + + -- Mark all found obstacles on F10 map for debugging. + local markobstacles=false + + -- Loop over all known parking spots + for _,parkingspot in pairs(parkingdata) do + + -- Coordinate of the parking spot. + local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE + local _termid=parkingspot.TerminalID + + 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))) + + else + + -- Scan a radius of 50 meters around the spot. + local _,_,_,_units,_statics,_sceneries=_spot:ScanObjects(scanradius, scanunits, scanstatics, scanscenery) + + -- Loop over objects within scan radius. + local occupied=false + + -- Check all units. + for _,unit in pairs(_units) do + -- 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) + + 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))) + end + + if scanunits and not _safe then + occupied=true + end + end + + -- Check all statics. + for _,static in pairs(_statics) do + local _vec3=static:getPoint() + local _coord=COORDINATE:NewFromVec3(_vec3) + local _dist=_coord:Get2DDistance(_spot) + local _safe=_overlap(aircraft, true, static, false,_dist) + + if markobstacles then + local l,x,y,z=_GetObjectSize(static) + _coord:MarkToAll(string.format("Static %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", static:getName(),x,y,z,l,_dist, _termid, tostring(_safe))) + end + + if scanstatics and not _safe then + occupied=true + end + end + + -- Check all scenery. + for _,scenery in pairs(_sceneries) do + local _vec3=scenery:getPoint() + local _coord=COORDINATE:NewFromVec3(_vec3) + local _dist=_coord:Get2DDistance(_spot) + local _safe=_overlap(aircraft, true, scenery, false,_dist) + + if markobstacles then + local l,x,y,z=_GetObjectSize(scenery) + _coord:MarkToAll(string.format("Scenery %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", scenery:getTypeName(),x,y,z,l,_dist, _termid, tostring(_safe))) + end + + if scanscenery and not _safe then + occupied=true + end + end + + -- Now check the already given spots so that we do not put a large aircraft next to one we already assigned a nearby spot. + for _,_takenspot in pairs(validspots) do + local _dist=_takenspot.Coordinate:Get2DDistance(_spot) + local _safe=_overlap(aircraft, true, aircraft, true,_dist) + if not _safe then + occupied=true + end + end + + --_spot:MarkToAll(string.format("Parking spot %d free=%s", parkingspot.TerminalID, tostring(not occupied))) + if occupied then + self:T(string.format("%s: Parking spot id %d occupied.", airport, _termid)) + else + self:E(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 + end + + end -- loop over units + + -- We found enough spots. + if nvalid>=_nspots then + return validspots + end + end -- check terminal type + end + + -- Retrun spots we found, even if there were not enough. + return validspots +end + +--- Function that checks if at leat one unit of a group has been spawned close to a spawn point on the runway. +-- @param #AIRBASE self +-- @param Wrapper.Group#GROUP group Group to be checked. +-- @param #number radius Radius around the spawn point to be checked. Default is 50 m. +-- @param #boolean despawn If true, the group is destroyed. +-- @return #boolean True if group is within radius around spawn points on runway. +function AIRBASE:CheckOnRunWay(group, radius, despawn) + + -- Default radius. + radius=radius or 50 + + -- We only check at real airbases (not FARPS or ships). + if self:GetDesc().category~=Airbase.Category.AIRDROME then + return false + end + + if group and group:IsAlive() then + + -- Debug. + self:T(string.format("%s, checking if group %s is on runway?",self:GetName(), group:GetName())) + + -- Get coordinates on runway. + local runwaypoints=self:GetParkingSpotsCoordinates(AIRBASE.TerminalType.Runway) + + -- Mark runway spawn points. + --[[ + for _i,_coord in pairs(runwaypoints) do + _coord:MarkToAll(string.format("runway %d",_i)) + end + ]] + + -- Get units of group. + local units=group:GetUnits() + + -- Loop over units. + for _,_unit in pairs(units) do + + local unit=_unit --Wrapper.Unit#UNIT + + -- Check if unit is alive and not in air. + if unit and unit:IsAlive() and not unit:InAir() then + self:T(string.format("%s, checking if unit %s is on runway?",self:GetName(), unit:GetName())) + + -- Loop over runway spawn points. + for _i,_coord in pairs(runwaypoints) do + + -- Distance between unit and spawn pos. + local dist=unit:GetCoordinate():Get2DDistance(_coord) + + -- Mark unit spawn points for debugging. + --unit:GetCoordinate():MarkToAll(string.format("unit %s distance to rwy %d = %d",unit:GetName(),_i, dist)) + + -- Check if unit is withing radius. + if dist radius %.1f m. Despawn = %s.", self:GetName(), unit:GetName(), group:GetName(),_i, dist, radius, tostring(despawn))) + --unit:FlareGreen() + end + + end + else + self:T(string.format("%s, checking if unit %s of group %s is on runway. Unit is NOT alive.",self:GetName(), unit:GetName(), group:GetName())) + end + end + else + self:T(string.format("%s, checking if group %s is on runway. Group is NOT alive.",self:GetName(), group:GetName())) + end + + return false +end + +--- Helper function to check for the correct terminal type including "artificial" ones. +-- @param #number Term_Type Termial type from getParking routine. +-- @param #AIRBASE.TerminalType termtype Terminal type from AIRBASE.TerminalType enumerator. +-- @return #boolean True if terminal types match. +function AIRBASE._CheckTerminalType(Term_Type, termtype) + + -- Nill check for Term_Type. + if Term_Type==nil then + return false + end + + -- If no terminal type is requested, we return true. BUT runways are excluded unless explicitly requested. + if termtype==nil then + if Term_Type==AIRBASE.TerminalType.Runway then + return false + else + return true + end + end + + -- Init no match. + local match=false + + -- Standar case. + if Term_Type==termtype then + match=true + end + + -- Artificial cases. Combination of terminal types. + if termtype==AIRBASE.TerminalType.OpenMedOrBig then + if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig then + match=true + end + elseif termtype==AIRBASE.TerminalType.HelicopterUsable then + if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig or Term_Type==AIRBASE.TerminalType.HelicopterOnly then + match=true + end + elseif termtype==AIRBASE.TerminalType.FighterAircraft then + if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig or Term_Type==AIRBASE.TerminalType.Shelter then + match=true + end + end + + return match +end \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Client.lua b/Moose Development/Moose/Wrapper/Client.lua index 71e2066cb..ada653ba3 100644 --- a/Moose Development/Moose/Wrapper/Client.lua +++ b/Moose Development/Moose/Wrapper/Client.lua @@ -8,7 +8,8 @@ -- -- === -- --- @module Client +-- @module Wrapper.Client +-- @image Wrapper_Client.JPG --- The CLIENT class @@ -16,9 +17,8 @@ -- @extends Wrapper.Unit#UNIT ---- # CLIENT class, extends @{Unit#UNIT} +--- Wrapper class of those **Units** defined within the Mission Editor that have the skillset defined as __Client__ or __Player__. -- --- Clients are those **Units** defined within the Mission Editor that have the skillset defined as __Client__ or __Player__. -- Note that clients are NOT the same as Units, they are NOT necessarily alive. -- The CLIENT class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__: -- @@ -179,11 +179,10 @@ function CLIENT:ShowBriefing() if not self.ClientBriefingShown then self.ClientBriefingShown = true local Briefing = "" - if self.ClientBriefing then + if self.ClientBriefing and self.ClientBriefing ~= "" then Briefing = Briefing .. self.ClientBriefing + self:Message( Briefing, 60, "Briefing" ) end - Briefing = Briefing .. " Press [LEFT ALT]+[B] to view the complete mission briefing." - self:Message( Briefing, 60, "Briefing" ) end return self @@ -275,7 +274,7 @@ end --- Return the DCSGroup of a Client. -- This function is modified to deal with a couple of bugs in DCS 1.5.3 -- @param #CLIENT self --- @return Dcs.DCSWrapper.Group#Group +-- @return DCS#Group The group of the Client. function CLIENT:GetDCSGroup() self:F3() @@ -349,10 +348,10 @@ function CLIENT:GetDCSGroup() end --- TODO: Check Dcs.DCSTypes#Group.ID +-- TODO: Check DCS#Group.ID --- Get the group ID of the client. -- @param #CLIENT self --- @return Dcs.DCSTypes#Group.ID +-- @return DCS#Group.ID function CLIENT:GetClientGroupID() local ClientGroup = self:GetDCSGroup() @@ -377,8 +376,8 @@ end -- @param #CLIENT self -- @return Wrapper.Unit#UNIT function CLIENT:GetClientGroupUnit() - self:F2() - + self:F2() + local ClientDCSUnit = Unit.getByName( self.ClientName ) self:T( self.ClientDCSUnit ) @@ -391,7 +390,7 @@ end --- Returns the DCSUnit of the CLIENT. -- @param #CLIENT self --- @return Dcs.DCSTypes#Unit +-- @return DCS#Unit function CLIENT:GetClientGroupDCSUnit() self:F2() @@ -412,8 +411,8 @@ function CLIENT:IsTransport() return self.ClientTransport end ---- Shows the @{AI_Cargo#CARGO} contained within the CLIENT to the player as a message. --- The @{AI_Cargo#CARGO} is shown using the @{Message#MESSAGE} distribution system. +--- Shows the @{AI.AI_Cargo#CARGO} contained within the CLIENT to the player as a message. +-- The @{AI.AI_Cargo#CARGO} is shown using the @{Core.Message#MESSAGE} distribution system. -- @param #CLIENT self function CLIENT:ShowCargo() self:F() @@ -434,11 +433,7 @@ function CLIENT:ShowCargo() end --- TODO (1) I urgently need to revise this. ---- A local function called by the DCS World Menu system to switch off messages. -function CLIENT.SwitchMessages( PrmTable ) - PrmTable[1].MessageSwitch = PrmTable[2] -end + --- The main message driver for the CLIENT. -- This function displays various messages to the Player logged into the CLIENT through the DCS World Messaging system. @@ -446,7 +441,7 @@ end -- @param #string Message is the text describing the message. -- @param #number MessageDuration is the duration in seconds that the Message should be displayed. -- @param #string MessageCategory is the category of the message (the title). --- @param #number MessageInterval is the interval in seconds between the display of the @{Message#MESSAGE} when the CLIENT is in the air. +-- @param #number MessageInterval is the interval in seconds between the display of the @{Core.Message#MESSAGE} when the CLIENT is in the air. -- @param #string MessageID is the identifier of the message when displayed with intervals. function CLIENT:Message( Message, MessageDuration, MessageCategory, MessageInterval, MessageID ) self:F( { Message, MessageDuration, MessageCategory, MessageInterval } ) diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index fe3d1378d..d037d734f 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -8,41 +8,39 @@ -- -- === -- --- @module Controllable - +-- @module Wrapper.Controllable +-- @image Wrapper_Controllable.JPG --- @type CONTROLLABLE --- @extends Wrapper.Positionable#POSITIONABLE --- @field Dcs.DCSWrapper.Controllable#Controllable DCSControllable The DCS controllable class. +-- @field DCS#Controllable DCSControllable The DCS controllable class. -- @field #string ControllableName The name of the controllable. +-- @extends Wrapper.Positionable#POSITIONABLE ---- # CONTROLLABLE class, extends @{Positionable#POSITIONABLE} --- --- CONTROLLABLE is a wrapper class to handle the "DCS Controllable objects", which are Groups and Units: +--- Wrapper class to handle the "DCS Controllable objects", which are Groups and Units: -- -- * Support all DCS Controllable APIs. -- * Enhance with Controllable specific APIs not in the DCS Controllable API set. -- * Handle local Controllable Controller. -- * Manage the "state" of the DCS Controllable. -- --- ## CONTROLLABLE constructor +-- # 1) CONTROLLABLE constructor -- -- The CONTROLLABLE class provides the following functions to construct a CONTROLLABLE instance: -- -- * @{#CONTROLLABLE.New}(): Create a CONTROLLABLE instance. -- --- ## CONTROLLABLE Task methods +-- # 2) CONTROLLABLE Task methods -- -- Several controllable task methods are available that help you to prepare tasks. --- These methods return a string consisting of the task description, which can then be given to either a @{Controllable#CONTROLLABLE.PushTask} or @{Controllable#SetTask} method to assign the task to the CONTROLLABLE. +-- These methods return a string consisting of the task description, which can then be given to either a @{Wrapper.Controllable#CONTROLLABLE.PushTask} or @{Wrapper.Controllable#SetTask} method to assign the task to the CONTROLLABLE. -- Tasks are specific for the category of the CONTROLLABLE, more specific, for AIR, GROUND or AIR and GROUND. -- Each task description where applicable indicates for which controllable category the task is valid. -- There are 2 main subdivisions of tasks: Assigned tasks and EnRoute tasks. -- --- ### Task assignment +-- ## 2.1) Task assignment -- -- Assigned task methods make the controllable execute the task where the location of the (possible) targets of the task are known before being detected. -- This is different from the EnRoute tasks, where the targets of the task need to be detected before the task can be executed. @@ -63,7 +61,7 @@ -- * @{#CONTROLLABLE.TaskHold}: (GROUND) Hold ground controllable from moving. -- * @{#CONTROLLABLE.TaskHoldPosition}: (AIR) Hold position at the current position of the first unit of the controllable. -- * @{#CONTROLLABLE.TaskLand}: (AIR HELICOPTER) Landing at the ground. For helicopters only. --- * @{#CONTROLLABLE.TaskLandAtZone}: (AIR) Land the controllable at a @{Zone#ZONE_RADIUS). +-- * @{#CONTROLLABLE.TaskLandAtZone}: (AIR) Land the controllable at a @{Core.Zone#ZONE_RADIUS). -- * @{#CONTROLLABLE.TaskOrbitCircle}: (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude. -- * @{#CONTROLLABLE.TaskOrbitCircleAtVec2}: (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. -- * @{#CONTROLLABLE.TaskRefueling}: (AIR) Refueling from the nearest tanker. No parameters. @@ -73,7 +71,7 @@ -- * @{#CONTROLLABLE.TaskRouteToZone}: (AIR + GROUND) Route the controllable to a given zone. -- * @{#CONTROLLABLE.TaskReturnToBase}: (AIR) Route the controllable to an airbase. -- --- ### EnRoute assignment +-- ## 2.2) EnRoute assignment -- -- EnRoute tasks require the targets of the task need to be detected by the controllable (using its sensors) before the task can be executed: -- @@ -86,7 +84,7 @@ -- * @{#CONTROLLABLE.EnRouteTaskFAC_EngageControllable}: (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets. -- * @{#CONTROLLABLE.EnRouteTaskTanker}: (AIR) Aircraft will act as a tanker for friendly units. No parameters. -- --- ### Task preparation +-- ## 2.3) Task preparation -- -- There are certain task methods that allow to tailor the task behaviour: -- @@ -95,7 +93,7 @@ -- * @{#CONTROLLABLE.TaskCondition}: Return a condition section for a controlled task. -- * @{#CONTROLLABLE.TaskControlled}: Return a Controlled Task taking a Task and a TaskCondition. -- --- ### Call a function as a Task +-- ## 2.4) Call a function as a Task -- -- A function can be called which is part of a Task. The method @{#CONTROLLABLE.TaskFunction}() prepares -- a Task that can call a GLOBAL function from within the Controller execution. @@ -104,27 +102,27 @@ -- -- Demonstration Mission: [GRP-502 - Route at waypoint to random point](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/GRP - Group Commands/GRP-502 - Route at waypoint to random point) -- --- ### Tasks at Waypoints +-- ## 2.5) Tasks at Waypoints -- -- Special Task methods are available to set tasks at certain waypoints. -- The method @{#CONTROLLABLE.SetTaskWaypoint}() helps preparing a Route, embedding a Task at the Waypoint of the Route. -- -- This creates a Task element, with an action to call a function as part of a Wrapped Task. -- --- ### Obtain the mission from controllable templates +-- ## 2.6) Obtain the mission from controllable templates -- -- Controllable templates contain complete mission descriptions. Sometimes you want to copy a complete mission from a controllable and assign it to another: -- -- * @{#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. -- --- ## CONTROLLABLE Command methods +-- # 3) Command methods -- -- Controllable **command methods** prepare the execution of commands using the @{#CONTROLLABLE.SetCommand} method: -- -- * @{#CONTROLLABLE.CommandDoScript}: Do Script command. -- * @{#CONTROLLABLE.CommandSwitchWayPoint}: Perform a switch waypoint command. -- --- ## Routing of Controllables +-- # 4) Routing of Controllables -- -- Different routing methods exist to route GROUPs and UNITs to different locations: -- @@ -132,11 +130,11 @@ -- * @{#CONTROLLABLE.RouteGroundTo}(): Make the GROUND Controllable to drive towards a specific coordinate. -- * @{#CONTROLLABLE.RouteAirTo}(): Make the AIR Controllable to fly towards a specific coordinate. -- --- ## Option methods +-- # 5) Option methods -- -- Controllable **Option methods** change the behaviour of the Controllable while being alive. -- --- ### Rule of Engagement: +-- ## 5.1) Rule of Engagement: -- -- * @{#CONTROLLABLE.OptionROEWeaponFree} -- * @{#CONTROLLABLE.OptionROEOpenFire} @@ -150,7 +148,7 @@ -- * @{#CONTROLLABLE.OptionROEReturnFirePossible} -- * @{#CONTROLLABLE.OptionROEEvadeFirePossible} -- --- ### Rule on thread: +-- ## 5.2) Reaction On Thread: -- -- * @{#CONTROLLABLE.OptionROTNoReaction} -- * @{#CONTROLLABLE.OptionROTPassiveDefense} @@ -164,6 +162,17 @@ -- * @{#CONTROLLABLE.OptionROTEvadeFirePossible} -- * @{#CONTROLLABLE.OptionROTVerticalPossible} -- +-- ## 5.3) Alarm state: +-- +-- * @{#CONTROLLABLE.OptionAlarmStateAuto} +-- * @{#CONTROLLABLE.OptionAlarmStateGreen} +-- * @{#CONTROLLABLE.OptionAlarmStateRed} +-- +-- ## 5.4) Jettison weapons: +-- +-- * @{#CONTROLLABLE.OptionAllowJettisonWeaponsOnThreat} +-- * @{#CONTROLLABLE.OptionKeepWeaponsOnThreat} +-- -- @field #CONTROLLABLE CONTROLLABLE = { ClassName = "CONTROLLABLE", @@ -173,7 +182,7 @@ CONTROLLABLE = { --- Create a new CONTROLLABLE from a DCSControllable -- @param #CONTROLLABLE self --- @param Dcs.DCSWrapper.Controllable#Controllable ControllableName The DCS Controllable name +-- @param #string ControllableName The DCS Controllable name -- @return #CONTROLLABLE self function CONTROLLABLE:New( ControllableName ) local self = BASE:Inherit( self, POSITIONABLE:New( ControllableName ) ) -- #CONTROLLABLE @@ -188,7 +197,7 @@ end --- Get the controller for the CONTROLLABLE. -- @param #CONTROLLABLE self --- @return Dcs.DCSController#Controller +-- @return DCS#Controller function CONTROLLABLE:_GetController() local DCSControllable = self:GetDCSObject() @@ -202,26 +211,6 @@ end -- Get methods ---- Returns the UNITs wrappers of the DCS Units of the Controllable (default is a GROUP). --- @param #CONTROLLABLE self --- @return #list The UNITs wrappers. -function CONTROLLABLE:GetUnits() - self:F2( { self.ControllableName } ) - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - local DCSUnits = DCSControllable:getUnits() - local Units = {} - for Index, UnitData in pairs( DCSUnits ) do - Units[#Units+1] = UNIT:Find( UnitData ) - end - self:T3( Units ) - return Units - end - - return nil -end - --- Returns the health. Dead controllables have health <= 1.0. -- @param #CONTROLLABLE self @@ -281,6 +270,26 @@ function CONTROLLABLE:GetLife0() return nil end +--- Returns relative minimum amount of fuel (from 0.0 to 1.0) a unit or group has in its internal tanks. +-- This method returns nil to ensure polymorphic behaviour! This method needs to be overridden by GROUP or UNIT. +-- @param #CONTROLLABLE self +-- @return #nil The CONTROLLABLE is not existing or alive. +function CONTROLLABLE:GetFuelMin() + self:F( self.ControllableName ) + + return nil +end + +--- Returns relative average amount of fuel (from 0.0 to 1.0) a unit or group has in its internal tanks. +-- This method returns nil to ensure polymorphic behaviour! This method needs to be overridden by GROUP or UNIT. +-- @param #CONTROLLABLE self +-- @return #nil The CONTROLLABLE is not existing or alive. +function CONTROLLABLE:GetFuelAve() + self:F( self.ControllableName ) + + return nil +end + --- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks. -- This method returns nil to ensure polymorphic behaviour! This method needs to be overridden by GROUP or UNIT. -- @param #CONTROLLABLE self @@ -292,15 +301,13 @@ function CONTROLLABLE:GetFuel() end - - -- Tasks --- Clear all tasks from the controllable. -- @param #CONTROLLABLE self -- @return #CONTROLLABLE function CONTROLLABLE:ClearTasks() - self:F2() + self:E( "ClearTasks" ) local DCSControllable = self:GetDCSObject() @@ -340,16 +347,26 @@ function CONTROLLABLE:PushTask( DCSTask, WaitTime ) local DCSControllable = self:GetDCSObject() if DCSControllable then - local Controller = self:_GetController() + + local DCSControllableName = self:GetName() -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. -- Therefore we schedule the functions to set the mission and options for the Controllable. - -- Controller:pushTask( DCSTask ) + -- Controller:pushTask( DCSTask ) + + local function PushTask( Controller, DCSTask ) + if self and self:IsAlive() then + local Controller = self:_GetController() + Controller:pushTask( DCSTask ) + else + BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } ) + end + end - if WaitTime then - self.TaskScheduler:Schedule( Controller, Controller.pushTask, { DCSTask }, WaitTime ) + if not WaitTime or WaitTime == 0 then + PushTask( self, DCSTask ) else - Controller:pushTask( DCSTask ) + self.TaskScheduler:Schedule( self, PushTask, { DCSTask }, WaitTime ) end return self @@ -360,15 +377,19 @@ end --- Clearing the Task Queue and Setting the Task on the queue from the controllable. -- @param #CONTROLLABLE self +-- @param DCS#Task DCSTask DCS Task array. +-- @param #number WaitTime Time in seconds, before the task is set. -- @return Wrapper.Controllable#CONTROLLABLE self function CONTROLLABLE:SetTask( DCSTask, WaitTime ) - self:F2( { DCSTask = DCSTask } ) + self:E( { "SetTask", WaitTime, DCSTask = DCSTask } ) local DCSControllable = self:GetDCSObject() if DCSControllable then local DCSControllableName = self:GetName() + + self:E( "Controllable Name = " .. DCSControllableName ) -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results. -- Therefore we schedule the functions to set the mission and options for the Controllable. @@ -377,9 +398,11 @@ function CONTROLLABLE:SetTask( DCSTask, WaitTime ) local function SetTask( Controller, DCSTask ) if self and self:IsAlive() then local Controller = self:_GetController() + --self:I( "Before SetTask" ) Controller:setTask( DCSTask ) + --self:I( "After SetTask" ) else - BASE:E( DCSControllableName .. " is not alive anymore. Cannot set DCSTask " .. DCSTask ) + BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } ) end end @@ -416,16 +439,27 @@ end --- Return a condition section for a controlled task. -- @param #CONTROLLABLE self --- @param Dcs.DCSTime#Time time +-- @param DCS#Time time -- @param #string userFlag -- @param #boolean userFlagValue -- @param #string condition --- @param Dcs.DCSTime#Time duration +-- @param DCS#Time duration -- @param #number lastWayPoint --- return Dcs.DCSTasking.Task#Task +-- 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, + } +--]] + local DCSStopCondition = {} DCSStopCondition.time = time DCSStopCondition.userFlag = userFlag @@ -440,9 +474,9 @@ end --- Return a Controlled Task taking a Task and a TaskCondition. -- @param #CONTROLLABLE self --- @param Dcs.DCSTasking.Task#Task DCSTask --- @param #DCSStopCondition DCSStopCondition --- @return Dcs.DCSTasking.Task#Task +-- @param DCS#Task DCSTask +-- @param DCS#DCSStopCondition DCSStopCondition +-- @return DCS#Task function CONTROLLABLE:TaskControlled( DCSTask, DCSStopCondition ) self:F2( { DCSTask, DCSStopCondition } ) @@ -462,8 +496,8 @@ end --- Return a Combo Task taking an array of Tasks. -- @param #CONTROLLABLE self --- @param Dcs.DCSTasking.Task#TaskArray DCSTasks Array of @{DCSTasking.Task#Task} --- @return Dcs.DCSTasking.Task#Task +-- @param DCS#TaskArray DCSTasks Array of @{DCSTasking.Task#Task} +-- @return DCS#Task function CONTROLLABLE:TaskCombo( DCSTasks ) self:F2( { DCSTasks } ) @@ -486,8 +520,8 @@ end --- Return a WrappedAction Task taking a Command. -- @param #CONTROLLABLE self --- @param Dcs.DCSCommand#Command DCSCommand --- @return Dcs.DCSTasking.Task#Task +-- @param DCS#Command DCSCommand +-- @return DCS#Task function CONTROLLABLE:TaskWrappedAction( DCSCommand, Index ) self:F2( { DCSCommand } ) @@ -510,22 +544,22 @@ end --- Set a Task at a Waypoint using a Route list. -- @param #CONTROLLABLE self -- @param #table Waypoint The Waypoint! --- @param Dcs.DCSTasking.Task#Task Task The Task structure to be executed! --- @return Dcs.DCSTasking.Task#Task +-- @param DCS#Task Task The Task structure to be executed! +-- @return DCS#Task function CONTROLLABLE:SetTaskWaypoint( Waypoint, Task ) Waypoint.task = self:TaskCombo( { Task } ) - self:T3( { Waypoint.task } ) + self:F( { Waypoint.task } ) return Waypoint.task end ---- Executes a command action +--- Executes a command action for the CONTROLLABLE. -- @param #CONTROLLABLE self --- @param Dcs.DCSCommand#Command DCSCommand +-- @param DCS#Command DCSCommand The command to be executed. -- @return #CONTROLLABLE self function CONTROLLABLE:SetCommand( DCSCommand ) self:F2( DCSCommand ) @@ -545,7 +579,7 @@ end -- @param #CONTROLLABLE self -- @param #number FromWayPoint -- @param #number ToWayPoint --- @return Dcs.DCSTasking.Task#Task +-- @return DCS#Task -- @usage -- --- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class. -- HeliGroup = GROUP:FindByName( "Helicopter" ) @@ -584,7 +618,7 @@ end -- -- @param #CONTROLLABLE self -- @param #boolean StopRoute true if the ground unit needs to stop, false if it needs to continue to move. --- @return Dcs.DCSTasking.Task#Task +-- @return DCS#Task function CONTROLLABLE:CommandStopRoute( StopRoute ) self:F2( { StopRoute } ) @@ -600,19 +634,145 @@ function CONTROLLABLE:CommandStopRoute( StopRoute ) end +--- Give an uncontrolled air controllable the start command. +-- @param #CONTROLLABLE self +-- @param #number delay (Optional) Delay before start command in seconds. +-- @return #CONTROLLABLE self +function CONTROLLABLE:StartUncontrolled(delay) + if delay and delay>0 then + SCHEDULER:New(nil, CONTROLLABLE.StartUncontrolled, {self}, delay) + else + self:SetCommand({id='Start', params={}}) + end + return self +end + +--- Give the CONTROLLABLE the command to activate a beacon. See [DCS_command_activateBeacon](https://wiki.hoggitworld.com/view/DCS_command_activateBeacon) on Hoggit. +-- For specific beacons like TACAN use the more convenient @{#BEACON} class. +-- Note that a controllable can only have one beacon activated at a time with the execption of ICLS. +-- @param #CONTROLLABLE self +-- @param Core.Radio#BEACON.Type Type Beacon type (VOR, DME, TACAN, RSBN, ILS etc). +-- @param Core.Radio#BEACON.System System Beacon system (VOR, DME, TACAN, RSBN, ILS etc). +-- @param #number Frequency Frequency in Hz the beacon is running on. Use @{#UTILS.TACANToFrequency} to generate a frequency for TACAN beacons. +-- @param #number UnitID The ID of the unit the beacon is attached to. Usefull if more units are in one group. +-- @param #number Channel Channel the beacon is using. For, e.g. TACAN beacons. +-- @param #string ModeChannel The TACAN mode of the beacon, i.e. "X" or "Y". +-- @param #boolean AA If true, create and Air-Air beacon. IF nil, automatically set if CONTROLLABLE depending on whether unit is and aircraft or not. +-- @param #string Callsign Morse code identification callsign. +-- @param #boolean Bearing If true, beacon provides bearing information - if supported by the unit the beacon is attached to. +-- @param #number Delay (Optional) Delay in seconds before the beacon is activated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing, Delay) + + AA=AA or self:IsAir() + UnitID=UnitID or self:GetID() + + -- Command + local CommandActivateBeacon= { + id = "ActivateBeacon", + params = { + ["type"] = Type, + ["system"] = System, + ["frequency"] = Frequency, + ["unitId"] = UnitID, + ["channel"] = Channel, + ["modeChannel"] = ModeChannel, + ["AA"] = AA, + ["callsign"] = Callsign, + ["bearing"] = Bearing, + } + } + + if Delay and Delay>0 then + SCHEDULER:New(nil, self.CommandActivateBeacon, {self, Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing}, Delay) + else + self:SetCommand(CommandActivateBeacon) + end + + return self +end + +--- Activate ICLS system of the CONTROLLABLE. The controllable should be an aircraft carrier! +-- @param #CONTROLLABLE self +-- @param #number Channel ICLS channel. +-- @param #number UnitID The ID of the unit the ICLS system is attached to. Useful if more units are in one group. +-- @param #string Callsign Morse code identification callsign. +-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated. +-- @return #CONTROLLABLE self +function CONTROLLABLE:CommandActivateICLS(Channel, UnitID, Callsign, Delay) + self:F() + + -- 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) + self:F() + + -- 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) + self:F() + + -- 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 + + -- TASKS FOR AIR CONTROLLABLES - - --- (AIR) Attack a Controllable. -- @param #CONTROLLABLE self -- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable 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.DCSTypes#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 / 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.DCSTypes#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.DCSTypes#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 DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param DCS#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. -- @param #boolean AttackQtyLimit (optional) The flag determines how to interpret attackQty parameter. If the flag is true then attackQty is a limit on maximal attack quantity for "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @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 } ) @@ -664,13 +824,13 @@ end -- @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.DCSTypes#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 / 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.DCSTypes#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#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. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @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 } ) @@ -700,48 +860,76 @@ end --- (AIR) Delivering weapon at the point on the ground. -- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. +-- @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.DCSTypes#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 / 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.DCSTypes#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#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. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType ) - self:F2( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType } ) +-- @param #boolean Divebomb (optional) Perform dive bombing. Default false. +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, Divebomb ) + 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 = { id = 'Bombing', params = { - point = Vec2, - groupAttack = GroupAttack or false, + x = Vec2.x, + y = Vec2.y, + groupAttack = _groupattack, expend = WeaponExpend or "Auto", - attackQtyLimit = AttackQty and true or false, - attackQty = AttackQty, - directionEnabled = Direction and true or false, - direction = Direction, - altitudeEnabled = Altitude and true or false, - altitude = Altitude or 30, + attackQtyLimit = false, --AttackQty and true or false, + attackQty = AttackQty or 1, + directionEnabled = _directionenabled, + direction = _direction, + altitudeEnabled = _altitudeenabled, + altitude = _altitude, weaponType = WeaponType, + --attackType=_attacktype, }, - }, + } - self:T3( { DCSTask } ) + self:E( { TaskBombing=DCSTask } ) return DCSTask end --- (AIR) Attacking the map object (building, structure, e.t.c). -- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Vec2 2D-coordinates of the point to deliver weapon at. +-- @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.DCSTypes#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 / 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.DCSTypes#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#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. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @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 } ) @@ -769,9 +957,9 @@ end --- (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed. -- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Point The point to hold the position. --- @param #number Altitude The altitude to hold the position. --- @param #number Speed The speed flying when holding the position. +-- @param DCS#Vec2 Point The point to hold the position. +-- @param #number Altitude The altitude [m] 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 } ) @@ -812,19 +1000,52 @@ function CONTROLLABLE:TaskOrbitCircleAtVec2( Point, Altitude, Speed ) 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. +-- @param #number Speed Speed [m/s] flying the orbit pattern +-- @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, + altitude = Altitude, + } + } + + 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 to hold the position. --- @param #number Speed The speed flying when holding the position. +-- @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. -- @return #CONTROLLABLE self -function CONTROLLABLE:TaskOrbitCircle( Altitude, Speed ) +function CONTROLLABLE:TaskOrbitCircle( Altitude, Speed, Coordinate ) self:F2( { self.ControllableName, Altitude, Speed } ) local DCSControllable = self:GetDCSObject() if DCSControllable then - local ControllablePoint = self:GetVec2() - return self:TaskOrbitCircleAtVec2( ControllablePoint, Altitude, Speed ) + local OrbitVec2 = Coordinate and Coordinate:GetVec2() or self:GetVec2() + return self:TaskOrbitCircleAtVec2( OrbitVec2, Altitude, Speed ) end return nil @@ -851,11 +1072,11 @@ end -- @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.DCSTypes#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 / 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.DCSTypes#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#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. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @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 } ) @@ -890,7 +1111,7 @@ end --- (AIR) Refueling from the nearest tanker. No parameters. -- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskRefueling() self:F2( { self.ControllableName } ) @@ -899,11 +1120,7 @@ function CONTROLLABLE:TaskRefueling() -- params = {} -- } - local DCSTask - DCSTask = { id = 'Refueling', - params = { - }, - }, + local DCSTask={id='Refueling', params={}} self:T3( { DCSTask } ) return DCSTask @@ -912,7 +1129,7 @@ end --- (AIR HELICOPTER) Landing at the ground. For helicopters only. -- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Point The point where to land. +-- @param DCS#Vec2 Point 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 ) @@ -949,7 +1166,7 @@ function CONTROLLABLE:TaskLandAtVec2( Point, Duration ) return DCSTask end ---- (AIR) Land the controllable at a @{Zone#ZONE_RADIUS). +--- (AIR) Land the controllable at a @{Core.Zone#ZONE_RADIUS). -- @param #CONTROLLABLE self -- @param Core.Zone#ZONE Zone The zone where to land. -- @param #number Duration The duration in seconds to stay on the ground. @@ -977,9 +1194,9 @@ end -- 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.DCSTypes#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 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.DCSTasking.Task#Task The DCS task structure. +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex } ) @@ -1018,12 +1235,12 @@ end -- 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 EscortControllable The controllable to be escorted. --- @param Dcs.DCSTypes#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 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 EngagementDistanceMax 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.DCSTypes#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @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. +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes ) self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes } ) @@ -1044,6 +1261,8 @@ function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, E LastWaypointIndexFlag = true end + TargetTypes=TargetTypes or {} + local DCSTask DCSTask = { id = 'Escort', params = { @@ -1065,12 +1284,13 @@ end --- (GROUND) Fire at a VEC2 point until ammunition is finished. -- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Vec2 The point to fire at. --- @param Dcs.DCSTypes#Distance Radius The radius of the zone to deploy the fire at. +-- @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). --- @return Dcs.DCSTasking.Task#Task The DCS task structure. -function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount ) - self:F2( { self.ControllableName, Vec2, Radius, AmmoCount } ) +-- @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', @@ -1096,6 +1316,10 @@ function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount ) DCSTask.params.expendQty = AmmoCount DCSTask.params.expendQtyEnabled = true end + + if WeaponType then + DCSTask.params.weaponType=WeaponType + end self:T3( { DCSTask } ) return DCSTask @@ -1103,7 +1327,7 @@ end --- (GROUND) Hold ground controllable from moving. -- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskHold() self:F2( { self.ControllableName } ) @@ -1132,9 +1356,9 @@ end -- @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.DCSTypes#AI.Task.Designation Designation (optional) Designation type. +-- @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.DCSTasking.Task#Task The DCS task structure. +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink ) self:F2( { self.ControllableName, AttackGroup, WeaponType, Designation, Datalink } ) @@ -1166,10 +1390,10 @@ end --- (AIR) Engaging targets of defined types. -- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#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.DCSTypes#AttributeNameArray TargetTypes Array of target categories allowed to engage. +-- @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. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEngageTargets( Distance, TargetTypes, Priority ) self:F2( { self.ControllableName, Distance, TargetTypes, Priority } ) @@ -1199,11 +1423,11 @@ end --- (AIR) Engaging a targets of defined types at circle-shaped zone. -- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Vec2 2D-coordinates of the zone. --- @param Dcs.DCSTypes#Distance Radius Radius of the zone. --- @param Dcs.DCSTypes#AttributeNameArray TargetTypes Array of target categories allowed to engage. +-- @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. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEngageTargetsInZone( Vec2, Radius, TargetTypes, Priority ) self:F2( { self.ControllableName, Vec2, Radius, TargetTypes, Priority } ) @@ -1237,12 +1461,12 @@ end -- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable to be attacked. -- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. -- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. --- @param Dcs.DCSTypes#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 / 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.DCSTypes#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.DCSTypes#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 DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param DCS#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude. -- @param #boolean AttackQtyLimit (optional) The flag determines how to interpret attackQty parameter. If the flag is true then attackQty is a limit on maximal attack quantity for "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @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 } ) @@ -1298,13 +1522,13 @@ end -- @param Wrapper.Unit#UNIT EngageUnit The UNIT. -- @param #number Priority (optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. -- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found. --- @param Dcs.DCSTypes#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 / 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.DCSTypes#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.DCSTypes#Distance Altitude (optional) Desired altitude to perform the unit engagement. +-- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction. +-- @param DCS#Distance Altitude (optional) Desired altitude to perform the unit engagement. -- @param #boolean Visible (optional) Unit must be visible. -- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @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 } ) @@ -1348,7 +1572,7 @@ end --- (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters. -- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskAWACS( ) self:F2( { self.ControllableName } ) @@ -1371,7 +1595,7 @@ end --- (AIR) Aircraft will act as a tanker for friendly units. No parameters. -- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskTanker( ) self:F2( { self.ControllableName } ) @@ -1396,7 +1620,7 @@ end --- (GROUND) Ground unit (EW-radar) will act as an EWR for friendly units (will provide them with information about contacts). No parameters. -- @param #CONTROLLABLE self --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskEWR( ) self:F2( { self.ControllableName } ) @@ -1426,9 +1650,9 @@ end -- @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.DCSTypes#AI.Task.Designation Designation (optional) Designation type. +-- @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.DCSTasking.Task#Task The DCS task structure. +-- @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 } ) @@ -1463,9 +1687,9 @@ end -- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC. -- If the task is assigned to the controllable lead unit will be a FAC. -- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Distance Radius The maximal distance from the FAC to a target. +-- @param DCS#Distance Radius The maximal distance from the FAC to a target. -- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:EnRouteTaskFAC( Radius, Priority ) self:F2( { self.ControllableName, Radius, Priority } ) @@ -1494,10 +1718,10 @@ end --- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. -- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Point The point where to wait. +-- @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.DCSTasking.Task#Task The DCS task structure +-- @return DCS#Task The DCS task structure function CONTROLLABLE:TaskEmbarking( Point, Duration, EmbarkingControllable ) self:F2( { self.ControllableName, Point, Duration, EmbarkingControllable.DCSControllable } ) @@ -1521,13 +1745,13 @@ end --- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius. -- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec2 Point The point where to wait. +-- @param DCS#Vec2 Point The point where to wait. -- @param #number Radius The radius of the embarking zone around the Point. --- @return Dcs.DCSTasking.Task#Task The DCS task structure. +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius ) self:F2( { self.ControllableName, Point, Radius } ) - local DCSTask --Dcs.DCSTasking.Task#Task + local DCSTask --DCS#Task DCSTask = { id = 'EmbarkToTransport', params = { x = Point.x, y = Point.y, @@ -1588,28 +1812,23 @@ end -- RouteToZone( GroundGroup, ZoneList[1] ) -- function CONTROLLABLE:TaskFunction( FunctionString, ... ) - self:F2( { FunctionString, arg } ) local DCSTask local DCSScript = {} DCSScript[#DCSScript+1] = "local MissionControllable = GROUP:Find( ... ) " + DCSScript[#DCSScript+1] = "env.info( 'TaskFunction: ' .. ( MissionControllable and MissionControllable:GetName() ) or 'No Group' )" if arg and arg.n > 0 then local ArgumentKey = '_' .. tostring( arg ):match("table: (.*)") self:SetState( self, ArgumentKey, arg ) DCSScript[#DCSScript+1] = "local Arguments = MissionControllable:GetState( MissionControllable, '" .. ArgumentKey .. "' ) " - --DCSScript[#DCSScript+1] = "MissionControllable:ClearState( MissionControllable, '" .. ArgumentKey .. "' ) " DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable, unpack( Arguments ) )" else DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )" end - DCSTask = self:TaskWrappedAction( - self:CommandDoScript( - table.concat( DCSScript ) - ) - ) + DCSTask = self:TaskWrappedAction(self:CommandDoScript(table.concat( DCSScript ))) self:T( DCSTask ) @@ -1622,7 +1841,7 @@ end --- (AIR + GROUND) Return a mission task from a mission template. -- @param #CONTROLLABLE self -- @param #table TaskMission A table containing the mission task. --- @return Dcs.DCSTasking.Task#Task +-- @return DCS#Task function CONTROLLABLE:TaskMission( TaskMission ) self:F2( Points ) @@ -1672,6 +1891,9 @@ do -- Patrol methods --- (GROUND) Patrol randomly to the waypoints the for the (parent) group. -- A random waypoint will be picked and the group will move towards that point. -- @param #CONTROLLABLE self + -- @param #number Speed Speed in km/h. + -- @param #string Formation The formation the group uses. + -- @param Core.Point#COORDINATE ToWaypoint The waypoint where the group should move to. -- @return #CONTROLLABLE function CONTROLLABLE:PatrolRouteRandom( Speed, Formation, ToWaypoint ) @@ -1723,6 +1945,9 @@ do -- Patrol methods --- (GROUND) Patrol randomly to the waypoints the for the (parent) group. -- A random waypoint will be picked and the group will move towards that point. -- @param #CONTROLLABLE self + -- @param #table ZoneList Table of zones. + -- @param #number Speed Speed in km/h the group moves at. + -- @param #string Formation (Optional) Formation the group should use. -- @return #CONTROLLABLE function CONTROLLABLE:PatrolZones( ZoneList, Speed, Formation ) @@ -1752,7 +1977,7 @@ do -- Patrol methods -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task local Route = {} - Route[#Route+1] = FromCoord:WaypointGround( 120 ) + Route[#Route+1] = FromCoord:WaypointGround( 20 ) Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation ) @@ -1770,7 +1995,7 @@ end --- Return a Misson task to follow a given route defined by Points. -- @param #CONTROLLABLE self -- @param #table Points A table of route points. --- @return Dcs.DCSTasking.Task#Task +-- @return DCS#Task function CONTROLLABLE:TaskRoute( Points ) self:F2( Points ) @@ -1781,336 +2006,512 @@ function CONTROLLABLE:TaskRoute( Points ) return DCSTask end ---- (AIR + GROUND) Make the Controllable move to fly to a given point. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:RouteToVec2( Point, Speed ) - self:F2( { Point, Speed } ) +do -- Route methods - local ControllablePoint = self:GetUnit( 1 ):GetVec2() - - local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = Speed - PointFrom.speed_locked = true - PointFrom.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local PointTo = {} - PointTo.x = Point.x - PointTo.y = Point.y - PointTo.type = "Turning Point" - PointTo.action = "Fly Over Point" - PointTo.speed = Speed - PointTo.speed_locked = true - PointTo.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self -end - ---- (AIR + GROUND) Make the Controllable move to a given point. --- @param #CONTROLLABLE self --- @param Dcs.DCSTypes#Vec3 Point The destination point in Vec3 format. --- @param #number Speed The speed to travel. --- @return #CONTROLLABLE self -function CONTROLLABLE:RouteToVec3( Point, Speed ) - self:F2( { Point, Speed } ) - - local ControllableVec3 = self:GetUnit( 1 ):GetVec3() - - local PointFrom = {} - PointFrom.x = ControllableVec3.x - PointFrom.y = ControllableVec3.z - PointFrom.alt = ControllableVec3.y - PointFrom.alt_type = "BARO" - PointFrom.type = "Turning Point" - PointFrom.action = "Turning Point" - PointFrom.speed = Speed - PointFrom.speed_locked = true - PointFrom.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local PointTo = {} - PointTo.x = Point.x - PointTo.y = Point.z - PointTo.alt = Point.y - PointTo.alt_type = "BARO" - PointTo.type = "Turning Point" - PointTo.action = "Fly Over Point" - PointTo.speed = Speed - PointTo.speed_locked = true - PointTo.properties = { - ["vnav"] = 1, - ["scale"] = 0, - ["angle"] = 0, - ["vangle"] = 0, - ["steer"] = 2, - } - - - local Points = { PointFrom, PointTo } - - self:T3( Points ) - - self:Route( Points ) - - return self -end - - - ---- Make the controllable to follow a given route. --- @param #CONTROLLABLE self --- @param #table Route A table of Route Points. --- @param #number DelaySeconds Wait for the specified seconds before executing the Route. --- @return #CONTROLLABLE The CONTROLLABLE. -function CONTROLLABLE:Route( Route, DelaySeconds ) - self:F2( Route ) - - local DCSControllable = self:GetDCSObject() - if DCSControllable then - local RouteTask = self:TaskRoute( Route ) -- Create a RouteTask, that will route the CONTROLLABLE to the Route. - self:SetTask( RouteTask, DelaySeconds or 1 ) -- Execute the RouteTask after the specified seconds (default is 1). - return self - end - - return nil -end - - ---- Make the GROUND Controllable to drive towards a specific point. --- @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 999 km/h. --- @param #string Formation (optional) The route point Formation, which is a text string that specifies exactly the Text in the Type of the route point, like "Vee", "Echelon Right". --- @param #number DelaySeconds Wait for the specified seconds before executing the Route. --- @return #CONTROLLABLE The CONTROLLABLE. -function CONTROLLABLE:RouteGroundTo( ToCoordinate, Speed, Formation, DelaySeconds ) - - local FromCoordinate = self:GetCoordinate() + --- (AIR + GROUND) Make the Controllable move to fly to a given point. + -- @param #CONTROLLABLE self + -- @param DCS#Vec3 Point The destination point in Vec3 format. + -- @param #number Speed The speed [m/s] to travel. + -- @return #CONTROLLABLE self + function CONTROLLABLE:RouteToVec2( Point, Speed ) + self:F2( { Point, Speed } ) - local FromWP = FromCoordinate:WaypointGround() - local ToWP = ToCoordinate:WaypointGround( Speed, Formation ) - - self:Route( { FromWP, ToWP }, DelaySeconds ) - - return self -end - ---- Make the GROUND Controllable to drive towards a specific point using (only) roads. --- @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 999 km/h. --- @param #number DelaySeconds Wait for the specified seconds before executing the Route. --- @return #CONTROLLABLE The CONTROLLABLE. -function CONTROLLABLE:RouteGroundOnRoad( ToCoordinate, Speed, DelaySeconds ) - - -- Current coordinate. - local FromCoordinate = self:GetCoordinate() + local ControllablePoint = self:GetUnit( 1 ):GetVec2() - -- Formation is set to on road. - local Formation="On Road" - - -- Path on road from current position to destination coordinate. - local path=FromCoordinate:GetPathOnRoad(ToCoordinate) - - -- Route, ground waypoints along roads. - local route={} - table.insert(route, FromCoordinate:WaypointGround(Speed, Formation)) - - -- Convert coordinates to ground waypoints and insert into table. - for _, coord in ipairs(path) do - table.insert(route, coord:WaypointGround(Speed, Formation)) - end - - -- Add the final coordinate because the final coordinate in path is last point on road. - local dist=ToCoordinate:Get2DDistance(path[#path]) - if dist>10 then - table.insert(route, ToCoordinate:WaypointGround(Speed, "Vee")) - end - - -- Route controllable to destination. - self:Route(route, DelaySeconds) - - return self -end - - ---- Make the AIR Controllable fly towards a specific point. --- @param #CONTROLLABLE self --- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. --- @param Core.Point#COORDINATE.RoutePointAltType AltType The altitude type. --- @param Core.Point#COORDINATE.RoutePointType Type The route point type. --- @param Core.Point#COORDINATE.RoutePointAction Action The route point action. --- @param #number Speed (optional) Speed in km/h. The default speed is 999 km/h. --- @param #number DelaySeconds Wait for the specified seconds before executing the Route. --- @return #CONTROLLABLE The CONTROLLABLE. -function CONTROLLABLE:RouteAirTo( ToCoordinate, AltType, Type, Action, Speed, DelaySeconds ) - - local FromCoordinate = self:GetCoordinate() - local FromWP = FromCoordinate:WaypointAir() - - local ToWP = ToCoordinate:WaypointAir( AltType, Type, Action, Speed ) - - self:Route( { FromWP, ToWP }, DelaySeconds ) - - return self -end - - ---- (AIR + GROUND) Route the controllable to a given zone. --- The controllable final destination point can be randomized. --- A speed can be given in km/h. --- A given formation can be given. --- @param #CONTROLLABLE self --- @param Core.Zone#ZONE Zone The zone where to route to. --- @param #boolean Randomize Defines whether to target point gets randomized within the Zone. --- @param #number Speed The speed. --- @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 / 1.6 - - + PointFrom.action = "Turning Point" + PointFrom.speed = Speed + PointFrom.speed_locked = true + PointFrom.properties = { + ["vnav"] = 1, + ["scale"] = 0, + ["angle"] = 0, + ["vangle"] = 0, + ["steer"] = 2, + } + + local PointTo = {} - local ZonePoint - - if Randomize then - ZonePoint = Zone:GetRandomVec2() - else - ZonePoint = Zone:GetVec2() - end - - PointTo.x = ZonePoint.x - PointTo.y = ZonePoint.y + PointTo.x = Point.x + PointTo.y = Point.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 / 1.6 - end - + PointTo.action = "Fly Over Point" + PointTo.speed = Speed + PointTo.speed_locked = true + PointTo.properties = { + ["vnav"] = 1, + ["scale"] = 0, + ["angle"] = 0, + ["vangle"] = 0, + ["steer"] = 2, + } + + local Points = { PointFrom, PointTo } - + self:T3( Points ) - + self:Route( Points ) - + return self end - - return nil -end - ---- (GROUND) Route the controllable to a given Vec2. --- A speed can be given in km/h. --- A given formation can be given. --- @param #CONTROLLABLE self --- @param #Vec2 Vec2 The Vec2 where to route to. --- @param #number Speed The speed. --- @param Base#FORMATION Formation The formation string. -function CONTROLLABLE:TaskRouteToVec2( Vec2, Speed, Formation ) - - local DCSControllable = self:GetDCSObject() - - if DCSControllable then - - local ControllablePoint = self:GetVec2() - + + --- (AIR + GROUND) Make the Controllable move to a given point. + -- @param #CONTROLLABLE self + -- @param DCS#Vec3 Point The destination point in Vec3 format. + -- @param #number Speed The speed [m/s] to travel. + -- @return #CONTROLLABLE self + function CONTROLLABLE:RouteToVec3( Point, Speed ) + self:F2( { Point, Speed } ) + + local ControllableVec3 = self:GetUnit( 1 ):GetVec3() + local PointFrom = {} - PointFrom.x = ControllablePoint.x - PointFrom.y = ControllablePoint.y + PointFrom.x = ControllableVec3.x + PointFrom.y = ControllableVec3.z + PointFrom.alt = ControllableVec3.y + PointFrom.alt_type = "BARO" PointFrom.type = "Turning Point" - PointFrom.action = Formation or "Cone" - PointFrom.speed = 20 / 1.6 - - + PointFrom.action = "Turning Point" + PointFrom.speed = Speed + PointFrom.speed_locked = true + PointFrom.properties = { + ["vnav"] = 1, + ["scale"] = 0, + ["angle"] = 0, + ["vangle"] = 0, + ["steer"] = 2, + } + + local PointTo = {} - - PointTo.x = Vec2.x - PointTo.y = Vec2.y + PointTo.x = Point.x + PointTo.y = Point.z + PointTo.alt = Point.y + PointTo.alt_type = "BARO" PointTo.type = "Turning Point" - - if Formation then - PointTo.action = Formation - else - PointTo.action = "Cone" - end - - if Speed then - PointTo.speed = Speed - else - PointTo.speed = 60 / 3.6 - end - + PointTo.action = "Fly Over Point" + PointTo.speed = Speed + PointTo.speed_locked = true + PointTo.properties = { + ["vnav"] = 1, + ["scale"] = 0, + ["angle"] = 0, + ["vangle"] = 0, + ["steer"] = 2, + } + + local Points = { PointFrom, PointTo } - + self:T3( Points ) - + self:Route( Points ) - + return self end + + + + --- Make the controllable to follow a given route. + -- @param #CONTROLLABLE self + -- @param #table Route A table of Route Points. + -- @param #number DelaySeconds (Optional) Wait for the specified seconds before executing the Route. Default is one second. + -- @return #CONTROLLABLE The CONTROLLABLE. + function CONTROLLABLE:Route( Route, DelaySeconds ) + self:F2( Route ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local RouteTask = self:TaskRoute( Route ) -- Create a RouteTask, that will route the CONTROLLABLE to the Route. + self:SetTask( RouteTask, DelaySeconds or 1 ) -- Execute the RouteTask after the specified seconds (default is 1). + return self + end + + return nil + end + + --- Make the controllable to push follow a given route. + -- @param #CONTROLLABLE self + -- @param #table Route A table of Route Points. + -- @param #number DelaySeconds (Optional) Wait for the specified seconds before executing the Route. Default is one second. + -- @return #CONTROLLABLE The CONTROLLABLE. + function CONTROLLABLE:RoutePush( Route, DelaySeconds ) + self:F2( Route ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local RouteTask = self:TaskRoute( Route ) -- Create a RouteTask, that will route the CONTROLLABLE to the Route. + self:PushTask( RouteTask, DelaySeconds or 1 ) -- Execute the RouteTask after the specified seconds (default is 1). + return self + end + + return nil + end + + + --- Stops the movement of the vehicle on the route. + -- @param #CONTROLLABLE self + -- @return #CONTROLLABLE + function CONTROLLABLE:RouteStop() + self:F(self:GetName() .. " RouteStop") + + local CommandStop = self:CommandStopRoute( true ) + self:SetCommand( CommandStop ) + + end + + --- Resumes the movement of the vehicle on the route. + -- @param #CONTROLLABLE self + -- @return #CONTROLLABLE + function CONTROLLABLE:RouteResume() + self:F( self:GetName() .. " RouteResume") + + local CommandResume = self:CommandStopRoute( false ) + self:SetCommand( CommandResume ) + + end + + --- Make the GROUND Controllable to drive towards a specific point. + -- @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 #string Formation (optional) The route point Formation, which is a text string that specifies exactly the Text in the Type of the route point, like "Vee", "Echelon Right". + -- @param #number DelaySeconds Wait for the specified seconds before executing the Route. + -- @return #CONTROLLABLE The CONTROLLABLE. + function CONTROLLABLE:RouteGroundTo( ToCoordinate, Speed, Formation, DelaySeconds ) + + local FromCoordinate = self:GetCoordinate() + + local FromWP = FromCoordinate:WaypointGround() + local ToWP = ToCoordinate:WaypointGround( Speed, Formation ) + + self:Route( { FromWP, ToWP }, DelaySeconds ) + + return self + end + + --- Make the GROUND Controllable to drive towards a specific point using (mostly) roads. + -- @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 #number DelaySeconds (Optional) Wait for the specified seconds before executing the Route. Default is one second. + -- @param #string OffRoadFormation (Optional) The formation at initial and final waypoint. Default is "Off Road". + -- @return #CONTROLLABLE The CONTROLLABLE. + function CONTROLLABLE:RouteGroundOnRoad( ToCoordinate, Speed, DelaySeconds, OffRoadFormation ) + + -- Defaults. + Speed=Speed or 20 + DelaySeconds=DelaySeconds or 1 + OffRoadFormation=OffRoadFormation or "Off Road" + + -- Get the route task. + local route=self:TaskGroundOnRoad(ToCoordinate, Speed, OffRoadFormation) + + -- Route controllable to destination. + self:Route( route, DelaySeconds ) + + return self + end + + --- Make the TRAIN Controllable to drive towards a specific point using railroads. + -- @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 #number DelaySeconds (Optional) Wait for the specified seconds before executing the Route. Default is one second. + -- @return #CONTROLLABLE The CONTROLLABLE. + function CONTROLLABLE:RouteGroundOnRailRoads( ToCoordinate, Speed, DelaySeconds) + + -- Defaults. + Speed=Speed or 20 + DelaySeconds=DelaySeconds or 1 + + -- Get the route task. + local route=self:TaskGroundOnRailRoads(ToCoordinate, Speed) + + -- Route controllable to destination. + self:Route( route, DelaySeconds ) + + return self + end + - return nil -end + + --- Make a task for a GROUND Controllable to drive towards a specific point using (mostly) roads. + -- @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 #string OffRoadFormation (Optional) The formation at initial and final waypoint. Default is "Off Road". + -- @param #boolean Shortcut (Optional) If true, controllable will take the direct route if the path on road is 10x longer or path on road is less than 5% of total path. + -- @param Core.Point#COORDINATE FromCoordinate (Optional) Explicit initial coordinate. Default is the position of the controllable. + -- @return DCS#Task Task. + -- @return #boolean If true, path on road is possible. If false, task will route the group directly to its destination. + function CONTROLLABLE:TaskGroundOnRoad( ToCoordinate, Speed, OffRoadFormation, Shortcut, FromCoordinate ) + self:F2({ToCoordinate=ToCoordinate, Speed=Speed, OffRoadFormation=OffRoadFormation}) + + -- Defaults. + Speed=Speed or 20 + OffRoadFormation=OffRoadFormation or "Off Road" + + -- Initial (current) coordinate. + FromCoordinate = FromCoordinate or self:GetCoordinate() + + -- Get path and path length on road including the end points (From and To). + local PathOnRoad, LengthOnRoad, GotPath =FromCoordinate:GetPathOnRoad(ToCoordinate, true) + + -- Get the length only(!) on the road. + local _,LengthRoad=FromCoordinate:GetPathOnRoad(ToCoordinate, false) + -- Off road part of the rout: Total=OffRoad+OnRoad. + local LengthOffRoad + local LongRoad + + -- Calculate the direct distance between the initial and final points. + local LengthDirect=FromCoordinate:Get2DDistance(ToCoordinate) + + if GotPath then + + -- Off road part of the rout: Total=OffRoad+OnRoad. + LengthOffRoad=LengthOnRoad-LengthRoad + + -- Length on road is 10 times longer than direct route or path on road is very short (<5% of total path). + LongRoad=LengthOnRoad and ((LengthOnRoad > LengthDirect*10) or (LengthRoad/LengthOnRoad*100<5)) + + -- Debug info. + self:T(string.format("Length on road = %.3f km", LengthOnRoad/1000)) + self:T(string.format("Length directly = %.3f km", LengthDirect/1000)) + self:T(string.format("Length fraction = %.3f km", LengthOnRoad/LengthDirect)) + self:T(string.format("Length only road = %.3f km", LengthRoad/1000)) + self:T(string.format("Length off road = %.3f km", LengthOffRoad/1000)) + self:T(string.format("Percent on road = %.1f", LengthRoad/LengthOnRoad*100)) + + end + + -- Route, ground waypoints along road. + local route={} + local canroad=false + + -- Check if a valid path on road could be found. + if GotPath and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly. + -- Check whether the road is very long compared to direct path. + if LongRoad and Shortcut then + + -- Road is long ==> we take the short cut. + table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) + table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) + + else + + -- Create waypoints. + table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) + table.insert(route, PathOnRoad[2]:WaypointGround(Speed, "On Road")) + table.insert(route, PathOnRoad[#PathOnRoad-1]:WaypointGround(Speed, "On Road")) + + -- Add the final coordinate because the final might not be on the road. + local dist=ToCoordinate:Get2DDistance(PathOnRoad[#PathOnRoad-1]) + if dist>10 then + table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) + table.insert(route, ToCoordinate:GetRandomCoordinateInRadius(10,5):WaypointGround(5, OffRoadFormation)) + table.insert(route, ToCoordinate:GetRandomCoordinateInRadius(10,5):WaypointGround(5, OffRoadFormation)) + end + + end + + canroad=true + else + + -- No path on road could be found (can happen!) ==> Route group directly from A to B. + table.insert(route, FromCoordinate:WaypointGround(Speed, OffRoadFormation)) + table.insert(route, ToCoordinate:WaypointGround(Speed, OffRoadFormation)) + + end + + return route, canroad + end + + --- Make a task for a TRAIN Controllable to drive towards a specific point using railroad. + -- @param #CONTROLLABLE self + -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. + -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h. + -- @return Task + function CONTROLLABLE:TaskGroundOnRailRoads(ToCoordinate, Speed) + 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 + end + + --- Make the AIR Controllable fly towards a specific point. + -- @param #CONTROLLABLE self + -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to. + -- @param Core.Point#COORDINATE.RoutePointAltType AltType The altitude type. + -- @param Core.Point#COORDINATE.RoutePointType Type The route point type. + -- @param Core.Point#COORDINATE.RoutePointAction Action The route point action. + -- @param #number Speed (optional) Speed in km/h. The default speed is 500 km/h. + -- @param #number DelaySeconds Wait for the specified seconds before executing the Route. + -- @return #CONTROLLABLE The CONTROLLABLE. + function CONTROLLABLE:RouteAirTo( ToCoordinate, AltType, Type, Action, Speed, DelaySeconds ) + + local FromCoordinate = self:GetCoordinate() + local FromWP = FromCoordinate:WaypointAir() + + local ToWP = ToCoordinate:WaypointAir( AltType, Type, Action, Speed ) + + self:Route( { FromWP, ToWP }, DelaySeconds ) + + return self + end + + + --- (AIR + GROUND) Route the controllable to a given zone. + -- The controllable final destination point can be randomized. + -- A speed can be given in km/h. + -- A given formation can be given. + -- @param #CONTROLLABLE self + -- @param Core.Zone#ZONE Zone The zone where to route to. + -- @param #boolean Randomize Defines whether to target point gets randomized within the Zone. + -- @param #number Speed The speed in m/s. Default is 5.555 m/s = 20 km/h. + -- @param Base#FORMATION Formation The formation string. + function CONTROLLABLE:TaskRouteToZone( Zone, Randomize, Speed, Formation ) + self:F2( Zone ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local ControllablePoint = self:GetVec2() + + local PointFrom = {} + PointFrom.x = ControllablePoint.x + PointFrom.y = ControllablePoint.y + PointFrom.type = "Turning Point" + PointFrom.action = Formation or "Cone" + PointFrom.speed = 20 / 3.6 + + + local PointTo = {} + local ZonePoint + + if Randomize then + ZonePoint = Zone:GetRandomVec2() + else + ZonePoint = Zone:GetVec2() + end + + PointTo.x = ZonePoint.x + PointTo.y = ZonePoint.y + PointTo.type = "Turning Point" + + if Formation then + PointTo.action = Formation + else + PointTo.action = "Cone" + end + + if Speed then + PointTo.speed = Speed + else + PointTo.speed = 20 / 3.6 + end + + local Points = { PointFrom, PointTo } + + self:T3( Points ) + + self:Route( Points ) + + return self + end + + return nil + end + + --- (GROUND) Route the controllable to a given Vec2. + -- A speed can be given in km/h. + -- A given formation can be given. + -- @param #CONTROLLABLE self + -- @param DCS#Vec2 Vec2 The Vec2 where to route to. + -- @param #number Speed The speed in m/s. Default is 5.555 m/s = 20 km/h. + -- @param Base#FORMATION Formation The formation string. + function CONTROLLABLE:TaskRouteToVec2( Vec2, Speed, Formation ) + + local DCSControllable = self:GetDCSObject() + + if DCSControllable then + + local ControllablePoint = self:GetVec2() + + local PointFrom = {} + PointFrom.x = ControllablePoint.x + PointFrom.y = ControllablePoint.y + PointFrom.type = "Turning Point" + PointFrom.action = Formation or "Cone" + PointFrom.speed = 20 / 3.6 + + + local PointTo = {} + + PointTo.x = Vec2.x + PointTo.y = Vec2.y + PointTo.type = "Turning Point" + + if Formation then + PointTo.action = Formation + else + PointTo.action = "Cone" + end + + if Speed then + PointTo.speed = Speed + else + PointTo.speed = 20 / 3.6 + end + + local Points = { PointFrom, PointTo } + + self:T3( Points ) + + self:Route( Points ) + + return self + end + + return nil + end + +end -- Route methods -- Commands --- Do Script command -- @param #CONTROLLABLE self -- @param #string DoScript --- @return #DCSCommand +-- @return DCS#DCSCommand function CONTROLLABLE:CommandDoScript( DoScript ) local DCSDoScript = { @@ -2146,7 +2547,7 @@ end ---- Return the route of a controllable by using the @{Database#DATABASE} class. +--- Return the route of a controllable by using the @{Core.Database#DATABASE} class. -- @param #CONTROLLABLE self -- @param #number Begin The route point from where the copy will start. The base route point is 0. -- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. @@ -2221,7 +2622,7 @@ function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRad local DetectionRWR = ( DetectRWR and DetectRWR == true ) and Controller.Detection.RWR or nil local DetectionDLINK = ( DetectDLINK and DetectDLINK == true ) and Controller.Detection.DLINK or nil - self:T( { DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK } ) + self:T2( { DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK } ) return self:_GetController():getDetectedTargets( DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK ) end @@ -2609,8 +3010,9 @@ function CONTROLLABLE:OptionAlarmStateGreen() if self:IsGround() then Controller:setOption( AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.GREEN ) - elseif self:IsShip() then - Controller:setOption( AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.GREEN ) + elseif self:IsShip() then + -- AI.Option.Naval.id.ALARM_STATE does not seem to exist! + --Controller:setOption( AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.GREEN ) end return self @@ -2679,7 +3081,49 @@ 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 + end + + return nil +end + + +--- Allow to Jettison of weapons upon threat. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionAllowJettisonWeaponsOnThreat() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.PROHIBIT_JETT, false ) + end + + return self + end + + return nil +end + + +--- Keep weapons upon threat. +-- @param #CONTROLLABLE self +-- @return #CONTROLLABLE self +function CONTROLLABLE:OptionKeepWeaponsOnThreat() + self:F2( { self.ControllableName } ) + + local DCSControllable = self:GetDCSObject() + if DCSControllable then + local Controller = self:_GetController() + + if self:IsAir() then + Controller:setOption( AI.Option.Air.id.PROHIBIT_JETT, true ) end return self @@ -2691,9 +3135,8 @@ end - --- Retrieve the controllable mission and allow to place function hooks within the mission waypoint plan. --- Use the method @{Controllable#CONTROLLABLE:WayPointFunction} to define the hook functions for specific waypoints. +-- Use the method @{Wrapper.Controllable#CONTROLLABLE:WayPointFunction} to define the hook functions for specific waypoints. -- Use the method @{Controllable@CONTROLLABLE:WayPointExecute) to start the execution of the new mission plan. -- Note that when WayPointInitialize is called, the Mission of the controllable is RESTARTED! -- @param #CONTROLLABLE self @@ -2781,16 +3224,3 @@ function CONTROLLABLE:IsAirPlane() return nil end -function CONTROLLABLE:GetSize() - - local DCSObject = self:GetDCSObject() - - if DCSObject then - return 1 - else - return 0 - end -end - - --- Message APIs \ No newline at end of file diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua index f46c2701b..81b203ca9 100644 --- a/Moose Development/Moose/Wrapper/Group.lua +++ b/Moose Development/Moose/Wrapper/Group.lua @@ -2,7 +2,9 @@ -- -- === -- --- The @{#GROUP} class is a wrapper class to handle the DCS Group objects: +-- The @{#GROUP} class is a wrapper class to handle the DCS Group objects. +-- +-- ## Features: -- -- * Support all DCS Group APIs. -- * Enhance with Group specific APIs not in the DCS Group API set. @@ -11,7 +13,16 @@ -- -- **IMPORTANT: ONE SHOULD NEVER SANATIZE these GROUP OBJECT REFERENCES! (make the GROUP object references nil).** -- --- See the detailed documentation on the GROUP class. +-- === +-- +-- For each DCS Group object alive within a running mission, a GROUP wrapper object (instance) will be created within the _@{DATABASE} object. +-- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Group objects are spawned (using the @{SPAWN} class). +-- +-- The GROUP class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference +-- using the DCS Group or the DCS GroupName. +-- +-- The GROUP methods will reference the DCS Group object by name when it is needed during API execution. +-- If the DCS Group object does not exist or is nil, the GROUP methods will return nil and may log an exception in the DCS.log file. -- -- === -- @@ -23,7 +34,8 @@ -- -- === -- --- @module Group +-- @module Wrapper.Group +-- @image Wrapper_Group.JPG --- @type GROUP @@ -31,41 +43,92 @@ -- @field #string GroupName The name of the group. ---- --- # GROUP class, extends @{Controllable#CONTROLLABLE} +--- Wrapper class of the DCS world Group object. -- --- For each DCS Group object alive within a running mission, a GROUP wrapper object (instance) will be created within the _@{DATABASE} object. --- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Group objects are spawned (using the @{SPAWN} class). --- --- The GROUP class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference --- using the DCS Group or the DCS GroupName. --- --- Another thing to know is that GROUP objects do not "contain" the DCS Group object. --- The GROUP methods will reference the DCS Group object by name when it is needed during API execution. --- If the DCS Group object does not exist or is nil, the GROUP methods will return nil and log an exception in the DCS.log file. --- -- The GROUP class provides the following functions to retrieve quickly the relevant GROUP instance: -- -- * @{#GROUP.Find}(): Find a GROUP instance from the _DATABASE object using a DCS Group object. -- * @{#GROUP.FindByName}(): Find a GROUP instance from the _DATABASE object using a DCS Group name. -- --- ## GROUP task methods +-- # 1. Tasking of groups -- --- A GROUP is a @{Controllable}. See the @{Controllable} task methods section for a description of the task methods. +-- A GROUP is derived from the wrapper class CONTROLLABLE (@{Wrapper.Controllable#CONTROLLABLE}). +-- See the @{Wrapper.Controllable} task methods section for a description of the task methods. +-- +-- But here is an example how a group can be assigned a task. +-- +-- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class. +-- +-- First we look up the objects. We create a GROUP object `HeliGroup`, using the @{#GROUP:FindByName}() method, looking up the `"Helicopter"` group object. +-- Same for the `"AttackGroup"`. +-- +-- local HeliGroup = GROUP:FindByName( "Helicopter" ) +-- local AttackGroup = GROUP:FindByName( "AttackGroup" ) +-- +-- Now we retrieve the @{Wrapper.Unit#UNIT} objects of the `AttackGroup` object, using the method `:GetUnits()`. +-- +-- local AttackUnits = AttackGroup:GetUnits() +-- +-- Tasks are actually text strings that we build using methods of GROUP. +-- So first, we declare an list of `Tasks`. +-- +-- local Tasks = {} +-- +-- Now we loop over the `AttackUnits` using a for loop. +-- We retrieve the `AttackUnit` using the `AttackGroup:GetUnit()` method. +-- Each `AttackUnit` found, will be attacked by `HeliGroup`, using the method `HeliGroup:TaskAttackUnit()`. +-- This method returns a string containing a command line to execute the task to the `HeliGroup`. +-- The code will assign the task string command to the next element in the `Task` list, using `Tasks[#Tasks+1]`. +-- This little code will take the count of `Task` using `#` operator, and will add `1` to the count. +-- This result will be the index of the `Task` element. +-- +-- for i = 1, #AttackUnits do +-- local AttackUnit = AttackGroup:GetUnit( i ) +-- Tasks[#Tasks+1] = HeliGroup:TaskAttackUnit( AttackUnit ) +-- end +-- +-- Once these tasks have been executed, a function `_Resume` will be called ... +-- +-- Tasks[#Tasks+1] = HeliGroup:TaskFunction( "_Resume", { "''" } ) +-- +-- --- @param Wrapper.Group#GROUP HeliGroup +-- function _Resume( HeliGroup ) +-- env.info( '_Resume' ) +-- +-- HeliGroup:MessageToAll( "Resuming",10,"Info") +-- end +-- +-- Now here is where the task gets assigned! +-- Using `HeliGroup:PushTask`, the task is pushed onto the task queue of the group `HeliGroup`. +-- Since `Tasks` is an array of tasks, we use the `HeliGroup:TaskCombo` method to execute the tasks. +-- The `HeliGroup:PushTask` method can receive a delay parameter in seconds. +-- In the example, `30` is given as a delay. +-- +-- +-- HeliGroup:PushTask( +-- HeliGroup:TaskCombo( +-- Tasks +-- ), 30 +-- ) +-- +-- That's it! +-- But again, please refer to the @{Wrapper.Controllable} task methods section for a description of the different task methods that are available. +-- +-- -- -- ### Obtain the mission from group templates -- -- Group templates contain complete mission descriptions. Sometimes you want to copy a complete mission from a group and assign it to another: -- --- * @{Controllable#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. +-- * @{Wrapper.Controllable#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template. -- -- ## GROUP Command methods -- --- A GROUP is a @{Controllable}. See the @{Controllable} command methods section for a description of the command methods. +-- A GROUP is a @{Wrapper.Controllable}. See the @{Wrapper.Controllable} command methods section for a description of the command methods. -- -- ## GROUP option methods -- --- A GROUP is a @{Controllable}. See the @{Controllable} option methods section for a description of the option methods. +-- A GROUP is a @{Wrapper.Controllable}. See the @{Wrapper.Controllable} option methods section for a description of the option methods. -- -- ## GROUP Zone validation methods -- @@ -76,7 +139,7 @@ -- * @{#GROUP.IsPartlyInZone}: Returns true if some units of the group are within a @{Zone}. -- * @{#GROUP.IsNotInZone}: Returns true if none of the group units of the group are within a @{Zone}. -- --- The zone can be of any @{Zone} class derived from @{Zone#ZONE_BASE}. So, these methods are polymorphic to the zones tested on. +-- The zone can be of any @{Zone} class derived from @{Core.Zone#ZONE_BASE}. So, these methods are polymorphic to the zones tested on. -- -- ## GROUP AI methods -- @@ -112,22 +175,25 @@ GROUPTEMPLATE.Takeoff = { --- 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 @{Database}. +-- It is merely added to the @{Core.Database}. -- @param #GROUP self -- @param #table GroupTemplate The GroupTemplate Structure exactly as defined within the mission editor. --- @param Dcs.DCScoalition#coalition.side CoalitionSide The coalition.side of the group. --- @param Dcs.DCSGroup#Group.Category CategoryID The Group.Category of the group. --- @param Dcs.DCScountry#country.id CountryID the country.id of the group. +-- @param DCS#coalition.side CoalitionSide The coalition.side of the group. +-- @param DCS#Group.Category CategoryID The Group.Category of the group. +-- @param DCS#country.id CountryID the country.id of the group. -- @return #GROUP self function GROUP:NewTemplate( GroupTemplate, CoalitionSide, CategoryID, CountryID ) local GroupName = GroupTemplate.name - _DATABASE:_RegisterGroupTemplate( GroupTemplate, CategoryID, CountryID, CoalitionSide, GroupName ) - self = BASE:Inherit( self, CONTROLLABLE:New( GroupName ) ) - self:F2( GroupName ) + + _DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, CategoryID, CountryID, GroupName ) + + local self = BASE:Inherit( self, CONTROLLABLE:New( GroupName ) ) self.GroupName = GroupName - - _DATABASE:AddGroup( GroupName ) - + + if not _DATABASE.GROUPS[GroupName] then + _DATABASE.GROUPS[GroupName] = self + end + self:SetEventPriority( 4 ) return self end @@ -140,7 +206,6 @@ end -- @return #GROUP self function GROUP:Register( GroupName ) local self = BASE:Inherit( self, CONTROLLABLE:New( GroupName ) ) -- #GROUP - self:F( GroupName ) self.GroupName = GroupName self:SetEventPriority( 4 ) @@ -151,7 +216,7 @@ end --- Find the GROUP wrapper class instance using the DCS Group. -- @param #GROUP self --- @param Dcs.DCSWrapper.Group#Group DCSGroup The DCS Group. +-- @param DCS#Group DCSGroup The DCS Group. -- @return #GROUP The GROUP. function GROUP:Find( DCSGroup ) @@ -174,7 +239,7 @@ end --- Returns the DCS Group. -- @param #GROUP self --- @return Dcs.DCSWrapper.Group#Group The DCS Group. +-- @return DCS#Group The DCS Group. function GROUP:GetDCSObject() local DCSGroup = Group.getByName( self.GroupName ) @@ -185,9 +250,9 @@ function GROUP:GetDCSObject() return nil end ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. +--- Returns the @{DCS#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. -- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. +-- @return DCS#Position The 3D position vectors of the POSITIONABLE. -- @return #nil The POSITIONABLE is not existing or alive. function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3() self:F2( self.PositionableName ) @@ -203,27 +268,27 @@ function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3 return nil end ---- Returns if the Group is alive. +--- Returns if the group is alive. -- The Group must: -- -- * Exist at run-time. -- * Has at least one unit. -- --- When the first @{Unit} of the Group is active, it will return true. --- If the first @{Unit} of the Group is inactive, it will return false. +-- When the first @{Wrapper.Unit} of the group is active, it will return true. +-- If the first @{Wrapper.Unit} of the group is inactive, it will return false. -- -- @param #GROUP self --- @return #boolean true if the Group is alive and active. --- @return #boolean false if the Group is alive but inactive. +-- @return #boolean true if the group is alive and active. +-- @return #boolean false if the group is alive but inactive. -- @return #nil if the group does not exist anymore. function GROUP:IsAlive() self:F2( self.GroupName ) - local DCSGroup = self:GetDCSObject() -- Dcs.DCSGroup#Group + local DCSGroup = self:GetDCSObject() -- DCS#Group if DCSGroup then if DCSGroup:isExist() then - local DCSUnit = DCSGroup:getUnit(1) -- Dcs.DCSUnit#Unit + local DCSUnit = DCSGroup:getUnit(1) -- DCS#Unit if DCSUnit then local GroupIsAlive = DCSUnit:isActive() self:T3( GroupIsAlive ) @@ -235,18 +300,68 @@ function GROUP:IsAlive() return nil end ---- Destroys the DCS Group and all of its DCS Units. --- Note that this destroy method also raises a destroy event at run-time. --- So all event listeners will catch the destroy event of this DCS Group. +--- Returns if the group is activated. -- @param #GROUP self -function GROUP:Destroy() +-- @return #boolean true if group is activated. +-- @return #nil The group is not existing or alive. +function GROUP:IsActive() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() -- DCS#Group + + if DCSGroup then + + local GroupIsActive = DCSGroup:getUnit(1):isActive() + return GroupIsActive + end + + return nil +end + + + +--- Destroys the DCS Group and all of its DCS Units. +-- Note that this destroy method also can raise a destroy event at run-time. +-- So all event listeners will catch the destroy event of this group for each unit in the group. +-- To raise these events, provide the `GenerateEvent` parameter. +-- @param #GROUP self +-- @param #boolean GenerateEvent If true, a crash or dead event for each unit is generated. If false, if no event is triggered. If nil, a RemoveUnit event is triggered. +-- @usage +-- -- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group. +-- Helicopter = GROUP:FindByName( "Helicopter" ) +-- Helicopter:Destroy( true ) +-- @usage +-- -- Ground unit example: destroy the Tanks and generate a S_EVENT_DEAD for each unit in the Tanks group. +-- Tanks = GROUP:FindByName( "Tanks" ) +-- Tanks:Destroy( true ) +-- @usage +-- -- Ship unit example: destroy the Ship silently. +-- Ship = GROUP:FindByName( "Ship" ) +-- Ship:Destroy() +-- +-- @usage +-- -- Destroy without event generation example. +-- Ship = GROUP:FindByName( "Boat" ) +-- Ship:Destroy( false ) -- Don't generate an event upon destruction. +-- +function GROUP:Destroy( GenerateEvent ) self:F2( self.GroupName ) local DCSGroup = self:GetDCSObject() if DCSGroup then for Index, UnitData in pairs( DCSGroup:getUnits() ) do - self:CreateEventCrash( timer.getTime(), UnitData ) + if GenerateEvent and GenerateEvent == true then + if self:IsAir() then + self:CreateEventCrash( timer.getTime(), UnitData ) + else + self:CreateEventDead( timer.getTime(), UnitData ) + end + elseif GenerateEvent == false then + -- Do nothing! + else + self:CreateEventRemoveUnit( timer.getTime(), UnitData ) + end end USERFLAG:New( self:GetName() ):Set( 100 ) DCSGroup:destroy() @@ -259,7 +374,7 @@ end --- Returns category of the DCS Group. -- @param #GROUP self --- @return Dcs.DCSWrapper.Group#Group.Category The category ID +-- @return DCS#Group.Category The category ID function GROUP:GetCategory() self:F2( self.GroupName ) @@ -299,7 +414,7 @@ end --- Returns the coalition of the DCS Group. -- @param #GROUP self --- @return Dcs.DCSCoalitionWrapper.Object#coalition.side The coalition side of the DCS Group. +-- @return DCS#coalition.side The coalition side of the DCS Group. function GROUP:GetCoalition() self:F2( self.GroupName ) @@ -315,8 +430,7 @@ end --- Returns the country of the DCS Group. -- @param #GROUP self --- @return Dcs.DCScountry#country.id The country identifier. --- @return #nil The DCS Group is not existing or alive. +-- @return DCS#country.id The country identifier or nil if the DCS Group is not existing or alive. function GROUP:GetCountry() self:F2( self.GroupName ) @@ -330,13 +444,155 @@ function GROUP:GetCountry() return nil end + +--- Check if at least one (or all) unit(s) has (have) a certain attribute. +-- See [hoggit documentation](https://wiki.hoggitworld.com/view/DCS_func_hasAttribute). +-- @param #GROUP self +-- @param #string attribute The name of the attribute the group is supposed to have. Valid attributes can be found in the "db_attributes.lua" file which is located at in "C:\Program Files\Eagle Dynamics\DCS World\Scripts\Database". +-- @param #boolean all If true, all units of the group must have the attribute in order to return true. Default is only one unit of a heterogenious group needs to have the attribute. +-- @return #boolean Group has this attribute. +function GROUP:HasAttribute(attribute, all) + + -- Get all units of the group. + local _units=self:GetUnits() + + local _allhave=true + local _onehas=false + + for _,_unit in pairs(_units) do + local _unit=_unit --Wrapper.Unit#UNIT + if _unit then + local _hastit=_unit:HasAttribute(attribute) + if _hastit==true then + _onehas=true + else + _allhave=false + end + end + end + + if all==true then + return _allhave + else + return _onehas + end +end + +--- Returns the maximum speed of the group. +-- If the group is heterogenious and consists of different units, the max speed of the slowest unit is returned. +-- @param #GROUP self +-- @return #number Speed in km/h. +function GROUP:GetSpeedMax() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + if DCSGroup then + + local Units=self:GetUnits() + + local speedmax=nil + + for _,unit in pairs(Units) do + local unit=unit --Wrapper.Unit#UNIT + local speed=unit:GetSpeedMax() + if speedmax==nil then + speedmax=speed + elseif speed The list of @{Wrapper.Unit} objects of the @{Wrapper.Group}. +function GROUP:GetUnits() + self:F2( { self.GroupName } ) + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local DCSUnits = DCSGroup:getUnits() + local Units = {} + for Index, UnitData in pairs( DCSUnits ) do + Units[#Units+1] = UNIT:Find( UnitData ) + end + self:T3( Units ) + return Units + end + + return nil +end + + +--- Returns a list of @{Wrapper.Unit} objects of the @{Wrapper.Group} that are occupied by a player. +-- @param #GROUP self +-- @return #list The list of player occupied @{Wrapper.Unit} objects of the @{Wrapper.Group}. +function GROUP:GetPlayerUnits() + self:F2( { self.GroupName } ) + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local DCSUnits = DCSGroup:getUnits() + local Units = {} + for Index, UnitData in pairs( DCSUnits ) do + local PlayerUnit = UNIT:Find( UnitData ) + if PlayerUnit:GetPlayerName() then + Units[#Units+1] = PlayerUnit + end + end + self:T3( Units ) + return Units + end + + return nil +end + + --- Returns the UNIT wrapper class with number UnitNumber. -- If the underlying DCS Unit does not exist, the method will return nil. . -- @param #GROUP self -- @param #number UnitNumber The number of the UNIT wrapper class to be returned. -- @return Wrapper.Unit#UNIT The UNIT wrapper class. function GROUP:GetUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) + self:F3( { self.GroupName, UnitNumber } ) local DCSGroup = self:GetDCSObject() @@ -354,9 +610,9 @@ end -- If the underlying DCS Unit does not exist, the method will return nil. . -- @param #GROUP self -- @param #number UnitNumber The number of the DCS Unit to be returned. --- @return Dcs.DCSWrapper.Unit#Unit The DCS Unit. +-- @return DCS#Unit The DCS Unit. function GROUP:GetDCSUnit( UnitNumber ) - self:F2( { self.GroupName, UnitNumber } ) + self:F3( { self.GroupName, UnitNumber } ) local DCSGroup = self:GetDCSObject() @@ -374,7 +630,7 @@ end -- @param #GROUP self -- @return #number The DCS Group size. function GROUP:GetSize() - self:F2( { self.GroupName } ) + self:F3( { self.GroupName } ) local DCSGroup = self:GetDCSObject() if DCSGroup then @@ -391,13 +647,84 @@ function GROUP:GetSize() return nil end + +--- Returns the average velocity Vec3 vector. +-- @param Wrapper.Group#GROUP self +-- @return DCS#Vec3 The velocity Vec3 vector +-- @return #nil The GROUP is not existing or alive. +function GROUP:GetVelocityVec3() + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup and DCSGroup:isExist() then + local GroupUnits = DCSGroup:getUnits() + local GroupCount = #GroupUnits + + local VelocityVec3 = { x = 0, y = 0, z = 0 } + + for _, DCSUnit in pairs( GroupUnits ) do + local UnitVelocityVec3 = DCSUnit:getVelocity() + VelocityVec3.x = VelocityVec3.x + UnitVelocityVec3.x + VelocityVec3.y = VelocityVec3.y + UnitVelocityVec3.y + VelocityVec3.z = VelocityVec3.z + UnitVelocityVec3.z + end + + VelocityVec3.x = VelocityVec3.x / GroupCount + VelocityVec3.y = VelocityVec3.y / GroupCount + VelocityVec3.z = VelocityVec3.z / GroupCount + + return VelocityVec3 + end + + BASE:E( { "Cannot GetVelocityVec3", Group = self, Alive = self:IsAlive() } ) + + return nil +end + + +--- Returns the average group height in meters. +-- @param Wrapper.Group#GROUP self +-- @param #boolean FromGround Measure from the ground or from sea level. Provide **true** for measuring from the ground. **false** or **nil** if you measure from sea level. +-- @return DCS#Vec3 The height of the group or nil if is not existing or alive. +function GROUP:GetHeight( FromGround ) + self:F2( self.GroupName ) + + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupUnits = DCSGroup:getUnits() + local GroupCount = #GroupUnits + + local GroupHeight = 0 + + for _, DCSUnit in pairs( GroupUnits ) do + local GroupPosition = DCSUnit:getPosition() + + if FromGround == true then + local LandHeight = land.getHeight( { x = GroupPosition.p.x, y = GroupPosition.p.z } ) + GroupHeight = GroupHeight + ( GroupPosition.p.y - LandHeight ) + else + GroupHeight = GroupHeight + GroupPosition.p.y + end + end + + return GroupHeight / GroupCount + end + + return nil +end + + + + --- --- Returns the initial size of the DCS Group. -- If some of the DCS Units of the DCS Group are destroyed, the initial size of the DCS Group is unchanged. -- @param #GROUP self -- @return #number The DCS Group initial size. function GROUP:GetInitialSize() - self:F2( { self.GroupName } ) + self:F3( { self.GroupName } ) local DCSGroup = self:GetDCSObject() if DCSGroup then @@ -427,8 +754,9 @@ function GROUP:GetDCSUnits() end ---- Activates a GROUP. +--- Activates a late activated GROUP. -- @param #GROUP self +-- @return #GROUP self function GROUP:Activate() self:F2( { self.GroupName } ) trigger.action.activateGroup( self:GetDCSObject() ) @@ -492,7 +820,7 @@ end --- Returns the current point (Vec2 vector) of the first DCS Unit in the DCS Group. -- @param #GROUP self --- @return Dcs.DCSTypes#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. +-- @return DCS#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group. function GROUP:GetVec2() self:F2( self.GroupName ) @@ -505,7 +833,7 @@ end --- Returns the current Vec3 vector of the first DCS Unit in the GROUP. -- @param #GROUP self --- @return Dcs.DCSTypes#Vec3 Current Vec3 of the first DCS Unit of the GROUP. +-- @return DCS#Vec3 Current Vec3 of the first DCS Unit of the GROUP. function GROUP:GetVec3() self:F2( self.GroupName ) @@ -554,13 +882,13 @@ function GROUP:GetCoordinate() end ---- Returns a random @{DCSTypes#Vec3} vector (point in 3D of the UNIT within the mission) within a range around the first UNIT of the GROUP. +--- Returns a random @{DCS#Vec3} vector (point in 3D of the UNIT within the mission) within a range around the first UNIT of the GROUP. -- @param #GROUP self -- @param #number Radius --- @return Dcs.DCSTypes#Vec3 The random 3D point vector around the first UNIT of the GROUP. +-- @return DCS#Vec3 The random 3D point vector around the first UNIT of the GROUP. -- @return #nil The GROUP is invalid or empty -- @usage --- -- If Radius is ignored, returns the Dcs.DCSTypes#Vec3 of first UNIT of the GROUP +-- -- If Radius is ignored, returns the DCS#Vec3 of first UNIT of the GROUP function GROUP:GetRandomVec3(Radius) self:F2(self.GroupName) @@ -600,11 +928,41 @@ function GROUP:GetHeading() end ---- Returns relative amount of fuel (from 0.0 to 1.0) the group has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. +--- Return the fuel state and unit reference for the unit with the least +-- amount of fuel in the group. +-- @param #GROUP self +-- @return #number The fuel state of the unit with the least amount of fuel +-- @return #Unit reference to #Unit object for further processing +function GROUP:GetFuelMin() + self:F3(self.ControllableName) + + if not self:GetDCSObject() then + BASE:E( { "Cannot GetFuel", Group = self, Alive = self:IsAlive() } ) + return 0 + end + + local min = 65535 -- some sufficiently large number to init with + local unit = nil + local tmp = nil + + for UnitID, UnitData in pairs( self:GetUnits() ) do + tmp = UnitData:GetFuel() + if tmp < min then + min = tmp + unit = UnitData + end + end + + return min, unit +end + +--- Returns relative amount of fuel (from 0.0 to 1.0) the group has in its +-- internal tanks. If there are additional fuel tanks the value may be +-- greater than 1.0. -- @param #GROUP self -- @return #number The relative amount of fuel (from 0.0 to 1.0). --- @return #nil The GROUP is not existing or alive. -function GROUP:GetFuel() +-- @return #nil The GROUP is not existing or alive. +function GROUP:GetFuelAvg() self:F( self.ControllableName ) local DCSControllable = self:GetDCSObject() @@ -627,13 +985,21 @@ function GROUP:GetFuel() return 0 end +--- Returns relative amount of fuel (from 0.0 to 1.0) the group has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0. +-- @param #GROUP self +-- @return #number The relative amount of fuel (from 0.0 to 1.0). +-- @return #nil The GROUP is not existing or alive. +function GROUP:GetFuel() + return self:GetFuelAvg() +end + do -- Is Zone methods --- Returns true if all units of the group are within a @{Zone}. -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is completely within the @{Zone#ZONE_BASE} +-- @return #boolean Returns true if the Group is completely within the @{Core.Zone#ZONE_BASE} function GROUP:IsCompletelyInZone( Zone ) self:F2( { self.GroupName, Zone } ) @@ -650,10 +1016,10 @@ function GROUP:IsCompletelyInZone( Zone ) return true end ---- Returns true if some units of the group are within a @{Zone}. +--- Returns true if some but NOT ALL units of the group are within a @{Zone}. -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is partially within the @{Zone#ZONE_BASE} +-- @return #boolean Returns true if the Group is partially within the @{Core.Zone#ZONE_BASE} function GROUP:IsPartlyInZone( Zone ) self:F2( { self.GroupName, Zone } ) @@ -678,10 +1044,18 @@ function GROUP:IsPartlyInZone( Zone ) end end +--- Returns true if part or all units of the group are within a @{Zone}. +-- @param #GROUP self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if the Group is partially or completely within the @{Core.Zone#ZONE_BASE}. +function GROUP:IsPartlyOrCompletelyInZone( Zone ) + return self:IsPartlyInZone(Zone) or self:IsCompletelyInZone(Zone) +end + --- Returns true if none of the group units of the group are within a @{Zone}. -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. --- @return #boolean Returns true if the Group is not within the @{Zone#ZONE_BASE} +-- @return #boolean Returns true if the Group is not within the @{Core.Zone#ZONE_BASE} function GROUP:IsNotInZone( Zone ) self:F2( { self.GroupName, Zone } ) @@ -697,6 +1071,23 @@ function GROUP:IsNotInZone( Zone ) return true end +--- Returns true if any units of the group are within a @{Core.Zone}. +-- @param #GROUP self +-- @param Core.Zone#ZONE_BASE Zone The zone to test. +-- @return #boolean Returns true if any unit of the Group is within the @{Core.Zone#ZONE_BASE} +function GROUP:IsAnyInZone( Zone ) + + if not self:IsAlive() then return false end + + for UnitID, UnitData in pairs( self:GetUnits() ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + if Zone:IsVec3InZone( Unit:GetVec3() ) then + return true + end + end + return false +end + --- Returns the number of UNITs that are in the @{Zone} -- @param #GROUP self -- @param Core.Zone#ZONE_BASE Zone The zone to test. @@ -838,10 +1229,10 @@ do -- AI methods -- @return #GROUP The GROUP. function GROUP:SetAIOnOff( AIOnOff ) - local DCSGroup = self:GetDCSObject() -- Dcs.DCSGroup#Group + local DCSGroup = self:GetDCSObject() -- DCS#Group if DCSGroup then - local DCSController = DCSGroup:getController() -- Dcs.DCSController#Controller + local DCSController = DCSGroup:getController() -- DCS#Controller if DCSController then DCSController:setOnOff( AIOnOff ) return self @@ -906,6 +1297,25 @@ end function GROUP:GetMinHeight() self:F2() + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupHeightMin = 999999999 + + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + local UnitData = UnitData -- DCS#Unit + + local UnitHeight = UnitData:getPoint() + + if UnitHeight < GroupHeightMin then + GroupHeightMin = UnitHeight + end + end + + return GroupHeightMin + end + + return nil end --- Returns the current maximum height of the group. @@ -915,6 +1325,25 @@ end function GROUP:GetMaxHeight() self:F2() + local DCSGroup = self:GetDCSObject() + + if DCSGroup then + local GroupHeightMax = -999999999 + + for Index, UnitData in pairs( DCSGroup:getUnits() ) do + local UnitData = UnitData -- DCS#Unit + + local UnitHeight = UnitData:getPoint() + + if UnitHeight > GroupHeightMax then + GroupHeightMax = UnitHeight + end + end + + return GroupHeightMax + end + + return nil end -- RESPAWNING @@ -948,7 +1377,7 @@ end --- Sets the CountryID of the group in a Template. -- @param #GROUP self --- @param Dcs.DCScountry#country.id CountryID The country ID. +-- @param DCS#country.id CountryID The country ID. -- @return #table function GROUP:SetTemplateCountry( Template, CountryID ) Template.CountryID = CountryID @@ -957,7 +1386,7 @@ end --- Sets the CoalitionID of the group in a Template. -- @param #GROUP self --- @param Dcs.DCSCoalitionWrapper.Object#coalition.side CoalitionID The coalition ID. +-- @param DCS#coalition.side CoalitionID The coalition ID. -- @return #table function GROUP:SetTemplateCoalition( Template, CoalitionID ) Template.CoalitionID = CoalitionID @@ -1027,7 +1456,7 @@ function GROUP:InitRandomizePositionRadius( OuterRadius, InnerRadius ) end ---- Respawn the @{Group} at a @{Point}. +--- Respawn the @{Wrapper.Group} at a @{Point}. -- The method will setup the new group template according the Init(Respawn) settings provided for the group. -- These settings can be provided by calling the relevant Init...() methods of the Group. -- @@ -1048,29 +1477,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 positons 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:F("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() @@ -1083,17 +1544,38 @@ function GROUP:Respawn( Template, Reset ) end 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 + + else -- Reset=false or nil + + -- Loop over template units. for UnitID, TemplateUnitData in pairs( Template.units ) do + self:F( "Reset" ) - local GroupUnitVec3 = { x = TemplateUnitData.x, y = TemplateUnitData.alt, z = TemplateUnitData.z } + + -- 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() @@ -1106,25 +1588,147 @@ function GROUP:Respawn( Template, Reset ) end 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 end - self:Destroy() - _DATABASE:Spawn( Template ) + -- Destroy old group. Dont trigger any dead/crash events since this is a respawn. + self:Destroy(false) + self:T({Template=Template}) + + -- Spawn new group. + _DATABASE:Spawn(Template) + + -- Reset events. self:ResetEvents() + return self end +--- Respawn a group at an airbase. +-- Note that the group has to be on parking spots at the airbase already in order for this to work. +-- So each unit of the group is respawned at exactly the same parking spot as it currently occupies. +-- @param Wrapper.Group#GROUP self +-- @param #table SpawnTemplate (Optional) The spawn template for the group. If no template is given it is exacted from the group. +-- @param Core.Spawn#SPAWN.Takeoff Takeoff (Optional) Takeoff type. Sould be either SPAWN.Takeoff.Cold or SPAWN.Takeoff.Hot. Default is SPAWN.Takeoff.Hot. +-- @param #boolean Uncontrolled (Optional) If true, spawn in uncontrolled state. +-- @return Wrapper.Group#GROUP Group spawned at airbase or nil if group could not be spawned. +function GROUP:RespawnAtCurrentAirbase(SpawnTemplate, Takeoff, Uncontrolled) -- R2.4 + self:F2( { SpawnTemplate, Takeoff, Uncontrolled} ) + -- Get closest airbase. Should be the one we are currently on. + local airbase=self:GetCoordinate():GetClosestAirbase() + + if airbase then + self:F2("Closest airbase = "..airbase:GetName()) + else + self:E("ERROR: could not find closest airbase!") + return nil + end + -- Takeoff type. Default hot. + Takeoff = Takeoff or SPAWN.Takeoff.Hot + + -- Coordinate of the airbase. + local AirbaseCoord=airbase:GetCoordinate() + + -- Spawn template. + SpawnTemplate = SpawnTemplate or self:GetTemplate() + + if SpawnTemplate then + + local SpawnPoint = SpawnTemplate.route.points[1] + + -- These are only for ships. + SpawnPoint.linkUnit = nil + SpawnPoint.helipadId = nil + SpawnPoint.airdromeId = nil + + -- Aibase id and category. + local AirbaseID = airbase:GetID() + local AirbaseCategory = airbase:GetDesc().category + + if AirbaseCategory == Airbase.Category.SHIP or AirbaseCategory == Airbase.Category.HELIPAD then + SpawnPoint.linkUnit = AirbaseID + SpawnPoint.helipadId = AirbaseID + elseif AirbaseCategory == Airbase.Category.AIRDROME then + SpawnPoint.airdromeId = AirbaseID + end + + + SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type + SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action + + -- Get the units of the group. + local units=self:GetUnits() + + local x + local y + for UnitID=1,#units do + + local unit=units[UnitID] --Wrapper.Unit#UNIT + + -- Get closest parking spot of current unit. Note that we look for occupied spots since the unit is currently sitting on it! + local Parkingspot, TermialID, Distance=unit:GetCoordinate():GetClosestParkingSpot(airbase) + + --Parkingspot:MarkToAll("parking spot") + self:T2(string.format("Closest parking spot distance = %s, terminal ID=%s", tostring(Distance), tostring(TermialID))) + + -- Get unit coordinates for respawning position. + local uc=unit:GetCoordinate() + --uc:MarkToAll(string.format("re-spawnplace %s terminal %d", unit:GetName(), TermialID)) + + SpawnTemplate.units[UnitID].x = uc.x --Parkingspot.x + SpawnTemplate.units[UnitID].y = uc.z --Parkingspot.z + SpawnTemplate.units[UnitID].alt = uc.y --Parkingspot.y + + SpawnTemplate.units[UnitID].parking = TermialID + SpawnTemplate.units[UnitID].parking_id = nil + + --SpawnTemplate.units[UnitID].unitId=nil + end + + --SpawnTemplate.groupId=nil + + SpawnPoint.x = SpawnTemplate.units[1].x --x --AirbaseCoord.x + SpawnPoint.y = SpawnTemplate.units[1].y --y --AirbaseCoord.z + SpawnPoint.alt = SpawnTemplate.units[1].alt --AirbaseCoord:GetLandHeight() + + SpawnTemplate.x = SpawnTemplate.units[1].x --x --AirbaseCoord.x + SpawnTemplate.y = SpawnTemplate.units[1].y --y --AirbaseCoord.z + + -- Set uncontrolled state. + SpawnTemplate.uncontrolled=Uncontrolled + + -- Destroy old group. + self:Destroy(false) + + -- Spawn new group. + _DATABASE:Spawn( SpawnTemplate ) + + -- Reset events. + self:ResetEvents() + + return self + end + + return nil +end --- Return the mission template of the group. @@ -1145,7 +1749,7 @@ function GROUP:GetTaskRoute() return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template.route.points ) end ---- Return the route of a group by using the @{Database#DATABASE} class. +--- Return the route of a group by using the @{Core.Database#DATABASE} class. -- @param #GROUP self -- @param #number Begin The route point from where the copy will start. The base route point is 0. -- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0. @@ -1215,8 +1819,7 @@ end --- Returns true if the first unit of the GROUP is in the air. -- @param Wrapper.Group#GROUP self --- @return #boolean true if in the first unit of the group is in the air. --- @return #nil The GROUP is not existing or not alive. +-- @return #boolean true if in the first unit of the group is in the air or #nil if the GROUP is not existing or not alive. function GROUP:InAir() self:F2( self.GroupName ) @@ -1234,23 +1837,40 @@ function GROUP:InAir() return nil end +--- Returns the DCS descriptor table of the nth unit of the group. +-- @param #GROUP self +-- @param #number n (Optional) The number of the unit for which the dscriptor is returned. +-- @return DCS#Object.Desc The descriptor of the first unit of the group or #nil if the group does not exist any more. +function GROUP:GetDCSDesc(n) + -- Default. + n=n or 1 + + local unit=self:GetUnit(n) + if unit and unit:IsAlive()~=nil then + local desc=unit:GetDesc() + return desc + end + + return nil +end + do -- Route methods - --- (AIR) Return the Group to an @{Airbase#AIRBASE}. + --- (AIR) Return the Group to an @{Wrapper.Airbase#AIRBASE}. -- The following things are to be taken into account: -- -- * The group is respawned to achieve the RTB, there may be side artefacts as a result of this. (Like weapons suddenly come back). -- * A group consisting out of more than one unit, may rejoin formation when respawned. -- * A speed can be given in km/h. If no speed is specified, the maximum speed of the first unit will be taken to return to base. - -- * When there is no @{Airbase} object specified, the group will return to the home base if the route of the group is pinned at take-off or at landing to a base. - -- * When there is no @{Airbase} object specified and the group route is not pinned to any airbase, it will return to the nearest airbase. + -- * When there is no @{Wrapper.Airbase} object specified, the group will return to the home base if the route of the group is pinned at take-off or at landing to a base. + -- * When there is no @{Wrapper.Airbase} object specified and the group route is not pinned to any airbase, it will return to the nearest airbase. -- -- @param #GROUP self - -- @param Wrapper.Airbase#AIRBASE RTBAirbase (optional) The @{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 Wrapper.Airbase#AIRBASE RTBAirbase (optional) The @{Wrapper.Airbase} to return to. If blank, the controllable will return to the nearest friendly airbase. + -- @param #number Speed (optional) The Speed, if no Speed is given, 80% of maximum Speed of the group is selected. + -- @return #GROUP self function GROUP:RouteRTB( RTBAirbase, Speed ) - self:F2( { RTBAirbase, Speed } ) + self:F( { RTBAirbase:GetName(), Speed } ) local DCSGroup = self:GetDCSObject() @@ -1258,17 +1878,19 @@ do -- Route methods if RTBAirbase then + -- If speed is not given take 80% of max speed. + local Speed=Speed or self:GetSpeedMax()*0.8 + + --[[ local GroupPoint = self:GetVec2() - local GroupVelocity = self:GetUnit(1):GetDesc().speedMax - + 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( @@ -1279,23 +1901,42 @@ do -- Route methods ) AirbaseAirPoint["airdromeId"] = RTBAirbase:GetID() - AirbaseAirPoint["speed_locked"] = true, + 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 ) - - self:Respawn(Template) + -- 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 @@ -1358,6 +1999,8 @@ do -- Players -- @return #nil The group has no players function GROUP:GetPlayerNames() + local HasPlayers = false + local PlayerNames = {} local Units = self:GetUnits() @@ -1367,11 +2010,36 @@ do -- Players if PlayerName and PlayerName ~= "" then PlayerNames = PlayerNames or {} table.insert( PlayerNames, PlayerName ) + HasPlayers = true end end + + if HasPlayers == true then + self:F2( PlayerNames ) + return PlayerNames + end - self:F2( PlayerNames ) - return PlayerNames + return nil + end + + + --- Get the active player count in the group. + -- @param #GROUP self + -- @return #number The amount of players. + function GROUP:GetPlayerCount() + + local PlayerCount = 0 + + local Units = self:GetUnits() + for UnitID, UnitData in pairs( Units or {} ) do + local Unit = UnitData -- Wrapper.Unit#UNIT + local PlayerName = Unit:GetPlayerName() + if PlayerName and PlayerName ~= "" then + PlayerCount = PlayerCount + 1 + end + end + + return PlayerCount end end diff --git a/Moose Development/Moose/Wrapper/Identifiable.lua b/Moose Development/Moose/Wrapper/Identifiable.lua index baa83eedb..13a5b3234 100644 --- a/Moose Development/Moose/Wrapper/Identifiable.lua +++ b/Moose Development/Moose/Wrapper/Identifiable.lua @@ -8,15 +8,14 @@ -- -- === -- --- @module Identifiable +-- @module Wrapper.Identifiable +-- @image MOOSE.JPG --- @type IDENTIFIABLE -- @extends Wrapper.Object#OBJECT -- @field #string IdentifiableName The name of the identifiable. ---- # IDENTIFIABLE class, extends @{Object#OBJECT} --- --- The IDENTIFIABLE class is a wrapper class to handle the DCS Identifiable objects: +--- Wrapper class to handle the DCS Identifiable objects. -- -- * Support all DCS Identifiable APIs. -- * Enhance with Identifiable specific APIs not in the DCS Identifiable API set. @@ -44,7 +43,7 @@ local _CategoryName = { --- Create a new IDENTIFIABLE from a DCSIdentifiable -- @param #IDENTIFIABLE self --- @param Dcs.DCSWrapper.Identifiable#Identifiable IdentifiableName The DCS Identifiable name +-- @param #string IdentifiableName The DCS Identifiable name -- @return #IDENTIFIABLE self function IDENTIFIABLE:New( IdentifiableName ) local self = BASE:Inherit( self, OBJECT:New( IdentifiableName ) ) @@ -62,7 +61,7 @@ end function IDENTIFIABLE:IsAlive() self:F3( self.IdentifiableName ) - local DCSIdentifiable = self:GetDCSObject() -- Dcs.DCSObject#Object + local DCSIdentifiable = self:GetDCSObject() -- DCS#Object if DCSIdentifiable then local IdentifiableIsAlive = DCSIdentifiable:isExist() @@ -110,7 +109,7 @@ end --- Returns category of the DCS Identifiable. -- @param #IDENTIFIABLE self --- @return Dcs.DCSWrapper.Object#Object.Category The category ID +-- @return DCS#Object.Category The category ID function IDENTIFIABLE:GetCategory() self:F2( self.ObjectName ) @@ -142,7 +141,7 @@ end --- Returns coalition of the Identifiable. -- @param #IDENTIFIABLE self --- @return Dcs.DCSCoalitionWrapper.Object#coalition.side The side of the coalition. +-- @return DCS#coalition.side The side of the coalition. -- @return #nil The DCS Identifiable is not existing or alive. function IDENTIFIABLE:GetCoalition() self:F2( self.IdentifiableName ) @@ -191,7 +190,7 @@ end --- Returns country of the Identifiable. -- @param #IDENTIFIABLE self --- @return Dcs.DCScountry#country.id The country identifier. +-- @return DCS#country.id The country identifier. -- @return #nil The DCS Identifiable is not existing or alive. function IDENTIFIABLE:GetCountry() self:F2( self.IdentifiableName ) @@ -207,17 +206,28 @@ function IDENTIFIABLE:GetCountry() self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) return nil end - +--- Returns country name of the Identifiable. +-- @param #IDENTIFIABLE self +-- @return #string Name of the country. +function IDENTIFIABLE:GetCountryName() + self:F2( self.IdentifiableName ) + local countryid=self:GetCountry() + for name,id in pairs(country.id) do + if countryid==id then + return name + end + end +end --- Returns Identifiable descriptor. Descriptor type depends on Identifiable category. -- @param #IDENTIFIABLE self --- @return Dcs.DCSWrapper.Identifiable#Identifiable.Desc The Identifiable descriptor. +-- @return DCS#Object.Desc The Identifiable descriptor. -- @return #nil The DCS Identifiable is not existing or alive. function IDENTIFIABLE:GetDesc() self:F2( self.IdentifiableName ) - local DCSIdentifiable = self:GetDCSObject() + local DCSIdentifiable = self:GetDCSObject() -- DCS#Object if DCSIdentifiable then local IdentifiableDesc = DCSIdentifiable:getDesc() @@ -229,6 +239,26 @@ function IDENTIFIABLE:GetDesc() return nil end +--- Check if the Object has the attribute. +-- @param #IDENTIFIABLE self +-- @param #string AttributeName The attribute name. +-- @return #boolean true if the attribute exists. +-- @return #nil The DCS Identifiable is not existing or alive. +function IDENTIFIABLE:HasAttribute( AttributeName ) + self:F2( self.IdentifiableName ) + + local DCSIdentifiable = self:GetDCSObject() + + if DCSIdentifiable then + local IdentifiableHasAttribute = DCSIdentifiable:hasAttribute( AttributeName ) + self:T2( IdentifiableHasAttribute ) + return IdentifiableHasAttribute + end + + self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" ) + return nil +end + --- Gets the CallSign of the IDENTIFIABLE, which is a blank by default. -- @param #IDENTIFIABLE self -- @return #string The CallSign of the IDENTIFIABLE. diff --git a/Moose Development/Moose/Wrapper/Object.lua b/Moose Development/Moose/Wrapper/Object.lua index fc3c11126..f57ff73e8 100644 --- a/Moose Development/Moose/Wrapper/Object.lua +++ b/Moose Development/Moose/Wrapper/Object.lua @@ -8,7 +8,8 @@ -- -- === -- --- @module Object +-- @module Wrapper.Object +-- @image MOOSE.JPG --- @type OBJECT @@ -16,9 +17,7 @@ -- @field #string ObjectName The name of the Object. ---- # OBJECT class, extends @{Base#BASE} --- --- OBJECT handles the DCS Object objects: +--- Wrapper class to hendle the DCS Object objects. -- -- * Support all DCS Object APIs. -- * Enhance with Object specific APIs not in the DCS Object API set. @@ -28,7 +27,7 @@ -- -- The OBJECT class provides the following functions to construct a OBJECT instance: -- --- * @{Object#OBJECT.New}(): Create a OBJECT instance. +-- * @{Wrapper.Object#OBJECT.New}(): Create a OBJECT instance. -- -- @field #OBJECT OBJECT = { @@ -42,7 +41,7 @@ OBJECT = { --- Create a new OBJECT from a DCSObject -- @param #OBJECT self --- @param Dcs.DCSWrapper.Object#Object ObjectName The Object name +-- @param DCS#Object ObjectName The Object name -- @return #OBJECT self function OBJECT:New( ObjectName, Test ) local self = BASE:Inherit( self, BASE:New() ) @@ -55,8 +54,7 @@ end --- Returns the unit's unique identifier. -- @param Wrapper.Object#OBJECT self --- @return Dcs.DCSWrapper.Object#Object.ID ObjectID --- @return #nil The DCS Object is not existing or alive. +-- @return DCS#Object.ID ObjectID or #nil if the DCS Object is not existing or alive. Note that the ID is passed as a string and not a number. function OBJECT:GetID() local DCSObject = self:GetDCSObject() @@ -73,6 +71,7 @@ end --- Destroys the OBJECT. -- @param #OBJECT self +-- @return #boolean true if the object is destroyed. -- @return #nil The DCS Unit is not existing or alive. function OBJECT:Destroy() @@ -80,7 +79,8 @@ function OBJECT:Destroy() if DCSObject then --BASE:CreateEventCrash( timer.getTime(), DCSObject ) - DCSObject:destroy() + DCSObject:destroy( false ) + return true end BASE:E( { "Cannot Destroy", Name = self.ObjectName, Class = self:GetClassName() } ) @@ -91,3 +91,5 @@ end + + diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 5a7f064c4..ee8c9b011 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -8,7 +8,8 @@ -- -- === -- --- @module Positionable +-- @module Wrapper.Positionable +-- @image Wrapper_Positionable.JPG --- @type POSITIONABLE.__ Methods which are not intended for mission designers, but which are used interally by the moose designer :-) -- @extends Wrapper.Identifiable#IDENTIFIABLE @@ -17,9 +18,7 @@ -- @extends Wrapper.Identifiable#IDENTIFIABLE ---- # POSITIONABLE class, extends @{Identifiable#IDENTIFIABLE} --- --- The POSITIONABLE class is a wrapper class to handle the POSITIONABLE objects: +--- Wrapper class to handle the POSITIONABLE objects. -- -- * Support all DCS APIs. -- * Enhance with POSITIONABLE specific APIs not in the DCS API set. @@ -61,7 +60,7 @@ POSITIONABLE.__.Cargo = {} --- Create a new POSITIONABLE from a DCSPositionable -- @param #POSITIONABLE self --- @param Dcs.DCSWrapper.Positionable#Positionable PositionableName The POSITIONABLE name +-- @param #string PositionableName The POSITIONABLE name -- @return #POSITIONABLE self function POSITIONABLE:New( PositionableName ) local self = BASE:Inherit( self, IDENTIFIABLE:New( PositionableName ) ) @@ -70,9 +69,135 @@ function POSITIONABLE:New( PositionableName ) return self end ---- Returns the @{DCSTypes#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. +--- Destroys the POSITIONABLE. +-- @param #POSITIONABLE self +-- @param #boolean GenerateEvent (Optional) true if you want to generate a crash or dead event for the unit. +-- @return #nil The DCS Unit is not existing or alive. +-- @usage +-- -- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group. +-- Helicopter = UNIT:FindByName( "Helicopter" ) +-- Helicopter:Destroy( true ) +-- @usage +-- -- Ground unit example: destroy the Tanks and generate a S_EVENT_DEAD for each unit in the Tanks group. +-- Tanks = UNIT:FindByName( "Tanks" ) +-- Tanks:Destroy( true ) +-- @usage +-- -- Ship unit example: destroy the Ship silently. +-- Ship = STATIC:FindByName( "Ship" ) +-- Ship:Destroy() +-- +-- @usage +-- -- Destroy without event generation example. +-- Ship = STATIC:FindByName( "Boat" ) +-- Ship:Destroy( false ) -- Don't generate an event upon destruction. +-- +function POSITIONABLE:Destroy( GenerateEvent ) + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + + if DCSObject then + + local UnitGroup = self:GetGroup() + local UnitGroupName = UnitGroup:GetName() + self:F( { UnitGroupName = UnitGroupName } ) + + if GenerateEvent and GenerateEvent == true then + if self:IsAir() then + self:CreateEventCrash( timer.getTime(), DCSObject ) + else + self:CreateEventDead( timer.getTime(), DCSObject ) + end + elseif GenerateEvent == false then + -- Do nothing! + else + self:CreateEventRemoveUnit( timer.getTime(), DCSObject ) + end + + USERFLAG:New( UnitGroupName ):Set( 100 ) + DCSObject:destroy() + end + + return nil +end + +--- Returns a pos3 table of the objects current position and orientation in 3D space. X, Y, Z values are unit vectors defining the objects orientation. +-- Coordinates are dependent on the position of the maps origin. -- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Position The 3D position vectors of the POSITIONABLE. +-- @return DCS#Position Table consisting of the point and orientation tables. +function POSITIONABLE:GetPosition() + self:F2( self.PositionableName ) + + local DCSPositionable = self:GetDCSObject() + + if DCSPositionable then + local PositionablePosition = DCSPositionable:getPosition() + self:T3( PositionablePosition ) + return PositionablePosition + end + + BASE:E( { "Cannot GetPositionVec3", Positionable = self, Alive = self:IsAlive() } ) + return nil +end + +--- Returns a {@DCS#Vec3} table of the objects current orientation in 3D space. X, Y, Z values are unit vectors defining the objects orientation. +-- X is the orientation parallel to the movement of the object, Z perpendicular and Y vertical orientation. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec3 X orientation, i.e. parallel to the direction of movement. +-- @return DCS#Vec3 Y orientation, i.e. vertical. +-- @return DCS#Vec3 Z orientation, i.e. perpendicular to the direction of movement. +function POSITIONABLE:GetOrientation() + local position=self:GetPosition() + if position then + return position.x, position.y, position.z + else + BASE:E( { "Cannot GetOrientation", Positionable = self, Alive = self:IsAlive() } ) + return nil, nil, nil + end +end + +--- Returns a {@DCS#Vec3} table of the objects current X orientation in 3D space, i.e. along the direction of movement. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec3 X orientation, i.e. parallel to the direction of movement. +function POSITIONABLE:GetOrientationX() + local position=self:GetPosition() + if position then + return position.x + else + BASE:E( { "Cannot GetOrientationX", Positionable = self, Alive = self:IsAlive() } ) + return nil + end +end + +--- Returns a {@DCS#Vec3} table of the objects current Y orientation in 3D space, i.e. vertical orientation. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec3 Y orientation, i.e. vertical. +function POSITIONABLE:GetOrientationY() + local position=self:GetPosition() + if position then + return position.y + else + BASE:E( { "Cannot GetOrientationY", Positionable = self, Alive = self:IsAlive() } ) + return nil + end +end + +--- Returns a {@DCS#Vec3} table of the objects current Z orientation in 3D space, i.e. perpendicular to direction of movement. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Vec3 Z orientation, i.e. perpendicular to movement. +function POSITIONABLE:GetOrientationZ() + local position=self:GetPosition() + if position then + return position.z + else + BASE:E( { "Cannot GetOrientationZ", Positionable = self, Alive = self:IsAlive() } ) + return nil + end +end + +--- Returns the @{DCS#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return DCS#Position The 3D position vectors of the POSITIONABLE. -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetPositionVec3() self:F2( self.PositionableName ) @@ -90,9 +215,9 @@ function POSITIONABLE:GetPositionVec3() return nil end ---- Returns the @{DCSTypes#Vec2} vector indicating the point in 2D of the POSITIONABLE within the mission. +--- Returns the @{DCS#Vec2} vector indicating the point in 2D of the POSITIONABLE within the mission. -- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec2 The 2D point vector of the POSITIONABLE. +-- @return DCS#Vec2 The 2D point vector of the POSITIONABLE. -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetVec2() self:F2( self.PositionableName ) @@ -129,7 +254,7 @@ function POSITIONABLE:GetPointVec2() local PositionablePointVec2 = POINT_VEC2:NewFromVec3( PositionableVec3 ) - self:T2( PositionablePointVec2 ) + --self:F( PositionablePointVec2 ) return PositionablePointVec2 end @@ -186,13 +311,13 @@ function POSITIONABLE:GetCoordinate() end ---- Returns a random @{DCSTypes#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission. +--- Returns a random @{DCS#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission. -- @param Wrapper.Positionable#POSITIONABLE self -- @param #number Radius --- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. +-- @return DCS#Vec3 The 3D point vector of the POSITIONABLE. -- @return #nil The POSITIONABLE is not existing or alive. -- @usage --- -- If Radius is ignored, returns the Dcs.DCSTypes#Vec3 of first UNIT of the GROUP +-- -- If Radius is ignored, returns the DCS#Vec3 of first UNIT of the GROUP function POSITIONABLE:GetRandomVec3( Radius ) self:F2( self.PositionableName ) @@ -221,9 +346,9 @@ function POSITIONABLE:GetRandomVec3( Radius ) return nil end ---- Returns the @{DCSTypes#Vec3} vector indicating the 3D vector of the POSITIONABLE within the mission. +--- Returns the @{DCS#Vec3} vector indicating the 3D vector of the POSITIONABLE within the mission. -- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The 3D point vector of the POSITIONABLE. +-- @return DCS#Vec3 The 3D point vector of the POSITIONABLE. -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetVec3() self:F2( self.PositionableName ) @@ -244,7 +369,7 @@ end --- Get the bounding box of the underlying POSITIONABLE DCS Object. -- @param #POSITIONABLE self --- @return Dcs.DCSTypes#Distance The bounding box of the POSITIONABLE. +-- @return DCS#Box3 The bounding box of the POSITIONABLE. -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetBoundingBox() --R2.1 self:F2() @@ -252,7 +377,7 @@ function POSITIONABLE:GetBoundingBox() --R2.1 local DCSPositionable = self:GetDCSObject() if DCSPositionable then - local PositionableDesc = DCSPositionable:getDesc() --Dcs.DCSTypes#Desc + local PositionableDesc = DCSPositionable:getDesc() --DCS#Desc if PositionableDesc then local PositionableBox = PositionableDesc.box return PositionableBox @@ -265,9 +390,32 @@ function POSITIONABLE:GetBoundingBox() --R2.1 end +--- Get the bounding radius of the underlying POSITIONABLE DCS Object. +-- @param #POSITIONABLE self +-- @param #number mindist (Optional) If bounding box is smaller than this value, mindist is returned. +-- @return DCS#Distance The bounding radius of the POSITIONABLE or #nil if the POSITIONABLE is not existing or alive. +function POSITIONABLE:GetBoundingRadius(mindist) + self:F2() + + local Box = self:GetBoundingBox() + + local boxmin=mindist or 0 + if Box then + local X = Box.max.x - Box.min.x + local Z = Box.max.z - Box.min.z + local CX = X / 2 + local CZ = Z / 2 + return math.max( math.max( CX, CZ ), boxmin ) + end + + BASE:E( { "Cannot GetBoundingRadius", Positionable = self, Alive = self:IsAlive() } ) + + return nil +end + --- Returns the altitude of the POSITIONABLE. -- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Distance The altitude of the POSITIONABLE. +-- @return DCS#Distance The altitude of the POSITIONABLE. -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetAltitude() self:F2() @@ -275,7 +423,7 @@ function POSITIONABLE:GetAltitude() local DCSPositionable = self:GetDCSObject() if DCSPositionable then - local PositionablePointVec3 = DCSPositionable:getPoint() --Dcs.DCSTypes#Vec3 + local PositionablePointVec3 = DCSPositionable:getPoint() --DCS#Vec3 return PositionablePointVec3.y end @@ -309,10 +457,22 @@ function POSITIONABLE:IsAboveRunway() end +function POSITIONABLE:GetSize() + + local DCSObject = self:GetDCSObject() + + if DCSObject then + return 1 + else + return 0 + end +end + + --- Returns the POSITIONABLE heading in degrees. -- @param Wrapper.Positionable#POSITIONABLE self --- @return #number The POSTIONABLE heading +-- @return #number The POSITIONABLE heading -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetHeading() local DCSPositionable = self:GetDCSObject() @@ -336,6 +496,52 @@ function POSITIONABLE:GetHeading() return nil end +-- Is Methods + +--- Returns if the unit is of an air category. +-- If the unit is a helicopter or a plane, then this method will return true, otherwise false. +-- @param #POSITIONABLE self +-- @return #boolean Air category evaluation result. +function POSITIONABLE:IsAir() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + self:T3( { UnitDescriptor.category, Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) + + local IsAirResult = ( UnitDescriptor.category == Unit.Category.AIRPLANE ) or ( UnitDescriptor.category == Unit.Category.HELICOPTER ) + + self:T3( IsAirResult ) + return IsAirResult + end + + return nil +end + +--- Returns if the unit is of an ground category. +-- If the unit is a ground vehicle or infantry, this method will return true, otherwise false. +-- @param #POSITIONABLE self +-- @return #boolean Ground category evaluation result. +function POSITIONABLE:IsGround() + self:F2() + + local DCSUnit = self:GetDCSObject() + + if DCSUnit then + local UnitDescriptor = DCSUnit:getDesc() + self:T3( { UnitDescriptor.category, Unit.Category.GROUND_UNIT } ) + + local IsGroundResult = ( UnitDescriptor.category == Unit.Category.GROUND_UNIT ) + + self:T3( IsGroundResult ) + return IsGroundResult + end + + return nil +end + --- Returns true if the POSITIONABLE is in the air. -- Polymorphic, is overridden in GROUP and UNIT. @@ -372,14 +578,14 @@ end --- Returns the POSITIONABLE velocity Vec3 vector. -- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The velocity Vec3 vector +-- @return DCS#Vec3 The velocity Vec3 vector -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetVelocityVec3() self:F2( self.PositionableName ) local DCSPositionable = self:GetDCSObject() - if DCSPositionable then + if DCSPositionable and DCSPositionable:isExist() then local PositionableVelocityVec3 = DCSPositionable:getVelocity() self:T3( PositionableVelocityVec3 ) return PositionableVelocityVec3 @@ -393,7 +599,7 @@ end --- Returns the POSITIONABLE height in meters. -- @param Wrapper.Positionable#POSITIONABLE self --- @return Dcs.DCSTypes#Vec3 The height of the positionable. +-- @return DCS#Vec3 The height of the positionable. -- @return #nil The POSITIONABLE is not existing or alive. function POSITIONABLE:GetHeight() --R2.1 self:F2( self.PositionableName ) @@ -421,7 +627,7 @@ function POSITIONABLE:GetVelocityKMH() local DCSPositionable = self:GetDCSObject() - if DCSPositionable then + if DCSPositionable and DCSPositionable:isExist() then local VelocityVec3 = self:GetVelocityVec3() local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec local Velocity = Velocity * 3.6 -- now it is in km/h. @@ -440,7 +646,7 @@ function POSITIONABLE:GetVelocityMPS() local DCSPositionable = self:GetDCSObject() - if DCSPositionable then + if DCSPositionable and DCSPositionable:isExist() then local VelocityVec3 = self:GetVelocityVec3() local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec self:T3( Velocity ) @@ -450,6 +656,168 @@ 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. +function POSITIONABLE:GetAoA() + + -- Get position of the unit. + local unitpos = self:GetPosition() + + if unitpos then + + -- Get velocity vector of the unit. + local unitvel = self:GetVelocityVec3() + + if unitvel and UTILS.VecNorm(unitvel)~=0 then + + -- Get wind vector including turbulences. + local wind=self:GetCoordinate():GetWindWithTurbulenceVec3() + + -- Include wind vector. + unitvel.x=unitvel.x-wind.x + unitvel.y=unitvel.y-wind.y + unitvel.z=unitvel.z-wind.z + + -- Unit velocity transformed into aircraft axes directions. + local AxialVel = {} + + -- Transform velocity components in direction of aircraft axes. + AxialVel.x = UTILS.VecDot(unitpos.x, unitvel) + AxialVel.y = UTILS.VecDot(unitpos.y, unitvel) + AxialVel.z = UTILS.VecDot(unitpos.z, unitvel) + + -- AoA is angle between unitpos.x and the x and y velocities. + local AoA = math.acos(UTILS.VecDot({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = AxialVel.y, z = 0})/UTILS.VecNorm({x = AxialVel.x, y = AxialVel.y, z = 0})) + + --Set correct direction: + if AxialVel.y > 0 then + AoA = -AoA + end + + -- Return AoA value in degrees. + return math.deg(AoA) + end + + end + + return nil +end + +--- Returns the unit's climb or descent angle. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number Climb or descent angle in degrees. Or 0 if velocity vector norm is zero (or nil). Or nil, if the position of the POSITIONABLE returns nil. +function POSITIONABLE:GetClimbAngle() + + -- Get position of the unit. + local unitpos = self:GetPosition() + + if unitpos then + + -- Get velocity vector of the unit. + local unitvel = self:GetVelocityVec3() + + if unitvel and UTILS.VecNorm(unitvel)~=0 then + + -- Calculate climb angle. + local angle=math.asin(unitvel.y/UTILS.VecNorm(unitvel)) + + -- Return angle in degrees. + return math.deg(angle) + else + return 0 + end + end + + return nil +end + +--- Returns the pitch angle of a unit. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number Pitch ange in degrees. +function POSITIONABLE:GetPitch() + + -- Get position of the unit. + local unitpos = self:GetPosition() + + if unitpos then + return math.deg(math.asin(unitpos.x.y)) + end + + return nil +end + +--- Returns the roll angle of a unit. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number Pitch ange in degrees. +function POSITIONABLE:GetRoll() + + -- Get position of the unit. + local unitpos = self:GetPosition() + + if unitpos then + + --first, make a vector that is perpendicular to y and unitpos.x with cross product + local cp = UTILS.VecCross(unitpos.x, {x = 0, y = 1, z = 0}) + + --now, get dot product of of this cross product with unitpos.z + local dp = UTILS.VecDot(cp, unitpos.z) + + --now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|) + local Roll = math.acos(dp/(UTILS.VecNorm(cp)*UTILS.VecNorm(unitpos.z))) + + --now, have to get sign of roll. + -- by convention, making right roll positive + -- to get sign of roll, use the y component of unitpos.z. For right roll, y component is negative. + + if unitpos.z.y > 0 then -- left roll, flip the sign of the roll + Roll = -Roll + end + + return math.deg(Roll) + end +end + +--- Returns the yaw angle of a unit. +-- @param Wrapper.Positionable#POSITIONABLE self +-- @return #number Yaw ange in degrees. +function POSITIONABLE:GetYaw() + + local unitpos = self:GetPosition() + if unitpos then + -- get unit velocity + local unitvel = self:GetVelocityVec3() + + if unitvel and UTILS.VecNorm(unitvel) ~= 0 then --must have non-zero velocity! + local AxialVel = {} --unit velocity transformed into aircraft axes directions + + --transform velocity components in direction of aircraft axes. + AxialVel.x = UTILS.VecDot(unitpos.x, unitvel) + AxialVel.y = UTILS.VecDot(unitpos.y, unitvel) + AxialVel.z = UTILS.VecDot(unitpos.z, unitvel) + + --Yaw is the angle between unitpos.x and the x and z velocities + --define right yaw as positive + local Yaw = math.acos(UTILS.VecDot({x = 1, y = 0, z = 0}, {x = AxialVel.x, y = 0, z = AxialVel.z})/UTILS.VecNorm({x = AxialVel.x, y = 0, z = AxialVel.z})) + + --now set correct direction: + if AxialVel.z > 0 then + Yaw = -Yaw + end + return Yaw + end + end + +end + --- Returns the message text with the callsign embedded (if there is one). -- @param #POSITIONABLE self @@ -472,7 +840,7 @@ end --- Returns a message with the callsign embedded (if there is one). -- @param #POSITIONABLE self -- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param DCS#Duration Duration The duration of the message. -- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. -- @return Core.Message#MESSAGE function POSITIONABLE:GetMessage( Message, Duration, Name ) --R2.1 changed callsign and name and using GetMessageText @@ -507,7 +875,7 @@ end -- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. -- @param #POSITIONABLE self -- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param DCS#Duration Duration The duration of the message. -- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. function POSITIONABLE:MessageToAll( Message, Duration, Name ) self:F2( { Message, Duration } ) @@ -524,12 +892,13 @@ end -- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. -- @param #POSITIONABLE self -- @param #string Message The message text --- @param Dcs.DCSTYpes#Duration Duration The duration of the message. --- @param Dcs.DCScoalition#coalition MessageCoalition The Coalition receiving the message. -function POSITIONABLE:MessageToCoalition( Message, Duration, MessageCoalition ) +-- @param DCS#Duration Duration The duration of the message. +-- @param DCS#coalition MessageCoalition The Coalition receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageToCoalition( Message, Duration, MessageCoalition, Name ) self:F2( { Message, Duration } ) - local Name = "" + local Name = Name or "" local DCSObject = self:GetDCSObject() if DCSObject then @@ -545,11 +914,12 @@ end -- @param #POSITIONABLE self -- @param #string Message The message text -- @param Core.Message#MESSAGE.Type MessageType The message type that determines the duration. --- @param Dcs.DCScoalition#coalition MessageCoalition The Coalition receiving the message. -function POSITIONABLE:MessageTypeToCoalition( Message, MessageType, MessageCoalition ) +-- @param DCS#coalition MessageCoalition The Coalition receiving the message. +-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. +function POSITIONABLE:MessageTypeToCoalition( Message, MessageType, MessageCoalition, Name ) self:F2( { Message, MessageType } ) - local Name = "" + local Name = Name or "" local DCSObject = self:GetDCSObject() if DCSObject then @@ -564,7 +934,7 @@ end -- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. -- @param #POSITIONABLE self -- @param #string Message The message text --- @param Dcs.DCSTYpes#Duration Duration The duration of the message. +-- @param DCS#Duration Duration The duration of the message. -- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. function POSITIONABLE:MessageToRed( Message, Duration, Name ) self:F2( { Message, Duration } ) @@ -581,7 +951,7 @@ end -- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. -- @param #POSITIONABLE self -- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param DCS#Duration Duration The duration of the message. -- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. function POSITIONABLE:MessageToBlue( Message, Duration, Name ) self:F2( { Message, Duration } ) @@ -598,7 +968,7 @@ end -- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. -- @param #POSITIONABLE self -- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param DCS#Duration Duration The duration of the message. -- @param Wrapper.Client#CLIENT Client The client object receiving the message. -- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. function POSITIONABLE:MessageToClient( Message, Duration, Client, Name ) @@ -612,11 +982,11 @@ function POSITIONABLE:MessageToClient( Message, Duration, Client, Name ) return nil end ---- Send a message to a @{Group}. +--- Send a message to a @{Wrapper.Group}. -- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. -- @param #POSITIONABLE self -- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param DCS#Duration Duration The duration of the message. -- @param Wrapper.Group#GROUP MessageGroup The GROUP object receiving the message. -- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. function POSITIONABLE:MessageToGroup( Message, Duration, MessageGroup, Name ) @@ -639,7 +1009,7 @@ function POSITIONABLE:MessageToGroup( Message, Duration, MessageGroup, Name ) return nil end ---- Send a message of a message type to a @{Group}. +--- Send a message of a message type to a @{Wrapper.Group}. -- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. -- @param #POSITIONABLE self -- @param #string Message The message text @@ -659,11 +1029,11 @@ function POSITIONABLE:MessageTypeToGroup( Message, MessageType, MessageGroup, Na return nil end ---- Send a message to a @{Set#SET_GROUP}. +--- Send a message to a @{Core.Set#SET_GROUP}. -- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. -- @param #POSITIONABLE self -- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param DCS#Duration Duration The duration of the message. -- @param Core.Set#SET_GROUP MessageSetGroup The SET_GROUP collection receiving the message. -- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. function POSITIONABLE:MessageToSetGroup( Message, Duration, MessageSetGroup, Name ) --R2.1 @@ -683,11 +1053,11 @@ function POSITIONABLE:MessageToSetGroup( Message, Duration, MessageSetGroup, Nam return nil end ---- Send a message to the players in the @{Group}. +--- Send a message to the players in the @{Wrapper.Group}. -- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message. -- @param #POSITIONABLE self -- @param #string Message The message text --- @param Dcs.DCSTypes#Duration Duration The duration of the message. +-- @param DCS#Duration Duration The duration of the message. -- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable. function POSITIONABLE:Message( Message, Duration, Name ) self:F2( { Message, Duration } ) @@ -700,18 +1070,18 @@ function POSITIONABLE:Message( Message, Duration, Name ) return nil end ---- Create a @{Radio#RADIO}, to allow radio transmission for this POSITIONABLE. +--- Create a @{Core.Radio#RADIO}, to allow radio transmission for this POSITIONABLE. -- Set parameters with the methods provided, then use RADIO:Broadcast() to actually broadcast the message -- @param #POSITIONABLE self --- @return #RADIO Radio +-- @return Core.Radio#RADIO Radio function POSITIONABLE:GetRadio() --R2.1 self:F2(self) return RADIO:New(self) end ---- Create a @{Radio#BEACON}, to allow this POSITIONABLE to broadcast beacon signals +--- Create a @{Core.Radio#BEACON}, to allow this POSITIONABLE to broadcast beacon signals -- @param #POSITIONABLE self --- @return #RADIO Radio +-- @return Core.Radio#RADIO Radio function POSITIONABLE:GetBeacon() --R2.1 self:F2(self) return BEACON:New(self) @@ -785,47 +1155,176 @@ function POSITIONABLE:GetLaserCode() --R2.1 return self.LaserCode end ---- Add cargo. --- @param #POSITIONABLE self --- @param Core.Cargo#CARGO Cargo --- @return #POSITIONABLE -function POSITIONABLE:AddCargo( Cargo ) - self.__.Cargo[Cargo] = Cargo - return self -end +do -- Cargo ---- Remove cargo. --- @param #POSITIONABLE self --- @param Core.Cargo#CARGO Cargo --- @return #POSITIONABLE -function POSITIONABLE:RemoveCargo( Cargo ) - self.__.Cargo[Cargo] = nil - return self -end - ---- Returns if carrier has given cargo. --- @param #POSITIONABLE self --- @return Core.Cargo#CARGO Cargo -function POSITIONABLE:HasCargo( Cargo ) - return self.__.Cargo[Cargo] -end - ---- Clear all cargo. --- @param #POSITIONABLE self -function POSITIONABLE:ClearCargo() - self.__.Cargo = {} -end - ---- Get cargo item count. --- @param #POSITIONABLE self --- @return Core.Cargo#CARGO Cargo -function POSITIONABLE:CargoItemCount() - local ItemCount = 0 - for CargoName, Cargo in pairs( self.__.Cargo ) do - ItemCount = ItemCount + Cargo:GetCount() + --- Add cargo. + -- @param #POSITIONABLE self + -- @param Core.Cargo#CARGO Cargo + -- @return #POSITIONABLE + function POSITIONABLE:AddCargo( Cargo ) + self.__.Cargo[Cargo] = Cargo + return self end - return ItemCount -end + + --- Get all contained cargo. + -- @param #POSITIONABLE self + -- @return #POSITIONABLE + function POSITIONABLE:GetCargo() + return self.__.Cargo + end + + + + --- Remove cargo. + -- @param #POSITIONABLE self + -- @param Core.Cargo#CARGO Cargo + -- @return #POSITIONABLE + function POSITIONABLE:RemoveCargo( Cargo ) + self.__.Cargo[Cargo] = nil + return self + end + + --- Returns if carrier has given cargo. + -- @param #POSITIONABLE self + -- @return Core.Cargo#CARGO Cargo + function POSITIONABLE:HasCargo( Cargo ) + return self.__.Cargo[Cargo] + end + + --- Clear all cargo. + -- @param #POSITIONABLE self + function POSITIONABLE:ClearCargo() + self.__.Cargo = {} + end + + --- Is cargo bay empty. + -- @param #POSITIONABLE self + function POSITIONABLE:IsCargoEmpty() + local IsEmpty = true + for _, Cargo in pairs( self.__.Cargo ) do + IsEmpty = false + break + end + return IsEmpty + end + + --- Get cargo item count. + -- @param #POSITIONABLE self + -- @return Core.Cargo#CARGO Cargo + function POSITIONABLE:CargoItemCount() + local ItemCount = 0 + for CargoName, Cargo in pairs( self.__.Cargo ) do + ItemCount = ItemCount + Cargo:GetCount() + end + return ItemCount + end + +-- --- Get Cargo Bay Free Volume in m3. +-- -- @param #POSITIONABLE self +-- -- @return #number CargoBayFreeVolume +-- function POSITIONABLE:GetCargoBayFreeVolume() +-- local CargoVolume = 0 +-- for CargoName, Cargo in pairs( self.__.Cargo ) do +-- CargoVolume = CargoVolume + Cargo:GetVolume() +-- end +-- return self.__.CargoBayVolumeLimit - CargoVolume +-- end +-- + + --- Get Cargo Bay Free Weight in kg. + -- @param #POSITIONABLE self + -- @return #number CargoBayFreeWeight + function POSITIONABLE:GetCargoBayFreeWeight() + + -- When there is no cargo bay weight limit set, then calculate this for this positionable! + if not self.__.CargoBayWeightLimit then + self:SetCargoBayWeightLimit() + end + + local CargoWeight = 0 + for CargoName, Cargo in pairs( self.__.Cargo ) do + CargoWeight = CargoWeight + Cargo:GetWeight() + end + return self.__.CargoBayWeightLimit - CargoWeight + end + +-- --- Get Cargo Bay Volume Limit in m3. +-- -- @param #POSITIONABLE self +-- -- @param #number VolumeLimit +-- function POSITIONABLE:SetCargoBayVolumeLimit( VolumeLimit ) +-- self.__.CargoBayVolumeLimit = VolumeLimit +-- end + + --- Set Cargo Bay Weight Limit in kg. + -- @param #POSITIONABLE self + -- @param #number WeightLimit + function POSITIONABLE:SetCargoBayWeightLimit( WeightLimit ) + + if WeightLimit then + self.__.CargoBayWeightLimit = WeightLimit + elseif self.__.CargoBayWeightLimit~=nil then + -- Value already set ==> Do nothing! + else + -- If weightlimit is not provided, we will calculate it depending on the type of unit. + + -- When an airplane or helicopter, we calculate the weightlimit based on the descriptor. + if self:IsAir() then + local Desc = self:GetDesc() + self:F({Desc=Desc}) + + local Weights = { + ["C-17A"] = 35000, --77519 cannot be used, because it loads way too much apcs and infantry., + ["C-130"] = 22000 --The real value cannot be used, because it loads way too much apcs and infantry., + } + + self.__.CargoBayWeightLimit = Weights[Desc.typeName] or ( Desc.massMax - ( Desc.massEmpty + Desc.fuelMassMax ) ) + else + local Desc = self:GetDesc() + + local Weights = { + ["M1126 Stryker ICV"] = 9, + ["M-113"] = 9, + ["AAV7"] = 25, + ["M2A1_halftrack"] = 9, + ["BMD-1"] = 9, + ["BMP-1"] = 8, + ["BMP-2"] = 7, + ["BMP-3"] = 8, + ["Boman"] = 25, + ["BTR-80"] = 9, + ["BTR_D"] = 12, + ["Cobra"] = 8, + ["LAV-25"] = 6, + ["M-2 Bradley"] = 6, + ["M1043 HMMWV Armament"] = 4, + ["M1045 HMMWV TOW"] = 4, + ["M1126 Stryker ICV"] = 9, + ["M1134 Stryker ATGM"] = 9, + ["Marder"] = 6, + ["MCV-80"] = 9, + ["MLRS FDDM"] = 4, + ["MTLB"] = 25, + ["TPZ"] = 10, + ["Ural-4320 APA-5D"] = 10, + ["GAZ-66"] = 8, + ["GAZ-3307"] = 12, + ["GAZ-3308"] = 14, + ["Tigr_233036"] = 6, + ["KAMAZ Truck"] = 12, + ["KrAZ6322"] = 12, + ["M 818"] = 12, + ["Ural-375"] = 12, + ["Ural-4320-31"] = 14, + ["Ural-4320T"] = 14, + } + + local CargoBayWeightLimit = ( Weights[Desc.typeName] or 0 ) * 95 + self.__.CargoBayWeightLimit = CargoBayWeightLimit + end + end + self:F({CargoBayWeightLimit = self.__.CargoBayWeightLimit}) + end +end --- Cargo --- Signal a flare at the position of the POSITIONABLE. -- @param #POSITIONABLE self diff --git a/Moose Development/Moose/Wrapper/Scenery.lua b/Moose Development/Moose/Wrapper/Scenery.lua index e999917b1..01d9af957 100644 --- a/Moose Development/Moose/Wrapper/Scenery.lua +++ b/Moose Development/Moose/Wrapper/Scenery.lua @@ -8,7 +8,8 @@ -- -- === -- --- @module Scenery +-- @module Wrapper.Scenery +-- @image Wrapper_Scenery.JPG @@ -16,10 +17,9 @@ -- @extends Wrapper.Positionable#POSITIONABLE ---- # SCENERY class, extends @{Positionable#POSITIONABLE} +--- Wrapper class to handle Scenery objects that are defined on the map. -- --- Scenery objects are defined on the map. --- The @{Scenery#SCENERY} class is a wrapper class to handle the DCS Scenery objects: +-- The @{Wrapper.Scenery#SCENERY} class is a wrapper class to handle the DCS Scenery objects: -- -- * Wraps the DCS Scenery objects. -- * Support all DCS Scenery APIs. diff --git a/Moose Development/Moose/Wrapper/Static.lua b/Moose Development/Moose/Wrapper/Static.lua index 28e7c364b..fb3d73296 100644 --- a/Moose Development/Moose/Wrapper/Static.lua +++ b/Moose Development/Moose/Wrapper/Static.lua @@ -8,17 +8,17 @@ -- -- === -- --- @module Static +-- @module Wrapper.Static +-- @image Wrapper_Static.JPG --- @type STATIC -- @extends Wrapper.Positionable#POSITIONABLE ---- # STATIC class, extends @{Positionable#POSITIONABLE} +--- Wrapper class to handle Static objects. -- --- Statics are **Static Units** defined within the Mission Editor. -- Note that Statics are almost the same as Units, but they don't have a controller. --- The @{Static#STATIC} class is a wrapper class to handle the DCS Static objects: +-- The @{Wrapper.Static#STATIC} class is a wrapper class to handle the DCS Static objects: -- -- * Wraps the DCS Static objects. -- * Support all DCS Static APIs. @@ -48,6 +48,24 @@ STATIC = { } +function STATIC:Register( StaticName ) + local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) ) + self.StaticName = StaticName + return self +end + + +--- Finds a STATIC from the _DATABASE using a DCSStatic object. +-- @param #STATIC self +-- @param DCS#StaticObject DCSStatic An existing DCS Static object reference. +-- @return #STATIC self +function STATIC:Find( DCSStatic ) + + local StaticName = DCSStatic:getName() + local StaticFound = _DATABASE:FindStatic( StaticName ) + return StaticFound +end + --- Finds a STATIC from the _DATABASE using the relevant Static Name. -- As an optional parameter, a briefing text can be given also. -- @param #STATIC self @@ -71,13 +89,60 @@ function STATIC:FindByName( StaticName, RaiseError ) return nil end -function STATIC:Register( StaticName ) - local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) ) - self.StaticName = StaticName - return self +--- Destroys the STATIC. +-- @param #STATIC self +-- @param #boolean GenerateEvent (Optional) true if you want to generate a crash or dead event for the static. +-- @return #nil The DCS StaticObject is not existing or alive. +-- @usage +-- -- Air static example: destroy the static Helicopter and generate a S_EVENT_CRASH. +-- Helicopter = STATIC:FindByName( "Helicopter" ) +-- Helicopter:Destroy( true ) +-- +-- @usage +-- -- Ground static example: destroy the static Tank and generate a S_EVENT_DEAD. +-- Tanks = UNIT:FindByName( "Tank" ) +-- Tanks:Destroy( true ) +-- +-- @usage +-- -- Ship static example: destroy the Ship silently. +-- Ship = STATIC:FindByName( "Ship" ) +-- Ship:Destroy() +-- +-- @usage +-- -- Destroy without event generation example. +-- Ship = STATIC:FindByName( "Boat" ) +-- Ship:Destroy( false ) -- Don't generate an event upon destruction. +-- +function STATIC:Destroy( GenerateEvent ) + self:F2( self.ObjectName ) + + local DCSObject = self:GetDCSObject() + + if DCSObject then + + local StaticName = DCSObject:getName() + self:F( { StaticName = StaticName } ) + + if GenerateEvent and GenerateEvent == true then + if self:IsAir() then + self:CreateEventCrash( timer.getTime(), DCSObject ) + else + self:CreateEventDead( timer.getTime(), DCSObject ) + end + elseif GenerateEvent == false then + -- Do nothing! + else + self:CreateEventRemoveUnit( timer.getTime(), DCSObject ) + end + + DCSObject:destroy() + end + + return nil end + function STATIC:GetDCSObject() local DCSStatic = StaticObject.getByName( self.StaticName ) @@ -88,21 +153,96 @@ function STATIC:GetDCSObject() return nil end +--- Returns a list of one @{Static}. +-- @param #STATIC self +-- @return #list A list of one @{Static}. +function STATIC:GetUnits() + self:F2( { self.StaticName } ) + local DCSStatic = self:GetDCSObject() + + local Statics = {} + + if DCSStatic then + Statics[1] = STATIC:Find( DCSStatic ) + self:T3( Statics ) + return Statics + end + + return nil +end + + + + function STATIC:GetThreatLevel() return 1, "Static" end ---- Respawn the @{Unit} using a (tweaked) template of the parent Group. --- @param #UNIT self +--- Respawn the @{Wrapper.Unit} using a (tweaked) template of the parent Group. +-- @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:ReSpawn( Coordinate, Heading ) +function STATIC:SpawnAt( Coordinate, Heading ) - - -- todo: need to fix country - local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName, country.id.USA ) + local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName ) SpawnStatic:SpawnFromPointVec2( Coordinate, Heading, self.StaticName ) 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) + + local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName, countryid ) + + SpawnStatic:ReSpawn() +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 ) + + local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName ) + + SpawnStatic:ReSpawnAt( Coordinate, Heading ) +end + + +--- 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 + else + return false + end +end diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 26d30ed91..16278596a 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -17,16 +17,14 @@ -- -- === -- --- @module Unit +-- @module Wrapper.Unit +-- @image Wrapper_Unit.JPG --- @type UNIT -- @extends Wrapper.Controllable#CONTROLLABLE ---- --- # UNIT class, extends @{Controllable#CONTROLLABLE} --- --- For each DCS Unit object alive within a running mission, a UNIT wrapper object (instance) will be created within the _@{DATABASE} object. +--- For each DCS Unit object alive within a running mission, a UNIT wrapper object (instance) will be created within the _@{DATABASE} object. -- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Unit objects are spawned (using the @{SPAWN} class). -- -- The UNIT class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference @@ -47,7 +45,7 @@ -- -- The DCS Unit APIs are used extensively within MOOSE. The UNIT class has for each DCS Unit API a corresponding method. -- To be able to distinguish easily in your code the difference between a UNIT API call and a DCS Unit API call, --- the first letter of the method is also capitalized. So, by example, the DCS Unit method @{DCSWrapper.Unit#Unit.getName}() +-- the first letter of the method is also capitalized. So, by example, the DCS Unit method @{DCS#Unit.getName}() -- is implemented in the UNIT class as @{#UNIT.GetName}(). -- -- ## Smoke, Flare Units @@ -75,7 +73,7 @@ -- -- ### Zones range -- --- To test whether the Unit is within a **zone**, use the @{#UNIT.IsInZone}() or the @{#UNIT.IsNotInZone}() methods. Any zone can be tested on, but the zone must be derived from @{Zone#ZONE_BASE}. +-- To test whether the Unit is within a **zone**, use the @{#UNIT.IsInZone}() or the @{#UNIT.IsNotInZone}() methods. Any zone can be tested on, but the zone must be derived from @{Core.Zone#ZONE_BASE}. -- -- ### Unit range -- @@ -118,13 +116,15 @@ end --- Finds a UNIT from the _DATABASE using a DCSUnit object. -- @param #UNIT self --- @param Dcs.DCSWrapper.Unit#Unit DCSUnit An existing DCS Unit object reference. +-- @param DCS#Unit DCSUnit An existing DCS Unit object reference. -- @return #UNIT self function UNIT:Find( DCSUnit ) - - local UnitName = DCSUnit:getName() - local UnitFound = _DATABASE:FindUnit( UnitName ) - return UnitFound + if DCSUnit then + local UnitName = DCSUnit:getName() + local UnitFound = _DATABASE:FindUnit( UnitName ) + return UnitFound + end + return nil end --- Find a UNIT in the _DATABASE using the name of an existing DCS Unit. @@ -147,7 +147,7 @@ end --- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#Unit +-- @return DCS#Unit function UNIT:GetDCSObject() local DCSUnit = Unit.getByName( self.UnitName ) @@ -159,28 +159,10 @@ function UNIT:GetDCSObject() return nil end ---- Destroys the UNIT. --- @param #UNIT self --- @return #nil The DCS Unit is not existing or alive. -function UNIT:Destroy() - self:F2( self.ObjectName ) - - local DCSObject = self:GetDCSObject() - - if DCSObject then - local UnitGroup = self:GetGroup() - local UnitGroupName = UnitGroup:GetName() - self:F( { UnitGroupName = UnitGroupName } ) - USERFLAG:New( UnitGroupName ):Set( 100 ) - --BASE:CreateEventCrash( timer.getTime(), DCSObject ) - DCSObject:destroy() - end - - return nil -end ---- Respawn the @{Unit} using a (tweaked) template of the parent Group. + +--- Respawn the @{Wrapper.Unit} using a (tweaked) template of the parent Group. -- -- This function will: -- @@ -189,20 +171,22 @@ end -- * Then it will respawn the re-modelled group. -- -- @param #UNIT self --- @param Dcs.DCSTypes#Vec3 SpawnVec3 The position where to Spawn the new Unit at. +-- @param Core.Point#COORDINATE Coordinate The position where to Spawn the new Unit at. -- @param #number Heading The heading of the unit respawn. -function UNIT:ReSpawn( SpawnVec3, Heading ) +function UNIT:ReSpawnAt( Coordinate, Heading ) + self:T( self:Name() ) local SpawnGroupTemplate = UTILS.DeepCopy( _DATABASE:GetGroupTemplateFromUnitName( self:Name() ) ) self:T( SpawnGroupTemplate ) local SpawnGroup = self:GetGroup() + self:T( { SpawnGroup = SpawnGroup } ) if SpawnGroup then local Vec3 = SpawnGroup:GetVec3() - SpawnGroupTemplate.x = SpawnVec3.x - SpawnGroupTemplate.y = SpawnVec3.z + SpawnGroupTemplate.x = Coordinate.x + SpawnGroupTemplate.y = Coordinate.z self:F( #SpawnGroupTemplate.units ) for UnitID, UnitData in pairs( SpawnGroup:GetUnits() ) do @@ -221,12 +205,13 @@ function UNIT:ReSpawn( SpawnVec3, Heading ) end for UnitTemplateID, UnitTemplateData in pairs( SpawnGroupTemplate.units ) do - self:T( UnitTemplateData.name ) + self:T( { UnitTemplateData.name, self:Name() } ) + SpawnGroupTemplate.units[UnitTemplateID].unitId = nil if UnitTemplateData.name == self:Name() then self:T("Adjusting") - SpawnGroupTemplate.units[UnitTemplateID].alt = SpawnVec3.y - SpawnGroupTemplate.units[UnitTemplateID].x = SpawnVec3.x - SpawnGroupTemplate.units[UnitTemplateID].y = SpawnVec3.z + SpawnGroupTemplate.units[UnitTemplateID].alt = Coordinate.y + SpawnGroupTemplate.units[UnitTemplateID].x = Coordinate.x + SpawnGroupTemplate.units[UnitTemplateID].y = Coordinate.z SpawnGroupTemplate.units[UnitTemplateID].heading = Heading self:F( { UnitTemplateID, SpawnGroupTemplate.units[UnitTemplateID], SpawnGroupTemplate.units[UnitTemplateID] } ) else @@ -261,6 +246,10 @@ function UNIT:ReSpawn( SpawnVec3, Heading ) i = i + 1 end end + + SpawnGroupTemplate.groupId = nil + + self:T( SpawnGroupTemplate ) _DATABASE:Spawn( SpawnGroupTemplate ) end @@ -296,7 +285,7 @@ end function UNIT:IsAlive() self:F3( self.UnitName ) - local DCSUnit = self:GetDCSObject() -- Dcs.DCSUnit#Unit + local DCSUnit = self:GetDCSObject() -- DCS#Unit if DCSUnit then local UnitIsAlive = DCSUnit:isExist() and DCSUnit:isActive() @@ -319,6 +308,9 @@ function UNIT:GetCallsign() if DCSUnit then local UnitCallSign = DCSUnit:getCallsign() + if UnitCallSign == "" then + UnitCallSign = DCSUnit:getName() + end return UnitCallSign end @@ -334,18 +326,30 @@ end function UNIT:GetPlayerName() self:F2( self.UnitName ) - local DCSUnit = self:GetDCSObject() + local DCSUnit = self:GetDCSObject() -- DCS#Unit if DCSUnit then local PlayerName = DCSUnit:getPlayerName() - if PlayerName == nil then - PlayerName = "" - end + -- TODO Workaround DCS-BUG-3 - https://github.com/FlightControl-Master/MOOSE/issues/696 +-- if PlayerName == nil or PlayerName == "" then +-- local PlayerCategory = DCSUnit:getDesc().category +-- if PlayerCategory == Unit.Category.GROUND_UNIT or PlayerCategory == Unit.Category.SHIP then +-- PlayerName = "Player" .. DCSUnit:getID() +-- end +-- end +-- -- Good code +-- if PlayerName == nil then +-- PlayerName = nil +-- else +-- if PlayerName == "" then +-- PlayerName = "Player" .. DCSUnit:getID() +-- end +-- end return PlayerName end - return nil + return nil end @@ -369,6 +373,45 @@ function UNIT:GetNumber() return nil end + +--- Returns the unit's max speed in km/h derived from the DCS descriptors. +-- @param #UNIT self +-- @return #number Speed in km/h. +function UNIT:GetSpeedMax() + self:F2( self.UnitName ) + + local Desc = self:GetDesc() + + if Desc then + local SpeedMax = Desc.speedMax + return SpeedMax*3.6 + end + + return nil +end + +--- Returns the unit's max range in meters derived from the DCS descriptors. +-- For ground units it will return a range of 10,000 km as they have no real range. +-- @param #UNIT self +-- @return #number Range in meters. +function UNIT:GetRange() + self:F2( self.UnitName ) + + local Desc = self:GetDesc() + + if Desc then + local Range = Desc.range --This is in nautical miles for some reason. But should check again! + if Range then + Range=UTILS.NMToMeters(Range) + else + Range=10000000 --10.000 km if no range + end + return Range + end + + return nil +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. @@ -379,7 +422,7 @@ function UNIT:GetGroup() local DCSUnit = self:GetDCSObject() if DCSUnit then - local UnitGroup = GROUP:Find( DCSUnit:getGroup() ) + local UnitGroup = GROUP:FindByName( DCSUnit:getGroup():getName() ) return UnitGroup end @@ -411,7 +454,7 @@ end --- Returns the Unit's ammunition. -- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#Unit.Ammo +-- @return DCS#Unit.Ammo -- @return #nil The DCS Unit is not existing or alive. function UNIT:GetAmmo() self:F2( self.UnitName ) @@ -428,7 +471,7 @@ end --- Returns the unit sensors. -- @param #UNIT self --- @return Dcs.DCSWrapper.Unit#Unit.Sensors +-- @return DCS#Unit.Sensors -- @return #nil The DCS Unit is not existing or alive. function UNIT:GetSensors() self:F2( self.UnitName ) @@ -492,7 +535,7 @@ end -- * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. -- @param #UNIT self -- @return #boolean Indicates if at least one of the unit's radar(s) is on. --- @return Dcs.DCSWrapper.Object#Object The object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. +-- @return DCS#Object The object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target. -- @return #nil The DCS Unit is not existing or alive. function UNIT:GetRadar() self:F2( self.UnitName ) @@ -512,7 +555,7 @@ end -- @return #number The relative amount of fuel (from 0.0 to 1.0). -- @return #nil The DCS Unit is not existing or alive. function UNIT:GetFuel() - self:F( self.UnitName ) + self:F3( self.UnitName ) local DCSUnit = self:GetDCSObject() @@ -524,16 +567,16 @@ function UNIT:GetFuel() return nil end ---- Returns the UNIT in a UNIT list of one element. +--- Returns a list of one @{Wrapper.Unit}. -- @param #UNIT self --- @return #list The UNITs wrappers. +-- @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 = {} + if DCSUnit then - local DCSUnits = DCSUnit:getUnits() - local Units = {} Units[1] = UNIT:Find( DCSUnit ) self:T3( Units ) return Units @@ -628,12 +671,9 @@ function UNIT:GetThreatLevel() if Descriptor then local Attributes = Descriptor.attributes - self:T( Attributes ) if self:IsGround() then - self:T( "Ground" ) - local ThreatLevels = { "Unarmed", "Infantry", @@ -670,8 +710,6 @@ function UNIT:GetThreatLevel() if self:IsAir() then - self:T( "Air" ) - local ThreatLevels = { "Unarmed", "Tanker", @@ -704,8 +742,6 @@ function UNIT:GetThreatLevel() if self:IsShip() then - self:T( "Ship" ) - --["Aircraft Carriers"] = {"Heavy armed ships",}, --["Cruisers"] = {"Heavy armed ships",}, --["Destroyers"] = {"Heavy armed ships",}, @@ -743,7 +779,6 @@ function UNIT:GetThreatLevel() end end - self:T2( ThreatLevel ) return ThreatLevel, ThreatText end @@ -754,7 +789,7 @@ end --- 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 @{Zone#ZONE_BASE} +-- @return #boolean Returns true if the unit is within the @{Core.Zone#ZONE_BASE} function UNIT:IsInZone( Zone ) self:F2( { self.UnitName, Zone } ) @@ -769,7 +804,7 @@ 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 @{Zone#ZONE_BASE} +-- @return #boolean Returns true if the unit is not within the @{Core.Zone#ZONE_BASE} function UNIT:IsNotInZone( Zone ) self:F2( { self.UnitName, Zone } ) @@ -815,51 +850,7 @@ end --- Is methods ---- Returns if the unit is of an air category. --- If the unit is a helicopter or a plane, then this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Air category evaluation result. -function UNIT:IsAir() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } ) - - local IsAirResult = ( UnitDescriptor.category == Unit.Category.AIRPLANE ) or ( UnitDescriptor.category == Unit.Category.HELICOPTER ) - - self:T3( IsAirResult ) - return IsAirResult - end - - return nil -end - ---- Returns if the unit is of an ground category. --- If the unit is a ground vehicle or infantry, this method will return true, otherwise false. --- @param #UNIT self --- @return #boolean Ground category evaluation result. -function UNIT:IsGround() - self:F2() - - local DCSUnit = self:GetDCSObject() - - if DCSUnit then - local UnitDescriptor = DCSUnit:getDesc() - self:T3( { UnitDescriptor.category, Unit.Category.GROUND_UNIT } ) - - local IsGroundResult = ( UnitDescriptor.category == Unit.Category.GROUND_UNIT ) - - self:T3( IsGroundResult ) - return IsGroundResult - end - - return nil -end --- Returns if the unit is a friendly unit. -- @param #UNIT self @@ -905,16 +896,37 @@ function UNIT:IsShip() end --- Returns true if the UNIT is in the air. --- @param Wrapper.Positionable#UNIT self +-- @param #UNIT self -- @return #boolean true if in the air. -- @return #nil The UNIT is not existing or alive. function UNIT:InAir() self:F2( self.UnitName ) - local DCSUnit = self:GetDCSObject() + -- Get DCS unit object. + local DCSUnit = self:GetDCSObject() --DCS#Unit if DCSUnit then + + -- Get DCS result of whether unit is in air or not. local UnitInAir = DCSUnit:inAir() + + -- Get unit category. + local UnitCategory = DCSUnit:getDesc().category + + -- If DCS says that it is in air, check if this is really the case, since we might have landed on a building where inAir()=true but actually is not. + -- This is a workaround since DCS currently does not acknoledge that helos land on buildings. + -- Note however, that the velocity check will fail if the ground is moving, e.g. on an aircraft carrier! + if UnitInAir==true and UnitCategory == Unit.Category.HELICOPTER then + local VelocityVec3 = DCSUnit:getVelocity() + local Velocity = UTILS.VecNorm(VelocityVec3) + local Coordinate = DCSUnit:getPoint() + local LandHeight = land.getHeight( { x = Coordinate.x, y = Coordinate.z } ) + local Height = Coordinate.y - LandHeight + if Velocity < 1 and Height <= 60 then + UnitInAir = false + end + end + self:T3( UnitInAir ) return UnitInAir end diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 4459578ec..b1da22f5c 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -21,7 +21,6 @@ Core/Radio.lua Core/Spawn.lua Core/SpawnStatic.lua Core/Goal.lua -Core/Cargo.lua Core/Spot.lua Wrapper/Object.lua @@ -35,6 +34,12 @@ Wrapper/Static.lua Wrapper/Airbase.lua Wrapper/Scenery.lua +Cargo/Cargo.lua +Cargo/CargoUnit.lua +Cargo/CargoSlingload.lua +Cargo/CargoCrate.lua +Cargo/CargoGroup.lua + Functional/Scoring.lua Functional/CleanUp.lua Functional/Movement.lua @@ -49,18 +54,42 @@ Functional/Range.lua Functional/ZoneGoal.lua Functional/ZoneGoalCoalition.lua Functional/ZoneCaptureCoalition.lua +Functional/Artillery.lua +Functional/Suppression.lua +Functional/PseudoATC.lua +Functional/Warehouse.lua + +Ops/Airboss.lua +Ops/RecoveryTanker.lua +Ops/RescueHelo.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 AI/AI_Bai.lua AI/AI_Formation.lua +AI/AI_Cargo.lua +AI/AI_Cargo_APC.lua +AI/AI_Cargo_Helicopter.lua +AI/AI_Cargo_Airplane.lua +AI/AI_Cargo_Dispatcher.lua +AI/AI_Cargo_Dispatcher_APC.lua +AI/AI_Cargo_Dispatcher_Helicopter.lua +AI/AI_Cargo_Dispatcher_Airplane.lua Actions/Act_Assign.lua Actions/Act_Route.lua @@ -71,12 +100,16 @@ Tasking/CommandCenter.lua Tasking/Mission.lua Tasking/Task.lua Tasking/TaskInfo.lua +Tasking/Task_Manager.lua Tasking/DetectionManager.lua Tasking/Task_A2G_Dispatcher.lua Tasking/Task_A2G.lua Tasking/Task_A2A_Dispatcher.lua Tasking/Task_A2A.lua Tasking/Task_Cargo.lua +Tasking/Task_Cargo_Transport.lua +Tasking/Task_Cargo_CSAR.lua +Tasking/Task_Cargo_Dispatcher.lua Tasking/TaskZoneCapture.lua Globals.lua